v0.42.0-net.3
Pre-releaseA 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 than glossed over. If you see a hang under this build, please capture a sample (macOS) or /proc/<pid>/task/*/{wchan,syscall} (linux) before killing it.
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.3/tinygo0.42.0-net.3.linux-amd64.tar.gz
tar xzf tinygo0.42.0-net.3.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.3
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 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})
Moving those roots down into
crypto/x509so that baretls.Dialworks was considered for this release and 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. -
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.Sysstill only supportsSetpgidandPgid. Everything elsesyscall.SysProcAttrcan ask for needs Go code to run in the child between the clone and the exec, which is precisely whatposix_spawndoes not offer, so those fields are refused by name rather than ignored.os.Process.Waitreaps withwait4on the pid directly, so a program that also reaps children itself, or that installs its ownSIGCHLDhandling, may race with it.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. This is unchanged from net.2 and net.1; the upstream port's own PR for it is still open.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.3 --repo yohimik/tinygo --json assets --jq '.assets[] | "\(.digest) \(.name)"'