Skip to content

linux: Go's own standard-library tests as the oracle for Linux guests - #588

Open
eKisNonos wants to merge 11 commits into
linux/go-guestsfrom
linux/go-std-suite
Open

eKisNonos wants to merge 11 commits into
linux/go-guestsfrom
linux/go-std-suite

Conversation

@eKisNonos

@eKisNonos eKisNonos commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What this does

Go's standard library ships thousands of tests, and most of them check how Linux behaves: files, pipes, timers, signals, sockets, processes. This branch runs those tests inside NØNOS as Linux guests and compares every result with the same test binary run on an ordinary Linux machine. Where the two disagree, NØNOS is wrong until shown otherwise.

It also fixes four things in the Linux personality that the tests turned up, and it lists everything else they found, with the test that shows it.

This branch builds on linux/go-guests (#582) and should merge after it. Its own work is the 11 commits and 10 files listed below.

The commits

  • f57a39231 Go's test binaries can be enrolled as guests. NONOS_LINUX_GO_SUITE=1 turns it on; the normal build does not change.
  • 861d2000e The small start program, gostd, can set environment variables such as GODEBUG for the test it starts.
  • c73bd4469 A package's test files go into the image with it, so each test runs from its own directory, as go test runs it.
  • e006fb8c8 gostd is rewritten in C. As a Go program it received a signal of its own before the test began and hung there.
  • 87c953f36 All of a package's source files go into the image when they fit. Some tests read them.
  • c948c98a9 The personality's heap grows from 16 to 40 MiB. Starting a 4.9 MB program ran it out of memory and took down every guest it was hosting.
  • 65d9c3fd2 The first program's bytes are freed once it is running. They were kept for the life of the personality.
  • 51f0dc8b6 Console output longer than 256 bytes now reaches the log. It was silently dropped, while the program was told it had been written.
  • f8c4711f0 A boot file longer than 1024 bytes is refused with a message. It used to be cut short without a word.
  • fc6e26ba3 A 12 MB test program, execbig, that runs itself a second time. It proves the fix in 65d9c3fd2.
  • f5b957b46 The suite's build rules move into their own file, GoSuite.mk, so the shared guest list grows by 3 lines instead of 37.

Results

Each test binary ran on the host from its package directory, with -test.v -test.short. On NØNOS it ran on one virtual CPU under QEMU's software emulation, with GODEBUG=asyncpreemptoff=1 (the reason is the first finding below). When a test hung or brought the whole program down, it was skipped and the package was run again, until every test had been reached. Counts include subtests.

package host passes NØNOS passes fails hung or crashed skipped not reached
sync 56 55 1 0 0 0
sync/atomic 107 102 0 1 5 0
time 418 411 1 5 7 0
context 65 64 1 0 0 0
runtime 955 677 16 62 134 101
os 735 359 353 1 40 2
io 79 75 4 0 0 0
io/fs 18 18 0 0 0 0
bufio 89 89 0 0 0 0
path/filepath 60 48 12 0 4 0
syscall 49 17 22 3 14 0
bytes 155 155 0 0 0 0
strings 125 125 0 0 0 0
strconv 96 96 0 0 1 0
sort 81 81 0 0 1 0
math 111 111 0 0 0 0
regexp 75 75 0 0 1 0
unicode/utf8 31 31 0 0 0 0
encoding/json 575 574 1 0 0 0
encoding/binary 156 156 0 0 0 0
encoding/base64 23 23 0 0 0 0
compress/gzip 23 22 1 0 1 0
archive/tar 100 99 1 0 0 0
hash/crc32 11 11 0 0 0 0
crypto/sha256 45 49 0 0 0 0
crypto/aes 117 117 0 0 0 0
crypto/ed25519 9 9 0 0 1 0
os/signal 31 12 13 2 2 2
os/exec 87 21 54 0 4 9
net 442 220 193 0 124 12
net/http 1315 114 25 20 3 1224
text/template 56 55 0 1 2 0
flag 29 28 1 0 0 0
log 11 11 0 0 0 0
fmt 79 79 0 0 1 0
all 35 6414 4189 699 95 345 1350

Sixteen packages match the host exactly: bufio, bytes, strings, strconv, sort, math, regexp, unicode/utf8, io/fs, encoding/binary, encoding/base64, hash/crc32, crypto/aes, crypto/ed25519, log and fmt.

crypto/sha256 passes four more tests on NØNOS than on the host, because the emulated CPU has instructions this host's CPU lacks, so four tests that skip on the host can run.

runtime and net/http are not fully reached; the reasons are under "Found, not fixed here".

How each fix was proven

Each fix was taken out again, the image rebuilt, and the same program booted. Every time, the old failure came back.

fix without it with it
heap Go's runtime tests (13.5 MB) never start: "memory allocation of 8388608 bytes failed", and the personality exits they run
freeing the first program execbig prints its first line, then "memory allocation of 16777216 bytes failed" both lines print and it exits 0, as on Linux
console context's TestCause and its 20 subtests never appear in the log all 65 context tests are reported
boot file a 1945-byte boot file is cut, and the test program fails on a broken argument the personality refuses the file and says why

Before these fixes, the first sync run reached none of its 56 tests.

Found, not fixed here

These belong to other parts of the Linux personality. Each item names a test that shows it.

Signals and processes

  • sigaltstack is accepted but ignored, so a signal handler always runs on the stack of the code it interrupted. Go asks for a separate signal stack. When a signal lands on a goroutine's stack instead, Go's handler waits forever. This is why every package hung at random points until preemption signals were turned off. Seen in time TestSleep and os/signal TestSignalTrace.
  • A crash in guest code, such as a nil pointer, ends the whole program. Linux sends it to the program's own handler, and Go turns it into a panic the test can recover. Seen in time TestIssue5745 and sync/atomic TestNilDeref.
  • A child process's exit status reads -1 instead of the code it exited with. Seen in flag TestExitCode.
  • pidfd_open and waitid are not implemented.

Sockets

  • setsockopt and getsockopt are not implemented. Go sets options on every socket it opens, so every Go server fails to listen.
  • Opening an IPv4 TCP socket sometimes fails with an I/O error, and IPv6 is not supported at all. Go's test web servers cannot start, which is why net/http stops early.
  • Unix datagram sockets and socketpair are missing.

Files and system

  • /proc/self/exe does not exist. Go uses it to find its own program, so most os/exec tests fail.
  • /dev/null, /dev/zero, /etc/group, /etc/passwd and /etc/hosts are missing.
  • Calls that take a directory handle, which Go's os.Root uses, are not implemented.
  • Symbolic links: lstat reports what a link points to instead of the link itself, and creating a file over a dangling link succeeds where Linux refuses.
  • A file opened for reading and writing cannot be read back after writing to it.
  • copy_file_range, pwrite64 and truncate are not implemented, and file timestamps cannot be set.
  • Writes in append mode land at the wrong place.

Not a fault of NØNOS

  • A few tests are slow under emulation and time out. time TestStopResult passes when given an hour.
  • Tests that build Go code skip or fail, because the guest has no Go toolchain.
  • Five tests skip because the guest has one CPU, which is by design.

Build

  • The image's signing step does not notice when only the list of guests changes. After a build without the suite, turning the suite on kept the old signatures, and every test program was refused.

Checks

  • The existing guests still pass: gohello, goconc, cthreads, gopoll and cwait pass, and threadfault reports its expected crash.
  • No kernel source changed, and the build warnings are the same as before.
  • The repository's gate scripts report nothing new.
  • Every commit builds on its own.

Running it

export NONOS_LINUX_GUESTS=1 NONOS_DEV=1 NONOS_LINUX_GO_SUITE=1
export NONOS_LINUX_GO_SUITE_PKGS="sync time os"
make nonos-mk-desktop-gui-prod && make nonos-mk-esp
make NONOS_LINUX_GO_SUITE_STORE=time LINUX_GUEST_BOOT_ARGS=boot-time target/qemu-virtio-blk.img.store.stamp

boot-time holds one argument per line: /bin/gostd, the package directory, GODEBUG=asyncpreemptoff=1, /bin/gstime, then the test flags. Boot the image and read the --- PASS, --- FAIL and --- SKIP lines from the serial log.

Go's own test suites check thousands of Linux behaviours, but nothing could
run them inside NONOS: go test runs each test binary from its package's
directory, beside testdata/, and the boot guest always starts at /.

NONOS_LINUX_GO_SUITE=1 now enrols the test binaries that `go test -c` makes
for the packages NONOS_LINUX_GO_SUITE_PKGS names, as guests gs<package>,
ids 5042 upward in list order. gostd (5040) changes to the directory named
first and becomes the program named second. A suite store holds gostd, the
packages NONOS_LINUX_GO_SUITE_STORE names, their testdata/ at the paths
they have on the build host, and Go's zone database, and nothing else,
since one test binary is 4 to 15 MB against the store's 16 MiB. The
default build does not change.
A Go test that hung inside the guest could not be looked into: the boot
guest's environment is fixed, so GODEBUG and GOTRACEBACK could not be set.

gostd now adds NAME=value words that come between the directory and the
program to the program's environment, as env(1) does. A run can ask Go's
scheduler to trace itself, or turn a Go setting on or off, from the boot
file alone.
Go runs each test binary in its package's directory, and some tests read
their own sources there: sync's ExampleOnceValues reads example_test.go.
The suite image carried only testdata/, so from / that example read
nothing and its result never came back, and a package without testdata/
had no directory for gostd to change into.

A suite image now also carries each package's *_test.go files at their
build-host paths, so every package runs from its own directory, as go test
runs it. NONOS_LINUX_GO_SUITE_SOURCES=0 leaves them out for runtime, whose
testdata/ alone nearly fills the store's 128 entries.
gostd was a Go program, and the Go runtime starts its own threads and asks
for SIGURG to preempt them. A Go test run with async preemption turned off
still hung before its first test: the SIGURG went to gostd's own handler,
before it reached the test, and a Go setting given to the test cannot reach
the program that starts it.

gostd is now static C: it changes directory, adds the NAME=value words to
the environment and execs the test, with no runtime, no threads and no
signal handlers. time now reaches its tests through it.
A test may read any file of its package, not only its own: io/fs TestGlob
globs for glob.go and path/filepath TestGlob for match.go. The image
carried only *_test.go, so both failed on NONOS and passed on the host.

Every .go file of the package is now packed when they fit the store's 128
entries, and only *_test.go otherwise (os and syscall, with 153 and 298
files). io/fs now passes all 18 of the tests the host passes.
A run read each program it starts or execs whole into one buffer that
doubles as it fills, inside a fixed 16 MiB heap. A Go test binary of
4.9 MB was enough: its execve asked for an 8 MiB buffer, the allocation
failed, and the personality ended with every guest it hosted
("memory allocation of 8388608 bytes failed", exit 134).

A run now takes a 40 MiB heap: the 24 MiB peak of reading the largest
program the kernel verifies (16 MiB), on top of the 16 MiB every run had.
Go's runtime (13.5 MB) and encoding/json (11.9 MB) test binaries now load.
The heap is committed when it is made, so each Linux run holds 40 MiB.
The program a run starts was held in the heap for the whole life of the
personality, though nothing reads it after it is mapped into the guest.
Every execve that followed had that much less room to read its own
program into.

start() now takes the launch rather than borrowing it, and its bytes are
freed when the program is running.
The kernel's debug channel refuses a line over 256 bytes whole, and the
console ignored that and told the guest every byte was written. Longer
writes vanished: Go prints a parallel test's results in one write, so
context's TestCause and its 20 subtests never reached the log.

The console now forwards in pieces of at most 256 bytes and returns the
count it carried, a short write if the log refuses part way. context now
reports all 65 of its tests.
The boot guest's file was read with a 1024-byte limit, and the store cuts
a longer file short without saying so. A long argument lost its end, and
the program ran with an argument nobody gave it: a -test.skip pattern
lost its closing parenthesis and the Go test binary exited at once.

A file over 1024 bytes is now refused with a line that says why.
Nothing showed whether the personality let a program's bytes go once it
was running: the Go test binaries that exec themselves open /dev/null
first, and fail there before any second image is read.

execbig is a static C program with 12 MiB of initialised data that execs
itself once and prints a line from each image. With the first image's
bytes released it prints both and ends with status 0, as on Linux; with
them held, the personality fails to allocate 16 MiB for the second read.
NONOS_LINUX_GO_SUITE_EXECBIG=1 enrols it on the id pair after the
packages, and the word execbig in NONOS_LINUX_GO_SUITE_STORE packs it.
The suite's block had grown Guests.mk, the registry every lane adds to, by
37 lines, past the size a file here is kept to.

It now lives in GoSuite.mk, which Guests.mk includes at the same place;
nothing it builds or packs changes. A suite store for io/fs still holds 27
entries and io/fs still passes all 18 of the tests the host passes.
@eKisNonos
eKisNonos changed the base branch from main to linux/go-guests September 29, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant