Skip to content

fix: RISC-V test portability and exec fd-redirect argv[0] preservation#147

Merged
congwang-mk merged 7 commits into
mainfrom
fix/riscv-test-portability
Jul 16, 2026
Merged

fix: RISC-V test portability and exec fd-redirect argv[0] preservation#147
congwang-mk merged 7 commits into
mainfrom
fix/riscv-test-portability

Conversation

@congwang-mk

@congwang-mk congwang-mk commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Fixes all of the RISC-V test failures, including one product bug that the RISC-V environment exposed but that exists on every architecture. Full suites verified on x86-64 (lib, integration, spec, ffi handler and C smoke tests); the argv[0] clobber reproduces on x86-64 and is regression-tested there.

1. /lib64 as an optional read grant (586b5cd, 3b98496)

RISC-V glibc and musl put the dynamic loader under /lib and have no /lib64 at all, so a mandatory /lib64 read grant aborts confinement. The builder-based tests already used fs_read_if_exists; the CLI, profile, and ffi layers had no equivalent:

  • cli_test.rs: args_for_host() drops -r /lib64 when absent.
  • profile_integration.rs: read_list() omits /lib64 from the TOML when absent.
  • ffi popen.rs/restore.rs/handler_smoke.rs: skip the fs_read when absent.
  • examples/openat_audit.rs: fs_read -> fs_read_if_exists.
  • tests/c/handler_smoke.c (the pure-C smoke test) granted fs_read("/lib64") unconditionally, so sandlock_run_with_handlers returned NULL on riscv64. It now grants /lib64 only when access(2) finds it.

On x86-64 (where /lib64 exists) the resulting arguments and policies are unchanged.

2. statx syscall number + rootfs-helper ETXTBSY (8275206)

  • test_seccomp_cow_statx_created_file hardcoded the x86-64 statx number (332) with only an aarch64 (291) special case; RISC-V uses the asm-generic table where statx is also 291, so the raw syscall returned ENOSYS. Use the file's existing machine() in ('aarch64', 'riscv64') convention.
  • test_chroot_* failed with ETXTBSY when build_test_rootfs fell back from hard_link to fs::copy (temp rootfs on a different filesystem than the compiled helper): the copy's writable fd can be inherited by a concurrent fork+exec in a parallel test, leaving the helper open for write when it is execve'd. Place the temp rootfs under CARGO_TARGET_TMPDIR (same filesystem as the helper) so the atomic hard_link path is taken.

3. Exec fd-redirect clobbered argv[0]: product bug on all arches (73c4cf2, be654d7, 7d18c24)

The coreutils: unknown program '3' failure was diagnosed on RISC-V hardware: Ubuntu rust-coreutils ships /bin/echo and /bin/cat as uutils multicall binaries that dispatch on basename(argv[0]).

The COW and chroot exec handlers redirect an exec by rewriting the child's path buffer to /proc/self/fd/N (a user-notif supervisor cannot change syscall registers). Shells pass the same buffer as the execve path and argv[0] (dash execs ./m with path == argv[0]), so the rewrite silently clobbered argv[0] on every architecture. GNU echo ignores argv[0] and hid the bug; a multicall binary dispatches on it and fails with basename "3". Reproduced on x86-64 with an argv0-printing binary before fixing.

The fix, rewrite_exec_path_to_fd() in seccomp/notif.rs, plans the rewrite against the full argv and envp pointer arrays and relocates every string the write window touches, patching the affected pointer slots:

  • strings starting inside the window (shell aliasing, packed argv[1]/envp strings), with a fixpoint loop because each relocation grows the window;
  • strings whose tail reaches the window from below (a path pointer aimed into the middle of a longer string), scanned downward with each gap segment read once;
  • layouts where the argv/envp pointer arrays themselves fall inside the window are refused with EFAULT, since the array addresses live in registers and cannot be moved.

All reads happen before any write, so relocated strings always carry their original bytes. String length is capped at the kernel's own MAX_ARG_STRLEN; the 8-byte pointer-slot width is safe because the BPF filter kills non-native-arch syscalls before they reach the notif fd.

