Skip to content

COW: mediate openat2 and renameat; build the restore stub without SSP - #206

Merged
congwang-mk merged 4 commits into
mainfrom
cow-syscall-gaps-and-stub-ssp
Sep 2, 2026
Merged

COW: mediate openat2 and renameat; build the restore stub without SSP#206
congwang-mk merged 4 commits into
mainfrom
cow-syscall-gaps-and-stub-ssp

Conversation

@congwang-mk

Copy link
Copy Markdown
Contributor

Summary

Two COW mediation gaps and one restore-stub build fix.

  • openat2 bypassed COW. The COW dispatch registration and the notif syscall list covered only openat and legacy open, so a raw openat2 under the workdir went straight to the kernel with the child's Landlock write grant. An O_CREAT/O_TRUNC open mutated the real workdir and survived an abort; a read-only open could see stale lower content past a whiteout. The handler now uses the shared decode_open_args, which reads flags and mode out of struct open_how for openat2.
  • renameat bypassed COW. Same shape: renameat2 and legacy rename were registered, renameat was not. On aarch64 this covered every ordinary rename, since libc's rename() compiles to renameat there. The chroot lists already carried it for that reason.
  • Restore stub built with a stack protector. A compiler that enables SSP by default (vanilla GCC; Ubuntu's spec exempts -ffreestanding) emits a canary load from %fs:0x28 in the freestanding stub, which runs with a zero thread pointer until it restores the checkpoint's. Every restore then died with SIGSEGV at address 0x28 before the READY handshake. build.rs now passes -fno-stack-protector, and build_static also treats the binary as stale when it is older than build.rs, since a flag change previously never recompiled an existing stub.

Tests

  • test_seccomp_cow_openat2_syscall: raw openat2 with a 24-byte open_how under an abort branch; asserts the file is visible inside and gone after abort.
  • test_seccomp_cow_renameat_syscall: raw renameat (264 on x86_64, 38 on aarch64, renameat2 fallback on riscv64) under an abort branch; asserts the original name is back after abort.
  • stub_carries_no_stack_protector: scans the built stub for the x86_64 canary load bytes. Validated against a stub deliberately compiled with -fstack-protector-all (3 hits) and the fixed stub (0 hits).

Both COW tests were confirmed to fail before their fix.

🤖 Generated with Claude Code

COW registered only openat and legacy open, and the notif list did the
same, so a raw openat2 under the workdir never reached the supervisor.
The kernel ran it against the real directory with the child's Landlock
write grant, so an O_CREAT or O_TRUNC open survived an abort and a
read-only open could see stale lower content past a whiteout. Reusing
decode_open_args also fixes the argument layout: for openat2 the flags
and mode live in a struct open_how in child memory, not in the syscall
args.

Signed-off-by: Cong Wang <cwang@multikernel.io>
A compiler that enables the stack protector by default (vanilla GCC;
Ubuntu's spec exempts -ffreestanding) emits a canary load from %fs:0x28
in the stub's prologue. The stub runs with a zero thread pointer until
it restores the checkpoint's, so that read faults at address 0x28 before
the READY handshake and every restore dies with SIGSEGV. Pass
-fno-stack-protector explicitly and add a unit test that scans the built
stub for the canary load.

The freshness check in build_static compared the binary only against
its C source, so a flag change here never recompiled a stub that
already existed in target/. Compare against build.rs as well.

Signed-off-by: Cong Wang <cwang@multikernel.io>
COW registered renameat2 and legacy rename but not renameat, and the
notif list did the same, so a raw renameat under the workdir went to
the kernel and renamed the real file, surviving an abort. On aarch64
this covered every ordinary rename: the ABI has no rename(2), so libc's
rename() compiles to renameat. The chroot lists already carried it for
that reason. renameat shares renameat2's first four arguments, so the
parser accepts both numbers in the same branch.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The fxsave/xstate frame constants only feed the x86_64 signal-frame
builder, which is already cfg-gated, so the riscv64 build warned about
four unused constants.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk merged commit bb88f4b into main Sep 2, 2026
17 checks passed
@congwang-mk
congwang-mk deleted the cow-syscall-gaps-and-stub-ssp branch September 2, 2026 22:01
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