Skip to content

Releases: yohimik/tinygo

v0.43.0-net.2

v0.43.0-net.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 06 Sep 16:49

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 e7d34c8c126e0eecd2ce711915f2f88d112d833a only 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/cookiejar package in the loader. Keep the port's surrounding net packages.
  • Add ListenConfig with 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.WaitDelay does not reliably bound output collection when a descendant keeps a pipe open. Closing a descriptor does not interrupt all active blocking pipe I/O. See testdata/os-exec-waitdelay in this source.
  • Raw tls.Dial on 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.
  • SysProcAttr supports Setpgid and Pgid. Other unsupported fields are rejected. Programs that reap children independently can race with Process.Wait.
  • signal.Ignored remains 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...

Read more

v0.43.0-net.1

v0.43.0-net.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 02 Sep 14:55

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#5630tinygo-org#5636 and tinygo-org/net tinygo-org#72tinygo-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=threadssignal.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 waitForEventsthe 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.Notify delivers SIGINT and SIGTERM, in about a millisecond.
  • signal.NotifyContext cancels its context, and the stop() it returns actually returns.
  • Two signals in a row both arrive.
  • signal.Stop returns promptly, delivery stops, and the default action is restored — a second signal terminates the process.
  • signal.Reset restores the default action.
  • signal.Ignore keeps 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's Wait returns.

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.Setpgid and Pgid are honoured through posix_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 to os.ErrNotImplementedSys.
  • net/http follows redirects: up to 10 hops, 301/302/303 switching to GET, 307/308 preserving method and body through GetBody, cross-host Authorization/Cookie stripping, Referer, intermediate bodies drained, CheckRedirect and ErrUseLastResponse honoured. Before net.3 a 302 was handed back with an empty body.
  • sync.RWMutex no 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, because syscall.ForkLock is an RWMutex that os.Pipe read-locks and os.StartProcess write-locks. Not a fork-specific bug: it is upstream TinyGo's src/sync/mutex.go, unchanged since 2024.
  • macOS: fcntl's variadic third argument is passed correctly. It had been read from stack residue, so syscall.CloseOnExec set FD_CLOEXEC or did not depending on the binary — and when it did not, every descriptor leaked into every child, which is exactly how cmd.Output() on a command with Stdin set 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 a fork() from Go would hand the child a single thread holding whatever locks the other threads owned.
  • ProcAttr.Files, .Dir and .Env all work, and a nil Env inherits the parent's environment.
  • Wait reaps with wait4 and retries on EINTR; ProcessState is real; Kill/Signal work and map ESRCH to os.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_spawn family, which the minimal macOS SDK omits.

Carried over from net.1

  • crypto/tls is 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_NOSIGPIPE on every socket.
  • HTTPS on macOS has trust roots, read by net/http from $SSL_CERT_FILE or /etc/ssl/cert.pem.
  • weak.runtime_makeStrongFromWeak is implemented, which crypto/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 SIGNAL after its full 8-second wait. This build: all 11 pass, notify in 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 — ignore and 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 8s and exits 3 on both its modes against net.3. Against this build: got signal: interrupt / clean exit, and for the NotifyContext mode ctx cancelled / stop() returned / clean exit — both exit 0.
  • os, sync and net package 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 a golang:1.27 container (go1.27.1): spawnprobe 16/16, sigprobe 11/11, netprobe tcp/tls/https/dns all green (https status: 200 OK, NXDOMAIN reported as isNotFound=true).
  • tinygo0.43.0-net.1.darwin-arm64.tar.gz, natively on macOS arm64 (go1.26.7): the same — spawnprobe 16/16, sigprobe 11/11, netprobe all four layers green.

Known gaps, unchanged from net.3

  • http.Client.Timeout is still not enforced. The port computes the deadline and uses it to decorate an error mess...
Read more

v0.42.0-net.4

