feat: mountx/exec - #9
Conversation
The proot-style spikes differ almost entirely in *which binaries they can serve*, so the comparison is only honest if the workload is identical and the linkage is the only variable. One C source built two ways (dynamic glibc, static musl) plus a no-libc variant issuing raw `syscall` instructions, which stands in for a Go binary without needing a Go toolchain. `probe-raw.c` needs `-ffreestanding -fno-builtin` or the compiler turns its hand-written loops back into calls to the libc it deliberately does not have. `demo-driver.ts` is the tree all three spikes are pointed at, including 3 MiB of deterministic bytes so a checksum catches a short read or a torn offset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`execUserns(driver, argv)` runs a command with an `FsDriver` visible to it and to nothing else on the machine: a FUSE mount made inside `unshare -Urm`, where `getuid()` is 0 and `src/fuse/mount.ts`'s ordinary root path works verbatim — no `fusermount3`, no native addon, no privileges. The mount never appears in the host's mount table and dies with the namespace. The driver stays in the parent because it has to. `unshare(CLONE_NEWUSER)` requires a single-threaded caller and Node never is; `setns(2)` has the same rule. So a helper enters the namespace, holds `/dev/fuse`, and pumps raw FUSE traffic over a unix socket to a `FuseSession` here — which is the "relay mode" the roadmap defers, arrived at from the other direction. FUSE messages are self-framing, so the socket needs no envelope of its own. Two hazards witnessed and handled: giving the spawned command a `cwd` inside the mount wedges the relay in `D` state at `fuse_get_req` forever (the documented `uv_spawn` deadlock, met from the inside), and a killed parent otherwise orphans a wedged mount. Spawns `unshare -U -r -m` rather than the long options: busybox 1.37 has `-r` and no `--map-root-user` at all, and this is verified working on bare Alpine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ocess The structural finding behind both no-mount spikes: a syscall interceptor does not need a filesystem, it needs a *client*. Path resolution, handle lifetimes, directory paging and error mapping are already settled by `src/9p/session.ts` and already covered by a conformance column, so an interceptor that speaks 9P is a wire adapter with an fd table and no filesystem logic at all. Constants and message layouts are transcribed from `src/9p/constants.ts` and `src/9p/protocol.ts`, same rule the TypeScript side is held to. Socket I/O goes through raw `syscall` instructions rather than libc, because in the `LD_PRELOAD` case this code *is* libc's `read`/`write` and calling them would re-enter the shim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kept as evidence, not as a direction. It works — `ls -la`, `cat`, `grep`, `tail`, `sha256sum`, `find`, `du` and a full `cp -r` all behave across 49 exported symbols — and it should not be shipped. Nothing references it outside its own runner and the comparison harness. Getting there took seven discoveries whose pattern is the finding: `statx` is reached internally without a PLT hop; `fopen` bypasses `read` entirely (the obvious bridge made `sha256sum` return the hash of the empty string with exit status 0); `__read_chk` is a separate symbol under the fortification that is default on Debian, Fedora and Ubuntu; `fdopendir` is where `find` dies; `*at()` resolution is needed in three symbols independently; `fts` dups the directory fd; `getxattr` must be answered or `ls -l` marks every mode string `?`. So the surface is the POSIX names crossed with the `64` suffix, the `__*_chk` suffix and the legacy `__xstat` forms, with the landing site fixed by the glibc a program was *compiled* against — a maintenance surface that grows with other projects' releases. And one hole cannot be closed from inside the process: a descriptor this shim creates does not survive `exec`, because the fd number is inherited while the table backing it is in memory `exec` discards. `wc -l < file` silently reads empty. Every one of its worst failures is a confident wrong answer rather than an error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
proot done the modern way, and the mechanism that actually answers the
question. A BPF filter traps eight filesystem syscalls and everything else runs
natively without leaving the kernel. Because the boundary is the syscall ABI
rather than glibc's exported symbols, the traced program's linkage is
invisible: a static musl binary and a no-libc raw-syscall binary — neither of
which spike B can see at all — pass byte-exact.
No descriptor is passed anywhere. The usual shape forks, has the child install
the filter and hands the listener back over `SCM_RIGHTS`; a filter is inherited
across fork and exec, so the supervisor installs on *itself* and keeps the
listener. The price is a rule the file must keep — after installing, the
supervisor may never make a trapped syscall, or it suspends waiting for a reply
only it can send. Violating it (an `open("/dev/null")` in the openat handler)
hung everything the first time anything opened a directory.
An `openat` of a regular file is answered by slurping it over 9P into a `memfd`
and injecting that with `SECCOMP_IOCTL_NOTIF_ADDFD`, so `read`/`lseek`/`mmap`
afterwards are native and untrapped. The cost is stated rather than hidden:
whole file in memory, no write-back, read-only as spiked.
Also measured: `fstat` is its own syscall number and `opendir` checks with it;
musl uses the legacy `open`(2)/`stat`(4)/`lstat`(6); duplicated descriptors are
identified by naming every injected `memfd` uniquely and reading it back from
`/proc/<pid>/fd`, which survives `dup`, `fork` and `exec` for free; and not
trapping `close` costs correctness rather than just memory, since fd numbers
are reused and a stale mapping shadowed a live one until eviction was added.
Portable to musl: `ioctl` goes through a raw syscall because glibc's prototype
takes `unsigned long` and musl's takes `int`, and the `SECCOMP_IOCTL_*` numbers
have the high bit set. Reads of tracee memory are clamped to the end of the
current page, since `process_vm_readv` fails the whole request if any part is
unmapped — an ASLR-dependent flake when a path was the env string at the top of
the stack.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Builds all three mechanisms and the three probe linkages, then runs one identical workload through each. Needs a Zig toolchain and unprivileged user namespaces; no root anywhere. Distinguishes "fail" from "WRONG DATA" deliberately: one of the three mechanisms was briefly capable of returning a clean, confident, wrong answer, and a harness that only checked exit status would have called it a pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`.agents/proot-plan.md` is the measured comparison and the recommendation: drop the `LD_PRELOAD` approach, land the user-namespace one for the common case, and pursue the seccomp one for the case that motivated the question — because the environments where "no kernel mount" is actually wanted are exactly the ones that withhold `/dev/fuse`, and there the FUSE route cannot be made to work from inside by any means. `.agents/environment.md` gains what was verified on this host while spiking: there is no `fusermount3` here at all, unprivileged user namespaces are the way around that, Node can never enter one itself, and seccomp user notification works unprivileged even under the filter the shell already runs beneath. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
`exec(driver, argv)` runs a command with a driver grafted onto its filesystem view: the command and everything it spawns see the driver at `$MOUNTX_ROOT`, and nothing else on the host does. `probeExec()` publishes what each mechanism can do here and why not, in the shape `probeTransports()` does, and `exec()` takes the first usable one in preference order — the user namespace where the kernel's FUSE is usable, the seccomp supervisor otherwise, which is the case that motivated the question in the first place: a container that withholds `/dev/fuse` withholds it from a namespace root too (`mknod` answers EPERM). Both arrive through `await import()`, so choosing one loads neither the other's codec nor the other's session, and the result is the mechanism's own result object with a `mechanism` discriminant defined on it — tagged, not wrapped. No fallback after a failure and no probe when a mechanism is named, both for `src/auto.ts`'s reasons. `src/exec/probe.ts` is import-light like `src/nfs/probe.ts` and `src/9p/probe.ts` — `node:fs` and nothing else — so asking never pulls in a codec. It names the causes a caller can act on rather than reporting one errno: no device node, a device that will not open (and why the namespace does not rescue that), each distribution's idiom for disabling unprivileged user namespaces, a missing `unshare`, an architecture the seccomp filter was not written for, and a supervisor that is not built. Deliberately outside `mountx/auto`, whose contract is a mountpoint this produces none of — the same line `mountx/s3` sits on. `execUserns()` comes up to shipping quality with it: the host verdict up front instead of an ENOENT from three processes away, `cwdRefusal()` for the witnessed spawn deadlock, a mountpoint that is created and checked here, and a relay failure that reaches the caller as an error rather than masquerading as the command's own exit status. `default_permissions` is no longer the default, because the kernel checking a driver's uid against a namespace that maps exactly one is what makes every write fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`spike-a.ts` and `spike-b.ts` become `demo-userns.ts` and `demo-preload.ts`, beside the `demo-driver.ts` they already shared: the spike numbering was scaffolding, and one of these two is now the shipping mechanism behind `mountx/exec` while the other is the rejected one. Both stay test benches rather than entry points, and both keep calling their mechanism by name — the value of `test/exec/compare.sh`'s matrix is that each column is one named mechanism and not whatever the picker would have chosen. `preload.ts` gains the verdict in its own header, so the file says what it is without a trip to `.agents/proot-plan.md`: rejected, kept as the evidence, reachable from its runner and the comparison harness and from nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`strategy.test.ts` is Tier 0 and answers for darwin, win32 and arm64 from any host through the `platform`/`arch`/`supervisor` overrides, the way `test/auto.test.ts` does: the preference order, the reason each mechanism is ruled out, the refusal to answer for Linux from a host that cannot read Linux's files, and the named-mechanism paths that must not consult the probe. `cwdRefusal()` is covered here too — checking it any other way costs a hung process. `userns.test.ts` is Tier 2 and needs no root, only `/dev/fuse`, user namespaces and `unshare`; it skips itself without them and, like the FUSE rootless suite, unless the threadpool has been raised. It does not re-test the filesystem — what the command sees is FUSE, which has its own conformance column — only what is different: the driver at `$MOUNTX_ROOT`, a write that lands in the driver, the mount staying out of the host's mount table, the command's own status surviving, and a relay failure arriving as an error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It goes in Transports rather than the guide for the reason S3 already did: a transport is everything between the driver and whatever is going to read it, and "whatever is going to read it" being one command rather than the machine does not change what the page has to explain. The section intro now leads with five ways rather than four, and the two that produce no mountpoint are grouped as such. Guide-level prose first — what it is for, what the two mechanisms are and which host facts decide between them, `$MOUNTX_ROOT` and the one rule about `cwd` — then the full export surface. The reference index gains the tenth subpath and a row in the platform table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The code map gains an `Exec` section at the density the rest of it keeps — what each file is, and the reasons behind the choices somebody would otherwise re-litigate: why the probe opens `/dev/fuse` instead of stat-ing it and why it does so before reading `/proc/filesystems`, why the namespace is entered by a child, why `default_permissions` is not a default, and why `LD_PRELOAD` is still in the tree while being reachable from nothing that ships. The tests section gains `test/exec/`, and the `test:rootless` line gains the one mount column that needs neither root nor a `fusermount3`. `.agents/proot-plan.md` was the spike write-up and is now the record behind a shipped feature, so it says so at the top and marks each mechanism's fate in its own table; its measurements are untouched. `default_permissions` is added to spike A's list of witnessed costs — it was not found during the spike because the spike's workload never wrote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`execUserns()` spawns the relay rather than importing it, so it has to be a file with a path — which is why it is a build entry of its own. What does not survive is the assumption that it stays *this* module's sibling: `src/exec/index.ts` reaches `userns.ts` through `await import()`, and obuild answers a dynamic import with a chunk, so the built module is `dist/_chunks/userns.mjs` and the relay is one directory over in `dist/exec/`. Caught by running the built package rather than the source, which is the only way this shows up at all. Both layouts are now checked, and a third would announce itself as the named error rather than as an ENOENT from `spawn`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spike's client could walk, open, read and list — enough to prove a traced process can be served, and nothing a program that writes can use. This adds `Tsetattr`, `Tmkdir`, `Tunlinkat`, `Trenameat`, `Tsymlink`, `Treadlink`, `Tlink`, `Tmknod`, `Tstatfs` and `Tfsync`, every one of them transcribed field for field from `src/9p/protocol.ts` the way the rest of the file already is. Two things the walk itself needed. Paths are now walked in `P9_MAXWELEM`-sized steps *in place* rather than refused past the sixteenth component, so a deep path costs one fid rather than an error; and `walkOnce` exposes a single step, which is what a supervisor resolving symlinks component by component needs — `Twalk` stops at a symlink and a server that resolved links itself would have no way to answer `lstat`. Fid numbers are recycled through a fixed free list, and only after a `Tclunk` the server acknowledged: a counter that only goes up is fine for a spike and wrong for a supervisor that outlives a `cp -r`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spike answered `openat` by slurping the whole file over 9P into a `memfd` and injecting that. It bought native `read`/`lseek`/`mmap` for free, and it had one failure mode that is the worst this project has: because a `memfd` is always writable and `write` was not trapped, a program's writes landed in a private in-memory copy and vanished. `dd conv=notrunc` reported success and changed nothing; `rm -f` reported success and the file was still there. Silent data loss. So the copy is gone. The injected descriptor is now an *empty* placeholder whose only jobs are to be a number the kernel agrees exists and to carry an identity through `/proc/<pid>/fd/<n>` that survives `dup`, `fork` and `exec` — and `read`, `write`, `lseek`, `readv`, `writev` and the `p*` forms are trapped and answered from the driver against a per-open-file-description offset. `dup` shares that offset, because the offset lives on the description and descriptors are reference counts on it, which is what POSIX says and what the `LD_PRELOAD` spike could not manage at all. **`close` is trapped now**, which needed the supervisor to stop sharing a filter with its tracee: the child installs the filter and hands the listener back over `SCM_RIGHTS`, the shape `native/src/main.zig` already implements for `fusermount3`. A supervisor with no filter of its own may call anything, so it may also trap `close` — and without that, descriptor numbers are reused immediately and a stale mapping shadows a live one. What else now works, none of which the spike attempted: `creat`, `mkdir`, `unlink`, `rmdir`, `rename`, `symlink`, `readlink`, `link`, `mknod`, `chmod`, `chown`, `utimensat` and the three legacy time calls, `truncate`, `access`, `statfs`, `fsync`, and a virtual working directory that `chdir`, `fchdir` and `getcwd` agree on, so `cd $MOUNTX_ROOT && ls` behaves. Symlinks are resolved here, component by component, exactly as `test/9p/client.ts` does it for the conformance column. Four things are refused rather than faked, because each of them would otherwise be a confident wrong answer against an empty placeholder: `mmap` of a file on the tree (`ENODEV`), `sendfile`/`splice` (`EINVAL`), `copy_file_range` (`EXDEV`) and `fallocate` (`ENOTSUP`) — the last three with the errno that makes callers fall back to `read`/`write`, which are answered properly. Extended attributes answer `ENOTSUP`, which is what the 9P transport answers anyway. `execve` of a binary living on the tree stays out of scope and is recorded as such. Every handler returns a reply value the loop sends, so "answered twice" and "never answered" are unrepresentable rather than merely avoided; the tables grow instead of silently dropping the 257th descriptor; and `seccomp_notif.pid` is finally read as what it is — a *thread* id — with descriptors, cwd and umask keyed on the thread group behind it, which is what a multi-threaded tracee needs. One bug worth naming because it was invisible: memoising the `/proc` identity walk under the new descriptor number looked like an obvious win and made `echo hi > $ROOT/file` claim the shell's standard output for the rest of the run, since the `dup2` that restores it cannot be seen as a replacement. The walk is no longer cached, and `dup2`/`dup3`/ `close_range` are trapped for the bindings that are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every transport in this repository carries a column of the matrix, and this is the seccomp supervisor's: `test/conformance.ts` unmodified, with the driver at the far end of a syscall boundary. It needed a shape the other columns did not. A transport is normally reached through a client library — `test/9p/client.ts` speaks 9P, `test/nfs/v4/client.ts` speaks COMPOUND — but this supervisor has no client, because its interface *is* the syscall ABI. The only way to drive it is to be a process making syscalls. So `seccomp-helper.ts` is a filesystem REPL that runs under the filter and does what it is told with `node:fs/promises`, and `seccomp-client.ts` is an `FsDriver` over the pipe to it. A single `fs.stat()` in the suite becomes a `statx(2)` in a traced process, a notification, a 9P walk, an `Rgetattr` and a `struct statx` written back into that process's memory, with nothing short-circuited anywhere. 65 of 66 cases pass; the skip is the root-only `lchown` one every column skips. Using `node:fs/promises` rather than the sync API is deliberate twice over: it is exactly what the loopback column runs against, so a disagreement is the supervisor's rather than the API's, and it puts every request on a libuv threadpool thread — so the notifications arrive from a thread that is not the one that started the process, which is the multi-threaded case the tables have to key on a thread *group* to survive. `seccomp-run.test.ts` is the narrower, more literal file: it runs the shell, `dd`, `mv` and a static no-libc binary, and asserts on the **driver** afterwards rather than on what the command printed — because "the command exited 0" is precisely what the old silent-data-loss failure looked like. It also covers eight concurrent tracee processes, a descriptor a child inherited after the parent closed it, a path deeper than one `Twalk` can carry, and four hundred sequential opens. `execSeccomp` grows two optional fields, `stdio` and `onSpawn`, because a command that is being *driven* needs its streams and its handle while the promise is still outstanding. Nothing existing changed shape. All three files skip cleanly without a Zig toolchain or off x86-64 Linux, and none of them needs root — which is the property the whole mechanism exists for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`walkTo` clunks its own fid before recursing into a followed symlink, and an `errdefer` then clunked it a *second* time on the way out of a resolution that failed. The server answers `EBADF` for a fid it has already forgotten, and that errno overwrote the `ENOENT` the caller was about to report — so a dangling symlink came back as `EBADF`, a symlink loop came back as `EBADF` instead of `ELOOP`, and an exclusive open of a link came back as `EBADF` instead of `EEXIST`. Cleanup is explicit now rather than deferred, which is the only shape that can express "this path already released it". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The harness compares three interception mechanisms on one workload, and until now that workload only read. Write-back is the axis that actually separates them, and it is the one where a failure is silent: the seccomp supervisor answered every write into an injected `memfd` that was then discarded, so `dd conv=notrunc` and `rm -f` both reported success and changed nothing. Four commands, four expected words. Anything else printed there is data being lost quietly, which is worse than failing. The entry point's own header stops calling itself a spike while it is here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A `cd` out of the tree that *fails* no longer clears the virtual working directory. A shell whose `cd /nowhere` failed is exactly where it was, and forgetting on the attempt meant every relative path afterwards resolved against the real working directory instead — somewhere else entirely. The supervisor carries no filter of its own, so it can simply ask the kernel whether the target is reachable before believing the process left. `mknod`'s `dev_t` is unpacked with glibc's own encoding (`bits/sysmacros.h`) rather than a mask that dropped a nibble of the minor number and all of the high major bits. And the thread-id cache is pruned on `exit_group`. It is the one table keyed on something that dies with the process rather than being reused, so it is also the one that would otherwise grow for the whole run of a command that spawns steadily. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`zig fmt` over the three new files, plus two things that were describing something that is no longer true: the placeholder descriptor's supervisor-side copy is closed the moment it is injected (identity comes from `/proc`, not from holding it open), and there is no way to destroy a file record at all — its fid is released when nothing names it, and the record stays so that a descriptor resolved through `/proc` can re-open it rather than being answered from an empty placeholder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`spike-c.ts` was held back from the rename while the seccomp supervisor was being implemented in parallel, to keep two concurrent efforts off the same file. It is `demo-seccomp.ts` now, with the same header the other two got, and `test/exec/compare.sh` selects all three columns by mechanism name rather than by a spike letter — which also fixes the harness, whose A and B columns had been pointing at paths the earlier rename moved. `.agents/proot-plan.md`'s account of spike C's costs is brought up to date: the `memfd` slurp and the read-only limitation are both closed, and the read-only one is worth keeping visible rather than deleting, because it did not fail cleanly — writes landed in an in-memory copy and vanished, which is the same silent-wrong-answer class this document rejects spike B for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`preload.ts`, `preload/shim.zig` and `demo-preload.ts` are gone. They were kept for a while as the evidence for their own rejection, which is a job prose does better than code: `.agents/proot-plan.md` still carries the measurements, and they are what justify the shape of the two mechanisms that remain — a boundary the kernel defines rather than one glibc does. The 9P client they shared with the supervisor stays, and moves to `src/exec/seccomp/p9.zig` now that `preload/` no longer names anything. It is still the reason the supervisor contains no filesystem. `test/exec/compare.sh` loses its third column and its shim build; both probe linkages and the write-back row are unchanged, and A and C still agree on every one of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Superseded by #10, which ships the user-namespace mechanism (A) on a branch cut fresh from Closing rather than merging, deliberately: this branch is where the seccomp user-notification supervisor (mechanism C) lives, and it is worth keeping reachable. Nothing of it is on #10. What is preserved here, at
Why C is not shipping now: the probe prefers A wherever the kernel's FUSE is usable, which is nearly everywhere, and C is only better when One thing found while porting and worth carrying forward: a child could not |
Draft.
mountx/exec— run a command with anFsDrivergrafted onto its filesystem view. New subpath, deliberately not wired intomountx/auto(auto's contract is a mountpoint; this produces a child's exit status).Started as a question — can we do a
proot-styleexec()without a real kernel mount? — and became three measured mechanisms, of which two ship. Full write-up in.agents/proot-plan.md.The strategy
exec()picks A (FUSE in an unprivileged user namespace) wherever the kernel's FUSE is usable, and C (seccomp user-notification supervisor) otherwise. Followingsrc/auto.ts's precedents exactly: the probe publishes what each mechanism can do here and why not; each arrives viaawait import(); no fallback after a failure (the probe decides once from host facts — "fallback" means the probe choosing C, not retrying C after A errored); no probe at all when a mechanism is named.The structural finding
An interceptor does not need a filesystem. It needs a client. Both no-mount mechanisms translate intercepted operations into 9P against an unmodified
createP9Server()— the same servermount9p()points v9fs at. Path resolution, handle lifetimes, directory paging and error mapping stay insrc/9p/session.ts. That's why one Zig 9P client (src/exec/preload/p9.zig) serves two completely different interception mechanisms, and why neither contains filesystem logic.Measured —
sh test/exec/compare.sh, no rootLD_PRELOADWrite-back — asserted against the driver, not against exit status:
Bare Alpine
unshare/mount)--cap-drop=ALL, uid 1000CONFIG_FUSE_FS+/dev/fuseCONFIG_SECCOMP_FILTER, built in, cannot be a modulealpine:latesthas no/dev/fuseand a userns root cannot create one (mknod→EPERM, verified) — which is exactly the case that selects C.Notable findings
default_permissionssilently made A read-only.unshare -rmaps one uid; a driver reporting the serving process's real uid is an identity the namespace doesn't map, so the kernel renders itnobodyand every write isEACCES— namespace-root'sCAP_DAC_OVERRIDEdoesn't reach an unmapped owner. Removed from the defaults; the mount carries noallow_other, so permission checking belongs to the driver.memfds are always writable andwritewas untrapped, so writes landed in the in-memory copy and vanished —dd conv=notruncandrm -fboth reported success and changed nothing. Now streamed per open-file-description with adup-shared offset, andcloseis trapped, which required dropping the shared filter: the child installs it and passes the listener back overSCM_RIGHTS, reusingnative/src/main.zig's cmsg transcription.unshare(CLONE_NEWUSER)needs a single-threaded caller and Node never is, so the driver can't live in the namespace and FUSE traffic comes back out over a socket.Coverage
test/exec/seccomp-conformance.test.tsruns the full conformance column through a traced process —test/conformance.tsunmodified, 65 passed / 1 skipped (the root-onlylchowncase every column skips). Plus Tier-0 supervisor tests, 24 Tier-2 real-command cases asserted against the driver, and strategy/probe tests answerable from any host. All skip cleanly without zig or off x86-64 Linux; none needs root.pnpm test: 2885 passed, 275 skipped, 0 failed. Built package verified end to end (dist/exec/index.mjspicks userns and round-trips a write).Deliberately not done
exec— a hole that cannot be closed from inside the process.mmapof a file on the tree (ENODEV, loud),execveoff the tree, arm64.mountx/autowiring.DYLD_INSERT_LIBRARIES. It stays NFS-mount territory.🤖 Generated with Claude Code