Skip to content

files: one client of the file servers, in toyos, for std and libc alike - #988

Merged
Japabu merged 4 commits into
mainfrom
wt/toyos-fd-served
Oct 21, 2026
Merged

Japabu merged 4 commits into
mainfrom
wt/toyos-fd-served

Conversation

@Japabu

@Japabu Japabu commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Draft, PR 3 of 4 in the descriptor work (fd-design.md revision 8). PR 2 was #978. PR 4 adds the inheritance rule (Q9) and gives served kinds to a child; its socket rule is with the owner as Q18.

Problem

std kept the process's file-server client in its own fs module: the directory capabilities, routing a path to the kernel or a file server, following links, and the open served files. A C program reached none of it. Every path under /home, /apps, /config, /state, /log and /boot answered ENOENT (issues/c-programs-name-no-file-a-file-server-holds.md).

Resulting behaviour

  • toyos::files holds that state in the copy of toyos that owns the descriptor table (fd: one descriptor table numbers every raw fd, and a child's descriptors are its fd:<n> endowments #978), behind toyos_fs_*. std and libc therefore share one set of connections and one table.
    • An open served file is a descriptor naming an object of toyos's own (tag SERVED_FILE). The object's word indexes a registry of open files, so no refcount is hand-rolled.
    • read, write, seek, fstat, truncate, fsync and close all dispatch on the descriptor, whichever server holds the file.
    • A capability's connection is shared between threads through a futex turnstile (toyos::sync::Turnstile). It replaces std's copied Mutex+Condvar one; sdk/std/copies and sys/turns.rs are deleted.
    • toyos gains alloc, through rustc-std-workspace-alloc under rustc-dep-of-std.
  • std: fs is a thin client of toyos::files, and a File is one descriptor whoever serves it.
    • File::flush is a no-op, as on every other platform. It used to be fsync, which made every BufWriter flush sync the whole volume. A File buffers nothing: each write belongs to the server once it answers, and Write::flush asks for no durability.
    • Stdin reads through toyos::files too.
  • libc: these calls now route through it, each refusal answered with POSIX's errno (errcode::of_files): open, read, write, lseek, fstat, ftruncate, fsync, stat, lstat, unlink, rmdir, mkdir, remove, rename, chdir, readlink, realpath, opendir, readdir, isatty and stdio's.
    • symlink works where a file server holds the name, and the server refuses a name that exists, as POSIX has it. It stays ENOSYS on the kernel's /tmp, whose SYS_SYMLINK replaces the name.
    • realpath resolves the names a file server holds as POSIX does, a name at a time (resolve::resolve_path, host-tested): every link is followed where it is met, a relative one against its own directory, and .. after a link is the parent of where it led. Once the whole path as it stands is the kernel's, it goes to the kernel's resolver, so a served link to /tmp or /system resolves there. A path under a folder grant deeper than one name (fs:/home/toy/Games without fs:/home) is walked, with the names above the capability passed through as the client routes past them.
    • A full descriptor table refuses an open with EMFILE on /tmp and on a served path alike. A full kernel handle table, which for descriptors that are kernel handles binds first, answers ENOMEM. That is the kernel's one word for a full table and for no memory alike, so it can't be EMFILE without a kernel word of its own (issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md). It used to fall to EINVAL.
    • A rename between two of the kernel's mounts answers EXDEV, as one between a server and the kernel does. The open file is installed only after the capability's turn is released, so its close on a full table can't wait on that turn.
    • A served directory's readdir gives d_type, but d_ino is 0: a server's listing names no node.
  • stdout and stderr write a descriptor through toyos::files, so a served file at 1 or 2 is written to correctly. The stream sink no longer runs fstat on an object that is not a kernel handle.
  • Also: a refused fd: endowment (fd: one descriptor table numbers every raw fd, and a child's descriptors are its fd:<n> endowments #978) is now reported on stderr, after the table's lock is released.
  • Test: the new corpus cases 211_home_files.c and 212_full_table.c. 212 fills the table with duplicates of one /home descriptor, which take no kernel handle, and opens a /tmp and a /home file, each answering EMFILE. It then fills the kernel's handle table with /dev/null opens (ENOMEM). 211, under /home:
    • it makes, writes, reads back at an offset, appends through stdio, links, resolves, renames by a relative path after chdir, lists and removes;
    • it checks the errnos for O_EXCL, a read-only write, readlink of a file, rmdir of a full directory and a gone name.

Decisions

  • Owner (the four-PR plan): giving a served file to a child is PR 4. Until then:

    • a descriptor that names no kernel handle is refused a child with EOPNOTSUPP (std's Unsupported), and so is an addopen of a served path;
    • because posix_spawn asks for descriptors 0 to 2 on every spawn, a C program that has dup2'd or freopen'd a served file onto stdio has every posix_spawn, system and popen refused, as is an adddup2 of a served descriptor;
    • std's Command with inherited stdio refuses in the same case.

    Before this PR, no served file could sit at 0 to 2, so this is new. It is recorded in issues/a-served-file-at-stdio-stops-every-spawn.md.

  • Mine:

    • no pread or pwrite, since libc has no caller of them;
    • the open files live in a registry the object's word indexes, rather than a raw Arc;
    • the turnstile sleeps on a private futex call, which a host test replaces with a yield.
  • Issue records:

    • issues/a-served-file-panics-when-asked-its-raw-fd.md is retired, because a served File is a descriptor;
    • the served-flush record becomes issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md;
    • create-new-on-a-kernel-path-is-not-exclusive names C's O_EXCL on /tmp as well;
    • the /tmp symlink record keeps only its /tmp exit;
    • the-kernel-resolves-a-path-by-rules-of-its-own describes realpath's two halves.
  • issues/c-programs-name-no-file-a-file-server-holds.md is retired. Its remaining exit, /system over the protocol and the kernel's file syscalls deleted, is already step 9's file half in issues/the-kernel-is-small-interrupts-post-and-threads-wait.md, which now says libc is a client.

Fork commits

fork commit what
ToyOSOrg/rust main 9d8bb4c347f library/Cargo.lock names toyos's alloc edge

The rust gitlink names it.

Checks (head 43270738e, on a8ea09d20)

check exit
cargo test -p toyos --lib (the files error encoding, path normalising, the turnstile) 0
cargo test -p toyos-libc-copies (errno codes against errno.h; 18 path-resolution tests, served trees beside a kernel one, among them a three-name capability) 0
negative controls: the kernel handoff asked at the first name (r2's behaviour), no kernel handoff, and following only the last name's links 101 each, on its own tests
cargo run -- --ci host 0
cargo run -- --build-only, --arch aarch64 0, 0
QEMU x86_64: the same 12 rows as #978 plus hosted_clang_hello 0 (13/13)

Metal (T14):

  • Round 1 at 2d3c5d8a6: 9/10. 206_libc_refusals failed only on fd: one descriptor table numbers every raw fd, and a child's descriptors are its fd:<n> endowments #978's stale F_DUPFD_CLOEXEC expectation, fixed here as a commit of its own. Until this PR lands, main fails 206_libc_refusals on metal.
  • Round 2 at 7cef3dd71: 144/145, the whole C corpus among them. 212_full_table failed: its /dev/null opens filled the kernel's handle table before the descriptor table, so the case tested nothing it claimed. It now fills the table with duplicates.
  • Round 3 (staged, not run): proctreecase (app_view, since the open path changed) and testcases (std_fs file_mtime disk_backtrace stream_handles and the 140 corpus cases with an expectation).

Evidence (masked): round 3 https://github.com/ToyOSOrg/evidence/tree/ed178f2641250fe12887bfa29d78109b05d9c2ad/r3; round 2 https://github.com/ToyOSOrg/evidence/tree/7b22cdbd83dfee95bb9b9fe00a6b66b89e56a7d2/r2; round 1 https://github.com/ToyOSOrg/evidence/tree/68a661d0ca78b24c43585ce6b27ede01a2a6cb42/r1

Not done here

🤖 Generated with Claude Code

@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #988, round 1 (head 2d3c5d8a6, base a8ea09d20, rust 9d8bb4c347f)

Architecture

The ownership is right. The routing, link following, directory connections and open served files move out of std's private fs module into the owner copy of toyos, behind toyos_fs_*, beside the toyos_fd_* table from #978. std and libc call the same C interface, so a mixed Rust+C program has one owner: libc's std-runtime build leaves fd-table off, and files::owner is gated on that feature. I found no second client and no second descriptor table. The other remaining direct kernel file calls in libc are the kernel half of realpath's resolver, uname's read of /system and posix_spawn's addopen, and each of them is meant to be there.

OPEN maps an object word to an Arc<OpenFile>. It is the owner's store for its own objects, not a second table, and it keeps a forged or stale word to a bounds-checked panic instead of a raw pointer. The SERVED_FILE tag is a reasonable way to recognise those objects, given that clippy rejects function-pointer comparison.

The toyos alloc edge goes through rustc-std-workspace-alloc only under rustc-dep-of-std. The root lock and the fork lock agree on it.

The fork commit 9d8bb4c347f is a one-line library/Cargo.lock edge. It is the tip of ToyOSOrg/rust main (ls-remote), its parent is the previous pin a56b4057c, and the gitlink names it. The pin is coherent.

Size: about +770 lines in toyos::files, against −596/+130 in std's fs.rs and the deleted sdk/std/copies and turns.rs. The test additions are small: the error round trip, absolute, the turnstile order, two resolve_served tests and the 119-line corpus case. The growth matches the job. Most of it is code that moved and code that libc now reaches.

The retired issue's exit is met in substance. There is one SDK client that std uses, libc sits on it, and a C case writes, reads back and lists under /home. That last part is still pending the metal result. The residue, /system over the protocol and the kernel's file syscalls deleted, is step 9's file half in the-kernel-is-small-…, as the PR says. The slug has no remaining citation.

Required

B1. Opening a served file when the table is full hangs the calling thread and every later user of that directory.

  • Where: sdk/toyos/src/files/owner.rs:115,151-167.
  • What happens: on_path holds the capability's Turn while served runs, and served_fd calls fd::install inside it. When the table is full, toyos_fd_install releases the object (fd.rs:281-284). That calls release_file, which drops the OpenFile, and OpenFile::drop calls self.cap.dir.lock(). The turnstile is not reentrant, so the thread waits forever on its own ticket. Every other thread that uses that capability then queues behind it.
  • Consequence: the open never returns, where it should fail with EMFILE. The old std code never dropped a served file under the turn, so this is new.
  • The kernel arm (:211) has a related bug: it maps Full to SyscallError::ResourceExhausted. errcode::of turns that into EINVAL, and std turns it into OutOfMemory. Before this PR, libc's install answered EMFILE through fd_errno, so this is a regression.
  • The served arm's mapping is also wrong: it maps Full to TooManyOpen, which C answers as ENFILE.
  • Fix: have the served closure return the fs::Opened, and build and install the OpenFile after the turn is dropped. Answer Error::Fd(Full) in both arms. Keep the rule that nothing reaches the fd table or a release while a turn is held.

B2. realpath of a served link whose target the kernel serves fails with ENOENT.

  • Where: sdk/libc/src/posix_io.rs:452-462, resolve.rs:132-147.
  • What happens: take /home/u/l pointing to /tmp/x, where x is a regular file. resolve_served reads the link and then calls read_link("/tmp/x"). files::read_link routes that to the kernel, and SYS_READLINK answers NotFound for a name that is not a link, which is the same conflation readlink() works around at :390. The closure turns only Served(NotALink) into None, so realpath fails with ENOENT for a file that exists. A relative target that leaves the server through an intermediate link fails the same way.
  • Fix: hand a target that routes to the kernel to the kernel resolver (resolve with TheKernel). Alternatively, treat Kernel(NotFound) as "is it a link?" by stat-ing, as readlink() does.
  • Also: a NotALink answer already proves the name exists, so exists costs a redundant round trip.
  • Test: a host test in path_resolution.rs with a served tree whose link leaves for a kernel tree pins this.

B3. Served realpath deviates from POSIX, and no issue records it (question 3).

  • The deviation: POSIX realpath answers a path with no ., .. or symbolic-link components. The served path leaves every link before the last name as written, and reads .. lexically through files::absolute. That is a real deviation: two spellings of one file canonicalise differently, which breaks the pattern of canonicalising a path and then comparing.
  • Where it is recorded: only in the PR body's "Not done here" and in the doc comment at resolve.rs:136.
  • A record this PR makes stale: issues/the-kernel-resolves-a-path-by-rules-of-its-own.md:28 still says libc's realpath answers by the kernel's rules, which is no longer true for served paths.
  • Fix, either way:
    • Resolve per component. Calling read_link on each prefix costs one round trip per name, and the server already answers NotALink or a target. This is the simpler outcome.
    • Or record the deviation, with an exit, in that issue or a new one.

B4. Issue records and a code comment that this change makes stale (issues/README.md, root "Update issue records directly affected").

  • issues/a-served-file-panics-when-asked-its-raw-fd.md: the defect is gone, because a served File is now a descriptor and as_raw_fd answers it. Retire the issue, or rewrite its exit if a guest check is still wanted. Its "a kernel handle that reaches the file's server" no longer describes the design.
  • issues/every-flush-of-a-served-file-syncs-its-whole-volume.md: File::flush is now Ok(()) (sdk/std/sys/fs.rs:346, where it used to be fsync), so half of the exit is met. The issue needs updating, and the PR body should mention the change, which it does not.
  • issues/create-new-on-a-kernel-path-is-not-exclusive.md: it cites std's to_flags in sdk/std/sys/fs.rs, which no longer exists. The defect now lives in toyos_fs_open's kernel arm (owner.rs:208), and it now reaches C too, because libc has just gained O_EXCL. A C lock file made with O_CREAT|O_EXCL on /tmp silently opens the existing file. Name both callers.
  • issues/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md: its exit asks for libc's symlink to go to the server, as std's does through on_path in sdk/std/sys/fs.rs. That is now partly true. Record what is met, and fix the location.
  • issues/a-link-on-a-kernel-mount-to-a-served-path-leads-nowhere.md:13 and issues/a-file-held-across-a-file-servers-restart-answers-gone.md:11 point to sdk/std/sys/fs.rs for code that is now toyos::files. The second should also say that libc shares the behaviour.
  • lib/manifest/src/grants.rs:60: the comment says std probes MAX_CAPABILITY_DEPTH "in sdk/std/sys/fs.rs". That constant is now toyos::files::MAX_CAPABILITY_DEPTH, and libc probes too.

B5. The corpus licence count was not updated (tests/CLAUDE.md: "changed membership updates the licence count").

  • Where: tests/corpus/tinycc/LICENSE:24-26.
  • What is wrong: it still says 330 files, 74 not upstream and 73 written here. With 211_home_files.c and .expect added, the tree holds 332 (git ls-tree at head: 332, at base: 330), so the figures should be 332, 76 and 75.

B6. The test seam ships in production: toyos::sync::Sleep, Futex and the S parameter on Turnstile/Turn (sdk/toyos/src/sync.rs:61-90).

  • The problem: they exist only so that the host test can swap the futex for a yield. That is public API and a type parameter shipped solely for a test, which root's rules forbid.
  • Fix: use a private wait and wake_all pair, with a #[cfg(test)] yield body and a futex body otherwise. The test stays as it is, and the public surface becomes Turnstile<T> and Turn.

B7. The PR description is wrong about the posix_spawn gap (question 6; the correction is the orchestrator's).

  • What the body says: only that an addopen of a served path answers EOPNOTSUPP, and that "std refuses such a file as stdio, as before".
  • The wider gap: spawnreq::plan asks held(fd) for 0 to 2 on every spawn. For a served object, fd::copy answers Refused::Syscall(InvalidArgument), so once a C program has dup2'd or freopen'd a served file onto stdout, every posix_spawn, system and popen fails with EINVAL. An adddup2 of a served descriptor fails the same way. std's Command with inherited stdio refuses in the same case.
  • Why "as before" is false: before this PR, no served file could sit at descriptors 0 to 2.
  • What is required: it is acceptable as a temporary gap before PR 4, but the body must state it accurately. Optional: answer both cases with the same errno (see S1).

Optional

  • S1. Answer a served object in fd::copy or errno_of as EOPNOTSUPP, matching open_handle, so that both halves of the PR 4 gap read the same way.
  • S2. libc::symlink's ENOSYS guard (posix_io.rs:415) asks is_served of the text. A served path that crosses an absolute link into /tmp reaches syscall::symlink through on_path's kernel arm, which displaces what the name held. Refusing in the kernel closure as well would make the guard hold.
  • S3. files::read_dir asks again whenever the listing outgrew the buffer, and the owner already retries up to LIST_ATTEMPTS. That fetches a large listing twice, through two loops. Answering the owner's Vec across the interface once would remove one of them.
  • S4. std's Stdin::read (sdk/std/sys/stdio.rs:41) is still kernel-only (on_open_fd). A served file dup2'd to descriptor 0 in a mixed program reads as Unsupported. files::read would match stdout and stderr.
  • S5. In of_served, InvalidName → ENAMETOOLONG reads oddly for "the volume cannot store that name"; EINVAL or EILSEQ fits better. LimitExceeded → EAGAIN invites a retry that will not succeed.
  • S6. The "Decisions" heading mixes owner rulings with the implementer's own choices, for example dropping pread/pwrite from the plan. Label which is which.

Questions asked

  1. The client move: the ownership is sound, there is one owner and one table, and the OPEN registry is justified. The defects are B1 and B6.
  2. The path and descriptor calls, and errno:
    • Correct on the paths I traced, apart from B1's errno, B2 and S5.
    • lseek: a negative SEEK_SET answers EINVAL through try_from.
    • write to a descriptor open only for reading: NotOpenForWriting answers EBADF, as POSIX has it.
    • readlink of a file answers EINVAL on both servers.
    • NotAFile answers EBADF, the same as before for a socket descriptor.
  3. realpath: a real deviation, and not recorded (B3). It also has a bug (B2).
  4. stdout and stderr: correct. ask no longer runs fstat on an opaque object. A served descriptor at 1 or 2 is written through files::write, and forget re-asks after dup2.
    • Watch for PR 4: once a served file can be stderr, a panic under a turn of that same capability would block its own message. Keep panics out from under turns.
  5. The retired issue: its exit is met, and the residue exists as claimed. Other records are now stale (B4).
  6. EOPNOTSUPP for addopen: clear. It is recorded only in the PR body and the doc comment, which is enough for a stage before PR 4, but the wider gap is unreported (B7).
  7. Fork commit and pin: coherent; see the architecture section.
  8. Negative control: none is needed for 211.
    • Why: at base, libc's mkdir goes to SYS_MKDIR, which does not serve /home, so the first line reads NO. ccheck compares every line with .expect, so the case is red at base by inspection. A control image would show nothing new.
    • The real gap: the C corpus runs only on metal, and the staged boot carries only 4 corpus cases. This PR moves every C open, read, write, lseek, stat and opendir on /tmp and /system onto new code.
    • Suggestion: put the whole corpus in that testcases boot. It adds members, not a boot, and it is the cheapest regression net for libc's kernel paths. hosted_clang_hello covers part of this today.
    • The metal results are still to come. This review assumes they pass.

SEND BACK

Japabu added a commit that referenced this pull request Oct 21, 2026
#978 implemented fcntl's F_DUPFD_CLOEXEC on the descriptor table: the
lowest free descriptor at or above the argument, marked close-on-exec, as
POSIX has it. 206_libc_refusals still expected the old refusal (-1,
ENOSYS), and as a metal-only corpus case nothing ran it before the T14 boot
of #988, which answered 10. The record of what libc refuses loses the
same stale lines, and F_DUPFD and F_GETFD's departures with them: both are
POSIX's now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
Review r1 of #988.

An open of a served file installed its descriptor on the capability's
turn. On a full table the table let the new open file go, whose close
asks the server on that same turn, so the thread waited on itself and
every later user of the directory behind it. The open now hands back
what it opened and installs it once the turn is let go, and a full table
answers the table's own refusal, EMFILE in C and QuotaExceeded in std, on
/tmp and on a served path alike; the kernel arm answered EINVAL and the
served one ENFILE. A corpus case fills the table and opens both.

libc's realpath resolves the names a file server holds as POSIX does, one
at a time: a link before the last name is followed, `..` after one is the
parent of where it led, and a relative one is read against its own
directory. At the first name the kernel holds it hands the rest to the
kernel's resolver, so a served link to /tmp or /system resolves there,
where it answered ENOENT for a file that exists.

The turnstile's sleep is private, a futex, and a yield in a host test,
rather than a public trait and type parameter that existed for the test.

A descriptor that names no kernel handle is refused a child with
EOPNOTSUPP (std's Unsupported), as an addopen of a served path is; a
kernel NotSupported is EOPNOTSUPP in C. A served refusal of a name the
volume cannot store is EINVAL. std's stdin reads whoever serves it.

Issue records this made stale are brought up to date:
- a served file's raw fd is retired;
- the served flush issue is now about fsync, since flush is a no-op as
  everywhere else;
- the kernel's non-exclusive create names C's O_EXCL too;
- /tmp's symlink is left with the remaining exit only;
- two records and grants.rs point at toyos::files;
- a served file at stdio stopping every spawn is recorded;
- the corpus licence counts the four new files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Oct 21, 2026
…0da367a21cc1: host tests, 2 resolver controls, --ci host, both builds, 13 QEMU rows, testcases staging with the whole corpus, round 1 T14 logs; masked
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 (7cef3dd71) answers review r1. Evidence: https://github.com/ToyOSOrg/evidence/tree/7b22cdbd83dfee95bb9b9fe00a6b66b89e56a7d2/r2

B1 (fixed).

  • The deadlock: the open's closures now hand back what they opened (Opened::Kernel(handle) or Opened::Served(cap, opened)). The descriptor is installed only once the turn is released, and on_path's doc says that served must not reach the table.
  • The errnos: both arms answer Error::Fd(Full), which is EMFILE in C and QuotaExceeded in std, replacing ResourceExhausted/EINVAL and TooManyOpen/ENFILE.
  • Test: the new corpus case 212_full_table fills the table with /dev/null. It then expects EMFILE from opens on both /tmp and /home, and a /home open once one descriptor is free. On the old code it would hang until the case's timeout. It is in the metal staging.

B2 and B3 (fixed, not recorded as a gap).

  • resolve::resolve_path walks a name at a time while a file server holds the names. Each name is lstat'd, and a link is read_link'd and spliced into the remaining names. That gives POSIX semantics: links before the last name are followed, .. after a link is the parent of where it led, and a relative link is read against its own directory.
  • At the first name the kernel holds, the rest goes to the kernel's resolve, so a served link to /tmp or /system resolves correctly. A path the kernel holds from its first name is unchanged.
  • exists is gone: the walk itself proves every name exists.
  • Tests: 4 new host tests over a served /home beside the kernel tree, including a link out to /tmp (as the last name and before it) and two spellings of one file answering one path. Two controls, the handoff removed and only last-name links followed, are each red.
  • the-kernel-resolves-a-path-by-rules-of-its-own.md now describes both halves.

B4 (done).

  • a-served-file-panics-when-asked-its-raw-fd is retired.
  • every-flush-… becomes an-fsync-of-a-served-file-syncs-its-whole-volume. flush is a no-op, which is right because a File buffers nothing and Write::flush asks for no durability. The exit is now about FSYNC alone, and the PR body says so.
  • create-new-on-a-kernel-path-is-not-exclusive names toyos_fs_open's kernel arm and C's O_CREAT|O_EXCL.
  • The /tmp symlink record says what is met (served symlink, EEXIST, readdir, asserted by 211) and keeps only the /tmp exit.
  • The link-on-a-kernel-mount and file-held-across-a-restart records, and grants.rs:60, now point at toyos::files and name libc.

B5 (fixed). The licence now counts 334, 78 and 77, with the 212 pair added.

B6 (fixed). wait and wake_all are private: futex calls normally, a yield under cfg(test). The public surface is Turnstile<T> and Turn, and the order test is unchanged.

B7 (stated and recorded).

  • The PR body states the wider gap: with a served file at descriptors 0 to 2, every posix_spawn, system and popen is refused, as are adddup2 of a served descriptor and std's inherited stdio.
  • It is recorded in issues/a-served-file-at-stdio-stops-every-spawn.md, with PR 4's EXPORT as the exit.
  • I took S1: fd::copy answers NotSupported, and C maps a kernel NotSupported to EOPNOTSUPP, so both halves of the gap read the same way.

Optionals:

  • S4, taken: std's stdin reads through files::read, and on_open_fd is folded back into on_fd.
  • S5, partly taken: InvalidName is now EINVAL. I left LimitExceeded as EAGAIN, because no errno fits better.
  • S6, taken: the Decisions section labels owner rulings and my own choices.
  • S2 and S3, not taken.

Also in this round: ae9f0fd3c corrects 206_libc_refusals.expect for #978's F_DUPFD_CLOEXEC. Main fails that case on metal until this lands.

Checks:

  • Host tests 0, --ci host 0, both builds 0, QEMU 13/13.
  • The testcases image is restaged with the whole corpus (140 cases) plus the std file binaries.

@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 (head 7cef3dd71, base a8ea09d20, rust 9d8bb4c347f unchanged)

I re-read the delta from 2d3c5d8a6 and checked each r1 blocker against the code.

r1 blockers

  • B1, the code: fixed. The open's closures answer Opened::Kernel or Opened::Served(cap, opened). The descriptor is installed in toyos_fs_open after on_path returns, so after the turn is dropped. A full table answers Error::Fd(Full) on both arms, which is EMFILE in C and QuotaExceeded in std. The comment on on_path names the rule. The test that claims to cover it is invalid; see N2.
  • B2 and B3: resolve_path replaces the last-name-only walk. Checked against POSIX:
    • each name is lstat'd;
    • a link is spliced into the remaining names, relative to the directory reached so far;
    • an absolute target clears the walk;
    • .. truncates the resolved prefix, so it is the parent of where a link led;
    • existence follows from the walk itself;
    • a kernel-held name hands the rest to the kernel's resolve, which fixes r1's ENOENT;
    • the two controls are red (fd7/checks.txt).
      The relative, absolute, chained, ..-after-link, loop, dangling, ENOTDIR and out-to-/tmp cases are all right. The handoff test is wrong for a capability deeper than one name; see N1.
  • B4: done. The two retired slugs have no remaining citations (git grep HEAD). The resolution issue now describes both halves.
  • B5: done. 334/78/77 matches the tree.
  • B6: done. The public surface is Turnstile<T> and Turn, and the test seam is a private cfg(test) pair.
  • B7: done. issues/a-served-file-at-stdio-stops-every-spawn.md is accurate and has a real exit.

New findings: required

N1. realpath fails with ENOENT for every path under a folder grant deeper than one name.

  • Where: sdk/libc/src/resolve.rs (resolve_path, the !served.holds(&next) handoff) with posix_io.rs TheServers::holds → files::is_served.
  • The trigger: an installed app holds fs:/home/toy/Games and not fs:/home (lib/manifest/src/grants.rs, images/system.toml:158, MAX_DEPTH = 4).
  • What happens: for /home/toy/Games/save, the walk asks holds("/home"). route finds no fs:/home, so it answers false, and the whole path goes to the kernel's resolve. The kernel holds only ROOT's empty /home, so the answer is ENOENT. r1 decided on the full path and had no such bug.
  • Why the tests miss it: the host test's Home::holds is a prefix test of /home, which models only a one-name capability.
  • Fix: decide the handoff on the full candidate. For example, a name whose remaining path is still under a held capability is walked as an ordinary directory and not handed off. Add a host case with a three-name capability and its two shallower kernel-held prefixes.

N2. 212_full_table cannot fill the descriptor table before the kernel's handle table, so it reds and never reaches B1's served path.

  • Why: MAX_FDS (4096) equals the kernel's MAX_HANDLES (RawHandle::MAX_SLOTS, 1 << 12). The process already holds kernel handles that no descriptor names: its namespace, the /home connection the case opened first, the stdio ring mapping and so on. So the loop of /dev/null opens is refused by the kernel first. TableFull becomes ResourceExhausted (kernel/src/object/handle.rs:47), and libc answers that as EINVAL (errcode::of's _ arm).
  • The expected outcome, by inspection, not measured:
    • "the table filled with EMFILE: NO";
    • /tmp gives EINVAL: NO;
    • the /home open needs no kernel handle and the table still has free slots, so it succeeds: NO.
  • What that leaves: the deadlock fix has no test, and the case's stated control ("the old code would hang") does not hold.
  • The fix: fill the table with descriptors that cost no kernel handle, for example dup of one served descriptor until EMFILE. A /home open then exercises the served install-after-turn path, which hangs at 2d3c5d8a6. A /tmp open exercises the kernel arm, where the handle is made and then released on Full.
  • Coordinator: the T14 corpus boot now running should show 212 red. Read it as this test defect, not as a fault in B1.

New findings: optional

  • S1 errno change: I audited every SyscallError::NotSupported the kernel answers to a path libc maps through errcode::of.
    • The only file-call shift is SYS_RENAME across kernel mounts (kernel/src/vfs.rs:557, e.g. /system to /tmp), which moves from EINVAL to EOPNOTSUPP. Neither is POSIX: the answer is EXDEV, which mv-style copy fallbacks test for, and which a served-to-kernel rename already gives through CrossesServers. Mapping the kernel arm's NotSupported in toyos_fs_rename to CrossesServers would make the two agree.
    • The others are tmpfs/bcachefs create_dir, which the VFS consumes internally; the elf/cache mismatch on dlopen; and device and machine calls libc never maps. The change is otherwise intended and harmless.
  • Kernel handle exhaustion: open on /tmp with the kernel's table full answers EINVAL (ResourceExhausted → _). POSIX has EMFILE. This predates the PR, but it is now visible next to 212. Record it or map it.
  • An extra paragraph: issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md carries a paragraph of history ("It used to be fsync…") that belongs in the commit. The exit stands without it.

SEND BACK (N1, N2)

Japabu added a commit that referenced this pull request Oct 21, 2026
Review r2 of #988.

realpath handed a path to the kernel at the first name no file server
held, so every path under a folder grant deeper than one name, such as
fs:/home/toy/Games without fs:/home, went to the kernel at /home and
answered ENOENT. The handoff is now asked of the whole path as it stands,
and a name above the capability that holds it is passed through as a
directory, as the client's routing passes it. A host case with a
three-name capability pins it.

212_full_table filled the table with /dev/null opens, each a kernel
handle, and on the T14 the kernel's handle table ran out first: its
4096 slots are MAX_FDS's, and the process's own handles take some of
them. libc answered that EINVAL, and the served open, which needs no
kernel handle, then succeeded. The case now fills the table with
duplicates of one /home descriptor, which take no kernel handle, so the
/home open runs the install-after-turn path that hung at 2d3c5d8, and
then fills the kernel's table on its own. toyos::fd::MAX_FDS is the
kernel's slot count by name.

libc answers the kernel's ResourceExhausted ENOMEM, as std answers it
OutOfMemory, where it fell to EINVAL. It is the kernel's one word for a
full handle table and for no memory, so a full table cannot yet be EMFILE;
issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md records it.

A rename between two of the kernel's mounts is EXDEV, as one between a
server and the kernel is, rather than EOPNOTSUPP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Oct 21, 2026
…d6d801fdddee: host tests, 3 resolver controls, --ci host, both builds, 13 QEMU rows, proctreecase and testcases staging, round 2 T14 log; masked
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3 (43270738e) answers review r2. Evidence: https://github.com/ToyOSOrg/evidence/tree/ed178f2641250fe12887bfa29d78109b05d9c2ad/r3

N1 (fixed). resolve_path now asks whether to hand off to the kernel about the whole path as it stands, lexically clean. A name above the capability that holds it is passed through as a directory, as the client's routing does. A new host case uses fs:/home/toy/Games without fs:/home: a file, a relative link, ./.. through the names above, and a sibling path that is the kernel's (ENOENT). r2's behaviour, the handoff asked at the first name, is a control and is red.

N2 (fixed). 212_full_table fills the table with dups of one /home descriptor, which take no kernel handle. The /home open then runs the install-after-turn path that hung at 2d3c5d8a6, and the /tmp open runs the kernel arm, where the handle is made and released on Full.

  • The case then fills the kernel's table on its own with /dev/null opens and expects ENOMEM.
  • toyos::fd::MAX_FDS is now RawHandle::MAX_SLOTS by name.

Optionals:

  • Rename, taken: a rename between two kernel mounts is CrossesServers, so EXDEV.
  • Kernel table exhaustion: mapped and recorded. ResourceExhausted is now ENOMEM, consistent with std's OutOfMemory, rather than falling to EINVAL. EMFILE needs a kernel word that tells a full table from no memory, and the owner's ruling is no kernel change for POSIX alone, so it is recorded in issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md.
  • History paragraph: dropped from the fsync issue.

Checks:

  • Host tests 0; 3 resolver controls each red; --ci host 0; both builds 0; QEMU 13/13.
  • Metal staged: proctreecase (app_view) and testcases (the whole corpus plus the std file binaries).

@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3 (head 43270738e, rust 9d8bb4c347f unchanged)

I read the delta from 7cef3dd71.

r2 blockers

  • N1: fixed.
    • The handoff: resolve_path now asks holds of the whole remaining path, lexically clean, and hands that path to the kernel only when no capability holds it.
    • Names above a deeper capability: for /home and /home/toy under fs:/home/toy/Games, the walk treats them as directories, as route does.
    • Paths I traced:
      • a served link to /tmp re-asks holds at the target's first name and is handed to the kernel there;
      • a .. that leaves the capability lexically is decided on the cleaned path;
      • a name beside the grant goes to the kernel, which answers ENOENT.
    • Tests: the three-name host case covers these paths, and r2's behaviour is kept as a red control (handoff-at-the-first-name exit 101).
    • A missing directory above the capability passes unchecked: in /home/toy/x/../Games/save with x absent, realpath answers /home/toy/Games/save. The client's own lexical routing in on_path opens the same file, so realpath and open agree, as the issue says they must. I accept this.
  • N2: fixed. dups of one served descriptor fill the table without kernel handles, which exercises both the case and its control:
    • The /tmp open: it makes a kernel handle, install refuses with Full, the table releases the handle, and the answer is EMFILE.
    • The /home open: this is the install-after-turn path. release_file takes the turn after on_path has dropped it, and the answer is EMFILE. At 2d3c5d8a6 this path hung.
    • One descriptor freed: the open succeeds.
    • The separate /dev/null fill: it reaches the kernel's table, which answers ENOMEM. Every printf in the case writes an already-open descriptor, so nothing needs a new handle while either table is full.
    • The MAX_FDS doc now says which limit binds which kind of descriptor.

Optionals taken

  • Rename: a kernel-to-kernel rename that the kernel answers NotSupported is now CrossesServers. That is EXDEV in C and CrossesDevices in std, consistent with a rename between a served path and a kernel one. It is correct, and the kernel answers NotSupported here only for different mounts (vfs.rs:557).
  • ResourceExhausted → ENOMEM: this is a general errno change.
    • Where it reaches: I found no libc caller that matched on the old EINVAL. The socket layer maps its own NetError.
    • Is it valid: ENOMEM is an errno POSIX allows for open, pipe and dup ("insufficient storage") and for mmap. Every call that used to answer EINVAL here now answers something accurate.
    • The weak spot: pthread_create and other callers whose POSIX answer for exhaustion is EAGAIN move from one wrong answer (EINVAL) to a less wrong one. That is not a regression.

The ENOMEM choice under the owner's ruling M

The choice is right:

  • The kernel has one word for a full handle table and for no memory. Inventing EMFILE in libc would guess, wrongly, whenever memory was what ran out.
  • A new kernel word for POSIX's sake is exactly the change the owner ruled out.
  • The new issue records the departure from POSIX, the evidence (212 asserts today's answer) and the exit.
  • It frames the condition correctly: the change waits on a reason native callers have to tell a full table from exhausted memory, not on POSIX. Whether such a reason exists, for example in std's ErrorKind or a supervisor's accounting, is the owner's call. The issue does not pre-empt it.

Findings

None required.

Optional:

  • fd7/checks.txt's first line reports the head as "on 2d3c5d8a6". The tested head 43270738e is right, but the parent the script printed is not this commit's parent (7cef3dd71). Correct the script's parent expression before reusing it as evidence.

The verdict depends on the metal results the coordinator will report: proctreecase app_view, and testcases with the std rows and the whole corpus of 140 cases, including 206, 211 and 212. Reconcile any red against its claim before landing. Nothing in this round's delta invalidates the r2 QEMU and host evidence: the delta is libc errno and resolver code, a corpus case and records, re-checked at 43270738e.

LAND (subject to the metal run being green)

@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Metal, T14 (ThinkPad T14 Gen 2), head 43270738e, run by the orchestrator: proctreecase and testcases both booted (rc 0). The judge reported 146 passed, 0 failed: app_view, std_fs, file_mtime, disk_backtrace, stream_handles and all 140 C corpus cases, including 206, 211 and 212.

@Japabu
Japabu marked this pull request as ready for review October 21, 2026 21:28
@Japabu
Japabu enabled auto-merge October 21, 2026 21:28
Japabu and others added 4 commits October 21, 2026 23:28
std kept the process's directory capabilities, its routing of a path to the
kernel or a file server, its link following and its open served files in
std's own fs module, so a C program reached none of them: every path under
/home, /apps, /config, /state, /log and /boot answered ENOENT.

toyos::files now keeps that state, in the copy of toyos that owns the
descriptor table, behind toyos_fs_*. An open served file is a descriptor
naming an object of toyos's own (tag SERVED_FILE), held in a registry its
object word indexes, so read, write, seek, fstat, truncate, fsync and close
dispatch on the descriptor whoever serves it. A capability's connection is
shared by threads through a futex turnstile, which replaces std's copied
Mutex and Condvar one (sdk/std/copies, sys/turns.rs go).

std's fs is a thin client of it. libc's open, read, write, lseek, fstat,
ftruncate, fsync, stat, lstat, unlink, rmdir, mkdir, rename, chdir,
readlink, realpath, opendir and readdir route through it, with each
refusal as POSIX's errno; symlink works where a file server holds the
name, which refuses one that exists, and stays ENOSYS on the kernel's.
stdout and stderr write a descriptor through it, so a served file at 1 or
2 is written as one. A corpus case writes, reads, links, resolves, renames,
lists and removes files under /home.

A posix_spawn addopen of a served path answers EOPNOTSUPP until a served
file can be given to a child. realpath of a served path follows its last
name's links, POSIX-relative to the link's directory; the server resolves
the names before it. A refused fd: endowment is now said on stderr, once
the table's lock is let go.

rust: library/Cargo.lock names toyos's alloc edge (9d8bb4c347f).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lowest free descriptor at or above the argument, marked close-on-exec, as
POSIX has it. 206_libc_refusals still expected the old refusal (-1,
ENOSYS), and as a metal-only corpus case nothing ran it before the T14 boot
of #988, which answered 10. The record of what libc refuses loses the
same stale lines, and F_DUPFD and F_GETFD's departures with them: both are
POSIX's now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 of #988.

An open of a served file installed its descriptor on the capability's
turn. On a full table the table let the new open file go, whose close
asks the server on that same turn, so the thread waited on itself and
every later user of the directory behind it. The open now hands back
what it opened and installs it once the turn is let go, and a full table
answers the table's own refusal, EMFILE in C and QuotaExceeded in std, on
/tmp and on a served path alike; the kernel arm answered EINVAL and the
served one ENFILE. A corpus case fills the table and opens both.

libc's realpath resolves the names a file server holds as POSIX does, one
at a time: a link before the last name is followed, `..` after one is the
parent of where it led, and a relative one is read against its own
directory. At the first name the kernel holds it hands the rest to the
kernel's resolver, so a served link to /tmp or /system resolves there,
where it answered ENOENT for a file that exists.

The turnstile's sleep is private, a futex, and a yield in a host test,
rather than a public trait and type parameter that existed for the test.

A descriptor that names no kernel handle is refused a child with
EOPNOTSUPP (std's Unsupported), as an addopen of a served path is; a
kernel NotSupported is EOPNOTSUPP in C. A served refusal of a name the
volume cannot store is EINVAL. std's stdin reads whoever serves it.

Issue records this made stale are brought up to date:
- a served file's raw fd is retired;
- the served flush issue is now about fsync, since flush is a no-op as
  everywhere else;
- the kernel's non-exclusive create names C's O_EXCL too;
- /tmp's symlink is left with the remaining exit only;
- two records and grants.rs point at toyos::files;
- a served file at stdio stopping every spawn is recorded;
- the corpus licence counts the four new files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 of #988.

realpath handed a path to the kernel at the first name no file server
held, so every path under a folder grant deeper than one name, such as
fs:/home/toy/Games without fs:/home, went to the kernel at /home and
answered ENOENT. The handoff is now asked of the whole path as it stands,
and a name above the capability that holds it is passed through as a
directory, as the client's routing passes it. A host case with a
three-name capability pins it.

212_full_table filled the table with /dev/null opens, each a kernel
handle, and on the T14 the kernel's handle table ran out first: its
4096 slots are MAX_FDS's, and the process's own handles take some of
them. libc answered that EINVAL, and the served open, which needs no
kernel handle, then succeeded. The case now fills the table with
duplicates of one /home descriptor, which take no kernel handle, so the
/home open runs the install-after-turn path that hung at 2d3c5d8, and
then fills the kernel's table on its own. toyos::fd::MAX_FDS is the
kernel's slot count by name.

libc answers the kernel's ResourceExhausted ENOMEM, as std answers it
OutOfMemory, where it fell to EINVAL. It is the kernel's one word for a
full handle table and for no memory, so a full table cannot yet be EMFILE;
issues/a-full-kernel-handle-table-answers-enomem-not-emfile.md records it.

A rename between two of the kernel's mounts is EXDEV, as one between a
server and the kernel is, rather than EOPNOTSUPP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the wt/toyos-fd-served branch from 4327073 to 2b122fe Compare October 21, 2026 21:57
Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Oct 21, 2026
…aeb547dc38528edf: --ci host, builds, 3 QEMU rows; masked
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto b9e4c5862 (#979, #984, #987). Head: 2b122fe851d1457a195a7e40aeb547dc38528edf.

The conflicts were all in issue text. Main's audits renamed things in five records that this PR edits or retires:

  • the two this PR retires (c-programs-name-no-file…, a-served-file-panics…) stay retired;
  • libc-refuses… takes main's text, which already drops the same F_DUPFD_CLOEXEC line;
  • the flush record keeps main's added FSYNC detail, under its new name;
  • the symlink and create-new records combine both sides, with main's file-server naming.

The code merged cleanly. The only file both sides touched is sdk/toyos/src/lib.rs's module list (#979 added job, and this PR adds files). Nothing outside this PR used the turnstile seam it removes.

The rust gitlink stays at 9d8bb4c347f, whose parent is main's a56b4057c.

Checks at the head: --ci host 0, both builds 0, and QEMU libc_children, console_image_boots and nvme_disk_boots 3/3. Evidence: https://github.com/ToyOSOrg/evidence/tree/9e8bb77f329b21ff20e5789ed0c92b1d6f03d166/r3-rebase

The T14's 146/146 at 43270738e carries over: the resolution changed only issue files, and the code merge was mechanical.

The checks script's parent expression is now git merge-base HEAD origin/main.

@Japabu
Japabu added this pull request to the merge queue Oct 21, 2026
Japabu added a commit that referenced this pull request Oct 21, 2026
The owner asked whether everything self-hosting needs is written down.
toyos-builds-itself.md held it in five lists in an order it called open:
what to build, what other tracks block, what stops M2, what M3 adds and
what else is owed. It is now one list in dependency order: each item has
its status, the issue that owns it and what blocks it, and the evidence it
rests on is below it, with libc's revision for the compiles.

The status words say only what is true: "direction set" is the owner's
ruling of what an item must do, with its design still engineering's. The
memory and socket designs, which are outside the tree and under review, are
named as such, and so is the sequencing the socket design proposes for the
descriptor work's spawn.

Folded in:
- C reaches served files (#975, #978, #988);
- #970's toolchain package;
- the memory direction;
- the process track's stages 4 to 7;
- socket sharing;
- the boot handoff, beside the path;
- the database feasibility study's libc findings;
- the issues that name this track as their owner, among them pread, libc++'s
  std::filesystem, and the two that keep a build from reproducing.

Each owner ruling of 2026-10-21 is recorded once, in its owning issue:
- the signal scope in the process track;
- sharing rather than moving, in the socket issue, whose exit described a
  move;
- the database rulings in a new track, split from the planning
  dispositions made under delegation.

Four findings get a home:
- unlink-while-open;
- libc's uid and gid;
- sigaction's false success;
- st_nlink 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged via the queue into main with commit 89372e9 Oct 21, 2026
4 checks passed
@Japabu
Japabu deleted the wt/toyos-fd-served branch October 21, 2026 22:53
Japabu added a commit that referenced this pull request Oct 21, 2026
The owner asked whether everything self-hosting needs is written down.
toyos-builds-itself.md held it in five lists in an order it called open:
what to build, what other tracks block, what stops M2, what M3 adds and
what else is owed. It is now one list in dependency order: each item has
its status, the issue that owns it and what blocks it, and the evidence it
rests on is below it, with libc's revision for the compiles.

The status words say only what is true: "direction set" is the owner's
ruling of what an item must do, with its design still engineering's. The
memory and socket designs, which are outside the tree and under review, are
named as such, and so is the sequencing the socket design proposes for the
descriptor work's spawn.

Folded in:
- C reaches served files (#975, #978, #988);
- #970's toolchain package;
- the memory direction;
- the process track's stages 4 to 7;
- socket sharing;
- the boot handoff, beside the path;
- the database feasibility study's libc findings;
- the issues that name this track as their owner, among them pread, libc++'s
  std::filesystem, and the two that keep a build from reproducing.

Each owner ruling of 2026-10-21 is recorded once, in its owning issue:
- the signal scope in the process track;
- sharing rather than moving, in the socket issue, whose exit described a
  move;
- the database rulings in a new track, split from the planning
  dispositions made under delegation.

Four findings get a home:
- unlink-while-open;
- libc's uid and gid;
- sigaction's false success;
- st_nlink 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
The owner asked whether everything self-hosting needs is written down.
toyos-builds-itself.md held it in five lists in an order it called open:
what to build, what other tracks block, what stops M2, what M3 adds and
what else is owed. It is now one list in dependency order: each item has
its status, the issue that owns it and what blocks it, and the evidence it
rests on is below it, with libc's revision for the compiles.

The status words say only what is true: "direction set" is the owner's
ruling of what an item must do, with its design still engineering's. The
memory and socket designs, which are outside the tree and under review, are
named as such, and so is the sequencing the socket design proposes for the
descriptor work's spawn.

Folded in:
- C reaches served files (#975, #978, #988);
- #970's toolchain package;
- the memory direction;
- the process track's stages 4 to 7;
- socket sharing;
- the boot handoff, beside the path;
- the database feasibility study's libc findings;
- the issues that name this track as their owner, among them pread, libc++'s
  std::filesystem, and the two that keep a build from reproducing.

Each owner ruling of 2026-10-21 is recorded once, in its owning issue:
- the signal scope in the process track;
- sharing rather than moving, in the socket issue, whose exit described a
  move;
- the database rulings in a new track, split from the planning
  dispositions made under delegation.

Four findings get a home:
- unlink-while-open;
- libc's uid and gid;
- sigaction's false success;
- st_nlink 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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