v0.42.0-net.4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Aug 10:09

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=threadssignal.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 waitForEventsthe 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.Notify delivers SIGINT and SIGTERM, in about a millisecond.
  • signal.NotifyContext cancels its context, and the stop() it returns actually returns.
  • Two signals in a row both arrive.
  • signal.Stop returns promptly, delivery stops, and the default action is restored — a second signal terminates the process.
  • signal.Reset restores the default action.
  • signal.Ignore keeps 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's Wait returns.

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.Setpgid and Pgid are honoured through posix_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 to os.ErrNotImplementedSys.
  • net/http follows redirects: up to 10 hops, 301/302/303 switching to GET, 307/308 preserving method and body through GetBody, cross-host Authorization/Cookie stripping, Referer, intermediate bodies drained, CheckRedirect and ErrUseLastResponse honoured. Before net.3 a 302 was handed back with an empty body.
  • sync.RWMutex no 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, because syscall.ForkLock is an RWMutex that os.Pipe read-locks and os.StartProcess write-locks. Not a fork-specific bug: it is upstream TinyGo's src/sync/mutex.go, unchanged since 2024.
  • macOS: fcntl's variadic third argument is passed correctly. It had been read from stack residue, so syscall.CloseOnExec set FD_CLOEXEC or did not depending on the binary — and when it did not, every descriptor leaked into every child, which is exactly how cmd.Output() on a command with Stdin set 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 a fork() from Go would hand the child a single thread holding whatever locks the other threads owned.
  • ProcAttr.Files, .Dir and .Env all work, and a nil Env inherits the parent's environment.
  • Wait reaps with wait4 and retries on EINTR; ProcessState is real; Kill/Signal work and map ESRCH to os.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_spawn family, which the minimal macOS SDK omits.

Carried over from net.1

  • crypto/tls is 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_NOSIGPIPE on every socket.
  • HTTPS on macOS has trust roots, read by net/http from $SSL_CERT_FILE or /etc/ssl/cert.pem.
  • weak.runtime_makeStrongFromWeak is implemented, which crypto/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 SIGNAL after its full 8-second wait. This build: all 11 pass, notify in 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 — ignore and 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 8s and exits 3 on both its modes against net.3. Against this build: got signal: interrupt / clean exit, and for the NotifyContext mode ctx cancelled / stop() returned / clean exit — both exit 0.
  • os, sync and net package 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.Timeout is 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 nil RootCAs sends verification straight to it. net/http supplies roots for itself, so http.Client over https:// 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/x509 so bare tls.Dial works remains deferred: TinyGo's GOROOT merge is per-directory, so putting any file in src/crypto/x509 drops 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_RCVTIMEO once and then retries read on EINTR without 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 with SA_RESTART and the interruptions are rare, but a program that takes a steady stream of signals during a...

Read more

v0.42.0-net.3

v0.42.0-net.3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Aug 07:32

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 RUnlock no longer saw the condition it was supposed to wake the writer on.
  • A reader released by Unlock re-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 a fork() 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, .Dir and .Env all work, and a nil Env inherits the parent's environment.
  • Wait reaps with wait4 and retries on EINTR; ProcessState is real, carrying the pid and the actual syscall.WaitStatus; Kill/Signal work and map ESRCH to os.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_spawn family, which the minimal macOS SDK omits.

Carried over from net.1

  • crypto/tls is 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_NOSIGPIPE on every socket.
  • HTTPS on macOS has trust roots, read by net/http from $SSL_CERT_FILE or /etc/ssl/cert.pem.
  • weak.runtime_makeStrongFromWeak is implemented, which crypto/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 stdin check on 5 runs out of 5, and hung at concurrent on every run once the descriptor leak was fixed but the RWMutex was 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, sync and net package 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/exec spawns per iteration — ran 5 × 5000 iterations on macOS arm64 and 5 × 5000 on linux/arm64 without stalling. Against a stock sync it 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, sync and net (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...

Read more

v0.42.0-net.2

v0.42.0-net.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Aug 03:26

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 a fork() 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_spawn moves the clone and the exec inside libc, where no Go code runs in between.
  • ProcAttr.Files, .Dir and .Env all work. Descriptors are laid out with dup2/close file actions, Dir with posix_spawn_file_actions_addchdir_np, and a nil Env inherits the parent's environment, as in upstream Go.
  • Wait reaps with wait4 and retries on EINTR, which is routine here rather than exceptional: a thread parked in wait4 is exactly the kind the collector has to interrupt.
  • ProcessState is real. It carries the pid and the actual syscall.WaitStatus, so exec.ExitError reports exit status N, and ExitCode, Exited, Success, Sys and String all work. A killed child is reported as signalled.
  • Kill/Signal work, and map ESRCH to os.ErrProcessDone — which is what exec.CommandContext expects 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.Pipe on macOS now sets FD_CLOEXEC on both ends. macOS has no pipe2, 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 how os/exec collects a command's output.
  • The darwin libSystem stub declares the posix_spawn family, 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/tls is the real one. TinyGo replaces crypto/tls with a stub whose handshake does nothing, so https:// produced a plaintext connection wearing the TLS API. On hosted linux and darwin the standard library's own crypto/tls is 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 reported Netdev not set for every dial. It now builds on darwin too, with SO_NOSIGPIPE on every socket.
  • HTTPS on macOS has trust roots. crypto/x509's macOS verifier is a stub in TinyGo, so net/http supplies a root pool of its own, read from $SSL_CERT_FILE or from /etc/ssl/cert.pem.
  • weak.runtime_makeStrongFromWeak is implemented, which crypto/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 version

TINYGOROOT 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 carries cmd.Dir into 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 nil RootCAs sends verification straight to it. net/http supplies roots for itself, so http.Client over https:// 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_FILE overrides the bundle that net/http loads, for a container image or a host without /etc/ssl/cert.pem.

  • The resolver is a stub resolver. It reads /etc/hosts and the nameserver lines of /etc/resolv.conf, and queries them over UDP. It does not use the macOS system resolver, so scoped and split-horizon DNS, mDNS .local names, and IPv6 nameservers are not supported. It returns one address per name, preferring A over AAAA.

Process-layer caveats, both platforms

  • ProcAttr.Sys is still rejected. syscall.SysProcAttr asks for things posix_spawn cannot express — setsid, credentials, a controlling terminal, process groups — so StartProcess returns sys setting not implemented rather than silently ignoring it. exec.Cmd only sets it if you do.
  • os.Process.Wait reaps with wait4 on the pid directly, so a program that also reaps children itself, or that installs its own SIGCHLD handling, may race with it.
  • os/signal is unchanged; this release did not touch signal delivery to the TinyGo process itself.

Source

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

v0.42.0-net.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Aug 22:02

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/tls is the real one. TinyGo replaces crypto/tls with a stub whose handshake does nothing, so https:// produced a plaintext connection wearing the TLS API. On hosted linux and darwin the standard library's own crypto/tls is 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 reported Netdev not set for every dial. It now builds on darwin too, with SO_NOSIGPIPE on every socket.
  • HTTPS on macOS has trust roots. crypto/x509's macOS verifier is a stub in TinyGo, so net/http supplies a root pool of its own, read from $SSL_CERT_FILE or from /etc/ssl/cert.pem.
  • weak.runtime_makeStrongFromWeak is implemented, which crypto/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 version

TINYGOROOT 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 nil RootCAs sends verification straight to it. net/http supplies roots for itself, so http.Client over https:// 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_FILE overrides the bundle that net/http loads, for a container image or a host without /etc/ssl/cert.pem.

  • The resolver is a stub resolver. It reads /etc/hosts and the nameserver lines of /etc/resolv.conf, and queries them over UDP. It does not use the macOS system resolver, so scoped and split-horizon DNS, mDNS .local names, and IPv6 nameservers are not supported. It returns one address per name, preferring A over AAAA.

Source

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)"'