Skip to content

M43: the sandbox -- compile and run an arbitrary mc program in isolation - #23

Merged
schivei merged 26 commits into
mainfrom
m43-sandbox
Sep 5, 2026
Merged

M43: the sandbox -- compile and run an arbitrary mc program in isolation#23
schivei merged 26 commits into
mainfrom
m43-sandbox

Conversation

@schivei

@schivei schivei commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

M43 -- the sandbox: mc sandbox run|exec|check (Layer 1)

Spec: docs/specs/M43.md (Layer 1; the 13 ratified decisions; § Implementation notes A-D, where every place the kernel disagreed with the spec is recorded with the measurement). stage0/, lib/ and tests/*.mc untouched (acceptance 12).

What lands

  • <mc/core_sandbox>, the sixth part: subcommand("sandbox", ...), written in mc against the kernel ABI through one host-layer shim per architecture (host_syscall6: eight #opcode words on AArch64, six emit() words on x86-64 verified with llvm-mc before the file existed) and a per-architecture number table (src/sysno*.mc; sandbox*.mc names no number). No external tool in the mechanism (decision 1/2).
  • The box: four processes -- P supervises, I unshares the six namespaces and builds the tree (tmpfs root, /mc ro, /src as overlayfs over the source directory, /lib+/lib64+/usr/lib+/etc/ld.so.cache ro when present, pivot_root), J is pid 1 of the pid namespace (an init that exits accepts no new process and a second unshare(CLONE_NEWPID) is EINVAL, so the spec's three became four), C is the step. Maps 0 <uid> 1 unprivileged / 0 0 65536 root, one code path.
  • The two walls: Landlock (ABI probed at runtime, 8 on the baseline) and seccomp-BPF with SECCOMP_USER_NOTIF + NEW_LISTENER; the listener reaches P by two pidfd_getfd hops (C -> J -> P, Yama scope 1 allows both).
  • The explain channel: every syscall outside the profile reaches P, which names it -- refused: syscall N (name), refused: open PATH (before the kernel even answers ENOENT), refused: mmap N bytes over the cap (M), refused: process limit (64), refused: execve -- and kills the box, exit 125; caps are killed: cpu limit (S s) / killed: wall clock (S s), exit 124; a box that cannot be set up is cannot ..., exit 126. A refused call is killed and not answered (answering woke the step and made the report non-deterministic).
  • Profiles are measured, not written: scripts/sandbox-trace.sh (strace -fc outside the box, both architectures, musl and glibc, the --allow=threads delta) generates src/sandbox_profiles.mc; --check diffs a fresh trace against the checked-in table both ways and runs in CI.
  • mc sandbox check: kernel, userns (a TWO-stage probe: with Ubuntu's apparmor_restrict_unprivileged_userns=1 the unshare SUCCEEDS and the denial comes at the first mount), landlock ABI, seccomp notif, overlay (loaded, not merely present), pidfd.
  • Hosts: Linux native (root and unprivileged); macOS/Windows refuse with the Lima command, exit 126; Docker only with --privileged. scripts/test-sandbox.sh delegates from macOS to Lima, then Docker, else one SKIPPED line; make test-sandbox is inside make check.

Measured (Ubuntu 26.04, kernel 7.0.0-30, Lima aarch64 + VPS x86_64, root and unprivileged, plus alpine:3 --privileged on 6.12): isolation cases 8/8 -> after the review 10 (shadow, connect, forkbomb, eightgib, forever, sleeper, clean, rocwd, nsclone, nsclone3, plus the linkbomb/ project with a hostile mc.toml), every one with the exact report line and exit code; the whole suite through mc sandbox run (31/31 aarch64, 29/29 x86_64, identical exit/stdout), mc sandbox exec on static AND dynamic (PT_INTERP) M42 binaries with only /lib bound, examples/lang taught and run inside (compile: execve 2), host tree untouched; clean.mc byte-identical to the unsandboxed run; --report files cmp-equal across runs; host process count and free memory unchanged across the isolation set. Final cells: Lima aarch64 root/unprivileged 55/55, VPS x86_64 root/unprivileged 53/53, CI four cells 55/55 x2 + 53/53 x2. Box cost: ~1.7-1.8 ms aarch64, ~4-4.3 ms x86_64, ~2.1 ms on the runners; the filter adds ~0.3 ms (+21%). What the kernel corrected in the spec is in § Implementation notes A-D: four processes not three, the maps root accepts, userxattr, /etc/ld.so.cache for glibc's loader, a refused call killed and not answered, strace -c dropping exit_group, profiles as a union over glibc versions, lex_readable without c_int() (an M45 miss that broke [include].paths on glibc hosts -- fixed here).

Globals: the whole milestone is ONE global (sb_state, an arena record with accessors) -- mc limits src/mc.mc globals 432/512 against the frozen seed's MAXGLOBALS, check-limits 17/17 under 90%.

Gates: make check RC 0, zero FAIL on macOS (budget 2848/3000, test 32/32, check-lex/ast/asm 134/134, check-obj 32/32 identical to the frozen seed, bootstrap at a fixed point with an empty --dump-asm diff, check-surface 32/32 + the eight AArch64 shim words, check-parts + the six x86-64 words + the part standing alone + a compiler WITHOUT the part refusing mc sandbox, check-shim rc 0 on both Linux hosts, check-limits 17/17, the Linux/Windows suites and --exe sets unchanged, test-sandbox 55 ok / 1 skipped via Lima, check-docs 192 symbols / 33 flags / 50 samples / 320 links, site 87 pages); make check-linux-host RC 0 over the four musl/gnu x aarch64/x86_64 cells with the cross proof; scripts/check-inert.sh against a mc1 from main: 33 objects and the five taught examples identical.

Goldens (five, rewritten per step after their own criterion; final): mc2.sha256 9e7b803f127cb6f1e059c1e6572a629bfa909cfbecbfae18b01abd1fd7a2d431 (empty --dump-asm diff + cmp mc2.o mc3.o); mc2-linux-arm64 182a4c6d…036679 and mc2-linux-x86_64 e63d09bc…d99ffa (deleted and re-recorded by make check-linux-host); mc2-windows-arm64 dcaac914…4256c3 and mc2-windows-x86_64 dfaf002c…a97b861 (cross-computed per tests/golden/README.md, also written byte for byte by build/mc2).

CI: two new jobs, The sandbox (linux/arm64) (ubuntu-24.04-arm) and The sandbox (linux/x86_64) (ubuntu-latest): the AppArmor sysctl flipped both ways with mc sandbox check asserted in each state (the sysctl=0 cell no local oracle could measure: userns: ok, exit 0), the unprivileged and root cells (a skip on a runner is a failure), sandbox-trace.sh --check against a fresh strace measurement, and docker run without --privileged (cannot unshare: EPERM, exit 126). Both become required checks after the merge (docs/ci.md § Branch protection). 14/14 green on the final head.

Release: minor (0.14.0). Layer 2 (the playground) stays priced, post-1.0.0.

🤖 Generated with Claude Code

schivei and others added 21 commits September 5, 2026 04:07
The probe discipline first: nothing of the sandbox exists until the one thing
that can sink it -- a hand-encoded system-call shim per architecture -- is
proved on both Linux hosts.

* src/sysno.mc: 60 SN_* names and SN_ABSENT. src/sandbox*.mc names no syscall
  number; the tables do, one per architecture.
* src/sysno_linux_aarch64.mc: sys6 as eight #opcode words -- mov x8,x0 then
  mov x0..x5,x1..x6 in ASCENDING order (x8 must read x0 before x0 is
  overwritten), then svc #0 -- plus the asm-generic/unistd.h numbers.
* src/sysno_linux_x86_64.mc: sys6 as six emit() words, 24 bytes of x86 cut into
  four-byte pieces with instructions straddling the boundaries. The seventh
  parameter is at [rbp+16] (M17 step B, docs/reference/objects.md § 4c) and
  becomes the sixth kernel argument in r9. Bytes from llvm-mc.
* src/host_linux.mc: host_syscall6 = sys6, host_sandbox_supported() = 1;
  src/host_macos.mc and src/host_windows.mc answer -ENOSYS, an all-absent table
  of the same length, and 0 -- so <mc/core_sandbox> compiles on every host and
  what refuses is host_os(), not a missing symbol.
* src/sandbox.mc + src/core_sandbox.mc: the sixth part. Step A carries the
  option parser, `mc sandbox check` and the refusals; run/exec say
  "not in this step" and exit 126.

Two things the kernel corrected in the spec, both measured on Ubuntu 26.04 /
kernel 7.0.0-30 (aarch64 and x86_64) and written into the code's comments:

1. § Risks 1 expects unshare(CLONE_NEWUSER) to fail with EPERM for an
   unprivileged unconfined process when
   kernel.apparmor_restrict_unprivileged_userns is 1. It SUCCEEDS. Since
   Ubuntu 24.04 the process is transitioned into the `unprivileged_userns`
   AppArmor profile, which denies every capability and grants no mount rule, so
   the first failure is the box's first mount: EACCES on
   mount(0,"/",0,MS_PRIVATE|MS_REC,0) and EPERM on sethostname. The check probe
   is therefore two stages, and a one-stage probe would print `ok` on a host
   where nothing works.
2. /proc/filesystems lists only LOADED filesystems, so `overlay: absent` was a
   lie on a machine that has the module and no container engine. The line is
   now `not loaded (modprobe overlay)`.

Measured, both hosts, both privilege states -- see the PR description.

The five goldens are rewritten once, each after its own criterion: mc2.sha256
after an empty `--dump-asm` diff between mc1 and mc2 and cmp mc2.o mc3.o; the
Linux pair deleted and re-recorded by `make check-linux-host` (four cells, each
after its own mc2l.o == mc3l.o and with the cross proof green); the Windows
pair cross-computed per tests/golden/README.md and reproduced by build/mc2.

stage0/, lib/ and tests/*.mc untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* scripts/check-surface.sh asserts the eight AArch64 words of sys6 against
  --dump-asm src/sysno_linux_aarch64.mc.
* scripts/check-parts.sh asserts the six x86-64 words against
  --dump-asm --machine=x86_64, and adds core_sandbox to the per-part case
  (<mc/core_min> + <mc/core_sandbox> compiles alone) and to the spelled-out
  six-part core, which still cmp's byte for byte against <mc/core>.
* scripts/check-shim.sh (make check-shim) is acceptance 1 itself: it GENERATES
  a probe -- tests/*.mc is untouchable for this milestone and the spec's file
  list names six other programs -- builds it with the compiler under test and
  RUNS it. Four assertions: getpid through the shim equals the libc's, openat
  of a missing path is -2, a write reaches fd 1, and a six-argument mmap at
  offset 4096 reads the byte that is there. The last is the one that matters:
  the sixth kernel argument is sys6's SEVENTH parameter, x6 on AArch64 and
  [rbp+16] on x86-64 -- the one place the two shims differ in kind.

check-shim is in the Linux `check` list, prints one SKIPPED line on macOS, and
is named in the Windows check-skipped report with its reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs/reference/sandbox.md (new). Its § "What is not isolated" names the
  kernel in its first sentence, as the architect's addition (b) requires, and
  says plainly what gVisor and Firecracker buy that this does not. The rest is
  the hosts table, `check` line by line with how each value is measured, the
  AppArmor section, the shim with both encodings and the three gates, and the
  part. Every claim that is a measurement says so and names the host.
* docs/reference/cli.md § 3c: the three verbs, the eleven options, the six
  `check` lines, and exit codes 124/125/126 in the table.
* docs/reference/hooks.md § 6: host_syscall6, host_sysno,
  host_sandbox_supported, with why a libc wrapper is not usable here.
* docs/reference/bundle.md: the sixth part, the four new names, 83 entries.
* docs/reference/diagnostics.md § 11b: every message `mc sandbox` can print.
* docs/README.md: sandbox.md on the map.
* docs/specs/M43.md § Implementation notes -- step A: eight notes, four of them
  corrections the kernel made to the spec, each with the numbers it was
  measured with.

make check-docs: 191 symbols, 32 flags, 20 toml keys, 10 directives,
48 samples, 301 links.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…l answered

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ree, the caps, the report

`mc sandbox run|exec` builds a box and runs something in it. Landlock, the
seccomp filter and the notification channel that names a refusal are step C;
src/sandbox_box.mc carries the one marked place they go, and exit code 125 is
defined and not produced yet.

  P  src/sandbox.mc      the option parser, the plan, the pipes, the uid/gid
                         maps, the wall clock, the report, the exit code
  I  src/sandbox_box.mc  unshare, the mount tree of the spec's section 3,
                         pivot_root, the inheritable caps
  J  src/sandbox_box.mc  pid 1 of the pid namespace, one child per step
  C  src/sandbox_box.mc  close_range, no_new_privs, the per-step caps, execve

Five things the kernel corrected, each measured on Ubuntu 26.04 / kernel
7.0.0-30, on linux/aarch64 and linux/x86_64, as root and unprivileged:

* a root map of `0 65534 1` leaves the CALLER unmapped, and an unmapped fsuid
  answers EOVERFLOW to every file it creates -- it was the box's first mkdir.
  Adding `1 0 1` + setuid(0) then failed the overlay's copy-up, which needs
  permission on the LOWER directory (CAP_DAC_OVERRIDE does not help: the
  inode's owner must be mapped). The root map is now the identity range
  `0 0 65536` and there is no setuid at all;
* RLIMIT_NPROC does not bind for root (copy_process exempts INIT_USER), and
  does bind for everyone else -- at 0 the first clone is EAGAIN, for fork and
  for the clone3 behind posix_spawn alike;
* a pid namespace dies with its init and cannot be re-unshared (EINVAL from
  copy_pid_ns), so J exists: one pid 1 for the whole box, running the steps;
* the cpu verdict is J's, not P's: P's wait4 sees the sum of every step. With
  soft = hard the kernel sends SIGKILL, not SIGXCPU, and the recorded rusage
  is a shade under the cap;
* overlayfs mounts on virtiofs and on ext4, with `userxattr`. The priced
  read-only fallback is built and no cell has reached it.

Two options beyond the spec's section 6, both forced by this repository's own
corpus: `--root DIR` (the tree that becomes /src, for a source that includes
`../lib/sys.mc`) and `--config NAME` (the project file, with [project].out
relative to its own directory).

Globals: the whole milestone owns one, sb_state, an arena record with named
offsets and accessors. `mc limits src/mc.mc` reports globals 422/512 -- step A's
sixteen sandbox globals became the record, so the row went DOWN from 437.

The five goldens are rewritten once, each after its own criterion: mc2 after an
empty `--dump-asm` diff between mc1 and mc2 and `cmp build/mc2.o build/mc3.o`,
the Linux pair by deleting them and running `make check-linux-host`, the Windows
pair cross-computed per tests/golden/README.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/test-sandbox.sh (make test-sandbox, inside make check) is seven parts:
`mc sandbox check` as the guard, the eight isolation programs of tests/sandbox/,
the whole tests/*.mc suite compiled AND run inside the box, `mc sandbox exec` on
two binaries built outside it (one dynamic, which reaches libc through the bind
of /lib and nothing else of the host), a project, the determinism of the report,
and the overhead number.

It contains no mechanism -- the box is `mc sandbox`, written in mc. On a Linux
host it runs natively; from macOS it cross-builds a Linux mc and hands the run
to Lima, else to `docker run --privileged`, else prints one SKIPPED line. The
box's own guard is what skips it on a kernel that cannot build one, which is
what happens inside the unprivileged containers of `make check-linux-host`:
`userns: EPERM`, printed, and check stays green.

Measured, all four cells green:

  linux/aarch64 root (Lima)        49 ok, 0 failed, 1 skipped, 1.37 ms per box
  linux/aarch64 unprivileged       49 ok, 0 failed, 1 skipped, 1.43 ms
  linux/x86_64 root (the VPS)      47 ok, 0 failed, 3 skipped, 4.12 ms
  linux/x86_64 unprivileged        47 ok, 0 failed, 3 skipped, 4.98 ms
  linux/aarch64 alpine:3 under docker --privileged (kernel 6.12.76)  48 ok

plus, unprivileged with the stock kernel.apparmor_restrict_unprivileged_userns=1
on both hosts, the honest refusal: `sandbox: cannot mount /: EACCES (apparmor
restricts unprivileged user namespaces: ...)`, exit 126.

tests/sandbox/ is eight programs, each carrying its own expectations. Two of
them record what step B can honestly claim and what it cannot: shadow.mc gets
ENOENT because there is no /etc in the box, not a named refusal, and connect.mc
gets ENETUNREACH from the empty network namespace. forkbomb.mc carries both
answers, because RLIMIT_NPROC binds for an ordinary user and not for root, and
it declares `fork` as i32: an i64 declaration read glibc's -1 as 4294967295 and
the bomb looked unbounded because its own test could not see a failure (M45).

examples/lang gains two Linux configs with no [target] section at all -- so the
pair is the host's and one file serves both architectures -- the second adding
`libc = "gnu"` because the executable writer's default loader is musl's and
every Linux host measured here is glibc.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ach host answered

docs/reference/sandbox.md goes from "status: step A" to the box: the four
processes and why there are four, the mount tree, the maps and the two kernel
answers that shaped them, the caps and the split between the ones I sets and the
ones C sets, the report grammar with its six lines and nineteen `cannot` sites,
the steps, and a table of what the five measured cells did. § What is not
isolated gains the process cap for root, with the reason from copy_process and
the two answers -- run it unprivileged, and wait for step C.

docs/reference/cli.md § 3c: --root, --config, the corrected --libc and --verbose
descriptions, an example of a run, and the note that 125 is defined and not
produced yet.

docs/build.md § M43: `make test-sandbox`, the three Linux kernels it looks for
in order (Lima, docker --privileged, none), the two `-gnu` cross-build targets
and why they exist, and the two host facts a new machine will need
(the AppArmor sysctl, `modprobe overlay`).

docs/specs/M43.md § Implementation notes -- step B: the ten kernel corrections
in the order they were hit, with the measurement that forced each one, plus the
measured table and the globals number.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cepts, the numbers

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Landlock and a seccomp filter with SECCOMP_RET_USER_NOTIF, installed by C in
the place step B left marked for them -- after PR_SET_NO_NEW_PRIVS and before
the byte that releases it. What the box could only make impossible, it can now
also NAME: `refused: open /etc/shadow`, `refused: syscall 198 (socket)`,
`refused: mmap 8589934592 bytes over the cap (268435456)`, each one line and
exit 125.

src/seccomp.mc (600 lines, 336 of them code) is the ABI and the policy: the
BPF builder over 8-byte sock_filter records, the Landlock ruleset and its
per-root rules, the three notification ioctls, and P's table. The filter's
shape is fixed and every jump in it is forward -- architecture, then the x32
bit, then one JEQ per allowed number, then the clone flag test, then the three
returns. The architecture comes from host_audit_arch(), a new host-layer
answer, because a value that changes with the machine belongs there and not in
a file that compiles for both.

Five calls are notified even though every profile contains them -- openat,
open, mmap, munmap, execve -- because they are what the table decides on; P
answers CONTINUE. Two are in every profile whatever the trace said, and they
are C's own: the read that waits for the sync byte and the close of its copy
of the listener.

The listener travels in two hops of pidfd_getfd, not the one the design drew:
P cannot name C, whose pid is a number in the box's namespace, and the field
that would tell it arrives only with the first notification. So C reports the
descriptor number to J, J takes it (it is C's parent), P takes it from J. Both
hops are between a process and its own descendant with the same real uid,
which is what Yama ptrace_scope = 1 allows -- the value on both hosts.

/etc/ld.so.cache comes into the box, bound read-only and granted read-only,
which is one host file more than the design's "no /etc" and is measured: with
it missing, glibc's loader takes a fallback path that issues calls the same
binary never issues on the host, and the profiles are measured on the host.

globals 422 -> 430 of the frozen seed's 512; everything new is in the one
sb_state record.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/sandbox-trace.sh runs strace -f over every tests/*.mc compiled and
then run, over mc build examples/lang, and over two probes it writes itself,
and records one list per (architecture, C library, step) in tools/sandbox/.
src/sandbox_profiles.mc is generated from the twelve of them; `make
sandbox-trace-check` re-traces the host it runs on and fails in either
direction, or when regenerating the file does not reproduce it byte for byte.
It is green on all four measured cells and exits 1 on a deliberate extra
entry.

Four things it had to learn the hard way, each recorded in the script:

  the trace runs OUTSIDE the box, because tracing the box would record the
  box's own unshare/mount/pivot_root and, once a filter exists, a call missing
  from the profile is refused before it can be observed;

  strace -c cannot be used: its summary drops exit_group, which never returns
  and is therefore never counted -- and a profile without exit_group refuses
  every program at its last instruction;

  the corpus needs a program that uses the C library. Every tests/*.mc writes
  with a raw write and allocates nothing. The probe allocates four megabytes
  in ONE block on purpose: that makes glibc advise MADV_HUGEPAGE every time,
  where a smaller heap did it in 5 runs out of 60 -- and a profile measured
  from a corpus that hits a call one run in twelve is a box that refuses a
  legitimate program one run in twelve;

  the glibc delta is per STEP. `read` is in musl's compile trace and not in
  its program trace; clone3 is in the compile trace only. One delta over both
  would have denied a glibc program its read and handed it a way to fork.

Two targets, neither in `make check`: the first writes generated source and
the second is a full corpus run per libc, which belongs in the sandbox CI job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tests/sandbox/ carries the step-C expectations: shadow is `refused: open
/etc/shadow` at exit 125, connect is `refused: syscall 198 (socket)` (41 on
x86-64), forkbomb is `refused: syscall 220 (clone)` and, run a second time
with --allow=threads, `refused: process limit (64)`. eightgib asks the kernel
for its eight gibibytes directly instead of through malloc, so the number in
the report is the number in the file -- glibc adds a page for the chunk header
and `mmap 8589938688` cannot be written down. sleeper gained --allow=threads,
because sleeping is a thing the default profile does not allow.

libcuser.mc is new and is the case the rest of the directory did not cover: an
ordinary C-library program, four megabytes of malloc and a file read through
stdio, which is what the profile has to allow without a word. It replaced
tests/linux/071-errno-malloc.mc in the `exec` check for a reason that is the
milestone's own policy: that program opens /nonexistent-m42/nope on purpose,
and a path under none of the box's roots is refused rather than answered
ENOENT.

The script grew the run_case helper, a second run per source when the header
asks for one, expectations that can name the architecture AND the C library
(musl on x86-64 forks with `fork` 57 where glibc uses `clone` 56), an
assertion that a present-and-empty stdout header means "no output", and the
two acceptance checks that look at the host: the process count and the
available memory before and after the isolation set.

Measured green on five cells: linux/aarch64 glibc root and unprivileged
(Lima), linux/x86_64 musl root and unprivileged (the VPS), and linux/aarch64
musl under docker --privileged -- 52, 52, 50, 50 and 50 ok, 0 failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/reference/sandbox.md is no longer "step B, and 125 is defined but never
produced". Three new sections: The two walls (the Landlock ABI ladder and what
each root is granted, the filter's shape instruction by instruction, the clone
flag test), The profiles (how they are measured, why outside the box, why not
strace -c, why the corpus needed a program that allocates, why the glibc delta
is per step) and The explain channel (the two pidfd hops, Yama, P's table, and
why a refused call is never answered). The box's tree gained the
/etc/ld.so.cache row and the reason it is there; What is not isolated now
counts the calls a profile actually allows and says which fork the compile
step can still make when the box was started by root.

The measured tables are step C's: five cells, and the box cost against step B
on the same quiet machine -- 1420 us -> 1717 us, +297 us, +21% for one
ruleset, one filter, two pidfd hops and a dozen notification round trips.

docs/reference/cli.md: 125 is produced now, --allow=threads says what it
really permits, --verbose says what it adds. docs/reference/diagnostics.md
gains the five refusal lines and the three new `cannot` sites.
docs/reference/hooks.md gains host_audit_arch(). docs/build.md gains `make
sandbox-trace`. docs/specs/M43.md gains fourteen numbered implementation
notes -- everything the kernel, the two C libraries and the two architectures
said that the design did not.

make check-docs: 192 symbols, 33 flags, 20 TOML keys, 10 directives, 48
samples, 308 links.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
make bundle before make bootstrap, as always: 86 files, raw 1016670 -> lz
477278, blob 478358 bytes. tools/bundle.list gained mc/sandbox_profiles and
mc/seccomp, so <mc/core_sandbox> is complete out of the binary alone.

The five goldens, each rewritten only after its own criterion:

  mc2.sha256  675d62a4... -> bb48b0b2...  after `diff <(mc1 --dump-asm
  src/mc.mc) <(mc2 --dump-asm src/mc.mc)` came out empty and cmp build/mc2.o
  build/mc3.o matched (1099928 bytes);

  the Linux pair deleted and re-recorded by `make check-linux-host`, which is
  green over all four cells (aarch64 and x86_64, musl and glibc), each after
  its own fixed point and with the cross proof against the macOS build/mc2.o:
  mc2-linux-arm64  394ce144..., mc2-linux-x86_64  8c9c7fab...;

  the Windows pair cross-computed per tests/golden/README.md -- the same
  source through --backend=coff-obj-arm64 and --backend=coff-obj-x86_64, which
  is by construction the object a Windows-hosted mc must write:
  f2d9b228... (1121582 B) and 3596c00b... (1150098 B), both produced byte for
  byte by build/mc2 as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…profiles, fourteen facts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t's trace

`sh scripts/sandbox-trace.sh --check` failed on Ubuntu 24.04 -- the version both
GitHub runners carry -- with the same compiler and the same corpus that pass on
Ubuntu 26.04, and the difference is glibc: 2.39 issues `rt_sigaction` at start-up
and spawns with `clone`, where 2.43 issues `madvise` and `getrandom` and spawns
with `clone3`. Two of those were MISSING from the table, which is not a
bookkeeping difference: a call the profile does not have is a legitimate program
refused at run time.

So the two directions of the comparison stop being one:

  * a call in the trace the list does not have FAILS, always;
  * a list entry this host did not need is a `note` line, because the list is
    the union over the C libraries and the C library VERSIONS the project
    supports and no single host can exercise them all;
  * `--union` adds what a host needs without erasing what another needs, and
    `--strict` restores the two-way failure for a single-host audit.

Measured, not assumed: `ubuntu:24.04` (glibc 2.39-0ubuntu8.7, aarch64) folded in
with `--union`, which adds `clone` and `rt_sigaction` to the aarch64 glibc
compile list and `clone`/`madvise` to its threads list -- one line in
src/sandbox_profiles.mc, SN_RT_SIGACTION in sbp_gnu_compile_aarch64 (the others
were already in the musl base or in the shared threads delta). The relaxed
`--check` is green on Lima (glibc 2.43) with two `note` lines, and the suite is
52 ok / 0 failed there, root and unprivileged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ere can run

Two jobs, `The sandbox (linux/arm64)` on ubuntu-24.04-arm and
`The sandbox (linux/x86_64)` on ubuntu-latest, each running the whole of
scripts/test-sandbox.sh twice: as an ordinary user and under sudo. That is the
four-cell matrix of docs/specs/M43.md § 8, and the unprivileged cell with
kernel.apparmor_restrict_unprivileged_userns=0 is the one state the project's
own machines cannot show -- Ubuntu ships the sysctl at 1, so what Lima and the
VPS can measure is only the refusal. The step prints `mc sandbox check` in both
states and requires exit 0 in the second.

What travels is a new artifact, `mc-linux-sandbox`: four EXECUTABLES, not
objects. Since M42 `mc build` writes a dynamic ELF itself, so cross-building a
runnable Linux compiler on macOS is half a second with no linker and no sysroot
-- the reason the M37 jobs ship an object does not apply. Two are glibc-linked
(the runners are glibc), two musl-linked for the alpine cell.

scripts/ci-sandbox-cell.sh is what holds a runner to more than a developer's
machine: the guard may not skip the run, and no isolation case, `exec`, project
or overhead measurement may be skipped -- only a test's own `// skip-linux:`
header is allowed. The summary must say `0 failed`.

Two more steps per job: `sandbox-trace.sh --check` re-measures the seccomp
profiles with strace on every pull request (acceptance 5 in CI), and
`docker run` WITHOUT --privileged must answer with a `sandbox: cannot ...` line
and exit 126 (acceptance 8's third refusal).

docs/ci.md gains the job section and both names in § Branch protection -- the
architect adds the two contexts after the merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/guide/99-sandbox.md (205 lines) is the task-oriented half: ask the kernel
first, a program that behaves, one that does not and the five refusals with
their exit codes, the four dials, `exec` on a binary you already have, a whole
project, the two hosts that have no box and the Ubuntu sysctl, what a box costs
(1.7 ms) and what it does not buy (the kernel), and the two commands to run it
in your own checks. Its two `mc` samples are compiled by `make check-docs` on
the machine that renders these docs -- a Mac, which has no box -- and the page
says so where a reader would otherwise assume they were run through one.

scripts/check-parts.sh gains the second half of acceptance 9: a compiler
assembled without <mc/core_sandbox> prints no `sandbox` usage line, and the word
on its command line is an ordinary file name -- `mc: cannot open: sandbox`, and
`mc sandbox check` is `mc: duplicate entry: check`, both exit 1. The contrast
case asserts that the whole compiler DOES print the two usage lines, so a
renamed subcommand cannot make the first assertion a tautology.

check-docs: 192 symbols, 33 flags, 20 toml keys, 10 directives, 50 samples,
317 links.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/sandbox_profiles.mc gained one entry (SN_RT_SIGACTION, the glibc 2.39 row),
and it is bundled as mc/sandbox_profiles, so the blob moves and every golden
with it: bundle 86 files, raw 1016691 -> lz 477285, blob 478365 B.

Rewritten only after `diff <(mc1 --dump-asm src/mc.mc) <(mc2 --dump-asm
src/mc.mc)` came out EMPTY and `cmp build/mc2.o build/mc3.o` matched
(1099928 B):

  mc2.sha256               f41c4eff3a2f85625534ea5afc4cc4923e228bff3142491bfa73a5801699919d
  mc2-linux-arm64.sha256   fe5f832e67b10a1e0ca3c651a36de3558b2a28f7034999d4eb8a34544a60402d
  mc2-linux-x86_64.sha256  eb9f949762d3f03d7da8a7aa716df75f5c7d9846054ff76451881551b5a5856d
  mc2-windows-arm64.sha256 82a46a1a3d85616f13ede3cace1c7b58c2a29ba1fc32debf592ead032da10d43
  mc2-windows-x86_64.sha256 323a137439cad5efde8baee1495e0d4a8ae51fc880bf7405a31045e975cad3f1

The Linux pair was deleted and re-recorded by `make check-linux-host` (Docker,
both architectures, both libcs, each after its own `mc2l.o == mc3l.o` and with
the cross proof against the macOS build/mc2.o green); the Windows pair is
cross-computed per tests/golden/README.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sandbox CI job failed on both runners, in the same place and with the same
line -- `mc: cannot open: /src/examples/lang/build/.mc-usage.toml`, the only
failure in 51 and 49 other cases -- and the cause is not the sandbox.

`open` returns a C `int`. The failure is -1 in the low 32 bits and the bits
above it are UNSPECIFIED; M42 wrote that hazard down and M45 narrowed every call
site with `c_int()` -- read_file, write_file, sysroot.mc, waitpid -- except this
one. On the runners (Ubuntu 24.04, glibc 2.39, kernel 6.17.0-1022-azure) a
failing `open` hands back 0x00000000ffffffff, so `fd < 0` was FALSE and
lex_readable answered "readable" for every file that does not exist. On the
project's own machines (glibc 2.43) the same call sign-extends and the bug is
invisible -- which is why four measured cells and a green make check never saw
it.

What it broke, on those hosts: `mc build` on a tree with no `build/` directory
yet. lim_read_usage asked lex_readable, was told the usage file was there, and
read_file (which does narrow) disagreed and died. The same false positive is in
the `[include].paths` search, where it makes a root that does not have the file
claim it does.

One line plus the reason. Objects do not move: scripts/check-inert.sh against a
build/mc1 from origin/main (3966268) is identical for all 33 objects and for the
five taught examples. `make check` RC 0, `make check-linux-host` RC 0 over four
cells, and the five goldens are rewritten once more, each after its own
criterion (empty --dump-asm diff, cmp mc2.o mc3.o at 1100464 B).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The two sandbox jobs are green (run 33962063476), so the numbers stop being a
plan. docs/reference/sandbox.md § Hosts gains the four CI cells beside the four
local ones -- kernel 6.17.0-1022-azure, glibc 2.39, Landlock abi 7, isolation
10/10 and the suite 31/31 and 29/29 in each, a box costing 2.1-2.2 ms -- plus
what `mc sandbox check` says in both states of the AppArmor sysctl and what a
container without --privileged says (`userns: EPERM`, then `sandbox: cannot
unshare: EPERM`, exit 126).

docs/specs/M43.md gains its step D implementation notes: the two things only a
runner could find (a profile is a union over C library VERSIONS; lex_readable
believed a file that was not there), the three smaller ones (the artifact is
four executables, the cell wrapper's no-skip rule, and Docker Desktop's
fakeowner bind mount refusing an execve inside the box), and the measured table.

check-docs: 192 symbols, 33 flags, 20 toml keys, 10 directives, 50 samples,
318 links. `make site check-site` RC 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…CI-only defects

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@schivei schivei added the release:minor Merging this PR bumps the minor version label Sep 5, 2026
schivei and others added 4 commits September 5, 2026 09:19
Three findings from the security review of the branch.

1. HIGH. A profile entry is a call the supervisor never sees, and `clone`
   (with `clone3` in the glibc delta) was measured into the COMPILE profile
   as one, because `mc build` forks. `mc build` also runs `[linker].cmd`, and
   that name comes out of the source tree's own mc.toml -- so an untrusted
   tree chose which binary the compile step executed and forked in a loop
   with nothing in the report but `compile: exit 1`: twelve children
   unprivileged, two hundred as root, where RLIMIT_NPROC does not bind at
   all. A clone(CLONE_NEWUSER|SIGCHLD) in the same position simply succeeded
   (`cloned 4`).

   sb_notified() now drops clone, clone3, fork and vfork from every profile,
   as it already dropped openat/open/mmap/munmap/execve, so all four always
   reach P. P asks two questions: any CLONE_NEW* bit is `refused: clone with
   namespace flags` -- read from args[0] for clone and, for clone3, out of
   the caller's struct clone_args with process_vm_readv, since BPF cannot
   follow a pointer -- and everything else is counted against a per-step
   process limit: 16 for a compile, 0 for a run, 64 with --allow=threads.
   One past it is `refused: process limit (N)`. RLIMIT_NPROC stays as the
   second wall and is loosened to 32 for the compile step, because the two
   walls race and the named one has to win.

   With --allow=threads the filter still short-cuts a REAL thread by its
   flags, so a threaded program pays no round trip per thread.

   scripts/sandbox-trace.sh still MEASURES the four calls into
   tools/sandbox/*.list -- that is what the trace saw -- but writes them into
   src/sandbox_profiles.mc as a comment, never as a row.

   tests/sandbox/forkbomb.mc loses its three per-(arch, libc) expectations:
   counting instead of naming the entry point gives one line everywhere.
   New: nsclone.mc, nsclone3.mc (raw clone3, syscall 435 on both
   architectures, the only way to exercise the struct read) and linkbomb/,
   the hostile project, with part 2b of scripts/test-sandbox.sh building its
   bomb into build/ so nothing binary is checked in. That part also exposed
   a defect in the script: `lf` was assigned in part 4, so part 2b built its
   program for the wrong C library and the box stopped at `refused: syscall
   95 (waitid)` instead of at the case under test.

2. LOW. sb_num accumulated without a bound, so `--mem 999999999999999999999999`
   wrapped and the box ran with a negative cap (measured: `refused: mmap 8192
   bytes over the cap (-3541500564788477952)`). It now stops at 10^12
   (`mc: --mem: number too large`), and each cap has a maximum -- 86400 s,
   1048576 MiB, 65536 MiB -- and a minimum of one.

3. INFO. Below Landlock ABI 6 the scoped signal and abstract-socket
   restrictions do not exist. `mc sandbox check` says so:
   `landlock: abi N (no scoped signals below 6)`, still exit 0.

Measured on Ubuntu 26.04 / kernel 7.0.0-30, four cells: linux/aarch64 glibc
(Lima) unprivileged 55 ok / root 55 ok, linux/x86_64 musl (the VPS) root
53 ok / unprivileged 53 ok, 0 failed in each, with the host's process count
equal before and after; the x86_64 glibc cases by hand; and
`sandbox-trace.sh --check` green on both hosts in both directions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/specs/M43.md gains a section "Implementation notes -- the review", with
each finding, how it was reproduced (the pre-fix numbers, both privileges and
both architectures) and the rule that replaced it.

docs/reference/sandbox.md: the notification table splits clone/fork/vfork from
clone3 (the struct read) and gains the per-step process limits and the reason
every RLIMIT_NPROC above them is looser; the filter section says the clone
block is a fast path for a thread and not a decision about processes; "What is
not isolated" loses the paragraph about the compile step's unbounded forks --
it is bounded now -- and gains the Landlock-ABI-6 bullet.

docs/reference/cli.md § 3c: the four maxima, why they are not the kernel's,
and the pre-fix wrap.

docs/reference/diagnostics.md: three option rows (`number too large`,
`at most N`, `must be at least 1`) and three report rows (`process limit (N)`
with its three values, `clone with namespace flags`, `clone3 with unreadable
arguments`).

docs/guide/99-sandbox.md: the refusal table, the caps paragraph, and the note
that root now loses only the wall BEHIND the counter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
make bundle re-run before bootstrapping (86 files, raw 1027114 -> lz 482081,
blob 483161 bytes): src/sandbox.mc, src/seccomp.mc, src/sandbox_box.mc and
src/sandbox_profiles.mc are all bundled.

The five goldens rewritten once, each only after its own criterion.

  mc2.sha256               f8b05c08...a173e1 -> 9e7b803f...a2d431
    after `diff <(mc1 --dump-asm src/mc.mc) <(mc2 --dump-asm src/mc.mc)` came
    out empty and `cmp build/mc2.o build/mc3.o` matched (1107056 bytes).
  mc2-linux-arm64.sha256   d8bdbeeb...da6b59 -> 182a4c6d...036679
  mc2-linux-x86_64.sha256  ed7c7f61...fd08dd -> e63d09bc...d99ffa
    deleted and re-recorded by `make check-linux-host` (RC 0, four cells: musl
    and glibc on both architectures), each after its own fixed point
    `mc2l.o == mc3l.o` and with the cross proof against the macOS build/mc2.o
    green in all four.
  mc2-windows-arm64.sha256   4de1c3c9...d48cf3 -> dcaac914...4256c3
  mc2-windows-x86_64.sha256  7da0aa71...1c0d01 -> dfaf002c...a97b861
    cross-computed per tests/golden/README.md, and build/mc2 produces both
    byte for byte as build/mc1 does.

macOS `make check` RC 0, zero FAIL; scripts/check-inert.sh against a compiler
built from the pre-review sources: 33 objects identical (tests/*.mc and
src/mc.mc) and the five taught examples identical too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…in ALLOW

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@schivei
schivei marked this pull request as ready for review September 5, 2026 12:34
@schivei
schivei requested a lite review from Copilot September 5, 2026 12:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces a large, security-critical sandbox feature with deep kernel/ABI interactions that warrants final human review despite strong test/CI coverage.

Pull request overview

Adds Layer 1 of the M43 sandbox (mc sandbox run|exec|check) as a new composable core part (<mc/core_sandbox>), enabling isolated compilation/execution of arbitrary mc programs on Linux using namespaces + Landlock + seccomp user-notify, with measured syscall profiles, tests, and CI coverage.

Changes:

  • Introduces <mc/core_sandbox> and the sandbox implementation (supervisor, box setup, seccomp/Landlock walls, syscall shim + per-arch syscall-number tables).
  • Adds measured syscall profile lists and a dedicated sandbox test corpus (including hostile-project coverage).
  • Wires the new part into bundling/docs/CI and fixes a Linux host issue in lex_readable by narrowing open()’s C int result via c_int().
File summaries
File Description
tools/sandbox/x86_64-musl-threads.list Measured syscall allowlist delta for musl/x86_64 threads.
tools/sandbox/x86_64-musl-program.list Measured syscall allowlist for musl/x86_64 program execution.
tools/sandbox/x86_64-musl-compile.list Measured syscall allowlist for musl/x86_64 compile step.
tools/sandbox/x86_64-gnu-threads.list Measured syscall allowlist delta for glibc/x86_64 threads.
tools/sandbox/x86_64-gnu-program.list Measured syscall allowlist for glibc/x86_64 program execution.
tools/sandbox/x86_64-gnu-compile.list Measured syscall allowlist for glibc/x86_64 compile step.
tools/sandbox/aarch64-musl-threads.list Measured syscall allowlist delta for musl/aarch64 threads.
tools/sandbox/aarch64-musl-program.list Measured syscall allowlist for musl/aarch64 program execution.
tools/sandbox/aarch64-musl-compile.list Measured syscall allowlist for musl/aarch64 compile step.
tools/sandbox/aarch64-gnu-threads.list Measured syscall allowlist delta for glibc/aarch64 threads.
tools/sandbox/aarch64-gnu-program.list Measured syscall allowlist for glibc/aarch64 program execution.
tools/sandbox/aarch64-gnu-compile.list Measured syscall allowlist for glibc/aarch64 compile step.
tools/bundle.list Bundles the new core part and sandbox-related modules.
src/core_sandbox.mc New core part wiring: registers sandbox subcommand and includes sandbox components.
src/sandbox.mc Supervisor/subcommand implementation: parsing, planning, reporting, and orchestration.
src/sandbox_box.mc Box implementation: namespace setup, mount tree, caps, step execution, and status reporting.
src/seccomp.mc Landlock + seccomp user-notify “two walls” implementation and notification ABI helpers.
src/sandbox_profiles.mc Generated profiles compiled into the binary (from scripts/sandbox-trace.sh).
src/sysno.mc Defines the syscall-name enum (SN_*) used by the sandbox.
src/sysno_linux_aarch64.mc AArch64 syscall shim (sys6) + syscall-number table + audit arch constant.
src/sysno_linux_x86_64.mc x86_64 syscall shim (sys6) + syscall-number table + audit arch constant.
src/host_linux.mc Provides Linux host syscall shim entry + host_sandbox_supported().
src/host_linux_aarch64.mc Includes the aarch64 syscall shim/table into the Linux host build.
src/host_linux_x86_64.mc Includes the x86_64 syscall shim/table into the Linux host build.
src/host_macos.mc Stubs sandbox host queries on macOS so <mc/core_sandbox> compiles everywhere.
src/host_windows.mc Stubs sandbox host queries on Windows so <mc/core_sandbox> compiles everywhere.
src/lex.mc Fixes lex_readable() by narrowing open()’s C int return value via c_int().
src/core.mc Includes the sixth core part (core_sandbox) in <mc/core>.
src/main.mc Calls mc_sandbox_init() and updates entry-point documentation for the six-part core.
scripts/test-sandbox.sh Runs sandbox isolation corpus + suite-through-sandbox; delegates from macOS when needed.
scripts/ci-sandbox-cell.sh CI helper to run sandbox “cells” (unprivileged/root) without skipping.
scripts/sandbox-trace.sh Generates/checks syscall profile lists from strace measurements.
scripts/check-surface.sh Adds assertions for the AArch64 syscall shim words via --dump-asm.
tests/sandbox/clean.mc Clean program used to validate sandboxed vs non-sandboxed equivalence.
tests/sandbox/shadow.mc Isolation test asserting named refusal for opening /etc/shadow.
tests/sandbox/connect.mc Isolation test asserting refusal for network-related syscalls.
tests/sandbox/eightgib.mc Isolation test asserting named refusal for oversized mmap.
tests/sandbox/forever.mc Isolation test asserting CPU-cap termination behavior.
tests/sandbox/sleeper.mc Isolation test asserting wall-clock cap behavior via libc sleep.
tests/sandbox/rocwd.mc Isolation test for --ro bind + --cwd behavior.
tests/sandbox/nsclone.mc Isolation test asserting refusal of clone with namespace flags.
tests/sandbox/nsclone3.mc Isolation test asserting refusal of clone3 with namespace flags.
tests/sandbox/forkbomb.mc Isolation test asserting process-limit behavior.
tests/sandbox/libcuser.mc Isolation test exercising dynamic loader / libc bindings inside box.
tests/sandbox/linkbomb/mc.toml Hostile project config: [linker].cmd points to an in-tree fork bomb.
tests/sandbox/linkbomb/bomb.mc The “linker” fork bomb program used by the hostile project test.
tests/sandbox/linkbomb/app.mc The benign project entry used by the hostile project test.
docs/README.md Adds sandbox guide/reference entries to the docs map.
docs/guide/99-sandbox.md New end-user guide for `mc sandbox run
docs/reference/sandbox.md New reference documentation for sandbox design/behavior/limits.
docs/specs/M43.md The ratified spec and measured implementation notes for M43.
.github/workflows/ci.yml Adds the two required sandbox CI jobs (arm64 + x86_64).
docs/ci.md Documents the new sandbox CI jobs and branch-protection contexts.
tests/golden/mc2.sha256 Updates golden hash after sandbox-related additions.
tests/golden/mc2-linux-arm64.sha256 Updates Linux arm64 golden hash after sandbox-related additions.
tests/golden/mc2-linux-x86_64.sha256 Updates Linux x86_64 golden hash after sandbox-related additions.
tests/golden/mc2-windows-arm64.sha256 Updates Windows arm64 golden hash after sandbox-related additions.
tests/golden/mc2-windows-x86_64.sha256 Updates Windows x86_64 golden hash after sandbox-related additions.
Review details
  • Files reviewed: 69/70 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/sandbox/eightgib.mc
Comment on lines +27 to +29
uptr p = mmap(0, 8589934592, 3, 0x22, -1, 0);
if (p == 0 || p + 1 == 0) { puts("mmap refused\n"); return 0; }
// If it ever succeeds, touch every page so the cap is measured and not

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0a7842e: the test now checks MAP_FAILED alone (p + 1 == 0; the redundant p == 0 is gone, with the mmap_min_addr reason in a comment).

Comment thread tests/sandbox/forever.mc Outdated
Comment on lines +3 to +6
// The CPU cap (§ 4): RLIMIT_CPU is soft = hard = --time, so the kernel sends
// SIGXCPU at the soft limit and the default action ends the process. It burns
// CPU rather than sleeping on purpose -- the wall clock is a different case
// and sleeper.mc is that one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0a7842e: the comment now says SIGKILL with soft = hard, as measured in step B and recorded in the spec/reference.

…ts MAP_FAILED alone

Copilot on #23: the cpu-cap comment described SIGXCPU where the kernel sends
SIGKILL when soft == hard (measured in step B, recorded in the spec and the
reference); and the mmap failure test carried a redundant p == 0 beside the
MAP_FAILED test -- an anonymous mapping is placed above vm.mmap_min_addr, so
only (uptr)-1 is a failure. Comments and one comparison; the reports and exit
codes do not move.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@schivei
schivei merged commit 5b3b1b2 into main Sep 5, 2026
14 checks passed
@schivei
schivei deleted the m43-sandbox branch September 5, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:minor Merging this PR bumps the minor version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants