Skip to content

v0.43.0-net.1

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 02 Sep 14:55
· 0 commits to dev since this release

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 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 slow read can see its deadline exceeded. Now that signals actually reach the program, this is worth knowing about; it was not reachable before.

  • os.Process.Wait reaps with wait4 on the pid directly, so a program that also reaps children itself, or installs its own SIGCHLD handling, may race with it.

  • ProcAttr.Sys supports only Setpgid and Pgid. Everything else needs Go code to run in the child between clone and exec, which posix_spawn does not offer.

  • The macOS resolver is a stub resolver. It reads /etc/hosts and the nameserver lines of /etc/resolv.conf and queries them over UDP. No system resolver, so no scoped or split-horizon DNS, no mDNS .local, no IPv6 nameservers. One address per name, preferring A over AAAA.

  • A binary from this toolchain needs macOS 10.15 or later (posix_spawn_file_actions_addchdir_np).

  • A low-rate hang survives on emulated linux/amd64 — x86-64 under Docker on Apple silicon — at roughly one run in 300 of the concurrent-spawn check. It is not caused by anything in this release: a stock net.2 toolchain hangs at the same rate on the same setup, and it has never been seen on native macOS arm64 or native linux/arm64. When caught, fifteen of seventeen threads were parked in FUTEX_LOCK_PI on distinct per-thread addresses — priority-inheritance futexes neither TinyGo nor musl uses — which points at the emulation layer. If you see a hang under this build, please capture a sample (macOS) or /proc/<pid>/task/*/{wchan,syscall} (linux) before killing it.

Two further pre-existing defects were inventoried while validating this release. Neither is new here, neither is caused by this fix, and neither is fixed by it:

  • -scheduler=tasks does not link on hosted linux or macOS. It fails with ld.lld: error: duplicate symbol: tinygo_task_exit, identically against net.3 and against this build, with a clean cache, on darwin/arm64, linux/arm64 and linux/amd64. The default -scheduler=threads is unaffected; this is only reachable by asking for the cooperative scheduler explicitly on a hosted target.
  • tinygo test os/signal has never linked, for the signal_ignored reason described above.

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.43.0-net.1/tinygo0.43.0-net.1.linux-amd64.tar.gz
tar xzf tinygo0.43.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; crypto/tls, os/exec and os/signal itself 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.43.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 ….

Source

Verifying downloads

GitHub records a SHA-256 digest for every asset below. To print them:

gh release view v0.43.0-net.1 --repo yohimik/tinygo --json assets --jq '.assets[] | "\(.digest)  \(.name)"'