Conversation
The file table held every call that names a file or a descriptor in one match of 77 lines, and the calls this lane adds would lengthen it further. Now stat and its forms, access, statfs, statx and readlink are answered from serve/table_meta.rs, which the file table falls back to when it has no arm. The table is the same; nothing is answered differently.
Nothing checked that a real Linux tool's output under the personality matches Linux's. bbsuite is a busybox sh script of 28 sections that runs more than forty applets: file and directory work, text tools, archives, pipes, redirects, subshells, background jobs, traps, ps, df and /dev. The same busybox runs it on the host at build time, through the same applet links and with the guest's environment and nothing of the host's own tools on the path; that output is the oracle. On NONOS the script compares its own output with the oracle's, less the lines that start with @ (they name a time or a pid), and prints PASS or FAIL with the number of lines that differ. /etc/passwd and /etc/group name root, as an Alpine tree does, so ls and ps print owners by name on both sides.
MkProcStat showed a caller its own row in full and every other row with its counters zeroed, unless it held AttestRead or ProcessControl. The Linux personality answers getrusage, times and /proc for the guests it hosts, and could read none of their ticks, faults or resident memory: every such figure would have had to read as zero. A supervisor now sees the full row of each process registered as its foreign guest, and still sees nothing more of anyone else's. The kernel reports; the personality decides what a guest is told.
A written file lived in the descriptor that wrote it: its bytes in the descriptor's own buffer, flushed whole at close, and its offset in the descriptor alone. A dup or a fork copied the descriptor without either, so the copy read EBADF and a write through it replaced the file at close with only what it wrote. A descriptor opened O_APPEND wrote at offset 0, and a write-only open that did not truncate lost the file's old bytes. A shell's own output and the output of the commands it forks overwrote one another in a redirected file: busybox sh left 3 of 148 lines. Now the family keeps one copy of each file it writes, by path (held/cache/): every descriptor on it, in every process, reads and writes that copy, as Linux's page cache gives them, and stat's size is its size. Each open makes an open file description (held/desc/), which a dup and a fork share: its number, O_APPEND, whether it reads, and its offset, so a child's write moves the parent's offset. A dup or fork's descriptor opens its own stream from the store when it reads. The copy goes to the store at close and fsync. A write open of the read-only tree is EROFS at open, a directory opened to write is EISDIR, O_EXCL is honoured, and fdatasync is fsync; a pipe or a socket cannot be synced (EINVAL).
F_DUPFD answered ENOSYS, because a duplicate needed a second handle on the store, and F_DUPFD_CLOEXEC was EINVAL. busybox sh moves the file it reads a script from, and the descriptors it saves around a builtin's redirection, above 10 with F_DUPFD_CLOEXEC, so it died on its first line: "sh: 3: Function not implemented". A file's description is the family's now and a duplicate reads its own stream when it needs one, so both commands make a second descriptor on the same open file at the lowest free number at or above the one asked for, EINVAL past the descriptor table and EMFILE when it is full. This is two arms in call/ctl.rs, a file of lane A's; the work is in file/calls/fdup.rs.
stat reported every file 0644 and every directory 0755 whatever chmod had said, one link, no times, and uid and gid 0 only by accident of a zeroed buffer; lstat followed links, so ls -l showed no link as a link; newfstatat refused a directory descriptor (ENOSYS) and ignored AT_SYMLINK_NOFOLLOW and AT_EMPTY_PATH; statx gave a third answer; access never said EROFS or EACCES; utimensat refused any time but "now for neither"; a listing had no "." or ".."; unlink could not remove a symbolic link; and a file the family had made but not yet written out did not exist to stat, ls or rmdir. Now one function answers for a path or a descriptor (meta/node/), and stat, lstat, fstat, fstatat and statx all fill from it. The store keeps no modes and one time, so the family keeps the rest for its own files: the mode each was made with less the umask, or chmod's (held/modes.rs), and the times utimensat, utimes and utime set, which stand until the next write (held/times.rs). The shared tree is read-only (EROFS for chmod, rename, unlink, mkdir, utimensat and access W_OK). A pipe or the console fstats as a FIFO, a socket as a socket. Directory descriptors keep their path across a chdir, a dirfd's high 32 bits are ignored as Linux ignores them, mkdir takes its mode, open takes O_CREAT's mode and O_NOFOLLOW, and rename and unlink carry the family's copy, mode and times along.
A guest found no /dev, /proc or /sys. busybox ps listed nothing, df found no mounts, Go read /sys for its huge-page size and /proc/self/exe for os.Executable and got ENOENT, and a shell's 2>/dev/null had no file. Now the personality makes the three trees at every read. /dev has null, zero, full, random, urandom and tty with Linux's numbers and behaviour (full is ENOSPC, tty ENXIO with no terminal), and the stdin, stdout, stderr and fd links into /proc/self/fd. /proc has a directory for each process of the family and none other, under the number the family's pid namespace gives it: exe, cwd, root, fd, fdinfo, maps, status, stat, statm, cmdline, environ, comm, limits, mounts, mountinfo, cgroup and task/<tid>; mem is listed and refused at open. The system-wide files answer from one table of what NONOS declares (file/system/declared/, also now uname's source) and from the kernel's counts for the family's own threads (file/system/cpu/); nothing of the machine, another capsule or another family is in any of them. /sys has the one file Go reads. Only the family knows its processes and their numbers, so it lends /proc a view of itself for each call that may read /proc and takes it back after (serve/family_view/): two lines in Family::answer, a file of lane A's. Paths follow /proc/self, /dev/fd, /proc/<pid>/cwd, root and exe as links, never out of the family's root. statfs and st_dev come from the family's mount table, and getppid now names a forked child's parent, as /proc/<pid>/stat does.
pwrite64, preadv, pwritev, preadv2, pwritev2, sendfile, copy_file_range, truncate, fallocate, fadvise64, close_range, openat2, sync, syncfs, creat and the twelve xattr calls answered ENOSYS, and ftruncate refused a regular file. busybox cat copies with sendfile, Go's io.Copy between files uses copy_file_range, and os.Truncate and the xattr calls are os and x/sys routines. Each now answers as Linux does. The positional and vector forms read and write at an offset without moving the descriptor's (ESPIPE on a pipe), and the v2 forms take -1 for the descriptor's own offset and flags 0. sendfile copies a file to a file or to the console, and answers EINVAL for a pipe or a socket, as Linux does for an output it cannot splice into, so its callers fall back to read and write. copy_file_range copies between two files. truncate and ftruncate resize the family's copy. fallocate is tmpfs's, since the family's writable directories are its tmpfs mounts: mode 0 grows, KEEP_SIZE keeps, PUNCH_HOLE with KEEP_SIZE zeroes, and the rest is EOPNOTSUPP. close_range closes or marks a range close-on-exec. openat2 honours RESOLVE_BENEATH, NO_XDEV, NO_SYMLINKS and NO_MAGICLINKS, answers CACHED with EAGAIN as Linux may, and refuses IN_ROOT with EINVAL, as a kernel refuses a flag it does not know. sync and syncfs put every file the family is writing into the store. The family keeps its files' extended attributes as tmpfs keeps them, with Linux's names, flags and errors; the read-only tree has none (EROFS).
inotify_init, inotify_init1, inotify_add_watch and inotify_rm_watch answered as unserved numbers. None of the programs this lane proves asks for them: the host's strace of busybox running bbsuite, of Go's os, io/fs and path/filepath test suites, and of the regression guests shows no inotify call, and Go's runtime makes none. The store sends no change events a watch could be built on. So each is refused on purpose, with its reason on the console and ENOSYS, which a program that can do without it (tail -f, a file watcher's polling fallback) takes as "not here" and goes on.
flock answered ENOSYS and fcntl's F_GETLK, F_SETLK, F_SETLKW and their OFD forms EINVAL, so Go's syscall.Flock and any program that locks a file failed. The family keeps one lock table (file/locks/lock/) with Linux's three kinds. A flock lock belongs to the open file description, so a dup or a fork shares it; it goes when the last descriptor on the description closes, anywhere in the family. A POSIX record lock belongs to the process and goes when it closes any descriptor on the file, or exits. An OFD lock is a record lock owned by a description. flock locks never meet record locks; POSIX and OFD locks meet each other, by overlapping byte ranges, a process's own POSIX locks replace and split each other, and F_GETLK reports the lock in the way with its owner's pid as the guest knows it. LOCK_NB and F_SETLK answer EAGAIN; flock without LOCK_NB and F_SETLKW wait, parked like a read on an empty pipe and tried again after every call, so the close, unlock or exit that frees the lock lets them in. The waiting route is one arm in serve/dispatch.rs, a file of lane A's, and one in waits::attempt; the fcntl commands are new arms in call/ctl.rs.
sysinfo, getrusage, times, getgroups, setgroups, setresuid, setresgid, getpriority, setpriority and personality answered ENOSYS, and getresuid and getresgid as unserved numbers although their constants were there. busybox free and uptime read sysinfo, time and Go's os.ProcessState read getrusage, and every shell asks for its groups. sysinfo answers from the declared system: the family's uptime, its memory limit as totalram and that less the family's resident memory as freeram, its thread count, and zero where the personality keeps no such thing (shared, buffer and swap memory, and the load, which is not measured). getrusage and times report what the kernel counted for the process's own threads, the calling thread's for RUSAGE_THREAD, and for RUSAGE_CHILDREN what each child used, recorded when it exits and counted once its parent has waited for it, as Linux counts it; a figure that is not measured is zero. A process's exit now also drops its POSIX locks at once and puts every file the family is writing in the store. Every id is root's and no guest holds a capability, so the rules are those of a process without CAP_SETUID, CAP_SETGID or CAP_SYS_NICE: the ids can be set only to 0, groups not at all (EPERM), and nice only raised (EACCES); the nice value is kept and shown in /proc, not acted on. personality answers PER_LINUX to a query and refuses any change by name.
The store loads at most 16 MiB and each guest's proof is 327526 bytes, so the test image had room for about the guests it already had: adding three more made nonos-store-pack refuse it, 19031316 payload bytes against 16777216, and every lane adding guests will meet the same wall. LINUX_GUEST_ONLY now names the programs under /linux/bin an image carries; every other entry stays. Unset, the image is what it was. The guests left out are still built, signed and enrolled.
On a failure bbsuite ran diff and counted the lines starting with < or >, but busybox's diff prints the unified form, so it counted 0 of 107 differing lines, and only the diff's first line reached the console. It now counts the unified form's lines and prints every line the run made, each prefixed "[C] bbsuite got: ", so one boot's console holds what is needed to diff it against the host's on the host.
Nothing proved the file calls beyond read and write, the locks, or the system-information calls. cfiles runs fourteen parts, each printing only what must read the same on Linux: pwrite64; preadv, pwritev and the v2 forms; sendfile; copy_file_range; truncate and ftruncate; fallocate as tmpfs serves it, and fadvise64; flock across a dup, another open and a forked child that waits; fcntl record locks by range, with a waiting F_SETLKW, F_GETLK's pid, release at close and at exit, and OFD locks; close_range; openat2's RESOLVE_ flags; sync, syncfs, fsync and fdatasync; the xattr calls; sysinfo, and getrusage and times against a busy loop and a waited child; the ids, groups, nice and personality. Every part runs, and a failing one names its line and the numbers seen. sh/oracle.sh runs a program as the personality runs it: root with no capability, in its own pid namespace with its own /proc, a tmpfs at /tmp, no terminal, and the guest's environment. On the host it prints "[C] cfiles PASS: 14 parts".
Nothing proved what a program finds in /dev, /proc and /sys, or that it finds nothing that is not its own. cproc runs seven parts. dev: each device's type, numbers and mode, and its reads and writes, /dev/tty's ENXIO with no terminal, and the /dev/stdin and /dev/fd links. self: its own stat, status, cmdline, environ, comm, exe, cwd, fd and fdinfo against what the calls say. maps: its stack in maps, statm, limits against getrlimit, mounts, mountinfo, cgroup, and a second thread under task/. system: meminfo against sysinfo, cpuinfo against the CPUs it may run on, uptime, loadavg, version and /proc/sys/kernel against uname, a boot id that holds and a uuid that does not, /sys's huge-page size. isolation: of pids 1 to 4096 only its own family's exist in /proc, a forked child's /proc/<pid>/stat and getppid both name its parent, and no way through /proc/self/root, a link or ".." climbs above the root, and a directory descriptor keeps its place across a chdir. mem: NONOS refuses /proc/self/mem, which Linux opens for the process itself. facts: no file names the machine's CPU brand, which the program reads with CPUID, nor the build host's CPU model, boot id or name, which the build puts in /etc/cproc-host. On the host, through sh/oracle.sh, it prints "[C] cproc PASS: 7 parts"; there mem opens and facts finds the host's CPU in /proc/cpuinfo, as each part says it expects of Linux.
Nothing proved that Go's file packages work under the personality. goos makes a tree with MkdirAll and reads it back with ReadDir, WalkDir and Glob; makes a file and a directory with CreateTemp and MkdirTemp and removes them; copies a file with io.Copy, which Go does with copy_file_range; and checks Chmod, Chtimes, Truncate, Rename, Symlink, Readlink and Lstat, Executable, Hostname against uname, NumCPU against the CPUs /proc/cpuinfo lists, Getwd, and an exclusive syscall.Flock refusing a second open until it is released. Nothing it prints depends on the machine. On the host, through sh/oracle.sh, it prints "[GO] goos PASS: 5 parts".
A path was normalised as text before any link in it was followed, so a
".." removed the name before it whether or not that name was a link.
cproc caught it: stat("/proc/self/root/..") answered /proc/<pid>, where
Linux, which follows root to / first, answers /. A link /tmp/x to /etc
made /tmp/x/.. /tmp, where Linux makes it /. No path left the family's
root either way, but the answers were not Linux's.
Now walk::walk takes a path a name at a time: it follows each link
where it stands, the image's and the ones /dev and /proc make, then goes
on from where the link led, and ".." goes to the parent of that; ".." at
the root stays there. A trailing slash follows the last name, and 40
links in one walk is where Linux stops. named_at and chdir join a name
to its directory, an absolute name standing as it is, and leave all of
this to the walk. openat2 checks its
RESOLVE_ rules against the same walk, step by step, so BENEATH now also
refuses a step out through a link, and an absolute or magic link, as
Linux does.
openat2 refused RESOLVE_IN_ROOT with EINVAL, so a program that opens a name as if the directory were its root, as container tools do, could not open anything that way. The path walk now takes a root. An absolute name or link target starts from it and ".." never climbs above it, and openat2 walks an IN_ROOT name that way from the directory descriptor. As on Linux, IN_ROOT follows no /proc magic link, and IN_ROOT with RESOLVE_BENEATH is EINVAL. Every other walk passes "/" as the root and walks as before.
preadv2 and pwritev2 answered EOPNOTSUPP for any RWF_ flag, so a program that asked for RWF_APPEND or RWF_DSYNC, as databases and log writers do, failed where Linux serves it. Now the flags Linux 6.1 knows are served. RWF_APPEND writes at the end of the file whatever the offset, and at offset -1 moves the descriptor's offset to the end and past what it wrote. RWF_DSYNC and RWF_SYNC put the write in the store before answering, as fsync would. RWF_HIPRI and RWF_NOWAIT hold as they are, since a read or write here never blocks. Any other flag is still EOPNOTSUPP.
cfiles did not ask for preadv2 and pwritev2's RWF_ flags or for openat2's RESOLVE_IN_ROOT, so nothing showed whether either behaves as on Linux. Its vector part now writes with RWF_APPEND|RWF_DSYNC at offset 0 and with RWF_APPEND at -1, checks where the bytes and the offset land, and checks that an unknown flag is EOPNOTSUPP. Its openat2 part opens "/f", "../../../f" and an absolute link under IN_ROOT, all of which land inside the directory, and checks that IN_ROOT with BENEATH is EINVAL. It also includes grp.h for setgroups. The host oracle passes all 14 parts built with gcc and with musl-gcc.
A signal frame put the sigcontext at +48 in the ucontext, and the blocked mask right after its 18 register words. Linux's ucontext is flags, link and a 24-byte stack_t, so the sigcontext is at +40. The mask is at +296, after the whole 256-byte sigcontext. A handler that reads or edits the interrupted registers through its ucontext found every register one word off, and Go's preemption does exactly that to inject a call. A handler that reads the mask found a register word there. The ucontext's layout now has a file of its own (call/sigstack/ucontext.rs), with Linux's offsets: the sigcontext at +40 and the mask at +296, and uc_stack reporting SS_DISABLE, since no alternate stack is kept yet. sigframe.rs and rt_sigreturn take the sigcontext's offset from it. The host proofs mount both files as the capsule does, and one checks the mask at +296 and SS_DISABLE in uc_stack. sigframe.rs is lane C's file, changed here because Go's signal handlers need it; it is no longer than it was.
sigaltstack said no alternate stack was set whatever a thread gave it, and every handler ran on the interrupted stack. Go gives each thread an alternate stack and installs its handlers with SA_ONSTACK. Its handler checks that it runs on that stack or on g0's, and otherwise takes the path for a thread it does not know, which never returns. A Go thread interrupted by SIGURG on a goroutine's stack spun there, so goos never finished. Now the family keeps each thread's stack (call/sigstack/altstack.rs), and sigaltstack (call/sigstack/sigaltstack.rs) sets and reports it. It reports SS_ONSTACK while the thread runs on the stack, SS_DISABLE when there is none, and 0 otherwise. It refuses a change while the thread is on the stack with EPERM, a stack smaller than MINSIGSTKSZ with ENOMEM, and unknown flags with EINVAL, and a refused request leaves the stack as it was. A handler installed with SA_ONSTACK gets its frame at the top of that stack unless the thread is on it already, and the ucontext's uc_stack reports the stack, as Linux's get_sigframe and __save_altstack do. An exec drops the process's stacks, as on Linux. The host proofs build frames with no alternate stack as before, and one checks that a frame lands at the top of an alternate stack with uc_stack naming it. signal.rs, deliver.rs and exec_load.rs are lane C's files, changed here because Go's os tests cannot finish without this. signal.rs gave its sigaltstack to the new file and is shorter; the other two gained a line each.
statfs wrote f_fsid at 48, f_namelen at 56, f_frsize at 64 and f_flags at 72. Linux's struct statfs has f_files and f_ffree at 40 and 48, so those four are at 56, 64, 72 and 80. A program read the fsid as the free inode count, the name length as the fsid, the fragment size as the name length and the flags as the fragment size. f_flags also carried only ST_RDONLY, where Linux always sets ST_VALID and adds the mount's nosuid, nodev, noexec and relatime. Now each field is at Linux's offset, and f_flags is ST_VALID plus the bits for the options the family's mount table gives the mount, the same options /proc/self/mounts lists.
Nothing checked statfs, so its fields could sit at the wrong offsets without a proof failing. cfiles now checks, for /tmp and /proc, the filesystem type, a name length of 255, a fragment size equal to the block size, and f_flags equal to ST_VALID plus the bits for the options /proc/self/mounts gives the same mount. It also checks that fstatfs on a file in /tmp agrees. The host oracle passes all 15 parts built with gcc and with musl-gcc.
Every write that reached the store answered EIO when the store refused it: close, fsync and sync putting a file in the store, mknod, link and rename. The store names its reason, "no space left" for a full store, so a program told EIO could not tell a full disk from a broken one. Now the reason becomes the errno a Linux filesystem gives: ENOSPC for a full store, EFBIG for a file too large for it, and EROFS, ENOENT, EACCES, EEXIST, EISDIR or ENOTEMPTY where the store says so. A reason with no Linux name is still EIO.
statfs gave every mount the same 1048576 blocks of 1024 bytes, half of them free, whatever was written. /proc and /sys seemed to have a gigabyte of room, the read-only root seemed half empty, and df could not show a file taking space. Now the sizes come from the family's own files and from nothing of the machine's. The read-only root is the bytes the store holds under the family's root, with none free. /dev, /proc and /sys have no blocks, as Linux reports for trees that hold no bytes. The private directories share one quota, half the family's memory, as Linux sizes a tmpfs it is given no size (declared::PRIVATE). Their free room is that quota less what the family keeps there, in the store and in copies not yet written. Putting a copy in the store that would go past the quota fails with ENOSPC. Blocks are 4096 bytes, as tmpfs counts them.
cfiles checked statfs's fields but not its sizes, so constant figures passed. Its statfs part now checks that /proc has no blocks and that free is at most the total, that writing 256 KiB to a file in /tmp takes exactly 64 blocks from the free count, and that unlinking the file gives them back. The host oracle passes all 15 parts built with gcc and with musl-gcc.
/proc/loadavg read "0.00 0.00 0.00" always, and sysinfo's loads were zero, because load was not measured. uptime, top and anything that throttles on load were told the family was idle while it was busy. Now both come from one measure (file/system/load/). Linux averages the tasks running or waiting to run, every five seconds, with fixed decay factors for one, five and fifteen minutes. The kernel does not say how long a thread waited for the CPU, so this averages what it does say: the share of each period the family's threads ran, from their ticks, counting processes that have exited. The factors, the fixed point and the rounding are Linux's, so a family that ran all of the last minute reads 0.63 as Linux does. A read gives the periods since the last read the share the family ran across all of them, since nothing samples in between. A host proof checks a busy minute, then an idle one, against values worked out with Linux's calc_load.
/proc/meminfo said Cached and Shmem were 0, and sysinfo's sharedram was 0, while the family held in memory the copy of every file it was writing. free(1) could not see those bytes, and MemFree counted them as free. Now the bytes the family's copies hold are Cached and Shmem in meminfo, as a tmpfs file's pages are on Linux, and sysinfo's sharedram. MemFree and freeram leave them out, and MemAvailable counts them back in, since a copy can be put in the store. Buffers, SwapCached, SwapTotal and SwapFree stay 0, because there are no block-device buffers and no swap.
/proc/<pid>/stat gave 0 for a process's children's CPU times and minor faults, though the family keeps what each waited-for child used. It also gave 0 for where the program's code and data start and end, and for the stack it started on, though the family placed all three. ps and top showed no child time, and a debugger found no code range. Now cutime, cstime and cminflt are what the children the process has waited for used. startcode and endcode bound the program's executable regions, and start_data and end_data its writable ones, leaving out the interpreter's. startstack is the stack pointer it started with, where argc is, as Linux's start_stack is. The fields are grouped as proc(5) groups them. The zeros left are Linux's own, and two are this view's limits, as the code says: no kernel flags are kept, and pending signals are not in the family's view.
write and pwrite64 on a file took at most 1 MiB and returned that count for anything larger. Linux writes a regular file whole, and code that checks write(fd, buf, n) == n without a retry loop, which is most code, failed on a 4 MiB write. Now a write to a file goes on in 1 MiB copies out of the guest until all of it is written. A failure after some bytes have landed returns the count so far, as on Linux. Pipes and sockets are unchanged.
fork mapped each region of the parent into the child with one peer call, and copied it with one more. The kernel takes at most a megabyte in one peer call, so a process with a larger region failed to fork, with ENOMEM. cfiles, with 3 MiB of static buffers, got ENOMEM from fork and then waited forever on a pipe its child was to write; cproc, with 5 MiB, got ENOMEM from its first fork. Now each region is mapped and copied a megabyte at a time, as the personality's own mappings and its reads and writes of guest memory already are. fork_copy.rs is lane C's file, changed here because the proof guests' large buffers found it.
cproc did not look at /proc/self/stat's code, data and stack bounds or its children's fields, at the load average, or at Shmem, so zeros there passed. Three parts now check them against facts the program sees itself. stat: main lies between startcode and endcode, an initialised global between start_data and end_data, and argv sits right after argc at startstack. After a child that touched 1 MiB of fresh memory and ran half a second is waited for, cutime and cstime are at least 20 ticks and cminflt at least 64. load: after eleven seconds on the CPU, the one-minute average in /proc/loadavg is above zero and within 0.05 of sysinfo's. memory: 4 MiB written to an open file in /tmp raise Shmem by at least 3 MiB, the host's own traffic allowed for, with Cached at least Shmem and sysinfo's sharedram at least 3 MiB. The host oracle passes all 10 parts built with gcc and with musl-gcc.
Nothing wrote more than 1 MiB in one call, so a write cut short passed. cfiles's pwrite part now writes 3 MiB with one write and 2 MiB with one pwrite, and checks that each returns its whole length and that the offset ends at 3 MiB. The host oracle passes all 15 parts built with gcc and with musl-gcc.
A process that exited passed its parent its own counts and the times of the children it had waited for, but not those children's page faults or context switches. A parent whose child ran a grandchild saw the grandchild's time in RUSAGE_CHILDREN and /proc/<pid>/stat's cutime, and not its switches or faults: cproc's parent saw 10 switches where the grandchild alone had 459. Now the waited children's faults and switches go up with their times, as Linux adds a child's cmin_flt and cnvcsw into its parent's.
cproc's stat part wanted at least 64 minor faults from a waited child that touched a fresh megabyte. NONOS backs a readable and writable anonymous mapping when it is made and a fork copies eagerly, so such a child takes no page fault and its true count is 0. The part failed on NONOS with a correct cminflt, and it passed on the host only because Linux maps on first touch. Now the child waits for a grandchild that spins and reports its own minflt and switches before it exits. The parent's cminflt and its RUSAGE_CHILDREN switches must be at least those, and its cutime must hold the grandchild's run. Without the previous commit the switch check fails on NONOS (10 against 459); the host passes it as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linux programs go looking for a lot more than read and write. They open /dev/null, read /proc/self/exe, call statfs before writing a big file, take flock on a lockfile, ask sysinfo how much memory there is, and Go's runtime wants a signal stack. This branch serves all of that for a Linux family on NONOS.
The rule I held to: every file that describes the system says only what NONOS declares, or what the kernel measured of the family's own threads. Nothing from the host, another capsule or another family leaks into /proc, /sys or any syscall answer.
This builds on #582 (
linux/go-guests); the 36 commits on top of it are the work here. Each commit carries one change and builds on its own.What works now
..after a symlink goes where Linux sends it. openat2 serves RESOLVE_BENEATH, IN_ROOT, NO_SYMLINKS and NO_MAGICLINKS.How it was checked
Three proof guests were written for this and run twice: once on NONOS (QEMU, one CPU) and once on the host under
sh/oracle.sh(root without capabilities, its own pid namespace and /proc, a tmpfs /tmp, no tty). Nothing they print depends on the machine, so the two runs can be compared line by line.cfiles and goos print exactly the same lines on both. cproc differs on two lines, on purpose: Linux lets a process open /proc/self/mem and names the real CPU in /proc/cpuinfo, and NONOS does neither.
bbsuite has one section it can't finish yet:
(echo bg1 > f1) & (echo bg2 > f2) & waitspins, because ash'swaitneeds rt_sigsuspend and SIGCHLD, which aren't served. With that one line taken out, everything else matches the host.The guests that were already there still pass from a clean build of this branch, all with unserved=0: gohello, goconc, gopoll (three boots), cthreads, threadfault (the worker's fault still ends the whole process) and cwait.
Negative controls
Every proof was checked the other way round too: take one piece of the fix out with a one-line change, rebuild, boot, and watch the matching part fail while the rest keeps passing.
unserved nr=18Two real bugs came out of this, and both are fixed here. A parent never received the faults and switches of a grandchild its child had waited for, which Linux does count. And chdir to an absolute path failed from any directory other than
/, which is what broke busybox's subshell section.Build and gates
-Wall, and goos passesgo vet.check_unreachableexits 1 on both for the same pre-existing site, which this branch doesn't touch.procstat_redact.rs(+7 -1). The kernel's PT_LOAD segments and section sizes are identical to the base, and only the .text bytes differ./* */, and there's no#[allow].What the system reports
nonos, version "NONOS Linux personality"A few figures are 0 because the kernel doesn't keep them, and the code writing each one says so: ru_maxrss is the resident size at the call (no peak is kept), ru_nivcsw is 0 because switches aren't split into voluntary and involuntary, ru_inblock/oublock are 0, and /proc//stat's flags and signal are 0. Minor fault counts are small but true: NONOS backs anonymous memory when it's mapped, so a process takes no fault where Linux would take one per page.
Not done here
file/mod.rs,call/mod.rs,serve/family.rs,serve/waits.rs,abi/nr_path.rsandGuests.mk. The new guests' make rules went intoGuestProofs.mkandGuestOnly.mkto keepGuests.mkfrom growing more.Follow-ups outside this branch
F_GETFLstill reports O_RDWR for every file. It should report how the file was opened, usingdesc::readsandFd::writable.PR_GET_NAMEanswers an empty name untilPR_SET_NAMEhas been called. Linux answers the comm, whichfile::exe_of(pid).commalready holds.call::sigstack::altstack::set)./proc/<pid>/exeis recorded from argv, so an exec whose argv[0] isn't the program's path reads ENOENT.program.pathinexec_load.rsis the right source.exec_resolve.rsstill normalises..before following links.file::followwalks it the way Linux does.mem_kb.LINUX_GUEST_ONLY="<names>"to carry only the guests they boot.