Export the syscall trampoline so raw syscalls interpose through the guard - #722
Conversation
…(guard suite + nm -D and LD_PRELOAD interposition proof pass; closes a raw-syscall interposition gap)
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 426247fc26
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…verified refuses under the export and passes through an unexported build; secondary test pinning the deployment invariant)
|
@codex review |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The interposition gap
The exec guard interposes the libc
execve/execveatfamily, but a program that calls the libcsyscall(SYS_execve, ...)wrapper directly was never routed through the guard. Thesyscalltrampoline existed inruntime/container/rust/src/lib.rsas a pair ofglobal_asm!blocks (.globl syscalltail-jumping toworkcell_syscall_shim), but an assembler.globldoes not make rustc export the symbol. rustc only puts#[no_mangle]Rust items in the cdylib's export list and localizes everything else through its generated version script (local: *). Sosyscallwas defined inside the.sobut absent from its dynamic symbol table, and nothing overrode the libc wrapper at load time. A rawsyscall(SYS_execve, ...)reached the kernel unguarded.Confirmed on the built cdylib:
nm -Dbefore this change listsworkcell_syscall_shimbut notsyscall.The export mechanism, and why the asm stays
The libc wrapper is variadic (
long syscall(long number, ...)), which Rust cannot express as a function definition, so the implementation must remain a bare register-forwarding trampoline: it tail-jumps intoworkcell_syscall_shimwith every argument register untouched, and the shim readsnumberplus six args and routesSYS_execve/SYS_execveattoguarded_execve/guarded_execveat.The repo already has exactly the right idiom for an exported register-forwarding trampoline:
define_variadic_exec_trampoline!, used forexecl/execlp/execle. It emits a#[unsafe(naked)] #[unsafe(no_mangle)] pub unsafe extern "C" fnwhose body is a singlejmp/bto the target. Being#[no_mangle], rustc places it in the export list, so it survives the version script and-Bsymbolic-functions. Replacing the twoglobal_asm!blocks with one invocation of that macro (syscall->workcell_syscall_shim) exports the symbol while preserving the identical register-forwarding behavior on both x86_64 and aarch64. This is a smaller, more idiomatic change than a bespoke--export-dynamic-symbollink arg (which the version script'slocal: *overrides anyway -- verified: it did not export the symbol) or a hand-written version script.After the change,
nm -Dlistssyscallas an exportedTsymbol.The test row
runtime/container/rust/tests/loader_forms.rsrecorded this form as pending because an in-process row would have reported a false pass while the deployed.sodid not actually interpose. With the export in place that gap is closed, so the row is flipped fromPending(RAW_SYSCALL_PENDING)to a realRefused(MUTABLE_NATIVE)expectation, reached through a newRawSyscallExecvecall variant that invokes the libcsyscall(SYS_execve, ...)wrapper.PENDING_FORMSdrops from 3 to 2, and the now-unusedRAW_SYSCALL_PENDINGconstant is removed. The two remaining pending rows (env cluster, split-at-equals) are unchanged. The harness control that must observe a real refusal still holds.Evidence
nm -D libworkcell_exec_guard.so: before -> onlyworkcell_syscall_shim; after ->syscallandworkcell_syscall_shim.syscall(SYS_execve, "/tmp/nativebin", ...)with the approved guard preload in its child env is refused with the mutable-native message under the patched.so; under the baseline.sothe same call reaches the kernel and runs, confirming the gap and that the export closes it.syscall(SYS_execveat, ..., AT_EMPTY_PATH)on a mutable shell script is still allowed, so the container-smoke raw-execveat expectation is preserved.cargo fmt --check,cargo clippy --all-targets -- -D warnings, andcargo test --locked --offlineall pass in the Linux container, including the now-active raw-syscall row and the deterministic exec-guard tests (a_replaced_pathname_no_longer_matches_its_descriptor, the snapshot/pin family). On darwin,cargo fmt --checkandcargo clippypass; the interposition assertions ran only in the Linux container.