Releases: yohimik/tinygo
Release list
v0.43.0-net.2
TinyGo 0.43.0-net.2
This is a test release of the yohimik fork, not an official TinyGo release. It targets hosted Linux and Darwin CLI applications that use HTTPS clients, files, environment variables, concurrent work, child processes and signals. It does not establish general Go server compatibility.
Source
- Upstream base is TinyGo 0.42.0, commit
bdc4a21a. - Release source is
95fba82a76c198a4543a031b29c14fc878dfc05f. - Net source is
0f460803c832e5edad095ce02731f1000496dd88. - The release differs from accepted candidate
e7d34c8c126e0eecd2ce711915f2f88d112d833aonly in the version constant.
The 0.43.0-net.N version denotes a fork prerelease. It does not mean that this build uses upstream TinyGo 0.43.0.
Changes since net.1
- Use the coordinated Darwin dynamic-import and variadic trampoline implementation from TinyGo PR 5612. This replaces the fork's duplicate fcntl wrapper path. Both fcntl regression checks remain.
- Map child file descriptors without destroying sources needed by later file actions. Cover cycles, repeated sources and sparse mappings. Close unnamed standard descriptors. Use valid Darwin close actions for descriptors that were not open.
- Include the Go
net/http/cookiejarpackage in the loader. Keep the port's surrounding net packages. - Add
ListenConfigwith explicit limits for unsupported options. Report kernel-assigned ephemeral ports and preserve listener zones. - Shut sockets down before close and prevent repeated close from closing a reused descriptor. This is not a complete poller or descriptor-lifetime implementation.
- Include the net submodule in the Nix test source checkout.
Real hosted TCP/TLS, HTTP redirects, DNS, process spawning, environment and directory handling, Setpgid, RWMutex and signal delivery fixes from net.1 remain included.
Downstream candidate acceptance
These results use the exact pre-version candidate. They are not claims that downstream releases have already shipped with net.2.
Dispat source 909dc401f3725a610604b77b0e790808cee9a524, with corrected harness 0990c6db, passed 796 test events with no failures or skips on each of native Darwin ARM64 and native Linux ARM64. Checks include TinyGo-built update fixtures, trusted TLS, unknown-CA refusal, update and original backup hashes, byte-identical offline rollback, and install --pipe success, failure and environment handling.
Crier source 7d687fc8a17cb0350d95698be0f74f6e8ce4b4b4 passed full raw and stripped E2E on native Linux ARM64 and emulated Linux AMD64. Each run has 144 top-level tests and 156 passing test events, with no failures or skips. Coverage includes TinyGo-built update fixtures, trusted TLS by name and IP, certificate and plaintext refusal, byte-identical rollback, real FFmpeg, helper processes, publishing, files and concurrent application work. The standard Go unit, E2E, FFmpeg, coverage and lint checks also pass.
Crier uses webrender and canvas for text, fonts, page layout and PNG rendering. Its unchanged 13-image comparison passes on both targets. AMD64 is exact for all images. ARM64 is exact for 12 images, with four event-card pixels differing by one channel value. Crier applies a two-line explicit-rounding build patch to webrender for both compilers. This makes gradient origins consistent across permitted fused and separately rounded Go arithmetic. It is an application build patch, not a TinyGo compiler fix or a relaxed pixel threshold. Crier's owner supplied the candidate acceptance report and logs, which were checked independently. The later published-artifact report is linked below.
Crier candidate sizes
Same source and target. Go is built with stripped release flags. TinyGo uses size optimization, no debug data and a separate strip step.
| Target | Go bytes | Stripped TinyGo bytes | Reduction |
|---|---|---|---|
| Linux ARM64 | 30,277,794 | 13,835,824 | 54.30% |
| Linux AMD64 | 32,518,306 | 16,446,968 | 49.42% |
These are candidate application measurements, not final downstream release asset sizes. AMD64 execution here used emulation. Neither these results nor the Dispat tests cover every Go package or every concurrency pattern.
Published Crier v1.1.1
Crier v1.1.1 is now public at source acac2f0eae0d98e42f442c7f4e6a36ccf9bc3665, built with this published toolchain. Its final acceptance report records all application checks and their limits. The public tag, asset sizes and report SHA-256 were checked.
Final Linux ARM64 bytes are 13,835,840 for stripped TinyGo versus 30,277,794 for standard Go, a 54.30% reduction. Final Linux AMD64 bytes are 16,447,000 versus 32,518,306, a 49.42% reduction. These final asset measurements supersede the candidate sizes above. The release SHA-256 manifest identifies each file.
Tiny assets are opt-in. Normal Crier installation and self-update still select standard Go assets. The published report retains the emulated generic os failures, the platform execution limits and the explicit-rounding renderer patch.
Related upstream work
Published Dispat v1.8.1 CLI
Dispat v1.8.1 CLI is public. Its binary size manifest records build source 40c5823673c9f7811397adeaf51c9e96a0be0905, Go1.26.8 and this TinyGo release. The release tag identifies release-record commit 3b8f2793e728e180db4e0700b050850d24c37482, not the build source. Public metadata and the downloaded manifest digest were checked.
Final Linux ARM64 sizes are 6,299,032 TinyGo bytes versus 9,896,098 standard Go bytes, 36.35% smaller. Linux AMD64 sizes are 6,697,528 versus 10,895,522, 38.53% smaller. These figures identify published CLI assets and do not replace the separately recorded acceptance evidence. The full release workflow has now completed successfully at the recorded build source, including its Windows, macOS and Ubuntu checks.
Upstream PR ownership
Runtime, process and loader work is tracked in TinyGo 5630, 5633, 5634, 5635, 5636 and 5655. The Darwin implementation follows owner PR 5612. Duplicate PR 5632 is closed. Signal PR 5631 is closed in favor of owner PR 5530; this fork still uses its tested signal implementation. TLS PR 5645 is closed in favor of 5633 and 5635.
Net changes are tracked in 72, 73, 74, 75, 80 and 82. Close and poller coordination continues in 77. This release does not include that full poller, the newer RoundTripper PR 83, or the Itoa shim PR 84. Redirect PR 72 and RoundTripper PR 83 require coordinated integration.
Known limits
http.Client.Timeout, in-flight deadline changes and full descriptor lifetime are not resolved. A signal can stretch the host netdev receive deadline after EINTR.os/exec.WaitDelaydoes not reliably bound output collection when a descendant keeps a pipe open. Closing a descriptor does not interrupt all active blocking pipe I/O. Seetestdata/os-exec-waitdelayin this source.- Raw
tls.Dialon Darwin needs an explicit root pool. The port's HTTP client supplies PEM roots, but this is not native keychain verification. - Darwin DNS uses the port's stub resolver, not the complete macOS resolver. Split DNS, scoped resolution and mDNS are not established.
SysProcAttrsupports Setpgid and Pgid. Other unsupported fields are rejected. Programs that reap children independently can race withProcess.Wait.signal.Ignoredremains unimplemented. Hosted cooperative scheduler link failures remain separate from the default threads scheduler.- Windows, baremetal and WebAssembly retain the previous TLS/process behavior. The real hosted TLS claim applies to Linux and Darwin only.
- macOS 10.15 or later is required for the spawn directory action. The minimum OS version has not been tested on an old Mac.
- Earlier emulated AMD64 stress found rare hangs. This release's successful application tests do not establish that the stress issue is fixed.
- TinyGo test compilation can require substantial memory. Bound test-build parallelism and use an external timeout for tests that can block.
Install and verify
Use a release asset for your host OS and architecture. Each tarball contains a tinygo directory with the compiler, runtime, libraries and targets. Keep them together and set TINYGOROOT to that directory if the compiler is installed separately. A compatible host Go toolchain is required. The candidate acceptance used Go 1.26.x; TinyGo accepts Go 1.25 through 1.27.
The fork can also be selected through dispat install github/yohimik/tinygo. Check that the resolved version is 0.43.0-net.2 and preserve the complete toolchain directory.
GitHub records SHA-256 digests for the release assets. Compare the downloaded files with the API digests before use. Downstream owners will repeat acceptance with these published assets before their own releases.
Release verificat...
v0.43.0-net.1
A test build of TinyGo with working host networking over real TLS, working process spawning, and working os/signal delivery on hosted linux and macOS. It exists so a downstream project can build and run a program whose release paths are all HTTPS, which shells out to the binaries it installs, and which is expected to shut down cleanly when a supervisor or a terminal sends it SIGINT. It is not an official TinyGo release and it is not built from tinygo-org/tinygo.
New in 0.43.0-net.1: rebased onto upstream 0.42.0
This release is the v0.42.0-net.4 content rebased onto upstream TinyGo 0.42.0 (bdc4a21a). There is no functional change. Every commit that made net.1 through net.4 is here unchanged; what moved underneath them is the base, from the 0.42 development tree to the released 0.42.0 tag. The only new commit is the version bump. Upstream's own additions between the two bases — order-dependent composite map hashes, a strlen wasm builtin, the 0.42.0 CHANGELOG — come along with the new base.
The eleven upstream pull requests tinygo-org/tinygo tinygo-org#5630–tinygo-org#5636 and tinygo-org/net tinygo-org#72–tinygo-org#75 carry the same changes; this line exists so a downstream project can use them before they land.
Versioning
While X.Y.Z-net.N is published, upstream has not released X.Y.Z. The 0.42.0-net.N releases predate upstream 0.42.0 and sort below it; this line moves to 0.43.0-net.N for that reason.
Carried over from net.4
os/signal delivers signals again
On hosted linux and macOS — both of which default to -scheduler=threads — signal.Notify and signal.NotifyContext never delivered a signal to the channel. Registration worked, which is what made it so quiet: the sigaction handler was installed, so SIGINT and SIGTERM stopped terminating the process, and a signal nobody had registered still terminated it normally. The process simply became unkillable by anything short of SIGKILL, and a program written to shut down on SIGINT sat there until its supervisor's timeout expired.
The receiving goroutine inside os/signal calls signal_recv, which parked itself with task.Pause when nothing was pending. The only thing that ever resumed it was checkSignals, and on the receive path the only caller of that is waitForEvents — the cooperative scheduler's idle hook. With threads there is no scheduler loop, so waitForEvents is never called and the receiver stayed parked forever. The signal handler had been doing its half of a futex handshake all along; there was nothing waiting on the other end of it.
signal_recv now blocks on a futex of its own that the handler wakes, on the same 0/1 protocol the handler already ran. It cannot share the existing signalFutex: sleepTicks waits on that one too and swaps it back to zero, so a time.Sleep anywhere in the program would have swallowed the wakeup meant for the receiver. The handler gains only an atomic store and a futex wake syscall, both safe to call from a signal handler on an arbitrary thread, and the collector's own stop-the-world signal is a separate handler that this does not touch.
signalWaitUntilIdle — which signal.Stop and signal.Reset call before returning — had the same problem from the other side. It spun on Gosched, which is a no-op with threads, so it burned a core until the receiver happened to drain the last signal. It now waits on a futex that signal_recv wakes when the pending set empties.
The cooperative path is unchanged. The two versions live in signal_cooperative.go and signal_threads.go, split on scheduler.threads the way the schedulers themselves are.
What works now, each verified as a stock-fails / this-release-passes pair on macOS arm64, linux/arm64 and linux/amd64:
signal.Notifydelivers SIGINT and SIGTERM, in about a millisecond.signal.NotifyContextcancels its context, and thestop()it returns actually returns.- Two signals in a row both arrive.
signal.Stopreturns promptly, delivery stops, and the default action is restored — a second signal terminates the process.signal.Resetrestores the default action.signal.Ignorekeeps the process alive with no reader.- A signal that was never notified still terminates the process (SIGHUP → 129).
- Delivery lands while every thread is churning locks and starting goroutines, and while the collector is under allocation pressure.
- The supervisor shape: a parent starts a child with
os/exec, signals it by pid, and the child shuts down and exits 0 while the parent'sWaitreturns.
signal.Ignored still does not link. The runtime has never implemented os/signal.signal_ignored, so a program that calls signal.Ignored fails at link time with linker could not find symbol os/signal.signal_ignored. This is pre-existing and unchanged — it is also why tinygo test os/signal cannot be added to the test suite; it fails to link identically against net.3 and against this build, before any test runs. The regression coverage for this fix is tests/sigprobe instead.
Carried over from net.3
SysProcAttr.SetpgidandPgidare honoured throughposix_spawnattr_setpgroup; every other field is refused by name (os: SysProcAttr.Setsid: sys setting not implemented) rather than the whole struct being rejected, and still unwraps toos.ErrNotImplementedSys.net/httpfollows redirects: up to 10 hops, 301/302/303 switching to GET, 307/308 preserving method and body throughGetBody, cross-hostAuthorization/Cookiestripping,Referer, intermediate bodies drained,CheckRedirectandErrUseLastResponsehonoured. Before net.3 a 302 was handed back with an empty body.sync.RWMutexno longer deadlocks. It counted lock-holding and queued readers in the same number and made both sides wait on predicates over it; two interleavings wedged it permanently, with no panic and no message. Reachable from ordinary code, becausesyscall.ForkLockis anRWMutexthatos.Piperead-locks andos.StartProcesswrite-locks. Not a fork-specific bug: it is upstream TinyGo'ssrc/sync/mutex.go, unchanged since 2024.- macOS:
fcntl's variadic third argument is passed correctly. It had been read from stack residue, sosyscall.CloseOnExecsetFD_CLOEXECor did not depending on the binary — and when it did not, every descriptor leaked into every child, which is exactly howcmd.Output()on a command withStdinset would hang forever.
Carried over from net.2
- Processes are started with
posix_spawn(3)on hosted linux (musl) and macOS (libSystem), not with a fork — these targets run the threads scheduler and collect with Boehm, so afork()from Go would hand the child a single thread holding whatever locks the other threads owned. ProcAttr.Files,.Dirand.Envall work, and a nilEnvinherits the parent's environment.Waitreaps withwait4and retries onEINTR;ProcessStateis real;Kill/Signalwork and mapESRCHtoos.ErrProcessDone.- The child gets an empty signal mask, which unlike a handler disposition would otherwise survive the
exec. - The darwin libSystem stub declares the
posix_spawnfamily, which the minimal macOS SDK omits.
Carried over from net.1
crypto/tlsis the real one on hosted linux and darwin; the no-op stub stays for baremetal, wasm and Windows.- The host netdev works on macOS, with
SO_NOSIGPIPEon every socket. - HTTPS on macOS has trust roots, read by
net/httpfrom$SSL_CERT_FILEor/etc/ssl/cert.pem. weak.runtime_makeStrongFromWeakis implemented, whichcrypto/tls's certificate cache needs to link.- The darwin libSystem stub declares the BSD socket API.
Targets without a process model or without signals — baremetal, wasm, Windows — keep exactly the previous stubs.
Verified
tests/sigprobe has eleven checks, each running the probe binary again as a child, waiting for it to print READY, signalling it by pid, and reading what the child made of it — the same shape a supervisor uses on a TinyGo-built CLI. Built with the ordinary Go toolchain it passes all eleven, which is what makes it usable as an oracle.
- macOS arm64, natively. Stock net.3: 9 of 11 checks failed, every delivery check reporting
NO SIGNALafter its full 8-second wait. This build: all 11 pass,notifyin under a millisecond. - linux/arm64 (container, native on the host). Same pair: 9 of 11 failed on net.3, all 11 pass here.
- linux/amd64 (emulated). Same pair: 9 of 11 failed on net.3, all 11 pass here.
- The two checks that passed on net.3 —
ignoreand an unregistered SIGHUP terminating the process — still pass, so nothing that used to work was traded away. - The reproducer this fix was filed against. A 40-line program that waits for SIGINT three ways prints
NO SIGNAL in 8sand exits 3 on both its modes against net.3. Against this build:got signal: interrupt/clean exit, and for theNotifyContextmodectx cancelled/stop() returned/clean exit— both exit 0. os,syncandnetpackage tests stay green on linux and macOS in CI, as do the compiler tests, for the commit this tag points at.
Re-verified for this release, against the published tarballs
Not against a local build: the artifacts attached below were downloaded from this release page and unpacked.
tinygo0.43.0-net.1.linux-arm64.tar.gz, in agolang:1.27container (go1.27.1):spawnprobe16/16,sigprobe11/11,netprobetcp/tls/https/dnsall green (https status: 200 OK, NXDOMAIN reported asisNotFound=true).tinygo0.43.0-net.1.darwin-arm64.tar.gz, natively on macOS arm64 (go1.26.7): the same —spawnprobe16/16,sigprobe11/11,netprobeall four layers green.
Known gaps, unchanged from net.3
http.Client.Timeoutis still not enforced. The port computes the deadline and uses it to decorate an error mess...
v0.42.0-net.4
A test build of the TinyGo 0.42 development tree with working host networking over real TLS, working process spawning, and working os/signal delivery on hosted linux and macOS. It exists so a downstream project can build and run a program whose release paths are all HTTPS, which shells out to the binaries it installs, and which is expected to shut down cleanly when a supervisor or a terminal sends it SIGINT. It is not an official TinyGo release and it is not built from tinygo-org/tinygo.
This is v0.42.0-net.3 plus one fix: signal.Notify never delivered anything.
New in net.4
os/signal delivers signals again
On hosted linux and macOS — both of which default to -scheduler=threads — signal.Notify and signal.NotifyContext never delivered a signal to the channel. Registration worked, which is what made it so quiet: the sigaction handler was installed, so SIGINT and SIGTERM stopped terminating the process, and a signal nobody had registered still terminated it normally. The process simply became unkillable by anything short of SIGKILL, and a program written to shut down on SIGINT sat there until its supervisor's timeout expired.
The receiving goroutine inside os/signal calls signal_recv, which parked itself with task.Pause when nothing was pending. The only thing that ever resumed it was checkSignals, and on the receive path the only caller of that is waitForEvents — the cooperative scheduler's idle hook. With threads there is no scheduler loop, so waitForEvents is never called and the receiver stayed parked forever. The signal handler had been doing its half of a futex handshake all along; there was nothing waiting on the other end of it.
signal_recv now blocks on a futex of its own that the handler wakes, on the same 0/1 protocol the handler already ran. It cannot share the existing signalFutex: sleepTicks waits on that one too and swaps it back to zero, so a time.Sleep anywhere in the program would have swallowed the wakeup meant for the receiver. The handler gains only an atomic store and a futex wake syscall, both safe to call from a signal handler on an arbitrary thread, and the collector's own stop-the-world signal is a separate handler that this does not touch.
signalWaitUntilIdle — which signal.Stop and signal.Reset call before returning — had the same problem from the other side. It spun on Gosched, which is a no-op with threads, so it burned a core until the receiver happened to drain the last signal. It now waits on a futex that signal_recv wakes when the pending set empties.
The cooperative path is unchanged. The two versions live in signal_cooperative.go and signal_threads.go, split on scheduler.threads the way the schedulers themselves are.
What works now, each verified as a stock-fails / this-release-passes pair on macOS arm64, linux/arm64 and linux/amd64:
signal.Notifydelivers SIGINT and SIGTERM, in about a millisecond.signal.NotifyContextcancels its context, and thestop()it returns actually returns.- Two signals in a row both arrive.
signal.Stopreturns promptly, delivery stops, and the default action is restored — a second signal terminates the process.signal.Resetrestores the default action.signal.Ignorekeeps the process alive with no reader.- A signal that was never notified still terminates the process (SIGHUP → 129).
- Delivery lands while every thread is churning locks and starting goroutines, and while the collector is under allocation pressure.
- The supervisor shape: a parent starts a child with
os/exec, signals it by pid, and the child shuts down and exits 0 while the parent'sWaitreturns.
signal.Ignored still does not link. The runtime has never implemented os/signal.signal_ignored, so a program that calls signal.Ignored fails at link time with linker could not find symbol os/signal.signal_ignored. This is pre-existing and unchanged — it is also why tinygo test os/signal cannot be added to the test suite; it fails to link identically against net.3 and against this build, before any test runs. The regression coverage for this fix is tests/sigprobe instead.
Carried over from net.3
SysProcAttr.SetpgidandPgidare honoured throughposix_spawnattr_setpgroup; every other field is refused by name (os: SysProcAttr.Setsid: sys setting not implemented) rather than the whole struct being rejected, and still unwraps toos.ErrNotImplementedSys.net/httpfollows redirects: up to 10 hops, 301/302/303 switching to GET, 307/308 preserving method and body throughGetBody, cross-hostAuthorization/Cookiestripping,Referer, intermediate bodies drained,CheckRedirectandErrUseLastResponsehonoured. Before net.3 a 302 was handed back with an empty body.sync.RWMutexno longer deadlocks. It counted lock-holding and queued readers in the same number and made both sides wait on predicates over it; two interleavings wedged it permanently, with no panic and no message. Reachable from ordinary code, becausesyscall.ForkLockis anRWMutexthatos.Piperead-locks andos.StartProcesswrite-locks. Not a fork-specific bug: it is upstream TinyGo'ssrc/sync/mutex.go, unchanged since 2024.- macOS:
fcntl's variadic third argument is passed correctly. It had been read from stack residue, sosyscall.CloseOnExecsetFD_CLOEXECor did not depending on the binary — and when it did not, every descriptor leaked into every child, which is exactly howcmd.Output()on a command withStdinset would hang forever.
Carried over from net.2
- Processes are started with
posix_spawn(3)on hosted linux (musl) and macOS (libSystem), not with a fork — these targets run the threads scheduler and collect with Boehm, so afork()from Go would hand the child a single thread holding whatever locks the other threads owned. ProcAttr.Files,.Dirand.Envall work, and a nilEnvinherits the parent's environment.Waitreaps withwait4and retries onEINTR;ProcessStateis real;Kill/Signalwork and mapESRCHtoos.ErrProcessDone.- The child gets an empty signal mask, which unlike a handler disposition would otherwise survive the
exec. - The darwin libSystem stub declares the
posix_spawnfamily, which the minimal macOS SDK omits.
Carried over from net.1
crypto/tlsis the real one on hosted linux and darwin; the no-op stub stays for baremetal, wasm and Windows.- The host netdev works on macOS, with
SO_NOSIGPIPEon every socket. - HTTPS on macOS has trust roots, read by
net/httpfrom$SSL_CERT_FILEor/etc/ssl/cert.pem. weak.runtime_makeStrongFromWeakis implemented, whichcrypto/tls's certificate cache needs to link.- The darwin libSystem stub declares the BSD socket API.
Targets without a process model or without signals — baremetal, wasm, Windows — keep exactly the previous stubs.
Verified
tests/sigprobe is new: eleven checks, each running the probe binary again as a child, waiting for it to print READY, signalling it by pid, and reading what the child made of it — the same shape a supervisor uses on a TinyGo-built CLI. Built with the ordinary Go toolchain it passes all eleven, which is what makes it usable as an oracle.
- macOS arm64, natively. Stock net.3: 9 of 11 checks failed, every delivery check reporting
NO SIGNALafter its full 8-second wait. This build: all 11 pass,notifyin under a millisecond. - linux/arm64 (container, native on the host). Same pair: 9 of 11 failed on net.3, all 11 pass here.
- linux/amd64 (emulated). Same pair: 9 of 11 failed on net.3, all 11 pass here.
- The two checks that passed on net.3 —
ignoreand an unregistered SIGHUP terminating the process — still pass, so nothing that used to work was traded away. - The reproducer this fix was filed against. A 40-line program that waits for SIGINT three ways prints
NO SIGNAL in 8sand exits 3 on both its modes against net.3. Against this build:got signal: interrupt/clean exit, and for theNotifyContextmodectx cancelled/stop() returned/clean exit— both exit 0. os,syncandnetpackage tests stay green on linux and macOS in CI, as do the compiler tests, for the commit this tag points at.
Known gaps, unchanged from net.3
-
http.Client.Timeoutis still not enforced. The port computes the deadline and uses it to decorate an error message, but nothing arms a timer or sets a deadline on the socket, so a request to a host that accepts a connection and then says nothing can block indefinitely. The upstream port's own PR for it is still open. -
A raw
tls.Dial(…, nil)still fails on macOS.crypto/x509's platform verifier is a stub on darwin, and a nilRootCAssends verification straight to it.net/httpsupplies roots for itself, sohttp.Clientoverhttps://is fine; code that dials TLS directly must pass its own pool:pem, _ := os.ReadFile("/etc/ssl/cert.pem") pool := x509.NewCertPool() pool.AppendCertsFromPEM(pem) conn, err := tls.Dial("tcp", host, &tls.Config{ServerName: name, RootCAs: pool})
Moving those roots down into
crypto/x509so baretls.Dialworks remains deferred: TinyGo's GOROOT merge is per-directory, so putting any file insrc/crypto/x509drops all of Go's, which means vendoring the whole package and pinning it to one Go version. -
A read deadline can stretch when a signal interrupts the read. The host netdev programs
SO_RCVTIMEOonce and then retriesreadonEINTRwithout re-checking the deadline, so each interruption restarts the full timeout rather than the remaining time. Writes re-arm the timeout every iteration and are bounded correctly. In practice the handler is installed withSA_RESTARTand the interruptions are rare, but a program that takes a steady stream of signals during a...
v0.42.0-net.3
A test build of the TinyGo 0.42 development tree with working host networking over real TLS and working process spawning on hosted linux and macOS. It exists so a downstream project can build and run a program whose release paths are all HTTPS and which shells out to the binaries it installs. It is not an official TinyGo release and it is not built from tinygo-org/tinygo.
This is v0.42.0-net.2 plus four fixes, three of which are deadlocks or silent data loss that a real CLI hit in its integration suite.
New in net.3
SysProcAttr.Setpgid works, and the rest is refused by name
StartProcess refused any non-nil ProcAttr.Sys outright, so every program that puts its children in their own process group — the ordinary way to run a script and still be able to signal the whole tree with kill(-pgid) — got sys setting not implemented instead of a process.
posix_spawn can express exactly that one request, through posix_spawnattr_setpgroup and POSIX_SPAWN_SETPGROUP, so Setpgid and Pgid are now honoured. Everything else is still refused, but the error now names the field that was set (os: SysProcAttr.Setsid: sys setting not implemented) rather than the struct that held it, and still unwraps to os.ErrNotImplementedSys. Linux declares a good deal more of SysProcAttr than Darwin does — Pdeathsig, Cloneflags, UidMappings, AmbientCaps, CgroupFD, PidFD and the rest — and every one of those is checked and named too. Nothing is silently ignored.
net/http follows redirects again
The port kept CheckRedirect but the redirect loop itself had been removed, so a 302 was handed straight back to the caller with an empty body. Downloading a GitHub release asset returned the redirect to release-assets.githubusercontent.com and zero bytes where Go returns 200 and the file.
Go's redirect handling is now ported into the client: up to 10 hops, 301/302/303 switching to GET without a body and 307/308 preserving method and body through GetBody, Location resolved against the current URL, Authorization/Cookie/WWW-Authenticate stripped when a hop crosses to a different host, Referer set, intermediate bodies drained and closed, and CheckRedirect honoured including ErrUseLastResponse.
sync.RWMutex could deadlock, silently
RWMutex counted readers that hold the lock and readers queued behind a waiting writer in the same number, and made both sides wait on predicates over it. Two interleavings wedge that permanently:
- A writer waited for the count to reach "no readers at all", but a reader arriving during that wait joined the same count, so the last holder's
RUnlockno longer saw the condition it was supposed to wake the writer on. - A reader released by
Unlockre-read the count rather than acquiring. A writer that arrived in between rebased it, sending the reader back to sleep after its wakeup had already been spent — while that writer waited for that same reader, having counted it when it arrived.
In both cases every party ends up parked with nobody left to wake anyone. No panic, no message, no exit.
This is reachable from ordinary code, because syscall.ForkLock is an RWMutex: os.Pipe read-locks it and os.StartProcess write-locks it, so a program that spawns processes and makes pipes at the same time could simply stop. The fix adopts the standard library's algorithm — a writer snapshots how many readers it found and waits only for those — and hands the lock over with counting semaphores rather than predicates, so a released waiter has genuinely acquired.
This bug is not new in this fork and it is not macOS-specific. It is upstream TinyGo's src/sync/mutex.go, unchanged since 2024, and the regression test added here fails on linux and macOS alike against a stock net.2 toolchain. What net.2 changed was the odds: it introduced the first two ForkLock call sites that a hosted binary hits on a hot path.
macOS: fcntl's third argument was garbage, so every descriptor leaked into every child
fcntl's third parameter is variadic, and on darwin/arm64 a variadic argument is passed on the stack rather than in a register. TinyGo called libc's fcntl through a plain three-argument function pointer, so the callee read that argument from whatever the stack happened to hold — the same problem open() has had a C wrapper for all along.
The symptom is quiet and looks intermittent: fcntl(fd, F_SETFD, FD_CLOEXEC) sets the flag or does not, depending on stack residue, which makes it deterministic for a given binary and different between binaries. syscall.CloseOnExec is the main caller, so when it failed every descriptor the program owned leaked into every process it started — and a child holding a duplicate of a pipe's write end keeps that pipe from ever reporting end of file, which is exactly how os/exec both collects a command's output and feeds it its stdin. cmd.Output() on a command with a Stdin set would hang forever.
Measured on macOS 26.6 arm64 before the fix: fcntl(fd, F_DUPFD, 100) returns EINVAL, and three F_SETFL calls with 0x4, 0x0 and 0x8 all leave F_GETFL reading 0x48. After it, F_DUPFD(100) returns 100 and each F_SETFL round-trips.
Note that net.2's release notes claimed os.Pipe on macOS set FD_CLOEXEC. It called the right function; the call just did not do anything.
Carried over from net.2
- Processes are started with
posix_spawn(3)on hosted linux (musl) and macOS (libSystem), not with a fork. These targets run the threads scheduler and collect with Boehm, so afork()from Go would hand the child a single thread holding whatever locks the other threads owned — malloc's among them — with the collector's stop-the-world signal free to land between the fork and the exec. ProcAttr.Files,.Dirand.Envall work, and a nilEnvinherits the parent's environment.Waitreaps withwait4and retries onEINTR;ProcessStateis real, carrying the pid and the actualsyscall.WaitStatus;Kill/Signalwork and mapESRCHtoos.ErrProcessDone.- The child gets an empty signal mask, which unlike a handler disposition would otherwise survive the
exec. - The darwin libSystem stub declares the
posix_spawnfamily, which the minimal macOS SDK omits.
Carried over from net.1
crypto/tlsis the real one on hosted linux and darwin; the no-op stub stays for baremetal, wasm and Windows.- The host netdev works on macOS, with
SO_NOSIGPIPEon every socket. - HTTPS on macOS has trust roots, read by
net/httpfrom$SSL_CERT_FILEor/etc/ssl/cert.pem. weak.runtime_makeStrongFromWeakis implemented, whichcrypto/tls's certificate cache needs to link.- The darwin libSystem stub declares the BSD socket API.
Targets without a process model — baremetal, wasm, Windows — keep exactly the previous stubs.
Verified
tests/spawnprobe grew four checks — a child with Setpgid leading its own process group and reachable by kill(-pgid), a non-zero Pgid joining an existing group, a plain spawn inheriting the parent's group, and a refused SysProcAttr field naming itself — for sixteen checks total.
- macOS arm64, natively. All sixteen pass. Before these fixes the same probe hung at the
stdincheck on 5 runs out of 5, and hung atconcurrenton every run once the descriptor leak was fixed but theRWMutexwas not. After: 20/20 full runs and 300 runs of the sixteen-concurrent-spawn check, with no hang. - linux/arm64. 200 full probe runs and the
os,syncandnetpackage tests, all green. - linux/amd64. The package tests plus 710 runs of the concurrent-spawn check.
- A separate stress program — six readers, three writers, three mutex holders, a channel pair and four piped
os/execspawns per iteration — ran 5 × 5000 iterations on macOS arm64 and 5 × 5000 on linux/arm64 without stalling. Against a stocksyncit wedges on macOS within the first iteration, on 5 attempts out of 5. - The macOS arm64 tarball attached below — not a patched toolchain, the artifact this release ships — runs all sixteen probe checks, passes
os,syncandnet(15 consecutive runs of each, no flakes), completes 3 × 5000 stress iterations, and downloads a GitHub release asset over its redirect:status=200 OK … bytes=180233452, which is the asset's exact size. - CI is green on Linux, macOS (arm64 and Intel) and Windows for the commit this tag points at.
The redirect work was checked against a local server for relative and absolute Location, cross-host header stripping, 303-after-POST becoming a GET, 307-after-POST preserving the body, ErrUseLastResponse, a bad Location, a 3xx with no Location, and loop detection at ten hops — sixteen cases, each producing output identical to real Go's — and against a real GitHub release asset, which now downloads 163,120,470 bytes with status 200 where net.2 returned status 302 and 0 bytes.
Two deterministic regression tests were added to the sync package's own suite; both fail against a stock net.2 toolchain on both linux and macOS and pass here.
One thing that is not fully explained
A low-rate hang survives on emulated linux/amd64 (x86-64 under Docker on Apple silicon): roughly one run in 300 of the concurrent-spawn check stalls indefinitely. It is not caused by anything in this release — a stock net.2 toolchain hangs at the same rate on the same setup (2 in 400, against 1 in 400 here). It has never been seen on native macOS arm64 or native linux/arm64.
When it was caught, /proc/<pid>/task/*/ showed fifteen of seventeen threads parked in FUTEX_LOCK_PI on distinct per-thread addresses — priority-inheritance futexes that neither TinyGo nor musl uses — and only two in ordinary FUTEX_WAIT on the fork's own words. That points at the emulation layer rather than at the toolchain, but it is not proven, and it is called out here rather...
v0.42.0-net.2
A test build of the TinyGo 0.42 development tree with working host networking over real TLS and working process spawning on hosted linux and macOS. It exists so a downstream project can build and run a program whose release paths are all HTTPS and which shells out to the binaries it installs. It is not an official TinyGo release and it is not built from tinygo-org/tinygo.
This is v0.42.0-net.1 plus a working os/exec.
New in net.2: os/exec starts processes
TinyGo's os package stubbed the process layer out. StartProcess rejected every ProcAttr carrying Dir, Sys or Files — and os/exec always passes three Files — so no command could ever be run; Wait, Kill and Signal returned operation not implemented, and ProcessState was an empty struct whose methods all reported failure. What did exist on linux, a bare fork() followed by execve() with no branch on the return value, was unsound anyway: parent and child both fell into the exec.
- Processes are started with
posix_spawn(3)on hosted linux (musl) and macOS (libSystem), not with a fork. These targets run the threads scheduler and collect with Boehm, so afork()from Go would hand the child a single thread holding whatever locks the other threads owned — malloc's among them — with the collector's stop-the-world signal free to land between the fork and the exec.posix_spawnmoves the clone and the exec inside libc, where no Go code runs in between. ProcAttr.Files,.Dirand.Envall work. Descriptors are laid out withdup2/closefile actions,Dirwithposix_spawn_file_actions_addchdir_np, and a nilEnvinherits the parent's environment, as in upstream Go.Waitreaps withwait4and retries onEINTR, which is routine here rather than exceptional: a thread parked inwait4is exactly the kind the collector has to interrupt.ProcessStateis real. It carries the pid and the actualsyscall.WaitStatus, soexec.ExitErrorreportsexit status N, andExitCode,Exited,Success,SysandStringall work. A killed child is reported as signalled.Kill/Signalwork, and mapESRCHtoos.ErrProcessDone— which is whatexec.CommandContextexpects when its context fires at the same moment the command finishes.- The child gets an empty signal mask. Unlike a handler disposition, a blocked mask survives an
exec, and the spawning thread may be carrying the collector's signal blocked. os.Pipeon macOS now setsFD_CLOEXECon both ends. macOS has nopipe2, so every pipe used to leak into every process started afterwards, and a child holding a duplicate of a write end keeps the pipe from ever reporting end of file — which is exactly howos/execcollects a command's output.- The darwin libSystem stub declares the
posix_spawnfamily, which the minimal macOS SDK omits.
Targets without a process model — baremetal, wasm, Windows — keep exactly the previous stubs.
Carried over from net.1
crypto/tlsis the real one. TinyGo replacescrypto/tlswith a stub whose handshake does nothing, sohttps://produced a plaintext connection wearing the TLS API. On hosted linux and darwin the standard library's owncrypto/tlsis used instead; the stub stays in place for baremetal, wasm and Windows targets.- The host netdev works on macOS.
src/net's raw-socket netdev was linux-only, so a darwin binary reportedNetdev not setfor every dial. It now builds on darwin too, withSO_NOSIGPIPEon every socket. - HTTPS on macOS has trust roots.
crypto/x509's macOS verifier is a stub in TinyGo, sonet/httpsupplies a root pool of its own, read from$SSL_CERT_FILEor from/etc/ssl/cert.pem. weak.runtime_makeStrongFromWeakis implemented, whichcrypto/tls's certificate cache needs to link.- The darwin libSystem stub declares the BSD socket API, which the minimal macOS SDK omits.
Verified
tests/spawnprobe — the process layer asked one piece at a time: output, combined stderr, exit status, cmd.Env, an inherited environment, cmd.Dir, stdin from a pipe, ExtraFiles as descriptor 3, no descriptor leak into an unrelated child, a context deadline killing sleep, a missing binary as os.ErrNotExist, and sixteen concurrent spawns. All twelve checks pass on linux/arm64 and linux/amd64, as does the os package's own test suite, including 200 spawns under continuous garbage collection.
tests/netprobe — TCP, TLS, plain HTTP, HTTPS and DNS — continues to report every layer working, unchanged from net.1.
macOS is verified by CI only. The os package tests, which now cover starting a process, collecting its exit status and honouring Dir and Files, run on the macOS job of this build. No darwin binary from this release has been exercised by hand; run tests/spawnprobe on a Mac before trusting it in anger.
Windows is unchanged and still gets the stubs. A Windows binary built with this toolchain compiles crypto/tls to the no-op handshake, so https:// there is plaintext wearing the TLS API, and os.StartProcess still returns operation not implemented. Only hosted linux and macOS were switched over.
Install
Each tinygo<version>.<os>-<arch>.tar.gz unpacks to a single tinygo/ directory holding bin/, lib/, src/ and targets/.
curl -L -O https://github.com/yohimik/tinygo/releases/download/v0.42.0-net.2/tinygo0.42.0-net.2.linux-amd64.tar.gz
tar xzf tinygo0.42.0-net.2.linux-amd64.tar.gz -C /usr/local/lib
export TINYGOROOT=/usr/local/lib/tinygo
export PATH=$PATH:$TINYGOROOT/bin
tinygo versionTINYGOROOT must point at that directory whenever bin/tinygo is not run from inside it.
A host Go toolchain is required. TinyGo runs go list and reads the standard library from $(go env GOROOT), so a Go toolchain must be installed on the machine that runs tinygo — including when cross-compiling. This build accepts Go 1.25 through 1.27; crypto/tls and os/exec themselves come from that toolchain's GOROOT, which is why the version matters more than usual here.
.deb packages are also attached; they install to /usr/local/lib/tinygo with a symlink at /usr/local/bin/tinygo.
Using it in a Dockerfile instead of the tinygo/tinygo image
Replace the toolchain, not the base image: start from golang:1.27, unpack the tarball, and point TINYGOROOT at it.
FROM golang:1.27
ARG TINYGO_VERSION=0.42.0-net.2
ARG TARGETARCH
RUN curl -fsSL -o /tmp/tinygo.tar.gz \
"https://github.com/yohimik/tinygo/releases/download/v${TINYGO_VERSION}/tinygo${TINYGO_VERSION}.linux-${TARGETARCH}.tar.gz" \
&& tar xzf /tmp/tinygo.tar.gz -C /usr/local/lib \
&& rm /tmp/tinygo.tar.gz
ENV TINYGOROOT=/usr/local/lib/tinygo
ENV PATH="/usr/local/lib/tinygo/bin:${PATH}"Cross-compiling to macOS works from that image with GOOS=darwin GOARCH=arm64 tinygo build ….
macOS caveats
-
A binary from this toolchain now needs macOS 10.15 or later.
posix_spawn_file_actions_addchdir_np, which carriescmd.Dirinto the child, arrived in 10.15, and the libSystem stub declares it unconditionally. The deployment target itself is unchanged (10.12 on amd64, 11.0 on arm64). -
A raw
tls.Dial(…, nil)still fails.crypto/x509's platform verifier is a stub on darwin, and a nilRootCAssends verification straight to it.net/httpsupplies roots for itself, sohttp.Clientoverhttps://is fine; code that dials TLS directly must pass its own pool:pem, _ := os.ReadFile("/etc/ssl/cert.pem") pool := x509.NewCertPool() pool.AppendCertsFromPEM(pem) conn, err := tls.Dial("tcp", host, &tls.Config{ServerName: name, RootCAs: pool})
-
SSL_CERT_FILEoverrides the bundle thatnet/httploads, for a container image or a host without/etc/ssl/cert.pem. -
The resolver is a stub resolver. It reads
/etc/hostsand thenameserverlines of/etc/resolv.conf, and queries them over UDP. It does not use the macOS system resolver, so scoped and split-horizon DNS, mDNS.localnames, and IPv6 nameservers are not supported. It returns one address per name, preferring A over AAAA.
Process-layer caveats, both platforms
ProcAttr.Sysis still rejected.syscall.SysProcAttrasks for thingsposix_spawncannot express —setsid, credentials, a controlling terminal, process groups — soStartProcessreturnssys setting not implementedrather than silently ignoring it.exec.Cmdonly sets it if you do.os.Process.Waitreaps withwait4on the pid directly, so a program that also reaps children itself, or that installs its ownSIGCHLDhandling, may race with it.os/signalis unchanged; this release did not touch signal delivery to the TinyGo process itself.
Source
- Compiler:
yohimik/tinygobranchrelease/net src/netsubmodule:yohimik/netbranchhost-netdev-darwin
Verifying downloads
GitHub records a SHA-256 digest for every asset below. To print them:
gh release view v0.42.0-net.2 --repo yohimik/tinygo --json assets --jq '.assets[] | "\(.digest) \(.name)"'
v0.42.0-net.1
A test build of the TinyGo 0.42 development tree with working host networking over real TLS on hosted linux and macOS. It exists so a downstream project can build and run a program whose release paths are all HTTPS. It is not an official TinyGo release and it is not built from tinygo-org/tinygo.
What is different from upstream dev
crypto/tlsis the real one. TinyGo replacescrypto/tlswith a stub whose handshake does nothing, sohttps://produced a plaintext connection wearing the TLS API. On hosted linux and darwin the standard library's owncrypto/tlsis used instead; the stub stays in place for baremetal, wasm and Windows targets.- The host netdev works on macOS.
src/net's raw-socket netdev was linux-only, so a darwin binary reportedNetdev not setfor every dial. It now builds on darwin too, withSO_NOSIGPIPEon every socket. - HTTPS on macOS has trust roots.
crypto/x509's macOS verifier is a stub in TinyGo, sonet/httpsupplies a root pool of its own, read from$SSL_CERT_FILEor from/etc/ssl/cert.pem. weak.runtime_makeStrongFromWeakis implemented, whichcrypto/tls's certificate cache needs to link.- The darwin libSystem stub declares the BSD socket API, which the minimal macOS SDK omits.
Verified
tests/netprobe — TCP, TLS, plain HTTP, HTTPS and DNS asked one layer at a time — reports every layer working on linux/amd64, linux/arm64, linux/arm, darwin/arm64 and darwin/amd64: TLS 1.3 handshakes against api.github.com with real certificate verification, 200 OK from https://api.github.com/, /etc/hosts honoured, and DNSError.IsNotFound set for a name that does not exist.
Windows is unchanged and still gets the stub. A Windows binary built with this toolchain compiles crypto/tls to the no-op handshake, so https:// there is plaintext wearing the TLS API, exactly as before. Only hosted linux and macOS were switched over.
Install
Each tinygo<version>.<os>-<arch>.tar.gz unpacks to a single tinygo/ directory holding bin/, lib/, src/ and targets/.
curl -L -O https://github.com/yohimik/tinygo/releases/download/v0.42.0-net.1/tinygo0.42.0-net.1.linux-amd64.tar.gz
tar xzf tinygo0.42.0-net.1.linux-amd64.tar.gz -C /usr/local/lib
export TINYGOROOT=/usr/local/lib/tinygo
export PATH=$PATH:$TINYGOROOT/bin
tinygo versionTINYGOROOT must point at that directory whenever bin/tinygo is not run from inside it.
A host Go toolchain is required. TinyGo runs go list and reads the standard library from $(go env GOROOT), so a Go toolchain must be installed on the machine that runs tinygo — including when cross-compiling. This build accepts Go 1.25 through 1.27 and was tested with all three; crypto/tls itself comes from that toolchain's GOROOT, which is why the version matters more than usual here.
.deb packages are also attached; they install to /usr/local/lib/tinygo with a symlink at /usr/local/bin/tinygo.
Using it in a Dockerfile instead of the tinygo/tinygo image
Replace the toolchain, not the base image: start from golang:1.27, unpack the tarball, and point TINYGOROOT at it.
FROM golang:1.27
ARG TINYGO_VERSION=0.42.0-net.1
ARG TARGETARCH
RUN curl -fsSL -o /tmp/tinygo.tar.gz \
"https://github.com/yohimik/tinygo/releases/download/v${TINYGO_VERSION}/tinygo${TINYGO_VERSION}.linux-${TARGETARCH}.tar.gz" \
&& tar xzf /tmp/tinygo.tar.gz -C /usr/local/lib \
&& rm /tmp/tinygo.tar.gz
ENV TINYGOROOT=/usr/local/lib/tinygo
ENV PATH="/usr/local/lib/tinygo/bin:${PATH}"Cross-compiling to macOS works from that image with GOOS=darwin GOARCH=arm64 tinygo build ….
macOS caveats
-
A raw
tls.Dial(…, nil)still fails.crypto/x509's platform verifier is a stub on darwin, and a nilRootCAssends verification straight to it.net/httpsupplies roots for itself, sohttp.Clientoverhttps://is fine; code that dials TLS directly must pass its own pool:pem, _ := os.ReadFile("/etc/ssl/cert.pem") pool := x509.NewCertPool() pool.AppendCertsFromPEM(pem) conn, err := tls.Dial("tcp", host, &tls.Config{ServerName: name, RootCAs: pool})
-
SSL_CERT_FILEoverrides the bundle thatnet/httploads, for a container image or a host without/etc/ssl/cert.pem. -
The resolver is a stub resolver. It reads
/etc/hostsand thenameserverlines of/etc/resolv.conf, and queries them over UDP. It does not use the macOS system resolver, so scoped and split-horizon DNS, mDNS.localnames, and IPv6 nameservers are not supported. It returns one address per name, preferring A over AAAA.
Source
- Compiler:
yohimik/tinygobranchrelease/net src/netsubmodule:yohimik/netbranchhost-netdev-darwin
Verifying downloads
GitHub records a SHA-256 digest for every asset below. To print them:
gh release view v0.42.0-net.1 --repo yohimik/tinygo --json assets --jq '.assets[] | "\(.digest) \(.name)"'