From 277178519e6987eab9aa881f436aeefe48d16099 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 24 Jul 2026 23:48:40 -0700 Subject: [PATCH] feat: inherit descriptors across child execution --- PLAN.md | 4 +- README.md | 9 +- crates/browser-runtime/src/lib.rs | 2 +- crates/linux-runtime/src/lib.rs | 156 ++++++++++++++++-- docs/architecture.md | 4 +- guest-tests/spawn-exec/README.md | 9 +- guest-tests/spawn-exec/parent.c | 37 +++++ guest-tests/spawn-exec/spawn-exec.aarch64.elf | Bin 1392 -> 1712 bytes web/tests/probe.spec.ts | 3 + 9 files changed, 198 insertions(+), 26 deletions(-) diff --git a/PLAN.md b/PLAN.md index 4937b05..0205075 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1889,7 +1889,9 @@ Compiler source diagnostics now have a product UI path. Clang-format stderr reco The first pipe checkpoint adds AArch64 `pipe2` with process-owned descriptors and a fixed 64 KiB FIFO queue. The initial contract deliberately requires `O_NONBLOCK`: reads distinguish `EAGAIN` from EOF, writes are partial at remaining capacity and return `EPIPE` after the reader closes, `fstat` identifies FIFO descriptors, `lseek` returns `ESPIPE`, close-on-exec shares the ordinary descriptor flag path, and pipe ends count against the open-file limit. A checked-in assembly fixture round-trips bytes through the pipe and passes on the native host and in Chromium. Blocking pipe suspension/wakeup and descriptor sharing across concurrent child processes remain the next orchestration checkpoint; build cache policy and interactive-performance work also remain. -The first direct child-process checkpoint implements a deliberately constrained spawn/exec model rather than claiming `fork`. AArch64 `clone` accepts exactly `CLONE_VM | CLONE_VFORK | SIGCHLD`, one suspended parent, and an optional child stack; it rejects nested or unreaped children, TID/TLS modes, other flags, and inherited nonstandard descriptors. PID 2 can run directly or replace itself from the guest filesystem, after which exit restores the parent CPU, memory, interpreter, signals, identity, and configuration while retaining aggregate resource counters, output, trace history, and filesystem mutations. `getpid`, `getppid`, `gettid`, and `wait4` expose the child identity and encoded exit status. Native and Chromium fixtures cover both direct child exit and child `execve` followed by parent resumption. Parent/child descriptor sharing, blocking pipe wakeups, general clone modes, build cache policy, and interactive-performance work remain. +The first direct child-process checkpoint implements a deliberately constrained spawn/exec model rather than claiming `fork`. AArch64 `clone` accepts exactly `CLONE_VM | CLONE_VFORK | SIGCHLD`, one suspended parent, and an optional child stack; it rejects nested or unreaped children, TID/TLS modes, and other flags. PID 2 can run directly or replace itself from the guest filesystem, after which exit restores the parent CPU, memory, interpreter, signals, identity, and configuration while retaining aggregate resource counters, output, trace history, and filesystem mutations. `getpid`, `getppid`, `gettid`, and `wait4` expose the child identity and encoded exit status. Native and Chromium fixtures cover both direct child exit and child `execve` followed by parent resumption. Blocking pipe wakeups, general clone modes, build cache policy, and interactive-performance work remain. + +The constrained child now inherits a cloned descriptor table backed by shared host open-file descriptions and process-owned pipe queues. Shared file offsets and child writes survive parent restoration, while a child close removes only its table entry and closes the underlying host handle only when no suspended-parent reference remains. Pipe reader/writer lifetime likewise accounts for both tables, so child `O_CLOEXEC` cleanup cannot discard the parent's endpoints or queued data. The spawn/exec fixture now passes bytes through an inherited close-on-exec pipe before replacement, then validates the data and EOF after `wait4`; native runtime and Chromium-facing coverage use that same ELF and filesystem image. Blocking pipe suspension/wakeup, general clone modes, build cache policy, and interactive-performance work remain. Do not begin the full web IDE before item 30 passes. diff --git a/README.md b/README.md index b07ccdd..436d644 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,11 @@ traceback behavior, and current limitations are summarized in Phase 6 includes static process replacement, a packaged Clang/LLD/musl toolchain, browser-edited C source, linked compiler diagnostics, bounded pipes, and a single-child spawn/exec model. `guest-tests/spawn-exec` suspends a parent, -runs a constrained vfork-style child that loads a project ELF, restores the -parent, and reaps the child's exact status. Native and Chromium hosts share the -same regressions. General fork/clone modes, inherited nonstandard descriptors, -concurrent children, and blocking pipe scheduling remain future milestones. +runs a constrained vfork-style child that inherits open files and pipes, loads +a project ELF with close-on-exec handling, restores the parent, and reaps the +child's exact status. Native and Chromium hosts share the same regressions. +General fork/clone modes, concurrent children, and blocking pipe scheduling +remain future milestones. The browser build generates its Memory64, JSPI, and P-code `.wasm` probes before starting Vite. Generated artifacts are not committed. Select **Uploaded AArch64 ELF** to run an external static executable with a chosen `argv[0]` and one argument per line; the executable is transferred directly to the runtime Worker. diff --git a/crates/browser-runtime/src/lib.rs b/crates/browser-runtime/src/lib.rs index f1d70a1..6c3900f 100644 --- a/crates/browser-runtime/src/lib.rs +++ b/crates/browser-runtime/src/lib.rs @@ -1423,7 +1423,7 @@ mod tests { assert_eq!(result.exit_code, 0); assert_eq!(result.stdout, "spawned child\nparent resumed\n"); assert!(result.stderr.is_empty()); - assert_eq!(result.dispatched_syscalls, 7); + assert_eq!(result.dispatched_syscalls, 13); } #[test] diff --git a/crates/linux-runtime/src/lib.rs b/crates/linux-runtime/src/lib.rs index 712a0de..934f01a 100644 --- a/crates/linux-runtime/src/lib.rs +++ b/crates/linux-runtime/src/lib.rs @@ -175,6 +175,9 @@ struct SuspendedParent { signal_stack: SignalStack, signal_mask: u64, next_mmap_address: GuestAddress, + file_descriptors: BTreeMap, + pipe_descriptors: BTreeMap, + descriptor_paths: BTreeMap>, descriptor_flags: BTreeMap, current_directory: Vec, executable_path: Vec, @@ -889,8 +892,6 @@ impl Process { || self.register(4) != 0 || self.suspended_parent.is_some() || self.exited_child.is_some() - || !self.file_descriptors.is_empty() - || !self.pipe_descriptors.is_empty() { self.set_return(Errno::InvalidArgument.return_value()); return; @@ -910,6 +911,9 @@ impl Process { signal_stack: self.signal_stack, signal_mask: self.signal_mask, next_mmap_address: self.next_mmap_address, + file_descriptors: self.file_descriptors.clone(), + pipe_descriptors: self.pipe_descriptors.clone(), + descriptor_paths: self.descriptor_paths.clone(), descriptor_flags: self.descriptor_flags.clone(), current_directory: self.current_directory.clone(), executable_path: self.executable_path.clone(), @@ -943,6 +947,9 @@ impl Process { self.signal_stack = parent.signal_stack; self.signal_mask = parent.signal_mask; self.next_mmap_address = parent.next_mmap_address; + self.file_descriptors = parent.file_descriptors; + self.pipe_descriptors = parent.pipe_descriptors; + self.descriptor_paths = parent.descriptor_paths; self.descriptor_flags = parent.descriptor_flags; self.current_directory = parent.current_directory; self.executable_path = parent.executable_path; @@ -2174,19 +2181,25 @@ impl Process { file_descriptor: u32, ) -> Result<(), Errno> { if let Some(handle) = self.file_descriptors.get(&file_descriptor).copied() { - filesystem.close(handle).map_err(filesystem_error_errno)?; + if !self.other_file_descriptor_references(file_descriptor, handle) { + filesystem.close(handle).map_err(filesystem_error_errno)?; + } self.file_descriptors.remove(&file_descriptor); self.descriptor_paths.remove(&file_descriptor); } else if let Some(end) = self.pipe_descriptors.remove(&file_descriptor) { let pipe_id = match end { PipeEnd::Read(pipe_id) => { - if let Some(pipe) = self.pipes.get_mut(&pipe_id) { + if !self.pipe_end_is_referenced(end) + && let Some(pipe) = self.pipes.get_mut(&pipe_id) + { pipe.reader_open = false; } pipe_id } PipeEnd::Write(pipe_id) => { - if let Some(pipe) = self.pipes.get_mut(&pipe_id) { + if !self.pipe_end_is_referenced(end) + && let Some(pipe) = self.pipes.get_mut(&pipe_id) + { pipe.writer_open = false; } pipe_id @@ -2206,6 +2219,30 @@ impl Process { Ok(()) } + fn other_file_descriptor_references(&self, descriptor: u32, handle: u64) -> bool { + self.file_descriptors + .iter() + .any(|(candidate, value)| *candidate != descriptor && *value == handle) + || self.suspended_parent.as_ref().is_some_and(|parent| { + parent + .file_descriptors + .values() + .any(|candidate| *candidate == handle) + }) + } + + fn pipe_end_is_referenced(&self, end: PipeEnd) -> bool { + self.pipe_descriptors + .values() + .any(|candidate| *candidate == end) + || self.suspended_parent.as_ref().is_some_and(|parent| { + parent + .pipe_descriptors + .values() + .any(|candidate| *candidate == end) + }) + } + fn close_all_descriptors(&mut self, filesystem: &mut F) { let descriptors = self .file_descriptors @@ -2218,7 +2255,6 @@ impl Process { } self.file_descriptors.clear(); self.pipe_descriptors.clear(); - self.pipes.clear(); self.descriptor_paths.clear(); self.descriptor_flags .retain(|descriptor, _| *descriptor < 3); @@ -2652,7 +2688,7 @@ mod tests { AT_FDCWD, AnonymousPipe, CHILD_PROCESS_ID, DESCRIPTOR_CLOEXEC, ExecutionError, ExecutionEvent, FCNTL_GET_DESCRIPTOR_FLAGS, INITIAL_PROCESS_ID, IOCTL_CLEAR_CLOSE_ON_EXEC, IOCTL_SET_CLOSE_ON_EXEC, OPEN_CLOEXEC, OPEN_DIRECTORY, OPEN_NOCTTY, OPEN_NOFOLLOW, - OPEN_NONBLOCK, OPEN_PATH, PIPE_CAPACITY_BYTES, Process, STANDARD_OUTPUT, + OPEN_NONBLOCK, OPEN_PATH, PIPE_CAPACITY_BYTES, PipeEnd, Process, STANDARD_OUTPUT, STAT_CHARACTER_MODE, STAT_FIFO_MODE, STAT_FILE_SIZE_OFFSET, STAT_MODE_OFFSET, STAT_REGULAR_MODE, STAT_SIZE, SUPPORTED_CLONE_FLAGS, SyscallEvent, SyscallOutcome, }; @@ -3650,25 +3686,115 @@ mod tests { } #[test] - fn constrained_clone_rejects_descriptors_and_wait4_preserves_unreaped_status_on_fault() { + fn constrained_clone_shares_file_and_pipe_descriptors() { let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); let mut process = Process::new(image).unwrap(); - let mut filesystem = NullFileSystem; - process.file_descriptors.insert(3, 1); + let mut filesystem = MemoryFileSystem::new(1024); + let shared_file = filesystem + .open( + b"/tmp/shared.txt", + FileOpenOptions { + access: FileAccess::ReadWrite, + flags: FileOpenFlags::CREATE, + }, + ) + .unwrap(); + process.file_descriptors.insert(3, shared_file); + process + .descriptor_paths + .insert(3, b"/tmp/shared.txt".to_vec()); + let pipe_descriptors = process.state.sp().checked_sub(32).unwrap(); + process.state.set_x(0, pipe_descriptors.get()).unwrap(); + process + .state + .set_x(1, OPEN_NONBLOCK | OPEN_CLOEXEC) + .unwrap(); + process.dispatch_pipe2(); + let mut descriptors = [0; 8]; + process + .memory + .read_exact(pipe_descriptors, &mut descriptors) + .unwrap(); + let reader = u32::from_le_bytes(descriptors[..4].try_into().unwrap()); + let writer = u32::from_le_bytes(descriptors[4..].try_into().unwrap()); + process.state.set_x(0, SUPPORTED_CLONE_FLAGS).unwrap(); for register in 1..=4 { process.state.set_x(register, 0).unwrap(); } process.dispatch_clone(); - assert_eq!(process.register(0), Errno::InvalidArgument.return_value()); - - process.file_descriptors.clear(); - process.state.set_x(0, SUPPORTED_CLONE_FLAGS).unwrap(); - process.dispatch_clone(); assert_eq!(process.register(0), 0); assert_eq!(process.current_process_id, CHILD_PROCESS_ID); + + let source = process.state.sp().checked_sub(64).unwrap(); + process.memory.write(source, b"child").unwrap(); + process.state.set_x(0, 3).unwrap(); + process.state.set_x(1, source.get()).unwrap(); + process.state.set_x(2, 5).unwrap(); + process.dispatch_file_write(&mut filesystem); + assert_eq!(process.register(0), 5); + process.state.set_x(0, 3).unwrap(); + process.dispatch_close(&mut filesystem); + assert_eq!(process.register(0), 0); + assert_eq!( + filesystem + .seek(shared_file, 0, FileSeekFrom::Current) + .unwrap(), + 5 + ); + + process.state.set_x(0, u64::from(writer)).unwrap(); + process.state.set_x(1, source.get()).unwrap(); + process.state.set_x(2, 5).unwrap(); + process.dispatch_file_write(&mut filesystem); + assert_eq!(process.register(0), 5); + process.close_on_exec_descriptors(&mut filesystem); + assert!(process.pipe_descriptors.is_empty()); + assert!(process.pipes.get(&1).unwrap().reader_open); + assert!(process.pipes.get(&1).unwrap().writer_open); + process.finish_child(&mut filesystem, 7); assert_eq!(process.register(0), CHILD_PROCESS_ID); + assert_eq!(process.file_descriptors.get(&3), Some(&shared_file)); + assert_eq!( + process.pipe_descriptors.get(&reader), + Some(&PipeEnd::Read(1)) + ); + assert_eq!( + process.pipe_descriptors.get(&writer), + Some(&PipeEnd::Write(1)) + ); + + let destination = process.state.sp().checked_sub(96).unwrap(); + process.state.set_x(0, u64::from(writer)).unwrap(); + process.dispatch_close(&mut filesystem); + process.state.set_x(0, u64::from(reader)).unwrap(); + process.state.set_x(1, destination.get()).unwrap(); + process.state.set_x(2, 16).unwrap(); + process.dispatch_read(&mut filesystem); + assert_eq!(process.register(0), 5); + let mut pipe_bytes = [0; 5]; + process + .memory + .read_exact(destination, &mut pipe_bytes) + .unwrap(); + assert_eq!(&pipe_bytes, b"child"); + process.state.set_x(0, u64::from(reader)).unwrap(); + process.dispatch_read(&mut filesystem); + assert_eq!(process.register(0), 0); + } + + #[test] + fn wait4_preserves_unreaped_status_on_fault() { + let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); + let mut process = Process::new(image).unwrap(); + let mut filesystem = NullFileSystem; + process.state.set_x(0, SUPPORTED_CLONE_FLAGS).unwrap(); + for register in 1..=4 { + process.state.set_x(register, 0).unwrap(); + } + process.dispatch_clone(); + process.finish_child(&mut filesystem, 7); process.state.set_x(0, CHILD_PROCESS_ID).unwrap(); process.state.set_x(1, 1).unwrap(); diff --git a/docs/architecture.md b/docs/architecture.md index 483c876..50ae221 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -72,7 +72,7 @@ Implements the Phase 2 ephemeral filesystem behind `binarrow-host-api`. It norma ### `binarrow-linux-runtime` -Consumes a loaded process image, owns its architectural execution state and descriptor table, and repeatedly runs the interpreter to structured supervisor-call stops. The dispatcher implements the process calls reached by the static Rust fixture plus `openat`, `close`, `lseek`, regular-file `read`/`write`, bounded nonblocking `pipe2`, constrained `clone`/`wait4`, PID queries, and static-ELF `execve`. Anonymous pipes are process-owned rather than host filesystem objects: each has a fixed 64 KiB queue, reader/writer lifetime, FIFO `fstat` identity, close-on-exec flags, EOF and broken-pipe behavior, and deterministic `EAGAIN` at empty/full nonblocking boundaries. Blocking pipe creation is rejected until the multi-process scheduler can suspend and wake readers and writers correctly. The first child-process mode accepts exactly `CLONE_VM | CLONE_VFORK | SIGCHLD`, supports an optional child stack, suspends one parent while PID 2 runs, permits the child to replace itself, restores the parent's CPU/memory/interpreter state at child exit, and exposes the encoded status through `wait4`. It rejects nested/unreaped children, TID/TLS features, other clone modes, and inherited nonstandard descriptors; this is a spawn/exec stepping stone, not general fork semantics. `*at` calls resolve absolute paths independently of their directory descriptor and relative paths against `AT_FDCWD` or the retained guest path of an open directory descriptor; `O_PATH`, no-follow, no-controlling-terminal, directory, and close-on-exec flags cover toolchain traversal without exposing host paths. Successful process replacement reads a bounded pathname and argument/environment vectors from the old address space, loads the new executable through `HostFileSystem`, rebuilds the process image, closes `O_CLOEXEC` descriptors, and retains the current directory, credentials, signal mask, ordinary descriptors, resource counters, and trace history. Failed replacement leaves the old process image and descriptors intact. File contents remain behind `HostFileSystem`; guest descriptor allocation, guest-memory copying, errno mapping, and open-file limits remain Linux-runtime responsibilities. Unsupported calls return `ENOSYS`, invalid arguments return Linux errno values, and terminal output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. Every completed dispatch also appends a project-owned trace event whose arguments are captured before return-register mutation; an explicit syscall budget bounds trace growth. +Consumes a loaded process image, owns its architectural execution state and descriptor table, and repeatedly runs the interpreter to structured supervisor-call stops. The dispatcher implements the process calls reached by the static Rust fixture plus `openat`, `close`, `lseek`, regular-file `read`/`write`, bounded nonblocking `pipe2`, constrained `clone`/`wait4`, PID queries, and static-ELF `execve`. Anonymous pipes are process-owned rather than host filesystem objects: each has a fixed 64 KiB queue, reader/writer lifetime, FIFO `fstat` identity, close-on-exec flags, EOF and broken-pipe behavior, and deterministic `EAGAIN` at empty/full nonblocking boundaries. Blocking pipe creation is rejected until the multi-process scheduler can suspend and wake readers and writers correctly. The first child-process mode accepts exactly `CLONE_VM | CLONE_VFORK | SIGCHLD`, supports an optional child stack, suspends one parent while PID 2 runs, permits the child to replace itself, restores the parent's CPU/memory/interpreter state at child exit, and exposes the encoded status through `wait4`. The cloned descriptor table shares host open-file descriptions and anonymous pipe queues with the parent, so offsets and bytes cross the child boundary while child `close` and `O_CLOEXEC` affect only its copies. It rejects nested/unreaped children, TID/TLS features, and other clone modes; this is a spawn/exec stepping stone, not general fork semantics. `*at` calls resolve absolute paths independently of their directory descriptor and relative paths against `AT_FDCWD` or the retained guest path of an open directory descriptor; `O_PATH`, no-follow, no-controlling-terminal, directory, and close-on-exec flags cover toolchain traversal without exposing host paths. Successful process replacement reads a bounded pathname and argument/environment vectors from the old address space, loads the new executable through `HostFileSystem`, rebuilds the process image, closes `O_CLOEXEC` descriptors, and retains the current directory, credentials, signal mask, ordinary descriptors, resource counters, and trace history. Failed replacement leaves the old process image and descriptors intact. File contents remain behind `HostFileSystem`; guest descriptor allocation, guest-memory copying, errno mapping, and open-file limits remain Linux-runtime responsibilities. Unsupported calls return `ENOSYS`, invalid arguments return Linux errno values, and terminal output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. Every completed dispatch also appends a project-owned trace event whose arguments are captured before return-register mutation; an explicit syscall budget bounds trace growth. ### `binarrow-browser-runtime` @@ -104,7 +104,7 @@ Phase 4 is complete for the initial Tier-1 scope: bounded profiling, scalar basi Phase 5 is complete. A shared native/browser CPython regression independently installs the standard-library snapshot, a multi-file project image, and a checksum-pinned official `packaging` 26.2 wheel image. The CLI's repeatable image-overlay option mirrors the browser install operation, while the browser disables conflicting controls until each asynchronous image request is acknowledged and persists the result in OPFS. The Linux runtime implements the close-on-exec ioctl requests used by CPython's directory-opening path. Project and third-party filesystem imports, retained wheel metadata, deterministic output, guest-path tracebacks, exception text, and exit status are verified in both hosts. Guest-side package resolution, network indexes, and native wheels remain outside this phase; Phase 6 moves to in-browser C compilation. -Phase 6 is in progress. Static `execve` replacement provides bounded guest-vector ingestion and Linux-compatible close-on-exec handling; the packaged Clang/LLD/musl path now compiles browser-edited source, links it, and executes the resulting ELF. Compiler-format stderr records navigate to editor source positions. Bounded `O_NONBLOCK` pipes move bytes through one process, and the constrained single-child scheduler now covers clone/exit/wait plus child `execve` with parent restoration in native and Chromium hosts. Descriptor sharing between parent and child, blocking wakeups, broader clone modes, cache policy, and interactive-performance work remain. +Phase 6 is in progress. Static `execve` replacement provides bounded guest-vector ingestion and Linux-compatible close-on-exec handling; the packaged Clang/LLD/musl path now compiles browser-edited source, links it, and executes the resulting ELF. Compiler-format stderr records navigate to editor source positions. Bounded `O_NONBLOCK` pipes move bytes through one process, and the constrained single-child scheduler now covers clone/exit/wait plus child `execve`, inherited open-file descriptions and pipe queues, and parent restoration in native and Chromium hosts. Blocking wakeups, broader clone modes, cache policy, and interactive-performance work remain. An official static AArch64 Linux Zig 0.16.0 distribution serves as an ignored LLVM/LLD compatibility probe while the final Clang package is selected and pruned. It now completes its version path and advances `zig cc` through a bounded 20-million-instruction startup/compilation run using project-local caches. The trace-derived additions are floating-point width conversion, NEON `rev32`, directory-relative `*at` operations, path-only descriptors, initial guest environment entries, and precise missing-parent errno behavior. The probe bundle and caches remain under `.tmp`; they are evidence and test input, not a shipped replacement for Clang/LLD. diff --git a/guest-tests/spawn-exec/README.md b/guest-tests/spawn-exec/README.md index 7f9b978..19acd3a 100644 --- a/guest-tests/spawn-exec/README.md +++ b/guest-tests/spawn-exec/README.md @@ -1,9 +1,12 @@ # Spawn/exec fixture The parent uses the runtime's constrained vfork-style `clone`, then the child -loads `/project/spawn/child` with `execve`. The child validates its argument, -prints `spawned child`, and exits 7. The restored parent reaps that exact status -with `wait4`, prints `parent resumed`, and exits zero. +passes data through an inherited nonblocking pipe before loading +`/project/spawn/child` with `execve`. The pipe uses `O_CLOEXEC`, proving that +closing the child's copies during `execve` leaves the suspended parent's copies +and queued data intact. The child validates its argument, prints `spawned +child`, and exits 7. The restored parent reaps that exact status with `wait4`, +validates the pipe data and EOF, prints `parent resumed`, and exits zero. The filesystem image and both ELFs are deterministic checked-in fixtures. Rebuild them with every cache and temporary file inside the repository: diff --git a/guest-tests/spawn-exec/parent.c b/guest-tests/spawn-exec/parent.c index 6c318cf..95bf312 100644 --- a/guest-tests/spawn-exec/parent.c +++ b/guest-tests/spawn-exec/parent.c @@ -1,4 +1,7 @@ enum { + SYS_CLOSE = 57, + SYS_PIPE2 = 59, + SYS_READ = 63, SYS_WRITE = 64, SYS_EXIT = 93, SYS_CLONE = 220, @@ -7,6 +10,8 @@ enum { CLONE_VM = 0x100, CLONE_VFORK = 0x4000, SIGCHLD = 17, + O_NONBLOCK = 0x800, + O_CLOEXEC = 0x80000, }; static long syscall5( @@ -46,11 +51,17 @@ __attribute__((noreturn)) static void exit_guest(long status) { __attribute__((noreturn)) void _start(void) { static const char executable[] = "/project/spawn/child"; static const char child_argument[] = "spawned"; + static const char handoff[] = "handoff"; static const char parent_message[] = "parent resumed\n"; static const char failure[] = "spawn exec failed\n"; const char *argv[] = {executable, child_argument, 0}; const char *envp[] = {0}; + char pipe_buffer[sizeof(handoff) - 1]; + int pipe_fds[2] = {-1, -1}; int status = 0; + if (syscall4(SYS_PIPE2, (long)pipe_fds, O_NONBLOCK | O_CLOEXEC, 0, 0) != 0) { + exit_guest(5); + } long child = syscall5( SYS_CLONE, CLONE_VM | CLONE_VFORK | SIGCHLD, @@ -60,6 +71,14 @@ __attribute__((noreturn)) void _start(void) { 0); if (child == 0) { + if (syscall4( + SYS_WRITE, + pipe_fds[1], + (long)handoff, + sizeof(handoff) - 1, + 0) != sizeof(handoff) - 1) { + exit_guest(5); + } (void)syscall4(SYS_EXECVE, (long)executable, (long)argv, (long)envp, 0); (void)syscall4(SYS_WRITE, 2, (long)failure, sizeof(failure) - 1, 0); exit_guest(2); @@ -69,6 +88,24 @@ __attribute__((noreturn)) void _start(void) { status != (7 << 8)) { exit_guest(3); } + if (syscall4(SYS_CLOSE, pipe_fds[1], 0, 0, 0) != 0 || + syscall4( + SYS_READ, + pipe_fds[0], + (long)pipe_buffer, + sizeof(pipe_buffer), + 0) != sizeof(pipe_buffer)) { + exit_guest(5); + } + for (unsigned long index = 0; index < sizeof(pipe_buffer); ++index) { + if (pipe_buffer[index] != handoff[index]) { + exit_guest(6); + } + } + if (syscall4(SYS_READ, pipe_fds[0], (long)pipe_buffer, 1, 0) != 0 || + syscall4(SYS_CLOSE, pipe_fds[0], 0, 0, 0) != 0) { + exit_guest(7); + } if (syscall4( SYS_WRITE, 1, diff --git a/guest-tests/spawn-exec/spawn-exec.aarch64.elf b/guest-tests/spawn-exec/spawn-exec.aarch64.elf index 4b26851d2490689cb3bbd6f34c16f37dbdb24c49..e3a196aebe99eb3bc6d1e4f427b9b8ad06b129c8 100644 GIT binary patch delta 757 zcmeyswSjkngpdjoBO?O?1A_yIU|?YQz%o%Qk5h#S!d02LP}~#FfhvV*34tnt(F_b= zlNbV?mZY#SFfiYSr6-407QM@ZgaZ zn8!4iMTCKY!GwWO;L&GEn#sjt- zoDM(#^E3Q>$*2{yxoZK4Ci@Iu!(L9^o}iPxYl=7+)a2wDIkOvC8A4tJJ6LkaG6>1G%0zqm~-BM@@`D-WHE=QWLR delta 478 zcmdnM`+;kMgpdpqBO?O?1A_yIU|?X_z&ueak5h&T!j+l0P}~#FfhvV*Q2=XXV1UsK z3=n~UrzI&YAfq5$h7F7mi4Bt(8A~SDGWs)>Fit+ls8!DaQsBbp;K3s+Fpp_2iwFY) zg9!rzgA5}hg9O9@aPmJhKA&LuUC^kC^3GJz#IJeatMs>It*_su#ixKRcuwf*2VXuE=vRd~9H12ytL#aN8=c zzx-N7bc%%HHSwTEM62CCkwHeG1)Lr_F}VRDr279#ij-T DOkQ!R diff --git a/web/tests/probe.spec.ts b/web/tests/probe.spec.ts index 14969fa..6e68ed4 100644 --- a/web/tests/probe.spec.ts +++ b/web/tests/probe.spec.ts @@ -126,6 +126,9 @@ test("replaces a guest process with an executable from an installed image", asyn await expect(page.getByLabel("System call trace")).toContainText( "execve(path=", ); + await expect(page.getByLabel("System call trace")).toContainText( + "pipe2(pipefd=", + ); await expect(page.getByLabel("System call trace")).toContainText( "fstat(fd=3", ); -- 2.51.2