Tests: 9 planner unit tests against fake child memory (aliased argv[0], packed argv[1], envp strings, tail overlap, window growth, array-in-window rejection, NULL arrays); a packed-argv integration test crafts the path and argv[1] in one buffer via raw execve(2) and fails on the pre-fix code.

4. Test portability against uutils coreutils (73c4cf2, 8d7b915)

  • test_seccomp_cow_exec_created_file copies tests/rootfs-helper instead of /bin/echo: a copied uutils echo dispatches on basename(argv[0]) = "m" and fails even outside a sandbox. The helper is itself busybox-style argv[0]-dispatched, so the test both avoids depending on the host coreutils flavor and regression-tests the argv[0] preservation above.
  • inject_bytes_delivers_synthetic_content_to_guest intercepts only openat, but uutils cat statx-es a path operand before opening it and failed on the host-nonexistent sentinel before the handler could fire. Feed cat via stdin redirection so the intercepted open is the shell's bare openat.

…i tests

RISC-V glibc and musl put the dynamic loader under /lib and have no
/lib64 at all. These tests granted /lib64 as a mandatory read (a plain
-r flag, a profile `read` entry, or an ffi fs_read call), so confinement
aborted on such hosts and the tests failed even though the sandbox is
fine. The builder-based tests already handled this with fs_read_if_exists;
the CLI, profile, and ffi layers had no equivalent.

Add /lib64 only when the host actually has it, mirroring fs_read_if_exists:
- cli_test.rs: args_for_host() drops `-r /lib64` when /lib64 is absent.
- profile_integration.rs: read_list() omits /lib64 from the TOML when absent.
- ffi popen.rs/restore.rs: skip /lib64 in the fs_read loop when absent.
- ffi handler_smoke.rs: gate the /lib64 fs_read on host presence.
- examples/openat_audit.rs: fs_read -> fs_read_if_exists.

On x86-64 (where /lib64 exists) the resulting arguments and policies are
unchanged; all cli_test and profile_integration cases still pass.
…per install

Two RISC-V-only integration failures, unrelated to /lib64:

- test_seccomp_cow_statx_created_file hardcoded the x86-64 statx syscall
  number (332) with only an aarch64 (291) special case. RISC-V uses the
  asm-generic syscall table where statx is 291 (like aarch64), so the raw
  syscall returned ENOSYS (errno 38). Match the file's own
  `machine() in ('aarch64', 'riscv64')` convention already used at lines
  282 and 342.

- test_chroot_* failed with ETXTBSY when build_test_rootfs fell back from
  hard_link to fs::copy. That fallback fires when the temp rootfs lands on
  a different filesystem than the compiled helper (e.g. /tmp on tmpfs);
  the copy's writable fd can be inherited by a concurrent fork+exec in
  another parallel test, leaving the helper open for write when it is
  execve'd. Place the temp rootfs under CARGO_TARGET_TMPDIR (same
  filesystem as the helper) so the atomic hard_link path is taken and no
  writable fd is ever opened on the executable.
…th to /proc/self/fd/N

Signed-off-by: Cong Wang <cwang@multikernel.io>
…tils portability

Signed-off-by: Cong Wang <cwang@multikernel.io>
…ath rewrite

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk force-pushed the fix/riscv-test-portability branch from be654d7 to a38646b Compare July 15, 2026 23:28
Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk force-pushed the fix/riscv-test-portability branch from a38646b to 7d18c24 Compare July 15, 2026 23:30
@congwang-mk congwang-mk changed the title fix(test): RISC-V test portability (/lib64 optional, statx nr, rootfs-helper ETXTBSY) fix: RISC-V test portability and exec fd-redirect argv[0] preservation Jul 15, 2026
@congwang-mk
congwang-mk merged commit 8b5eccf into main Jul 16, 2026
13 checks passed
@congwang-mk
congwang-mk deleted the fix/riscv-test-portability branch July 16, 2026 16:59
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