diff --git a/Makefile b/Makefile index c7dbcade..e54b1949 100644 --- a/Makefile +++ b/Makefile @@ -197,6 +197,12 @@ $(BUILD_DIR)/test-pthread: tests/test-pthread.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread +# test-process-lifecycle creates a worker to verify that process PIDs and +# thread TIDs share one namespace-wide allocator across fork children. +$(BUILD_DIR)/test-process-lifecycle: tests/test-process-lifecycle.c src/utils.h | $(BUILD_DIR) + @echo " CROSS $< (with -lpthread)" + $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -Isrc -o $@ $< -lpthread + # test-thread-churn creates >64 threads to force thread-table slot reuse. $(BUILD_DIR)/test-thread-churn: tests/test-thread-churn.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" diff --git a/docs/internals.md b/docs/internals.md index fa285303..680a4771 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -135,6 +135,10 @@ Apple HVF imposes a handful of constraints that shape the rest of the design: handled by the HVC #12 system-instruction trap path. - Only `HV_SYS_REG_*` constants from Hypervisor.framework may be used for register IDs. +- `hv_vcpu_t` value zero is a valid handle (normally the first vCPU in a VM), + not an invalid sentinel. `guest_t` and `thread_entry_t` therefore track + handle ownership with a separate `vcpu_valid` flag; signal preemption, + quiesce, ptrace, and teardown must consult that flag before calling HVF. - Cross-thread vCPU register access is unreliable; all register access must happen on the owning thread. This drives the snapshot protocol used by both ptrace and the GDB stub. @@ -462,9 +466,9 @@ are supported per VM. `src/runtime/thread.c` and `thread.h`: -- `thread_entry_t` per thread holds the vCPU handle, host pthread, per-thread - signal mask, `clear_child_tid` (for `CLONE_CHILD_CLEARTID`), and the - thread's `SP_EL1` exception stack. +- `thread_entry_t` per thread holds the vCPU handle plus its explicit validity, + host pthread, per-thread signal mask, `clear_child_tid` (for + `CLONE_CHILD_CLEARTID`), and the thread's `SP_EL1` exception stack. - `_Thread_local current_thread` gives O(1) access from syscall handlers. - Each thread receives a 4 KiB EL1 exception stack carved out of the shim data region. @@ -541,7 +545,45 @@ state transfer: 4. Child receives state, creates its own VM, restores registers directly into EL0 (bypassing the shim `_start` so callee-saved GPRs survive), and enters the vCPU loop with `X0 = 0` (the child return from `clone`). -5. Parent records the child in the process table and returns the child PID. +5. Before spawning, the parent allocates a namespace-wide guest PID and + reserves both a local process-table slot and shared lifecycle entry. After + IPC succeeds it commits the child's host PID, releases the child into guest + code, and returns the child PID. Allocation or admission failure returns + `EAGAIN` without allowing an untracked child to run. + +### Process Lifecycle And Guest PID 1 + +The first guest process in an elfuse invocation has guest PID 1 and therefore +becomes the fallback parent for orphaned descendants when no living child +subreaper is closer. elfuse does not insert a hidden init process and does not +automatically discard an adopted child's exit status merely because its new +parent is PID 1. As on Linux, the new parent must consume that status with a +`wait*()` call, or explicitly select no-zombie semantics with +`SIGCHLD = SIG_IGN` or `SA_NOCLDWAIT`. + +The invocation-scoped lifecycle registry keeps the Linux wait-format terminal +status (including signal and core-dump bits) and the exiting process's resource +usage until the new parent consumes it. A parent already blocked in `wait*()` +periodically imports newly adopted descendants; adoption of an already exited +child also sends `SIGCHLD` to wake the adopter. Both the per-process wait table +and the invocation-wide lifecycle registry grow geometrically with the actual +fork-family population; the on-disk registry serializes only its live records. +An empty newly created registry is initialized on first use; a nonempty record +set that cannot be read or validated fails closed instead of being overwritten +as empty. Fork admission is reserved before the host helper starts, so +allocation or registry failure fails the new fork instead of silently dropping +a waitable child. Pre-spawn registry reservations are not imported as adopted +children, and a matching local reserved slot remains authoritative through the +registry-publish/local-commit window. + +An application runtime used directly as guest PID 1 may not perform that +reaper role. In that case, adopted terminal statuses remain in elfuse's +invocation-scoped lifecycle registry, and a direct macOS child of the PID 1 +elfuse process may also remain a host zombie until the guest waits, changes its +SIGCHLD disposition, or exits. Guest waitability and host-process cleanup are +separate concerns: a future host-only reaper could collect the macOS process +while retaining its exit status for a later guest wait, but no such background +reaper is currently provided. ### CoW Fork Path diff --git a/src/core/bootstrap.c b/src/core/bootstrap.c index 481929a1..09dacfa8 100644 --- a/src/core/bootstrap.c +++ b/src/core/bootstrap.c @@ -673,6 +673,7 @@ int guest_bootstrap_create_vcpu(guest_t *g, HV_CHECK(hv_vcpu_create(&vcpu, &vexit, NULL)); startup_trace_step("hv_vcpu_create", t0); g->vcpu = vcpu; + g->vcpu_valid = true; g->exit = vexit; *out_vcpu = vcpu; *out_vexit = vexit; diff --git a/src/core/guest.c b/src/core/guest.c index 02250cea..f809f364 100644 --- a/src/core/guest.c +++ b/src/core/guest.c @@ -669,11 +669,11 @@ void guest_destroy(guest_t *g) * VM. This prevents hv_vm_destroy from racing with active vCPUs that may * still be running if thread join timed out during exit_group. */ - thread_destroy_all_vcpus(); - if (g->vcpu) { + bool main_vcpu_destroyed = thread_destroy_all_vcpus(g->vcpu, g->vcpu_valid); + if (g->vcpu_valid && !main_vcpu_destroyed) hv_vcpu_destroy(g->vcpu); - g->vcpu = 0; - } + g->vcpu_valid = false; + g->vcpu = 0; /* Unmap each HVF segment. hv_vm_destroy releases all stage-2 state * regardless, but unmapping explicitly keeps invariants clean for * downstream tools (Instruments, leak detectors). diff --git a/src/core/guest.h b/src/core/guest.h index 5d791948..be10cd14 100644 --- a/src/core/guest.h +++ b/src/core/guest.h @@ -450,6 +450,8 @@ typedef struct { uint64_t ttbr0; /* TTBR0 value (IPA of L0 page table) */ uint64_t ttbr1; /* TTBR1 value (IPA of L0 kernel page table; 0 if unused) */ hv_vcpu_t vcpu; /* vCPU handle */ + bool vcpu_valid; /* Handle value zero is valid; track ownership separately. + */ hv_vcpu_exit_t *exit; /* vCPU exit info */ uint32_t ipa_bits; /* IPA bits requested from HVF */ diff --git a/src/main.c b/src/main.c index 2f6863ca..3168797c 100644 --- a/src/main.c +++ b/src/main.c @@ -656,7 +656,7 @@ int main(int argc, char **argv) /* vcpu_run_loop owns guest execution until exit, fatal signal, or timeout. */ - exit_code = vcpu_run_loop(vcpu, vexit, &g, verbose, timeout_sec); + exit_code = vcpu_run_loop(vcpu, vexit, &g, verbose, timeout_sec, NULL); /* Tear down debugger state before joining workers: a worker parked in * gdb_stub_handle_stop() stays active (not deactivated) until this diff --git a/src/runtime/forkipc.c b/src/runtime/forkipc.c index 03cadcee..788036ac 100644 --- a/src/runtime/forkipc.c +++ b/src/runtime/forkipc.c @@ -306,6 +306,20 @@ int fork_child_main(int ipc_fd, return 1; } + /* Do not enter guest code until the parent has committed both the local + * process-table slot and the shared lifecycle entry. EOF here means fork + * admission failed, so this helper exits without exposing a child whose + * PID the parent cannot later wait for. + */ + uint8_t admission_ready = 0; + if (fork_ipc_read_all(ipc_fd, &admission_ready, sizeof(admission_ready)) < + 0 || + admission_ready != 1) { + log_error("fork-child: parent did not commit child admission"); + guest_destroy(&g); + return 1; + } + /* POSIX: "Signals pending to the parent shall not be pending to the child." * Clear the entire shared pending set before applying state. The child's * single thread starts with an empty private set (thread_register_main @@ -325,6 +339,7 @@ int fork_child_main(int ipc_fd, hv_vcpu_exit_t *vexit; HV_CHECK(hv_vcpu_create(&vcpu, &vexit, NULL)); g.vcpu = vcpu; + g.vcpu_valid = true; g.exit = vexit; /* Restore system registers. For fork children, the child enables the MMU @@ -443,6 +458,12 @@ int fork_child_main(int ipc_fd, * know which guest to update. */ signal_set_shim_globals_guest(&g); + /* The parent may exit after publishing this child but before bootstrap + * reaches the guest loop. Pull any reparent transaction that arrived in + * that window after the shim cache is live, so the fork header's original + * PPID cannot overwrite the adopter selected by the lifecycle registry. + */ + proc_lifecycle_sync_self(&g); /* Same for the fd-table hooks. Must precede any fd_alloc the child performs * (the fd-table-restore step has already run above, but those slots are * populated via direct memcpy of the parent's entries; subsequent @@ -465,8 +486,11 @@ int fork_child_main(int ipc_fd, log_debug("fork-child: entering vCPU loop"); /* The child resumes from the captured fork frame and returns 0 to EL0. */ - int exit_code = vcpu_run_loop(vcpu, vexit, &g, verbose, timeout_sec); + int wait_status = 0; + int exit_code = + vcpu_run_loop(vcpu, vexit, &g, verbose, timeout_sec, &wait_status); + proc_process_exit(wait_status); guest_destroy(&g); return exit_code; } @@ -649,6 +673,8 @@ static int64_t sys_clone_thread(hv_vcpu_t parent_vcpu, /* Allocate guest TID */ int64_t child_tid = proc_alloc_pid(); + if (child_tid < 0) + return child_tid; /* Allocate thread table slot */ if (stack_map_start >= stack_map_end) @@ -823,13 +849,15 @@ static void *thread_create_and_run(void *arg) * thread_quiesce_siblings, and thread_destroy_all_vcpus can scan it * concurrently; they read t->vcpu under thread_lock, so an unlocked store * here is a data race (flagged by ThreadSanitizer). Until this store the - * scanners observe the zeroed handle and skip the slot; the startup_failed - * path below deactivates before destroying, so a scan that does observe the - * handle only ever hands HVF a live vCPU. + * scanners observe vcpu_valid=false and skip the slot; the startup_failed + * path below deactivates before destroying, so a scan that does observe a + * valid handle only ever hands HVF a live vCPU. The separate flag is + * required because handle value zero is valid for the first vCPU. */ pthread_mutex_t *tlock = thread_get_lock(); pthread_mutex_lock(tlock); t->vcpu = vcpu; + t->vcpu_valid = true; t->vexit = vexit; /* A PTRACE_INTERRUPT that raced this bring-up (arrived before the handle * was published) recorded a pending request it could not deliver via @@ -967,7 +995,7 @@ startup_ok:; log_debug("thread tid=%lld starting on vCPU", (long long) t->guest_tid); - vcpu_run_loop(vcpu, vexit, g, verbose, 0); + vcpu_run_loop(vcpu, vexit, g, verbose, 0, NULL); /* Robust futex cleanup: walk the robust list and set FUTEX_OWNER_DIED on * each held lock, then wake one waiter. Must happen before CLEARTID so @@ -1004,6 +1032,10 @@ startup_ok:; log_debug("thread tid=%lld exiting", (long long) t->guest_tid); + pthread_mutex_lock(tlock); + t->vcpu_valid = false; + t->vcpu = 0; + pthread_mutex_unlock(tlock); hv_vcpu_destroy(vcpu); thread_deactivate(t); @@ -1044,6 +1076,8 @@ static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu, { /* Allocate guest TID */ int64_t child_tid = proc_alloc_pid(); + if (child_tid < 0) + return child_tid; /* Allocate thread table slot */ if (stack_map_start >= stack_map_end) @@ -1142,8 +1176,8 @@ static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu, * parent's wait4 can reap it. sys_clone_vm already returned the child tid, so * dropping the slot with thread_deactivate (which clears active) would make * wait4 skip it and leave the parent unable to collect a status. Publish the - * exit status and clear t->vcpu under the thread lock, so thread_interrupt_all - * and thread_destroy_all_vcpus (both skip a null handle) cannot hand the + * exit status and invalidate t->vcpu under the thread lock, so + * thread_interrupt_all and thread_destroy_all_vcpus cannot hand the * torn-down vCPU to HVF, then destroy the handle outside the lock. Keep the * slot active for wait4. Reports SIGKILL because the child never ran a guest * instruction. Does not trigger exit_group: only the child failed. @@ -1153,6 +1187,8 @@ static void vm_clone_report_bringup_failure(thread_entry_t *t) pthread_mutex_t *lock = thread_get_lock(); pthread_mutex_lock(lock); hv_vcpu_t dying = t->vcpu; + bool dying_valid = t->vcpu_valid; + t->vcpu_valid = false; t->vcpu = 0; t->vm_exited = true; t->vm_exit_status = LINUX_SIGKILL; /* WIFSIGNALED, WTERMSIG == SIGKILL */ @@ -1164,7 +1200,7 @@ static void vm_clone_report_bringup_failure(thread_entry_t *t) thread_fork_release_counted_locked(t); pthread_cond_broadcast(&t->ptrace_cond); pthread_mutex_unlock(lock); - if (dying) + if (dying_valid) hv_vcpu_destroy(dying); } @@ -1196,6 +1232,7 @@ static void *vm_clone_thread_run(void *arg) pthread_mutex_t *tlock = thread_get_lock(); pthread_mutex_lock(tlock); t->vcpu = vcpu; + t->vcpu_valid = true; t->vexit = vexit; /* Deliver a PTRACE_INTERRUPT that raced bring-up; see * thread_create_and_run. vm-clone children are the usual ptrace targets. @@ -1251,7 +1288,8 @@ static void *vm_clone_thread_run(void *arg) log_debug("vm_clone tid=%lld starting on vCPU", (long long) t->guest_tid); - int exit_code = vcpu_run_loop(vcpu, vexit, g, verbose, 0); + int wait_status = 0; + int exit_code = vcpu_run_loop(vcpu, vexit, g, verbose, 0, &wait_status); /* CLONE_CHILD_CLEARTID cleanup. Same ordering as thread_entry: drain * deferred stack munmaps before waking the joiner so the parent does not @@ -1281,7 +1319,7 @@ static void *vm_clone_thread_run(void *arg) pthread_mutex_t *lock = thread_get_lock(); pthread_mutex_lock(lock); t->vm_exited = true; - t->vm_exit_status = (exit_code & 0xFF) << 8; + t->vm_exit_status = wait_status; pthread_cond_broadcast(&t->ptrace_cond); pthread_mutex_unlock(lock); @@ -1313,6 +1351,10 @@ static void *vm_clone_thread_run(void *arg) thread_wake_exit_waiters(); } + pthread_mutex_lock(lock); + t->vcpu_valid = false; + t->vcpu = 0; + pthread_mutex_unlock(lock); hv_vcpu_destroy(vcpu); /* Keep the slot active until the parent collects status with wait4. The * slot is freed when thread_ptrace_wait reads vm_exited. @@ -1505,6 +1547,36 @@ int64_t sys_clone(hv_vcpu_t vcpu, posix_spawn_file_actions_addinherit_np(&file_actions, vfork_notify_fds[1]); + /* Allocate and reserve all guest-visible bookkeeping before creating the + * host helper. Linux fork failure must not leave a child running, and a + * successful fork must always return a PID that wait4/waitid can track. + */ + proc_registry_sync_self_pgid(g); + int64_t child_pgid = proc_get_pgid(); + int64_t child_guest_pid = proc_alloc_pid(); + if (child_guest_pid < 0) { + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&spawn_attr); + close(sock_fds[0]); + close(sock_fds[1]); + if (vfork_notify_fds[0] >= 0) + close(vfork_notify_fds[0]); + if (vfork_notify_fds[1] >= 0) + close(vfork_notify_fds[1]); + return child_guest_pid; + } + int reserve_rc = proc_reserve_child(child_guest_pid, child_pgid); + if (reserve_rc < 0) { + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&spawn_attr); + close(sock_fds[0]); + close(sock_fds[1]); + if (vfork_notify_fds[0] >= 0) + close(vfork_notify_fds[0]); + if (vfork_notify_fds[1] >= 0) + close(vfork_notify_fds[1]); + return reserve_rc; + } extern char **environ; pid_t child_host_pid; int spawn_ret = posix_spawn(&child_host_pid, self_path, &file_actions, @@ -1514,6 +1586,7 @@ int64_t sys_clone(hv_vcpu_t vcpu, if (spawn_ret != 0) { log_error("clone: posix_spawn failed: %s", strerror(spawn_ret)); + proc_cancel_child(child_guest_pid); close(sock_fds[0]); close(sock_fds[1]); if (vfork_notify_fds[0] >= 0) @@ -1536,11 +1609,6 @@ int64_t sys_clone(hv_vcpu_t vcpu, } int ipc_sock = sock_fds[0]; - /* Allocate guest PID before serialization so the child header carries its - * final Linux-visible identity. - */ - int64_t child_guest_pid = proc_alloc_pid(); - /* Quiesce sibling vCPUs for snapshot consistency. In multithreaded guests, * sibling vCPUs may be actively mutating guest memory during the fork * snapshot (CoW or legacy IPC copy). Without quiescing them, the child @@ -1652,14 +1720,6 @@ int64_t sys_clone(hv_vcpu_t vcpu, } } - /* Refresh our own process group from the registry before capturing it: a - * parent-side setpgid(this, ...) updates the registry but not the local - * cached pgid, so both the child's inherited group (hdr.pgid below) and the - * publish that follows would otherwise carry -- and re-stamp the registry - * with -- the stale group, misrouting later kill(0)/kill(-pgid). - */ - proc_registry_sync_self_pgid(g); - /* Snapshot of the semantic region array, populated after the memory dump * but before sibling vCPUs resume. Declared up front so all goto paths to * fail_snapshot can free it unconditionally. Header @@ -1695,7 +1755,7 @@ int64_t sys_clone(hv_vcpu_t vcpu, .nice = proc_get_nice(), .absock_namespace_id = absock_get_namespace_id(), .sid = proc_get_sid(), - .pgid = proc_get_pgid(), + .pgid = child_pgid, .is_rosetta = g->is_rosetta, .rosetta_guest_base = g->rosetta_guest_base, .rosetta_va_base = g->rosetta_va_base, @@ -1808,6 +1868,16 @@ int64_t sys_clone(hv_vcpu_t vcpu, goto fail_snapshot; } + if (proc_register_child(child_host_pid, child_guest_pid, child_pgid) < 0) { + log_error("clone: failed to commit child bookkeeping"); + goto fail_snapshot; + } + uint8_t admission_ready = 1; + if (fork_ipc_write_all(ipc_sock, &admission_ready, + sizeof(admission_ready)) < 0) { + log_error("clone: failed to release admitted child"); + goto fail_snapshot; + } /* The process-state payload includes the SCM_RIGHTS handoff for region * backing fds. Keep siblings quiesced until that send completes so a * concurrent munmap/remap cannot close or recycle the captured fd numbers. @@ -1822,12 +1892,6 @@ int64_t sys_clone(hv_vcpu_t vcpu, * its own MAP_PRIVATE view of the same file. */ - /* Register after successful IPC so wait4/waitid can observe the child. Seed - * the child's group from the same value sent in the fork header so the - * parent's table matches the group the child actually inherited. - */ - proc_register_child(child_host_pid, child_guest_pid, hdr.pgid); - /* CLONE_VFORK suspends the parent until the child exits or execs. The * emulator cannot observe guest exec completion across the helper process, * so it waits for the helper to exit. @@ -1866,6 +1930,7 @@ int64_t sys_clone(hv_vcpu_t vcpu, return child_guest_pid; fail_snapshot: + proc_cancel_child(child_guest_pid); free(regions_snapshot); if (snapshot_shm_fd >= 0) close(snapshot_shm_fd); diff --git a/src/runtime/procemu.c b/src/runtime/procemu.c index d340d75d..77a4a5e5 100644 --- a/src/runtime/procemu.c +++ b/src/runtime/procemu.c @@ -15,6 +15,11 @@ */ #define MAPS_ENTRY_MAX 256 +/* Bound the transient host-PID snapshot used by /proc/net enumeration. This + * is an output-work limit, not the dynamically growing lifecycle-table cap. + */ +#define PROC_NET_PID_SNAPSHOT_MAX 1024 + /* Column at which the region name starts in /proc/self/maps output. Matches * observed Linux kernel formatting (verified via strace). */ @@ -840,9 +845,9 @@ typedef bool (*proc_net_socket_visitor)(const struct socket_fdinfo *sinfo, */ static void proc_net_for_each_socket(proc_net_socket_visitor visit, void *ctx) { - pid_t pids[PROC_TABLE_SIZE + 1]; + pid_t pids[PROC_NET_PID_SNAPSHOT_MAX + 1]; pids[0] = getpid(); - int npids = 1 + proc_get_child_pids(pids + 1, PROC_TABLE_SIZE); + int npids = 1 + proc_get_child_pids(pids + 1, PROC_NET_PID_SNAPSHOT_MAX); for (int p = 0; p < npids; p++) { struct proc_fdinfo fdinfo[512]; diff --git a/src/runtime/thread.c b/src/runtime/thread.c index 7cedafe4..d50ee482 100644 --- a/src/runtime/thread.c +++ b/src/runtime/thread.c @@ -122,6 +122,7 @@ void thread_register_main(hv_vcpu_t vcpu, thread_entry_t *t = &thread_table[0]; t->guest_tid = tid; t->vcpu = vcpu; + t->vcpu_valid = true; t->vexit = vexit; t->host_thread = pthread_self(); t->host_thread_needs_join = false; /* Never join the process main thread */ @@ -564,13 +565,17 @@ void thread_join_workers(void) } } -void thread_destroy_all_vcpus(void) +bool thread_destroy_all_vcpus(hv_vcpu_t main_vcpu, bool main_vcpu_valid) { + bool main_destroyed = false; pthread_mutex_lock(&thread_lock); THREAD_FOR_EACH_ACTIVE (t) { - if (!t->vcpu) + if (!t->vcpu_valid) continue; + if (main_vcpu_valid && t->vcpu == main_vcpu) + main_destroyed = true; hv_vcpu_destroy(t->vcpu); + t->vcpu_valid = false; t->vcpu = 0; thread_free_sp_el1_locked(t); __atomic_store_n(&t->active, 0, __ATOMIC_RELEASE); @@ -581,6 +586,7 @@ void thread_destroy_all_vcpus(void) } atomic_store(&active_thread_count, 0); pthread_mutex_unlock(&thread_lock); + return main_destroyed; } void thread_interrupt_all(void) @@ -593,7 +599,7 @@ void thread_interrupt_all(void) pthread_mutex_lock(&thread_lock); THREAD_FOR_EACH_ACTIVE (t) - if (t->vcpu) /* skip slots whose vCPU was already torn down */ + if (t->vcpu_valid) /* skip bring-up/torn-down slots */ vcpus[count++] = t->vcpu; pthread_mutex_unlock(&thread_lock); @@ -643,7 +649,7 @@ void thread_quiesce_siblings(void) continue; t->fork_counted = true; targets++; - if (t->vcpu) + if (t->vcpu_valid) vcpus[count++] = t->vcpu; } diff --git a/src/runtime/thread.h b/src/runtime/thread.h index 057b98e8..d0b45988 100644 --- a/src/runtime/thread.h +++ b/src/runtime/thread.h @@ -37,6 +37,9 @@ typedef struct thread_entry { int64_t guest_tid; /* Linux TID (unique per thread) */ hv_vcpu_t vcpu; /* HVF vCPU handle for this thread */ + bool vcpu_valid; /* Handle has been published and not destroyed. + * hv_vcpu_t value 0 is valid, so the handle + * itself cannot be used as a sentinel. */ hv_vcpu_exit_t *vexit; /* vCPU exit info pointer */ pthread_t host_thread; /* macOS host thread running this vCPU */ bool host_thread_needs_join; /* host_thread was created joinable and nobody @@ -138,7 +141,7 @@ typedef struct thread_entry { bool ptrace_cleanup_pending; /* Destroy condvars after last waiter leaves */ int ptrace_cont_sig; /* Signal to inject on resume (0=none) */ bool ptrace_interrupt_pending; /* PTRACE_INTERRUPT arrived while the vCPU - * was still in bring-up (t->vcpu == 0), so + * was still in bring-up (!vcpu_valid), so * it could not be delivered via * hv_vcpus_exit; the worker self-kicks at * publish to deliver it. Under thread_lock. @@ -314,7 +317,7 @@ void thread_join_workers(void); /* Destroy all active worker vCPUs. Called during guest_destroy to ensure no * vCPUs remain active before hv_vm_destroy(). */ -void thread_destroy_all_vcpus(void); +bool thread_destroy_all_vcpus(hv_vcpu_t main_vcpu, bool main_vcpu_valid); /* Interrupt all active vCPUs by calling hv_vcpus_exit(). Used for signal * preemption: when a signal is queued while a vCPU is running in a tight loop diff --git a/src/syscall/proc-identity.c b/src/syscall/proc-identity.c index 31e34af6..9af7e005 100644 --- a/src/syscall/proc-identity.c +++ b/src/syscall/proc-identity.c @@ -17,6 +17,7 @@ #include "syscall/proc.h" static _Atomic int64_t guest_pid = 1, parent_pid = 0; +static _Atomic bool child_subreaper; static _Atomic uint32_t emu_uid = GUEST_UID, emu_euid = GUEST_UID; static _Atomic uint32_t emu_suid = GUEST_UID, emu_gid = GUEST_GID; static _Atomic uint32_t emu_egid = GUEST_GID, emu_sgid = GUEST_GID; @@ -43,6 +44,7 @@ void proc_identity_init(void) { guest_pid = 1; parent_pid = 0; + child_subreaper = false; uint32_t uid = GUEST_UID; uint32_t gid = GUEST_GID; @@ -354,3 +356,18 @@ void proc_set_identity(int64_t pid, int64_t ppid) guest_pid = pid; parent_pid = ppid; } + +void proc_set_ppid(int64_t ppid) +{ + parent_pid = ppid; +} + +void proc_set_child_subreaper(bool enabled) +{ + child_subreaper = enabled; +} + +bool proc_get_child_subreaper(void) +{ + return child_subreaper; +} diff --git a/src/syscall/proc-identity.h b/src/syscall/proc-identity.h index 27fc0be0..8bfcd38e 100644 --- a/src/syscall/proc-identity.h +++ b/src/syscall/proc-identity.h @@ -7,4 +7,13 @@ #pragma once +#include +#include + void proc_identity_init(void); + +/* Update only the parent identity after orphan reparenting. */ +void proc_set_ppid(int64_t ppid); + +void proc_set_child_subreaper(bool enabled); +bool proc_get_child_subreaper(void); diff --git a/src/syscall/proc.c b/src/syscall/proc.c index 48e9e629..8d28088d 100644 --- a/src/syscall/proc.c +++ b/src/syscall/proc.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include /* flock() */ @@ -74,10 +75,16 @@ static _Atomic bool rosetta_enabled = true; */ static _Atomic bool rosetta_active = false; -/* Process table for tracking fork children */ -static proc_entry_t proc_table[PROC_TABLE_SIZE]; -static int64_t next_guest_pid = 2; +/* Process table for tracking direct and adopted fork children. Start small so + * lifecycle tests exercise growth deterministically; expand under pid_lock as + * the fork family grows. No pointer into this array survives unlocking. + */ +#define PROC_TABLE_INITIAL_CAPACITY 8U +static proc_entry_t proc_table_initial[PROC_TABLE_INITIAL_CAPACITY]; +static proc_entry_t *proc_table = proc_table_initial; +static size_t proc_table_capacity = PROC_TABLE_INITIAL_CAPACITY; static pthread_mutex_t pid_lock = PTHREAD_MUTEX_INITIALIZER; /* Lock order: 6 */ +static pthread_mutex_t autoreap_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t pid_cond = PTHREAD_COND_INITIALIZER; /* Signaled on child exit */ @@ -124,12 +131,42 @@ static _Atomic int exit_group_code = 0; static void proc_registry_publish(pid_t host_pid, int64_t guest_pid_val, int64_t pgid); +static int flock_retry(int fd, int op); +static void lifecycle_publish_self(void); +static int lifecycle_reserve_child(int64_t guest_pid, + int64_t ppid, + int64_t pgid); +static int lifecycle_publish_child(pid_t host_pid, + int64_t guest_pid, + int64_t ppid, + int64_t pgid); +static void lifecycle_update_pgid(int64_t guest_pid, int64_t pgid); +static void lifecycle_import_children(void); +static bool lifecycle_query_exit(int64_t guest_pid, + int *status, + pid_t *host_pid, + int64_t *pgid, + struct rusage *rusage, + bool *rusage_valid); +static void lifecycle_consume(int64_t guest_pid); +static void lifecycle_ack_reparent(int64_t guest_pid, int64_t ppid); +static bool lifecycle_reparent_complete(int64_t guest_pid, int64_t ppid); +static int proc_send_reparent(pid_t host_pid, + int64_t target_guest_pid, + int64_t new_ppid); +static void proc_notify_reparent(pid_t host_pid, + int64_t target_guest_pid, + int64_t new_ppid); +static int64_t proc_wait_autoreap_children(int pid, int options); void proc_init(void) { proc_identity_init(); - next_guest_pid = 2; - memset(proc_table, 0, sizeof(proc_table)); + if (proc_table != proc_table_initial) + free(proc_table); + proc_table = proc_table_initial; + proc_table_capacity = PROC_TABLE_INITIAL_CAPACITY; + memset(proc_table_initial, 0, sizeof(proc_table_initial)); proc_state_init(); thread_init(); futex_init(); @@ -183,6 +220,7 @@ static void proc_init_child_entry(proc_entry_t *entry, int64_t pgid) { entry->active = true; + entry->reserved = false; entry->host_pid = host_pid; entry->guest_pid = guest_pid_val; /* Seed with the group the child inherited at fork. The caller passes the @@ -193,20 +231,37 @@ static void proc_init_child_entry(proc_entry_t *entry, entry->pgid = pgid; entry->exited = false; entry->exit_status = 0; + entry->rusage_valid = false; + entry->rusage_accounted = false; + entry->host_waitable = true; + memset(&entry->rusage, 0, sizeof(entry->rusage)); } static proc_entry_t *proc_find_free_entry(void) { - for (int i = 0; i < PROC_TABLE_SIZE; i++) { - if (!proc_table[i].active) + for (size_t i = 0; i < proc_table_capacity; i++) { + if (!proc_table[i].active && !proc_table[i].reserved) return &proc_table[i]; } - return NULL; + + if (proc_table_capacity > (size_t) INT_MAX / 2) + return NULL; + size_t old_capacity = proc_table_capacity; + size_t new_capacity = old_capacity * 2; + proc_entry_t *grown = calloc(new_capacity, sizeof(*grown)); + if (!grown) + return NULL; + memcpy(grown, proc_table, old_capacity * sizeof(*grown)); + if (proc_table != proc_table_initial) + free(proc_table); + proc_table = grown; + proc_table_capacity = new_capacity; + return &proc_table[old_capacity]; } static proc_entry_t *proc_find_host_entry(pid_t host_pid) { - for (int i = 0; i < PROC_TABLE_SIZE; i++) { + for (size_t i = 0; i < proc_table_capacity; i++) { if (proc_table[i].active && proc_table[i].host_pid == host_pid) return &proc_table[i]; } @@ -215,33 +270,20 @@ static proc_entry_t *proc_find_host_entry(pid_t host_pid) static proc_entry_t *proc_find_guest_entry(int64_t guest_pid_val) { - for (int i = 0; i < PROC_TABLE_SIZE; i++) { + for (size_t i = 0; i < proc_table_capacity; i++) { if (proc_table[i].active && proc_table[i].guest_pid == guest_pid_val) return &proc_table[i]; } return NULL; } -static int proc_write_full(int host_fd, const void *buf, size_t len) +static proc_entry_t *proc_find_reserved_guest_entry(int64_t guest_pid_val) { - const uint8_t *p = buf; - size_t remaining = len; - - while (remaining > 0) { - ssize_t n = write(host_fd, p, remaining); - if (n < 0) { - if (errno == EINTR) - continue; - return -1; - } - if (n == 0) { - errno = EIO; - return -1; - } - p += (size_t) n; - remaining -= (size_t) n; + for (size_t i = 0; i < proc_table_capacity; i++) { + if (proc_table[i].reserved && proc_table[i].guest_pid == guest_pid_val) + return &proc_table[i]; } - return 0; + return NULL; } int rseq_try_abort(guest_t *g, @@ -285,82 +327,173 @@ int rseq_try_abort(guest_t *g, return result; } -int64_t proc_alloc_pid(void) +static bool process_pid_sequence_path(char *out, size_t out_size) { - pthread_mutex_lock(&pid_lock); - int64_t pid = next_guest_pid++; - pthread_mutex_unlock(&pid_lock); - return pid; + char dir[PATH_MAX]; + size_t n = confstr(_CS_DARWIN_USER_TEMP_DIR, dir, sizeof(dir)); + if (n == 0 || n > sizeof(dir)) + return false; + int len = snprintf(out, out_size, "%selfuse-pidseq-%llu", dir, + (unsigned long long) absock_get_namespace_id()); + return len > 0 && (size_t) len < out_size; } -/* Try to reap exited children from the process table. Calls waitpid with - * WNOHANG on each active entry; entries whose host process has exited are - * freed. - * - * Returns the number of slots reclaimed. - */ -static int proc_reap_finished(void) +int64_t proc_alloc_pid(void) { - int reaped = 0; - for (int i = 0; i < PROC_TABLE_SIZE; i++) { - if (!proc_table[i].active) - continue; - if (proc_table[i].exited) { - /* Already marked exited but never waited; free the slot */ - proc_table[i].active = false; - reaped++; - continue; - } - int status; - struct rusage ru; - pid_t ret = wait4(proc_table[i].host_pid, &status, WNOHANG, &ru); - if (ret > 0) { - /* Child exited; free the slot */ - proc_children_cpu_add(&ru); - proc_table[i].active = false; - reaped++; + static _Atomic bool owner_sequence_reset; + char path[PATH_MAX]; + if (!process_pid_sequence_path(path, sizeof(path))) + return -LINUX_EAGAIN; + + if (absock_get_namespace_id() == (uint64_t) getpid() && + !atomic_exchange(&owner_sequence_reset, true)) + unlink(path); + + int fd = open(path, O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + if (fd < 0) + return -LINUX_EAGAIN; + if (flock_retry(fd, LOCK_EX) != 0) { + close(fd); + return -LINUX_EAGAIN; + } + + int64_t result = -LINUX_EAGAIN; + struct stat st; + int64_t next = 2; + if (fstat(fd, &st) != 0) + goto out; + if (st.st_size != 0 && st.st_size != (off_t) sizeof(next)) + goto out; + if (st.st_size == (off_t) sizeof(next)) { + size_t done = 0; + while (done < sizeof(next)) { + ssize_t n = pread(fd, (uint8_t *) &next + done, sizeof(next) - done, + (off_t) done); + if (n < 0 && errno == EINTR) + continue; + if (n <= 0) + goto out; + done += (size_t) n; } } - return reaped; + if (next < 2 || next > INT_MAX) + goto out; + + int64_t pid = next; + next++; + size_t done = 0; + while (done < sizeof(next)) { + ssize_t n = pwrite(fd, (const uint8_t *) &next + done, + sizeof(next) - done, (off_t) done); + if (n < 0 && errno == EINTR) + continue; + if (n <= 0) + goto out; + done += (size_t) n; + } + if (ftruncate(fd, (off_t) sizeof(next)) != 0) + goto out; + result = pid; + +out: + flock_retry(fd, LOCK_UN); + close(fd); + return result; } -int proc_register_child(pid_t host_pid, int64_t guest_pid_val, int64_t pgid) +int proc_reserve_child(int64_t guest_pid_val, int64_t pgid) { + /* Explicit SIG_IGN/SA_NOCLDWAIT children are not guest-waitable. Reclaim + * any that have already terminated before consuming another table slot, + * so a workload that intentionally never calls wait does not retain stale + * bookkeeping entries indefinitely. + */ + if (signal_sigchld_autoreap()) + (void) proc_wait_autoreap_children(-1, WNOHANG); + pthread_mutex_lock(&pid_lock); proc_entry_t *entry = proc_find_free_entry(); - if (entry) { - proc_init_child_entry(entry, host_pid, guest_pid_val, pgid); + if (!entry) { + size_t capacity = proc_table_capacity; pthread_mutex_unlock(&pid_lock); - proc_registry_publish(host_pid, guest_pid_val, pgid); - proc_registry_publish_self(); - return 0; + log_error( + "cannot grow process table beyond %zu slots for child PID " + "%lld", + capacity, (long long) guest_pid_val); + return -LINUX_EAGAIN; } + memset(entry, 0, sizeof(*entry)); + entry->reserved = true; + entry->guest_pid = guest_pid_val; + entry->pgid = pgid; + pthread_mutex_unlock(&pid_lock); - /* Table full. Try reaping exited children, then retry. */ - if (proc_reap_finished() > 0) - entry = proc_find_free_entry(); - if (entry) { - proc_init_child_entry(entry, host_pid, guest_pid_val, pgid); - pthread_mutex_unlock(&pid_lock); - proc_registry_publish(host_pid, guest_pid_val, pgid); - proc_registry_publish_self(); + if (lifecycle_reserve_child(guest_pid_val, proc_get_pid(), pgid) == 0) return 0; + + pthread_mutex_lock(&pid_lock); + entry = proc_find_reserved_guest_entry(guest_pid_val); + if (entry) + memset(entry, 0, sizeof(*entry)); + pthread_mutex_unlock(&pid_lock); + return -LINUX_EAGAIN; +} + +int proc_register_child(pid_t host_pid, int64_t guest_pid_val, int64_t pgid) +{ + if (lifecycle_publish_child(host_pid, guest_pid_val, proc_get_pid(), + pgid) != 0) + return -LINUX_EAGAIN; + + pthread_mutex_lock(&pid_lock); + proc_entry_t *entry = proc_find_reserved_guest_entry(guest_pid_val); + if (!entry) { + pthread_mutex_unlock(&pid_lock); + return -LINUX_EAGAIN; } + proc_init_child_entry(entry, host_pid, guest_pid_val, pgid); + pthread_cond_broadcast(&pid_cond); pthread_mutex_unlock(&pid_lock); - log_error("process table full (%d slots), child PID %lld dropped", - PROC_TABLE_SIZE, (long long) guest_pid_val); - return -1; + proc_registry_publish(host_pid, guest_pid_val, pgid); + proc_registry_publish_self(); + return 0; +} + +void proc_cancel_child(int64_t guest_pid_val) +{ + pthread_mutex_lock(&pid_lock); + proc_entry_t *entry = proc_find_guest_entry(guest_pid_val); + if (!entry) + entry = proc_find_reserved_guest_entry(guest_pid_val); + if (entry) + memset(entry, 0, sizeof(*entry)); + pthread_cond_broadcast(&pid_cond); + pthread_mutex_unlock(&pid_lock); + lifecycle_consume(guest_pid_val); } void proc_mark_child_exited(pid_t host_pid, int status) { + int64_t gpid = -1; pthread_mutex_lock(&pid_lock); proc_entry_t *entry = proc_find_host_entry(host_pid); - if (entry) { + if (entry) + gpid = entry->guest_pid; + pthread_mutex_unlock(&pid_lock); + + if (gpid > 0) { + int guest_status = status; + if (lifecycle_query_exit(gpid, &guest_status, NULL, NULL, NULL, NULL)) + status = guest_status; + } + + pthread_mutex_lock(&pid_lock); + entry = proc_find_host_entry(host_pid); + if (entry && entry->guest_pid == gpid) { entry->exited = true; entry->exit_status = status; - int64_t gpid = entry->guest_pid; + entry->rusage_accounted = true; pthread_cond_broadcast(&pid_cond); pthread_mutex_unlock(&pid_lock); proc_pidfd_notify_exit(gpid); @@ -412,6 +545,510 @@ static bool process_registry_path(char *out, size_t out_size) return len > 0 && (size_t) len < out_size; } +/* The process-group registry above intentionally contains only live host + * members. Linux lifecycle state has different retention rules: an exited + * child must remain discoverable until a guest wait consumes it, including + * after its original host parent exits. Keep that state in a separate binary + * registry protected by flock so unrelated signal/group readers stay simple. + */ +#define LIFECYCLE_MAGIC 0x454C464CU /* "ELFL" */ +#define LIFECYCLE_VERSION 5 +#define LIFECYCLE_INITIAL_CAPACITY 8U + +typedef struct { + pid_t host_pid; + int64_t guest_pid; + int64_t ppid; + int64_t pgid; + bool subreaper; + bool exited; + bool reparent_pending; + bool rusage_valid; + int exit_status; + struct rusage rusage; +} lifecycle_entry_t; + +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t count; + uint32_t _capacity; /* Runtime allocation size; ignored when loading. */ + lifecycle_entry_t entries[]; +} lifecycle_registry_t; + +#define LIFECYCLE_HEADER_SIZE offsetof(lifecycle_registry_t, entries) + +static bool lifecycle_registry_path(char *out, size_t out_size) +{ + char dir[PATH_MAX]; + size_t n = confstr(_CS_DARWIN_USER_TEMP_DIR, dir, sizeof(dir)); + if (n == 0 || n > sizeof(dir)) + return false; + int len = snprintf(out, out_size, "%selfuse-life-%llu", dir, + (unsigned long long) absock_get_namespace_id()); + return len > 0 && (size_t) len < out_size; +} + +static bool lifecycle_registry_size(uint32_t capacity, size_t *size_out) +{ + if ((size_t) capacity > + (SIZE_MAX - LIFECYCLE_HEADER_SIZE) / sizeof(lifecycle_entry_t)) + return false; + *size_out = + LIFECYCLE_HEADER_SIZE + (size_t) capacity * sizeof(lifecycle_entry_t); + return true; +} + +static lifecycle_registry_t *lifecycle_registry_alloc(uint32_t capacity) +{ + size_t size; + if (!lifecycle_registry_size(capacity, &size)) + return NULL; + lifecycle_registry_t *registry = calloc(1, size); + if (registry) + registry->_capacity = capacity; + return registry; +} + +static lifecycle_registry_t *lifecycle_registry_empty(void) +{ + lifecycle_registry_t *registry = + lifecycle_registry_alloc(LIFECYCLE_INITIAL_CAPACITY); + if (registry) { + registry->magic = LIFECYCLE_MAGIC; + registry->version = LIFECYCLE_VERSION; + } + return registry; +} + +static lifecycle_registry_t *lifecycle_load_locked(int fd) +{ + struct stat st; + lifecycle_registry_t header = {0}; + if (fstat(fd, &st) != 0) + return NULL; + if (st.st_size == 0) + return lifecycle_registry_empty(); + if (st.st_size < (off_t) LIFECYCLE_HEADER_SIZE || + lseek(fd, 0, SEEK_SET) != 0 || + read_all(fd, &header, LIFECYCLE_HEADER_SIZE, true) < 0 || + header.magic != LIFECYCLE_MAGIC || header.version != LIFECYCLE_VERSION) + return NULL; + + size_t disk_size; + if (!lifecycle_registry_size(header.count, &disk_size) || + disk_size > (size_t) LLONG_MAX || st.st_size != (off_t) disk_size) + return NULL; + + uint32_t capacity = header.count > LIFECYCLE_INITIAL_CAPACITY + ? header.count + : LIFECYCLE_INITIAL_CAPACITY; + lifecycle_registry_t *registry = lifecycle_registry_alloc(capacity); + if (!registry) + return NULL; + registry->magic = header.magic; + registry->version = header.version; + registry->count = header.count; + if (header.count > 0 && + read_all(fd, registry->entries, + (size_t) header.count * sizeof(lifecycle_entry_t), true) < 0) { + free(registry); + return NULL; + } + return registry; +} + +static int lifecycle_save_locked(int fd, const lifecycle_registry_t *registry) +{ + size_t disk_size; + if (registry->count > registry->_capacity || + !lifecycle_registry_size(registry->count, &disk_size) || + disk_size > (size_t) LLONG_MAX) + return -1; + if (ftruncate(fd, 0) != 0 || lseek(fd, 0, SEEK_SET) != 0) + return -1; + return write_all(fd, registry, disk_size); +} + +static int lifecycle_open_locked(char *path, size_t path_size) +{ + static _Atomic bool owner_reset_done; + if (!lifecycle_registry_path(path, path_size)) + return -1; + if (absock_get_namespace_id() == (uint64_t) getpid() && + !atomic_exchange(&owner_reset_done, true)) + unlink(path); + int fd = open(path, O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + if (fd < 0) + return -1; + if (flock_retry(fd, LOCK_EX) != 0) { + close(fd); + return -1; + } + return fd; +} + +static lifecycle_entry_t *lifecycle_find_guest(lifecycle_registry_t *registry, + int64_t guest_pid) +{ + for (uint32_t i = 0; i < registry->count; i++) + if (registry->entries[i].guest_pid == guest_pid) + return ®istry->entries[i]; + return NULL; +} + +static lifecycle_entry_t *lifecycle_upsert(lifecycle_registry_t **registry_ptr, + int64_t guest_pid) +{ + lifecycle_registry_t *registry = *registry_ptr; + lifecycle_entry_t *entry = lifecycle_find_guest(registry, guest_pid); + if (entry) + return entry; + if (registry->count == registry->_capacity) { + if (registry->_capacity > UINT32_MAX / 2) + return NULL; + uint32_t old_capacity = registry->_capacity; + uint32_t new_capacity = + old_capacity ? old_capacity * 2 : LIFECYCLE_INITIAL_CAPACITY; + size_t new_size; + if (!lifecycle_registry_size(new_capacity, &new_size)) + return NULL; + lifecycle_registry_t *grown = realloc(registry, new_size); + if (!grown) + return NULL; + memset( + &grown->entries[old_capacity], 0, + (size_t) (new_capacity - old_capacity) * sizeof(lifecycle_entry_t)); + grown->_capacity = new_capacity; + *registry_ptr = grown; + registry = grown; + } + entry = ®istry->entries[registry->count++]; + memset(entry, 0, sizeof(*entry)); + entry->guest_pid = guest_pid; + return entry; +} + +static void lifecycle_unlock_close(int fd) +{ + flock_retry(fd, LOCK_UN); + close(fd); +} + +static int lifecycle_reserve_child(int64_t guest_pid, + int64_t ppid, + int64_t pgid) +{ + int result = -1; + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return -1; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + lifecycle_entry_t *entry = lifecycle_upsert(®istry, guest_pid); + if (entry) { + entry->host_pid = 0; + entry->ppid = ppid; + entry->pgid = pgid; + entry->subreaper = false; + entry->exited = false; + entry->reparent_pending = false; + entry->rusage_valid = false; + entry->exit_status = 0; + memset(&entry->rusage, 0, sizeof(entry->rusage)); + result = lifecycle_save_locked(fd, registry); + } + free(registry); + } + lifecycle_unlock_close(fd); + return result; +} + +static int lifecycle_publish_child(pid_t host_pid, + int64_t guest_pid, + int64_t ppid, + int64_t pgid) +{ + int result = -1; + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return -1; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + lifecycle_entry_t *entry = lifecycle_find_guest(registry, guest_pid); + if (entry) { + entry->host_pid = host_pid; + /* The reservation owns the parent/group fields, while an exit or + * reparent transaction that won this race owns terminal state. */ + if (!entry->exited && !entry->reparent_pending) { + entry->ppid = ppid; + entry->pgid = pgid; + } + result = lifecycle_save_locked(fd, registry); + } + free(registry); + } + lifecycle_unlock_close(fd); + return result; +} + +static void lifecycle_publish_self(void) +{ + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + lifecycle_entry_t *entry = lifecycle_upsert(®istry, proc_get_pid()); + if (entry) { + entry->host_pid = getpid(); + /* A parent-exit transaction writes the authoritative adopter + * before notifying this process. Do not let an unrelated publish + * from the child overwrite that value with its stale local PPID + * while the reparent control record is still pending. + */ + if (!entry->reparent_pending) + entry->ppid = proc_get_ppid(); + entry->pgid = proc_get_pgid(); + entry->subreaper = proc_get_child_subreaper(); + if (!entry->exited) + entry->exit_status = 0; + (void) lifecycle_save_locked(fd, registry); + } + free(registry); + } + lifecycle_unlock_close(fd); +} + +static void lifecycle_update_pgid(int64_t guest_pid, int64_t pgid) +{ + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + lifecycle_entry_t *entry = lifecycle_find_guest(registry, guest_pid); + if (entry) { + entry->pgid = pgid; + (void) lifecycle_save_locked(fd, registry); + } + free(registry); + } + lifecycle_unlock_close(fd); +} + +static bool lifecycle_query_exit(int64_t guest_pid, + int *status, + pid_t *host_pid, + int64_t *pgid, + struct rusage *rusage, + bool *rusage_valid) +{ + bool exited = false; + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return false; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + lifecycle_entry_t *entry = lifecycle_find_guest(registry, guest_pid); + if (entry) { + if (status) + *status = entry->exit_status; + if (host_pid) + *host_pid = entry->host_pid; + if (pgid) + *pgid = entry->pgid; + if (rusage) + *rusage = entry->rusage; + if (rusage_valid) + *rusage_valid = entry->rusage_valid; + exited = entry->exited; + } + free(registry); + } + lifecycle_unlock_close(fd); + return exited; +} + +static int lifecycle_guest_terminal_status(int64_t guest_pid, int host_status) +{ + int guest_status = host_status; + if (lifecycle_query_exit(guest_pid, &guest_status, NULL, NULL, NULL, NULL)) + return guest_status; + return host_status; +} + +static void lifecycle_consume(int64_t guest_pid) +{ + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + for (uint32_t i = 0; i < registry->count; i++) { + if (registry->entries[i].guest_pid != guest_pid) + continue; + registry->entries[i] = registry->entries[registry->count - 1]; + registry->count--; + (void) lifecycle_save_locked(fd, registry); + break; + } + free(registry); + } + lifecycle_unlock_close(fd); +} + +static void lifecycle_ack_reparent(int64_t guest_pid, int64_t ppid) +{ + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + lifecycle_entry_t *entry = lifecycle_find_guest(registry, guest_pid); + if (entry && entry->ppid == ppid && entry->reparent_pending) { + entry->reparent_pending = false; + (void) lifecycle_save_locked(fd, registry); + } + free(registry); + } + lifecycle_unlock_close(fd); +} + +static bool lifecycle_reparent_complete(int64_t guest_pid, int64_t ppid) +{ + bool complete = false; + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return false; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + lifecycle_entry_t *entry = lifecycle_find_guest(registry, guest_pid); + /* A missing/consumed child or one that has already exited no longer + * needs its live shim PPID cache updated. + */ + complete = !entry || entry->exited || + (entry->ppid == ppid && !entry->reparent_pending); + free(registry); + } + lifecycle_unlock_close(fd); + return complete; +} + +void proc_lifecycle_sync_self(guest_t *g) +{ + int64_t ppid = -1; + bool pending = false; + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + lifecycle_entry_t *entry = + lifecycle_find_guest(registry, proc_get_pid()); + if (entry) { + ppid = entry->ppid; + pending = entry->reparent_pending; + } + free(registry); + } + lifecycle_unlock_close(fd); + + if (ppid <= 0) + return; + proc_set_ppid(ppid); + shim_globals_publish_pid(g, proc_get_pid(), ppid); + if (pending) + lifecycle_ack_reparent(proc_get_pid(), ppid); +} + +static void proc_register_adopted_local(const lifecycle_entry_t *source) +{ + bool registered = false; + pthread_mutex_lock(&pid_lock); + proc_entry_t *entry = proc_find_guest_entry(source->guest_pid); + if (entry && entry->host_waitable) { + pthread_mutex_unlock(&pid_lock); + return; + } + /* A direct child is visible in the lifecycle registry before its local + * admission transaction commits. The registry's host PID may already have + * been published while the local slot is still reserved, so checking only + * source->host_pid would leave a second race window. The reserved slot is + * authoritative: proc_register_child() will commit this same PID shortly. + */ + if (proc_find_reserved_guest_entry(source->guest_pid)) { + pthread_mutex_unlock(&pid_lock); + return; + } + if (!entry) + entry = proc_find_free_entry(); + if (entry) { + if (!entry->active) + proc_init_child_entry(entry, source->host_pid, source->guest_pid, + source->pgid); + entry->host_pid = source->host_pid; + entry->pgid = source->pgid; + entry->host_waitable = false; + if (source->exited) { + entry->exited = true; + entry->exit_status = source->exit_status; + entry->rusage = source->rusage; + entry->rusage_valid = source->rusage_valid; + pthread_cond_broadcast(&pid_cond); + } + registered = true; + } else { + /* Both tables grow geometrically, so an adopted child should acquire a + * local slot unless the host cannot allocate memory. Keep its shared + * lifecycle record intact and report the failure rather than silently + * losing wait ownership; a later import may succeed after memory + * pressure subsides. + */ + log_error( + "cannot grow process table while importing adopted child " + "PID %lld", + (long long) source->guest_pid); + } + pthread_mutex_unlock(&pid_lock); + if (registered && source->exited) + proc_pidfd_notify_exit(source->guest_pid); +} + +static void lifecycle_import_children(void) +{ + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) + return; + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (registry) { + int64_t self = proc_get_pid(); + for (uint32_t i = 0; i < registry->count; i++) { + lifecycle_entry_t *entry = ®istry->entries[i]; + /* host_pid==0 is a pre-spawn reservation, not a live or waitable + * child. The local reserved-slot check in + * proc_register_adopted_local() also closes the later window after + * lifecycle_publish_child() but before local admission commit. + */ + if (entry->host_pid > 0 && entry->guest_pid != self && + entry->ppid == self) + proc_register_adopted_local(entry); + } + free(registry); + } + lifecycle_unlock_close(fd); +} + static void proc_registry_reset_if_owner(const char *path) { static _Atomic bool reset_done; @@ -429,7 +1066,7 @@ typedef struct { int64_t pgid; } registry_entry_t; -#define REGISTRY_MAX_ENTRIES (PROC_TABLE_SIZE * 4) +#define REGISTRY_MAX_ENTRIES 4096 /* flock() retrying past EINTR. Returns 0 on success, -1 on failure. */ static int flock_retry(int fd, int op) @@ -442,16 +1079,16 @@ static int flock_retry(int fd, int op) } /* Read @fd from its current offset and invoke @cb once per newline-terminated - * record, passing a NUL-terminated copy. Records must fit in 63 bytes; both the - * registry ("hostpid guestpid pgid") and the signal transport ("ns tgpid sig") - * use short numeric lines. Overlong records and an unterminated trailing token - * are dropped -- every writer appends a whole record under an exclusive lock, - * so a partial line only appears after a crash mid-write. + * record, passing a NUL-terminated copy. Records must fit in 159 bytes; both + * the registry ("hostpid guestpid pgid") and signal/control transport records + * use bounded numeric lines. Overlong records and an unterminated trailing + * token are dropped -- every writer appends a whole record under an exclusive + * lock, so a partial line only appears after a crash mid-write. */ static void for_each_record(int fd, void (*cb)(char *rec, void *ctx), void *ctx) { char chunk[8192]; - char line[64]; + char line[160]; size_t linelen = 0; bool overlong = false; ssize_t r; @@ -621,7 +1258,7 @@ static void proc_registry_publish(pid_t host_pid, (long long) entries[i].guest_pid, (long long) entries[i].pgid); if (len > 0 && (size_t) len < sizeof(lineb) && - proc_write_full(fd, lineb, (size_t) len) < 0) + write_all(fd, lineb, (size_t) len) < 0) break; } } @@ -632,6 +1269,7 @@ static void proc_registry_publish(pid_t host_pid, void proc_registry_publish_self(void) { proc_registry_publish(getpid(), proc_get_pid(), proc_get_pgid()); + lifecycle_publish_self(); } void proc_registry_sync_self_pgid(guest_t *g) @@ -697,7 +1335,8 @@ int proc_send_guest_signal(pid_t host_pid, int64_t target_guest_pid, int signum) const char *our_path = elfuse_self_path(&our_len); char tpath[PROC_PIDPATHINFO_MAXSIZE]; int tlen = proc_pidpath(host_pid, tpath, sizeof(tpath)); - if (our_len <= 0 || tlen != our_len || memcmp(tpath, our_path, our_len)) { + if (our_len <= 0 || tlen != our_len || + memcmp(tpath, our_path, our_len) != 0) { errno = ESRCH; return -1; } @@ -739,7 +1378,7 @@ int proc_send_guest_signal(pid_t host_pid, int64_t target_guest_pid, int signum) close(fd); return -1; } - int wrc = proc_write_full(fd, line, (size_t) len); + int wrc = write_all(fd, line, (size_t) len); flock_retry(fd, LOCK_UN); if (wrc < 0) { close(fd); @@ -753,11 +1392,201 @@ int proc_send_guest_signal(pid_t host_pid, int64_t target_guest_pid, int signum) return kill(host_pid, SIGUSR2); } +void proc_process_exit(int wait_status) +{ + struct rusage self_rusage; + bool self_rusage_valid = getrusage(RUSAGE_SELF, &self_rusage) == 0; + char path[PATH_MAX]; + int fd = lifecycle_open_locked(path, sizeof(path)); + if (fd < 0) { + /* The direct-parent notification still improves SIGCHLD timing if the + * lifecycle registry is unavailable. + */ + int64_t parent_guest_pid = proc_get_ppid(); + pid_t parent_host_pid = getppid(); + if (parent_guest_pid > 0 && parent_host_pid > 1) + (void) proc_send_guest_signal(parent_host_pid, parent_guest_pid, + LINUX_SIGCHLD); + return; + } + + lifecycle_registry_t *registry = lifecycle_load_locked(fd); + if (!registry) { + lifecycle_unlock_close(fd); + return; + } + + int64_t self_pid = proc_get_pid(); + lifecycle_entry_t *self = lifecycle_upsert(®istry, self_pid); + if (!self) { + free(registry); + lifecycle_unlock_close(fd); + return; + } + self->host_pid = getpid(); + if (self->ppid <= 0) + self->ppid = proc_get_ppid(); + self->pgid = proc_get_pgid(); + self->subreaper = proc_get_child_subreaper(); + self->exited = true; + self->exit_status = wait_status; + self->rusage_valid = self_rusage_valid; + if (self_rusage_valid) + self->rusage = self_rusage; + + /* Linux adopts descendants at the nearest living subreaper, otherwise at + * namespace PID 1. Walk the registry's guest-parent chain while holding the + * namespace lock so concurrent exits cannot produce a split decision. + */ + int64_t adopter_pid = -1; + int64_t ancestor = self->ppid; + for (uint32_t depth = 0; depth < registry->count && ancestor > 0; depth++) { + lifecycle_entry_t *candidate = lifecycle_find_guest(registry, ancestor); + if (!candidate) + break; + if (!candidate->exited && candidate->subreaper) { + adopter_pid = candidate->guest_pid; + break; + } + if (candidate->guest_pid == 1) + break; + ancestor = candidate->ppid; + } + if (adopter_pid < 0) { + lifecycle_entry_t *init = lifecycle_find_guest(registry, 1); + if (init && !init->exited) + adopter_pid = 1; + } + pid_t adopter_host_pid = -1; + lifecycle_entry_t *adopter = lifecycle_find_guest(registry, adopter_pid); + if (adopter && !adopter->exited) + adopter_host_pid = adopter->host_pid; + + lifecycle_entry_t *reparented = + calloc(registry->count ? registry->count : 1, sizeof(*reparented)); + uint32_t nreparented = 0; + if (adopter_pid > 0 && reparented) { + for (uint32_t i = 0; i < registry->count; i++) { + lifecycle_entry_t *child = ®istry->entries[i]; + if (child->guest_pid == self_pid || child->ppid != self_pid) + continue; + child->ppid = adopter_pid; + child->reparent_pending = !child->exited; + reparented[nreparented++] = *child; + } + } + + pid_t parent_host_pid = -1; + int64_t parent_guest_pid = self->ppid; + lifecycle_entry_t *parent = + lifecycle_find_guest(registry, parent_guest_pid); + if (parent && !parent->exited) + parent_host_pid = parent->host_pid; + + (void) lifecycle_save_locked(fd, registry); + free(registry); + lifecycle_unlock_close(fd); + + for (uint32_t i = 0; i < nreparented; i++) { + if (reparented[i].exited) { + /* The exiting process is not necessarily a child of the adopter, + * so its own SIGCHLD goes elsewhere. Notify the adopter explicitly + * for every already-terminal child that became waitable there. */ + if (adopter_host_pid > 0) + (void) proc_send_guest_signal(adopter_host_pid, adopter_pid, + LINUX_SIGCHLD); + } else { + proc_notify_reparent(reparented[i].host_pid, + reparented[i].guest_pid, adopter_pid); + } + } + free(reparented); + + if (parent_host_pid > 0) + (void) proc_send_guest_signal(parent_host_pid, parent_guest_pid, + LINUX_SIGCHLD); +} + +static int proc_send_reparent(pid_t host_pid, + int64_t target_guest_pid, + int64_t new_ppid) +{ + int our_len; + const char *our_path = elfuse_self_path(&our_len); + char tpath[PROC_PIDPATHINFO_MAXSIZE]; + int tlen = proc_pidpath(host_pid, tpath, sizeof(tpath)); + if (our_len <= 0 || tlen != our_len || + memcmp(tpath, our_path, our_len) != 0) { + errno = ESRCH; + return -1; + } + + char path[PATH_MAX]; + if (!signal_transport_path(path, sizeof(path), host_pid)) { + errno = ENAMETOOLONG; + return -1; + } + int fd = open(path, O_CREAT | O_APPEND | O_WRONLY | O_CLOEXEC | O_NOFOLLOW, + 0600); + if (fd < 0) + return -1; + + char line[128]; + int len = snprintf(line, sizeof(line), "R %llu %lld %lld\n", + (unsigned long long) absock_get_namespace_id(), + (long long) target_guest_pid, (long long) new_ppid); + if (len < 0 || (size_t) len >= sizeof(line)) { + close(fd); + errno = EINVAL; + return -1; + } + if (flock_retry(fd, LOCK_EX) != 0) { + close(fd); + return -1; + } + int wrc = write_all(fd, line, (size_t) len); + flock_retry(fd, LOCK_UN); + close(fd); + if (wrc < 0) + return -1; + return kill(host_pid, SIGUSR2); +} + +static void proc_notify_reparent(pid_t host_pid, + int64_t target_guest_pid, + int64_t new_ppid) +{ + int last_error = 0; + + /* SIGUSR2 is a standard signal, so multiple doorbells can coalesce. The + * file record remains durable, but a process exiting immediately after one + * successful kill must not assume the receiver already updated its shim + * identity cache. Retry until the child acknowledges the registry's + * pending transaction, with a bounded 50ms exit-path delay. + */ + for (int attempt = 0; attempt < 20; attempt++) { + if (lifecycle_reparent_complete(target_guest_pid, new_ppid)) + return; + if (proc_send_reparent(host_pid, target_guest_pid, new_ppid) < 0) + last_error = errno; + usleep(2500); + } + + if (!lifecycle_reparent_complete(target_guest_pid, new_ppid)) { + if (last_error) + log_warn("reparent notification to guest pid %lld failed: %s", + (long long) target_guest_pid, strerror(last_error)); + else + log_warn("guest pid %lld did not acknowledge reparent to %lld", + (long long) target_guest_pid, (long long) new_ppid); + } +} + int proc_get_direct_child_pids(pid_t *out, int max_pids) { int count = 0; pthread_mutex_lock(&pid_lock); - for (int i = 0; i < PROC_TABLE_SIZE && count < max_pids; i++) { + for (size_t i = 0; i < proc_table_capacity && count < max_pids; i++) { if (proc_table[i].active && !proc_table[i].exited) out[count++] = proc_table[i].host_pid; } @@ -779,6 +1608,8 @@ int proc_set_child_pgid(int64_t guest_pid_val, int64_t pgid) pthread_mutex_unlock(&pid_lock); if (host_pid > 0) proc_registry_publish(host_pid, guest_pid_val, pgid); + if (host_pid > 0) + lifecycle_update_pgid(guest_pid_val, pgid); return ret; } @@ -979,6 +1810,7 @@ int64_t sys_ptrace(guest_t *g, return 0; /* Already stopped */ } hv_vcpu_t vcpu = target->vcpu; + bool vcpu_valid = target->vcpu_valid; /* If the tracee is still in vCPU bring-up (handle not yet published), * hv_vcpus_exit cannot reach it, and dropping the interrupt would lose * it silently. Record it under thread_lock: the worker checks this flag @@ -987,11 +1819,11 @@ int64_t sys_ptrace(guest_t *g, * (also under thread_lock), so exactly one of the two paths delivers * it. */ - if (!vcpu) + if (!vcpu_valid) target->ptrace_interrupt_pending = true; pthread_mutex_unlock(tlock); - if (vcpu) + if (vcpu_valid) hv_vcpus_exit(&vcpu, 1); return 0; } @@ -1090,10 +1922,207 @@ int write_rusage_to_guest(guest_t *g, uint64_t gva, const struct rusage *ru) */ static void proc_deactivate_slot_if_matches(int slot, pid_t host_pid) { + int64_t guest_pid = -1; pthread_mutex_lock(&pid_lock); - if (proc_table[slot].active && proc_table[slot].host_pid == host_pid) + if (proc_table[slot].active && proc_table[slot].host_pid == host_pid) { + guest_pid = proc_table[slot].guest_pid; proc_table[slot].active = false; + } pthread_mutex_unlock(&pid_lock); + if (guest_pid > 0) + lifecycle_consume(guest_pid); +} + +static bool proc_refresh_external_child(int64_t guest_pid) +{ + int status = 0; + pid_t host_pid = -1; + int64_t pgid = 0; + struct rusage rusage; + bool rusage_valid = false; + bool exited = lifecycle_query_exit(guest_pid, &status, &host_pid, &pgid, + &rusage, &rusage_valid); + if (!exited) + return false; + + pthread_mutex_lock(&pid_lock); + proc_entry_t *entry = proc_find_guest_entry(guest_pid); + if (entry && !entry->host_waitable) { + entry->host_pid = host_pid; + entry->pgid = pgid; + entry->exited = true; + entry->exit_status = status; + entry->rusage = rusage; + entry->rusage_valid = rusage_valid; + pthread_cond_broadcast(&pid_cond); + } + pthread_mutex_unlock(&pid_lock); + proc_pidfd_notify_exit(guest_pid); + return true; +} + +static void proc_account_entry_locked(proc_entry_t *entry) +{ + if (entry->rusage_valid && !entry->rusage_accounted) { + proc_children_cpu_add(&entry->rusage); + entry->rusage_accounted = true; + } +} + +void proc_autoreap_exited_children(void) +{ + /* The signal-drain thread and a guest rt_sigaction thread can both enter + * here while SIGCHLD is transitioning away from an auto-reap disposition. + * Serialize the whole host-reap/local-deactivate/registry-consume sequence + * so rt_sigaction cannot return while another reaper has consumed the + * shared lifecycle entry but still exposes the local wait slot. + */ + pthread_mutex_lock(&autoreap_lock); + + /* Adopted descendants may not have a local table slot until their new + * parent performs a wait. Import them first so an explicit no-zombie + * disposition applies equally to direct and adopted children. + */ + lifecycle_import_children(); + + for (size_t i = 0;; i++) { + pthread_mutex_lock(&pid_lock); + if (i >= proc_table_capacity) { + pthread_mutex_unlock(&pid_lock); + break; + } + if (!proc_table[i].active) { + pthread_mutex_unlock(&pid_lock); + continue; + } + int64_t guest_pid = proc_table[i].guest_pid; + pid_t host_pid = proc_table[i].host_pid; + bool host_waitable = proc_table[i].host_waitable; + pthread_mutex_unlock(&pid_lock); + + if (!lifecycle_query_exit(guest_pid, NULL, NULL, NULL, NULL, NULL)) + continue; + + if (!host_waitable) { + pthread_mutex_lock(&pid_lock); + proc_entry_t *entry = proc_find_guest_entry(guest_pid); + if (entry && !entry->host_waitable) + entry->active = false; + pthread_mutex_unlock(&pid_lock); + lifecycle_consume(guest_pid); + proc_pidfd_notify_exit(guest_pid); + continue; + } + + int status = 0; + struct rusage ru; + pid_t ret; + do { + ret = wait4(host_pid, &status, 0, &ru); + } while (ret < 0 && errno == EINTR); + if (ret == host_pid) { + proc_children_cpu_add(&ru); + proc_pidfd_notify_exit(guest_pid); + proc_deactivate_slot_if_matches((int) i, host_pid); + } else if (ret < 0 && errno == ECHILD) { + /* A concurrent consuming wait won the host reap. The disposition + * still makes this child non-waitable to subsequent guest waits. + */ + proc_deactivate_slot_if_matches((int) i, host_pid); + } + } + + pthread_mutex_unlock(&autoreap_lock); +} + +static bool proc_wait_selector_matches(const proc_entry_t *entry, + int pid, + int64_t caller_pgid) +{ + if (!entry->active) + return false; + if (pid == -1) + return true; + if (pid > 0) + return entry->guest_pid == pid; + + int64_t target_pgid = pid == 0 ? caller_pgid : -(int64_t) pid; + return entry->pgid == target_pgid; +} + +static int64_t proc_wait_autoreap_children(int pid, int options) +{ + int64_t caller_pgid = proc_get_pgid(); + for (;;) { + bool found = false; + bool still_active = false; + + pthread_mutex_lock(&pid_lock); + for (size_t i = 0; i < proc_table_capacity; i++) { + if (!proc_wait_selector_matches(&proc_table[i], pid, caller_pgid)) + continue; + found = true; + if (proc_table[i].exited) { + int64_t guest_pid = proc_table[i].guest_pid; + proc_account_entry_locked(&proc_table[i]); + proc_table[i].active = false; + pthread_mutex_unlock(&pid_lock); + lifecycle_consume(guest_pid); + pthread_mutex_lock(&pid_lock); + continue; + } + + if (!proc_table[i].host_waitable) { + int64_t guest_pid = proc_table[i].guest_pid; + pthread_mutex_unlock(&pid_lock); + bool exited = proc_refresh_external_child(guest_pid); + pthread_mutex_lock(&pid_lock); + proc_entry_t *entry = proc_find_guest_entry(guest_pid); + if (exited && entry && entry->exited) { + proc_account_entry_locked(entry); + entry->active = false; + pthread_mutex_unlock(&pid_lock); + lifecycle_consume(guest_pid); + pthread_mutex_lock(&pid_lock); + } else if (entry) { + still_active = true; + } + continue; + } + + pid_t host_pid = proc_table[i].host_pid; + int64_t guest_pid = proc_table[i].guest_pid; + pthread_mutex_unlock(&pid_lock); + int status = 0; + struct rusage ru; + pid_t ret = wait4(host_pid, &status, WNOHANG, &ru); + pthread_mutex_lock(&pid_lock); + if (ret == host_pid) { + proc_children_cpu_add(&ru); + proc_entry_t *entry = proc_find_host_entry(host_pid); + if (entry) + entry->active = false; + pthread_mutex_unlock(&pid_lock); + lifecycle_consume(guest_pid); + pthread_mutex_lock(&pid_lock); + } else if (ret == 0) { + still_active = true; + } else { + proc_entry_t *entry = proc_find_host_entry(host_pid); + if (entry) + entry->active = false; + } + } + pthread_mutex_unlock(&pid_lock); + + if (!found || !still_active) + return -LINUX_ECHILD; + if (options & 1) /* WNOHANG */ + return 0; + if (proc_exit_group_requested()) + return -LINUX_EINTR; + usleep(1000); + } } /* sys_wait4. */ @@ -1104,6 +2133,10 @@ int64_t sys_wait4(guest_t *g, int options, uint64_t rusage_gva) { + lifecycle_import_children(); + if (signal_sigchld_autoreap()) + return proc_wait_autoreap_children(pid, options); + /* First check for ptraced or vm-clone children in the thread table. * thread_ptrace_wait handles both ptrace-stopped and vm-exited states. */ @@ -1143,7 +2176,7 @@ int64_t sys_wait4(guest_t *g, */ for (;;) { bool found_any_child = false; - for (int i = 0; i < PROC_TABLE_SIZE; i++) { + for (size_t i = 0; i < proc_table_capacity; i++) { if (!proc_table[i].active) continue; found_any_child = true; @@ -1151,22 +2184,42 @@ int64_t sys_wait4(guest_t *g, /* Already reaped (from CLONE_VFORK wait) */ int64_t gpid = proc_table[i].guest_pid; int32_t linux_status = proc_table[i].exit_status; + struct rusage ru = proc_table[i].rusage; + bool ru_valid = proc_table[i].rusage_valid; + proc_account_entry_locked(&proc_table[i]); proc_table[i].active = false; pthread_mutex_unlock(&pid_lock); + lifecycle_consume(gpid); if (status_gva && guest_write_small(g, status_gva, &linux_status, sizeof(linux_status)) < 0) return -LINUX_EFAULT; - /* Pre-exited entries have no rusage; zero-fill. */ if (rusage_gva) - write_rusage_to_guest(g, rusage_gva, - &(struct rusage) {0}); + write_rusage_to_guest( + g, rusage_gva, + ru_valid ? &ru : &(struct rusage) {0}); return gpid; } + if (!proc_table[i].host_waitable) { + int64_t external_gpid = proc_table[i].guest_pid; + pthread_mutex_unlock(&pid_lock); + bool ready = proc_refresh_external_child(external_gpid); + pthread_mutex_lock(&pid_lock); + if (ready) { + /* Restart the scan so the newly marked exited entry is + * returned immediately instead of falling through to + * the blocking sleep below. + */ + i = -1; + continue; + } + continue; + } + pid_t host_pid = proc_table[i].host_pid; int64_t gpid = proc_table[i].guest_pid; - int slot = i; + int slot = (int) i; pthread_mutex_unlock(&pid_lock); int status; @@ -1174,6 +2227,8 @@ int64_t sys_wait4(guest_t *g, pid_t ret = wait4(host_pid, &status, mac_options | WNOHANG, &ru); if (ret > 0) { + if (WIFEXITED(status) || WIFSIGNALED(status)) + status = lifecycle_guest_terminal_status(gpid, status); /* Credit CPU only on a terminal report. mac_options may * carry WUNTRACED/WCONTINUED, and a stop/continue report * is a snapshot of a still-running child: crediting it @@ -1206,6 +2261,18 @@ int64_t sys_wait4(guest_t *g, if (!found_any_child) { pthread_mutex_unlock(&pid_lock); + lifecycle_import_children(); + pthread_mutex_lock(&pid_lock); + bool imported_child = false; + for (size_t i = 0; i < proc_table_capacity; i++) { + if (proc_table[i].active) { + imported_child = true; + break; + } + } + if (imported_child) + continue; + pthread_mutex_unlock(&pid_lock); return -LINUX_ECHILD; } if (mac_options & WNOHANG) { @@ -1232,30 +2299,50 @@ int64_t sys_wait4(guest_t *g, struct timespec ts; timespec_deadline_in_ms(&ts, 100); pthread_cond_timedwait(&pid_cond, &pid_lock, &ts); + pthread_mutex_unlock(&pid_lock); + lifecycle_import_children(); + pthread_mutex_lock(&pid_lock); } } /* Wait for specific guest PID */ - for (int i = 0; i < PROC_TABLE_SIZE; i++) { + for (size_t i = 0; i < proc_table_capacity; i++) { if (proc_table[i].active && proc_table[i].guest_pid == pid) { if (proc_table[i].exited) { int64_t gpid = proc_table[i].guest_pid; int32_t linux_status = proc_table[i].exit_status; + struct rusage ru = proc_table[i].rusage; + bool ru_valid = proc_table[i].rusage_valid; + proc_account_entry_locked(&proc_table[i]); proc_table[i].active = false; pthread_mutex_unlock(&pid_lock); + lifecycle_consume(gpid); if (status_gva && guest_write_small(g, status_gva, &linux_status, sizeof(linux_status)) < 0) return -LINUX_EFAULT; - /* Pre-exited entries have no rusage; zero-fill. */ if (rusage_gva) - write_rusage_to_guest(g, rusage_gva, &(struct rusage) {0}); + write_rusage_to_guest( + g, rusage_gva, ru_valid ? &ru : &(struct rusage) {0}); return gpid; } + if (!proc_table[i].host_waitable) { + int64_t external_gpid = proc_table[i].guest_pid; + pthread_mutex_unlock(&pid_lock); + if (proc_refresh_external_child(external_gpid)) + return sys_wait4(g, pid, status_gva, options, rusage_gva); + if (mac_options & WNOHANG) + return 0; + if (proc_exit_group_requested()) + return -LINUX_EINTR; + usleep(1000); + return sys_wait4(g, pid, status_gva, options, rusage_gva); + } + pid_t host_pid = proc_table[i].host_pid; int64_t gpid = proc_table[i].guest_pid; - int slot = i; + int slot = (int) i; pthread_mutex_unlock(&pid_lock); int status; @@ -1287,6 +2374,8 @@ int64_t sys_wait4(guest_t *g, } } if (ret > 0) { + if (WIFEXITED(status) || WIFSIGNALED(status)) + status = lifecycle_guest_terminal_status(gpid, status); /* Same terminal-report gate as the P_ALL branch above. */ if (WIFEXITED(status) || WIFSIGNALED(status)) proc_children_cpu_add(&ru); @@ -1305,8 +2394,6 @@ int64_t sys_wait4(guest_t *g, } /* Re-validate slot: another thread may have reaped it */ proc_deactivate_slot_if_matches(slot, host_pid); - /* Queue SIGCHLD for parent process */ - signal_queue(LINUX_SIGCHLD); return gpid; } else if (ret == 0) { return 0; /* WNOHANG */ @@ -1341,6 +2428,16 @@ int64_t sys_wait4(guest_t *g, #define P_PID 1 #define P_PGID 2 +static int64_t waitid_zero_siginfo(guest_t *g, uint64_t infop_gva) +{ + if (infop_gva == 0) + return 0; + uint8_t zeros[SIGINFO_SIZE] = {0}; + if (guest_write_small(g, infop_gva, zeros, sizeof(zeros)) < 0) + return -LINUX_EFAULT; + return 0; +} + int64_t sys_waitid(guest_t *g, int idtype, int64_t id, @@ -1362,6 +2459,7 @@ int64_t sys_waitid(guest_t *g, /* Convert idtype+id to a waitpid-compatible pid argument */ pid_t wait_pid; + int64_t wait_pgid = -1; switch (idtype) { case P_ALL: wait_pid = -1; @@ -1370,7 +2468,10 @@ int64_t sys_waitid(guest_t *g, wait_pid = (pid_t) id; break; case P_PGID: - wait_pid = -(pid_t) id; + if (id < 0 || id > INT_MAX) + return -LINUX_EINVAL; + wait_pgid = id == 0 ? proc_get_pgid() : id; + wait_pid = id == 0 ? 0 : -(pid_t) id; break; case 3: { /* P_PIDFD */ int64_t resolved = proc_pidfd_lookup_pid((int) id); @@ -1383,6 +2484,14 @@ int64_t sys_waitid(guest_t *g, return -LINUX_EINVAL; } + lifecycle_import_children(); + if (signal_sigchld_autoreap()) { + int64_t result = proc_wait_autoreap_children((int) wait_pid, options); + if (result == 0) + return waitid_zero_siginfo(g, infop_gva); + return result; + } + /* Search process table for matching entry. P_ALL must scan all children * (not block on the first non-exited one), so the wait loop always use * WNOHANG in the inner loop and retry with timedwait if the caller @@ -1392,39 +2501,44 @@ int64_t sys_waitid(guest_t *g, for (;;) { bool found_any = false; - for (int i = 0; i < PROC_TABLE_SIZE; i++) { + for (size_t i = 0; i < proc_table_capacity; i++) { if (!proc_table[i].active) continue; - /* Match: P_ALL matches any, P_PID/P_PIDFD match guest_pid */ + /* Match exactly the requested guest PID or process group. */ if ((idtype == P_PID || idtype == 3 /* P_PIDFD */) && proc_table[i].guest_pid != wait_pid) continue; + if (idtype == P_PGID && proc_table[i].pgid != wait_pgid) + continue; found_any = true; int status; pid_t ret; - int32_t gpid32 = (int32_t) proc_table[i].guest_pid; + int64_t entry_gpid = proc_table[i].guest_pid; + int32_t gpid32 = (int32_t) entry_gpid; if (proc_table[i].exited) { /* Already reaped (from CLONE_VFORK wait) */ status = proc_table[i].exit_status; ret = proc_table[i].host_pid; + } else if (!proc_table[i].host_waitable) { + int64_t external_gpid = proc_table[i].guest_pid; + pthread_mutex_unlock(&pid_lock); + bool ready = proc_refresh_external_child(external_gpid); + pthread_mutex_lock(&pid_lock); + if (ready) { + /* The table now contains the lifecycle-registry status; + * rescan to use the common exited/WNOWAIT path. + */ + i = -1; + } + continue; } else { pid_t host_pid = proc_table[i].host_pid; pthread_mutex_unlock(&pid_lock); struct rusage ru; ret = wait4(host_pid, &status, WNOHANG, &ru); - /* Credit only a terminal report, and only when this call - * actually consumes the reap: WNOWAIT must leave the child - * waitable with its CPU uncounted until the later consuming - * wait, but this inner wait4() has no WNOWAIT of its own, so - * it consumes the host zombie regardless of the guest's - * WNOWAIT request. - */ - if (ret > 0 && !(options & LINUX_WNOWAIT) && - (WIFEXITED(status) || WIFSIGNALED(status))) - proc_children_cpu_add(&ru); if (ret == 0) { /* This child hasn't exited yet; continue checking others * (P_ALL must scan all children). @@ -1436,7 +2550,20 @@ int64_t sys_waitid(guest_t *g, pthread_mutex_lock(&pid_lock); continue; /* Child may have been reaped concurrently */ } + status = lifecycle_guest_terminal_status(entry_gpid, status); pthread_mutex_lock(&pid_lock); + /* Host wait4 necessarily consumes the host zombie. Preserve + * the status/rusage in the guest process table so Linux + * WNOWAIT remains repeatable and a later consuming wait can + * account and remove it exactly once. + */ + if (proc_table[i].active && + proc_table[i].host_pid == host_pid) { + proc_table[i].exited = true; + proc_table[i].exit_status = status; + proc_table[i].rusage = ru; + proc_table[i].rusage_valid = true; + } } /* Fill siginfo_t in guest memory */ @@ -1475,16 +2602,39 @@ int64_t sys_waitid(guest_t *g, * re-locking (another thread may have reused it while the wait loop * released the lock for waitpid). */ + int64_t consumed_gpid = -1; if (!(options & LINUX_WNOWAIT) && proc_table[i].active && - proc_table[i].host_pid == ret) + proc_table[i].host_pid == ret) { + consumed_gpid = proc_table[i].guest_pid; + proc_account_entry_locked(&proc_table[i]); proc_table[i].active = false; + } pthread_mutex_unlock(&pid_lock); + if (consumed_gpid > 0) + lifecycle_consume(consumed_gpid); return 0; /* waitid returns 0 on success */ } if (!found_any) { pthread_mutex_unlock(&pid_lock); + lifecycle_import_children(); + pthread_mutex_lock(&pid_lock); + bool imported_match = false; + for (size_t i = 0; i < proc_table_capacity; i++) { + if (!proc_table[i].active) + continue; + if ((idtype == P_PID || idtype == 3) && + proc_table[i].guest_pid != wait_pid) + continue; + if (idtype == P_PGID && proc_table[i].pgid != wait_pgid) + continue; + imported_match = true; + break; + } + if (imported_match) + continue; + pthread_mutex_unlock(&pid_lock); return -LINUX_ECHILD; } @@ -1493,17 +2643,16 @@ int64_t sys_waitid(guest_t *g, /* Per POSIX/Linux: zero siginfo when WNOHANG returns with no * waitable children, so callers can distinguish via si_pid. */ - if (infop_gva) { - uint8_t zeros[SIGINFO_SIZE] = {0}; - guest_write_small(g, infop_gva, zeros, SIGINFO_SIZE); - } - return 0; + return waitid_zero_siginfo(g, infop_gva); } /* Blocking: wait on condvar (100ms timeout as safety net) */ struct timespec ts; timespec_deadline_in_ms(&ts, 100); pthread_cond_timedwait(&pid_cond, &pid_lock, &ts); + pthread_mutex_unlock(&pid_lock); + lifecycle_import_children(); + pthread_mutex_lock(&pid_lock); } } @@ -1610,8 +2759,18 @@ static void unlink_own_transport(void) others = true; break; } - if (!others) + if (!others) { unlink(path); + + /* These files share the process-family namespace and have no readers + * once the owner is the last live host member. Avoid leaving one + * fixed-size lifecycle registry and PID counter behind per run. + */ + if (lifecycle_registry_path(path, sizeof(path))) + unlink(path); + if (process_pid_sequence_path(path, sizeof(path))) + unlink(path); + } flock_retry(fd, LOCK_UN); close(fd); } @@ -1673,16 +2832,39 @@ typedef struct { int64_t my_guest_pid; } sig_drain_ctx_t; -/* Parse one NUL-terminated "namespace target_guest_pid signum" transport record - * and queue the signal only when both the fork-family namespace and the - * intended guest pid match this process. A namespace mismatch means the host - * pid was recycled from an unrelated session; a guest-pid mismatch means it was - * recycled onto a different guest in this session. Either way the delivery is - * dropped. +/* Parse a signal record ("namespace target_guest_pid signum") or a reparent + * control record ("R namespace target_guest_pid new_ppid"). Both are accepted + * only when namespace and target identity match this process. */ static void sig_drain_cb(char *rec, void *vctx) { sig_drain_ctx_t *c = vctx; + if (rec[0] == 'R') { + char *p = rec + 1, *end; + errno = 0; + unsigned long long ns = strtoull(p, &end, 10); + if (end == p || errno == ERANGE) + return; + p = end; + errno = 0; + long long tgpid = strtoll(p, &end, 10); + if (end == p || errno == ERANGE) + return; + p = end; + errno = 0; + long long new_ppid = strtoll(p, &end, 10); + if (end == p || errno == ERANGE || *end != '\0') + return; + if (ns == c->my_ns && tgpid == c->my_guest_pid && new_ppid > 0) { + proc_set_ppid((int64_t) new_ppid); + if (signal_refresh_identity_cache()) { + lifecycle_publish_self(); + lifecycle_ack_reparent((int64_t) tgpid, (int64_t) new_ppid); + } + } + return; + } + char *p = rec, *end; unsigned long long ns = strtoull(p, &end, 10); if (end == p) @@ -1699,8 +2881,11 @@ static void sig_drain_cb(char *rec, void *vctx) if (end == p || *end != '\0') return; if (ns == c->my_ns && tgpid == c->my_guest_pid && - RANGE_CHECK(signum, 1, LINUX_NSIG)) + RANGE_CHECK(signum, 1, LINUX_NSIG)) { + if (signum == LINUX_SIGCHLD && signal_sigchld_autoreap()) + proc_autoreap_exited_children(); signal_queue((int) signum); + } } static void drain_external_guest_signal(void) @@ -1781,9 +2966,11 @@ int vcpu_run_loop(hv_vcpu_t vcpu, hv_vcpu_exit_t *vexit, guest_t *g, bool verbose, - int timeout_sec) + int timeout_sec, + int *wait_status_out) { int exit_code = 0; + (void) signal_take_termination_wait_status(); bool running = true; int iter = 0; const int is_main = (timeout_sec > 0); @@ -2924,5 +4111,10 @@ int vcpu_run_loop(hv_vcpu_t vcpu, if (is_main) alarm(0); + if (wait_status_out) { + int signal_status = signal_take_termination_wait_status(); + *wait_status_out = + signal_status != 0 ? signal_status : (exit_code & 0xff) << 8; + } return exit_code; } diff --git a/src/syscall/proc.h b/src/syscall/proc.h index 03830413..4ca8c166 100644 --- a/src/syscall/proc.h +++ b/src/syscall/proc.h @@ -33,6 +33,10 @@ void proc_init(void); int64_t proc_get_pid(void); int64_t proc_get_ppid(void); +/* Linux child-subreaper state, published through the fork-family registry. */ +void proc_set_child_subreaper(bool enabled); +bool proc_get_child_subreaper(void); + typedef struct { const char *path; size_t len; @@ -293,10 +297,9 @@ const char *proc_resolve_sysroot_create_path(const char *path, /* Process table (for fork/clone children). */ -#define PROC_TABLE_SIZE 64 - typedef struct { - bool active; /* Slot is in use */ + bool active; /* Slot is visible to guest wait operations */ + bool reserved; /* Fork admission reserved this slot before spawn */ pid_t host_pid; /* macOS process ID of child elfuse instance */ int64_t guest_pid; /* Guest-visible PID assigned to child */ int64_t pgid; /* Child's process group, inherited at fork and updated @@ -304,13 +307,27 @@ typedef struct { */ bool exited; /* Child has exited */ int exit_status; /* wait status (as returned by waitpid) */ + bool rusage_valid; + bool rusage_accounted; + bool host_waitable; /* false for a child adopted from another host parent */ + struct rusage rusage; } proc_entry_t; -/* Register a child process in the process table. - * Returns 0 on success, -1 if the table is full. +/* Reserve bookkeeping before creating a helper process. A successful + * reservation guarantees proc_register_child() can make the child visible + * after fork IPC completes. Returns 0 or a negative Linux errno. + */ +int proc_reserve_child(int64_t guest_pid, int64_t pgid); + +/* Commit a previously reserved child after fork IPC is ready to release it. + * Returns 0 or a negative Linux errno if the reservation was lost or the + * lifecycle registry could not be updated. */ int proc_register_child(pid_t host_pid, int64_t guest_pid, int64_t pgid); +/* Roll back a reservation (or a just-committed entry on final IPC failure). */ +void proc_cancel_child(int64_t guest_pid); + /* Mark a child as exited by host PID (for CLONE_VFORK wait). */ void proc_mark_child_exited(pid_t host_pid, int status); @@ -371,6 +388,17 @@ int proc_get_namespace_targets(proc_signal_target_t *out, /* Publish the caller's current guest pid/pgid to the fork-family registry. */ void proc_registry_publish_self(void); +/* Reap only children whose shared lifecycle entry is already terminal. */ +void proc_autoreap_exited_children(void); + +/* Pull authoritative PPID state into a newly bootstrapped fork child. */ +void proc_lifecycle_sync_self(guest_t *g); + +/* Notify the guest parent that this process reached a terminal state. Called + * by fork-child teardown before the host process exits. + */ +void proc_process_exit(int wait_status); + /* Pull a parent-published pgid update into this process's local identity. */ void proc_registry_sync_self_pgid(guest_t *g); @@ -479,4 +507,5 @@ int vcpu_run_loop(hv_vcpu_t vcpu, hv_vcpu_exit_t *vexit, guest_t *g, bool verbose, - int timeout_sec); + int timeout_sec, + int *wait_status_out); diff --git a/src/syscall/signal.c b/src/syscall/signal.c index 1632d7e7..27739643 100644 --- a/src/syscall/signal.c +++ b/src/syscall/signal.c @@ -43,6 +43,7 @@ /* Signal state (module-level, process-wide). */ static signal_state_t sig_state; +static _Thread_local int termination_wait_status; /* Per-thread pending fault info. When a synchronous fault (BRK, segfault, etc.) * needs to deliver a signal, the caller sets this before @@ -685,6 +686,31 @@ const signal_state_t *signal_get_state(void) return &sig_state; } +static bool sigaction_autoreaps_sigchld(const linux_sigaction_t *act) +{ + return act->sa_handler == LINUX_SIG_IGN || + (act->sa_flags & LINUX_SA_NOCLDWAIT) != 0; +} + +bool signal_sigchld_autoreap(void) +{ + bool result; + pthread_mutex_lock(&sig_lock); + const linux_sigaction_t *act = &sig_state.actions[LINUX_SIGCHLD - 1]; + result = sigaction_autoreaps_sigchld(act); + pthread_mutex_unlock(&sig_lock); + return result; +} + +bool signal_refresh_identity_cache(void) +{ + guest_t *g = atomic_load_explicit(&attention_guest, memory_order_acquire); + if (!g) + return false; + shim_globals_publish_pid(g, proc_get_pid(), proc_get_ppid()); + return true; +} + void signal_set_state(const signal_state_t *state) { if (!state) @@ -1145,6 +1171,7 @@ int64_t signal_rt_sigaction(guest_t *g, int idx = signum - 1; + bool reap_exited_sigchld = false; pthread_mutex_lock(&sig_lock); /* Return old action if requested */ @@ -1176,10 +1203,21 @@ int64_t signal_rt_sigaction(guest_t *g, (act.sa_flags & LINUX_SA_RESETHAND) ? " SA_RESETHAND" : "", (act.sa_flags & LINUX_SA_NODEFER) ? " SA_NODEFER" : ""); + /* If SIGCHLD was in an automatic-reap disposition, consume children + * that reached a terminal lifecycle state before replacing it. This + * closes the exit-notification race where the guest restores SIG_DFL + * after observing child teardown but before the transport doorbell is + * drained. + */ + reap_exited_sigchld = + signum == LINUX_SIGCHLD && + sigaction_autoreaps_sigchld(&sig_state.actions[idx]); sig_state.actions[idx] = act; } pthread_mutex_unlock(&sig_lock); + if (reap_exited_sigchld) + proc_autoreap_exited_children(); return 0; } @@ -1923,6 +1961,20 @@ static int deliver_signal_locked(hv_vcpu_t vcpu, return 1; } +static void signal_record_termination(int signum) +{ + termination_wait_status = signum; + if (signal_default_disposition(signum) == SIG_DISP_CORE) + termination_wait_status |= 0x80; +} + +int signal_take_termination_wait_status(void) +{ + int status = termination_wait_status; + termination_wait_status = 0; + return status; +} + int signal_deliver(hv_vcpu_t vcpu, guest_t *g, int *exit_code) { pthread_mutex_lock(&sig_lock); @@ -1971,7 +2023,10 @@ int signal_deliver(hv_vcpu_t vcpu, guest_t *g, int *exit_code) */ refresh_pending_hint_locked(); - return deliver_signal_locked(vcpu, g, signum, rt_info, exit_code); + int result = deliver_signal_locked(vcpu, g, signum, rt_info, exit_code); + if (result < 0) + signal_record_termination(signum); + return result; } int signal_deliver_fault(hv_vcpu_t vcpu, guest_t *g, int signum, int *exit_code) @@ -2007,7 +2062,10 @@ int signal_deliver_fault(hv_vcpu_t vcpu, guest_t *g, int signum, int *exit_code) } signal_rt_info_t rt_info = signal_default_info(signum); - return deliver_signal_locked(vcpu, g, signum, rt_info, exit_code); + int result = deliver_signal_locked(vcpu, g, signum, rt_info, exit_code); + if (result < 0) + signal_record_termination(signum); + return result; } /* rt_sigreturn. */ diff --git a/src/syscall/signal.h b/src/syscall/signal.h index 237ec3fc..b0a1c533 100644 --- a/src/syscall/signal.h +++ b/src/syscall/signal.h @@ -56,6 +56,7 @@ /* Linux sigaction flags. */ #define LINUX_SA_SIGINFO 0x00000004 +#define LINUX_SA_NOCLDWAIT 0x00000002 #define LINUX_SA_ONSTACK 0x08000000 #define LINUX_SA_RESTART 0x10000000 #define LINUX_SA_NODEFER 0x40000000 @@ -314,6 +315,12 @@ void signal_set_shim_globals_guest(guest_t *g); */ int signal_deliver(hv_vcpu_t vcpu, guest_t *g, int *exit_code); +/* Return and clear the Linux wait-format status recorded when the current + * vCPU thread terminated because of a signal. Returns zero after a normal + * syscall exit or when no fatal signal was delivered. + */ +int signal_take_termination_wait_status(void); + /* Deliver a synchronous fault signal directly to the faulting (current) thread, * bypassing the process-wide pending set. The caller must have set the fault * info via signal_set_fault_info() immediately before. Same return convention @@ -382,6 +389,17 @@ int64_t signal_sigaltstack(guest_t *g, uint64_t ss_gva, uint64_t old_ss_gva); /* Get/set signal state (for fork IPC serialization). */ const signal_state_t *signal_get_state(void); + +/* True only for the explicit Linux no-zombie dispositions. SIGCHLD's default + * disposition is ignore but does not imply automatic reaping. + */ +bool signal_sigchld_autoreap(void); + +/* Refresh the shim identity cache after an asynchronous reparent message. + * Returns false while a fork child is still bootstrapping and has not + * registered its guest cache yet. + */ +bool signal_refresh_identity_cache(void); void signal_set_state(const signal_state_t *state); /* Snapshot or consume pending signals for signalfd. signal_peek_signalfd() diff --git a/src/syscall/syscall.c b/src/syscall/syscall.c index e81e0470..6eee2991 100644 --- a/src/syscall/syscall.c +++ b/src/syscall/syscall.c @@ -823,7 +823,7 @@ static void thread_force_exit_cb(thread_entry_t *t, void *ctx) * via thread_for_each, so this read is race-free. Same guard as * thread_interrupt_all / thread_quiesce_siblings. */ - if (!t->vcpu) + if (!t->vcpu_valid) return; hv_vcpus_exit(&t->vcpu, 1); } @@ -1377,15 +1377,11 @@ static int64_t sc_prctl(guest_t *g, case LINUX_PR_GET_DUMPABLE: return 1; case LINUX_PR_SET_CHILD_SUBREAPER: - /* Accept silently. elfuse's process model already reaps all children - * within the VM; the flag has no additional effect. - */ + proc_set_child_subreaper(x1 != 0); + proc_registry_publish_self(); return 0; case LINUX_PR_GET_CHILD_SUBREAPER: { - /* Always report "subreaper is set," consistent with the - * accept-and-ignore behavior of PR_SET_CHILD_SUBREAPER. - */ - int32_t val = 1; + int32_t val = proc_get_child_subreaper() ? 1 : 0; if (x1 && guest_write_small(g, x1, &val, sizeof(val)) < 0) return -LINUX_EFAULT; return 0; diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 18a20920..abfb6d0c 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -603,6 +603,8 @@ run_unit_tests() test_check "$runner" "test-clone-childtid" "PASS" "$bindir/test-clone-childtid" test_check "$runner" "test-exec" "exec-works" "$bindir/test-exec" "$bindir/echo-test" exec-works test_check "$runner" "test-fork-exec" "PASS" "$bindir/test-fork-exec" "$bindir/echo-test" + test_rc "$runner" "test-process-lifecycle" 0 \ + "$bindir/test-process-lifecycle" test_check "$runner" "test-cloexec" "PASS" "$bindir/test-cloexec" test_rc "$runner" "test-exec-limits" 0 "$bindir/test-exec-limits" test_rc "$runner" "test-session" 0 "$bindir/test-session" diff --git a/tests/test-process-lifecycle.c b/tests/test-process-lifecycle.c new file mode 100644 index 00000000..96535833 --- /dev/null +++ b/tests/test-process-lifecycle.c @@ -0,0 +1,1556 @@ +/* + * Process lifecycle compatibility tests + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Portable probes for guest-observable Linux fork/wait behavior. The same + * binary is intended to run under elfuse and the qemu-aarch64 reference lane. + * Keep host implementation details out of the assertions. + * + * Initial coverage: + * PID-01..02 process and thread IDs are unique across the fork family + * WAIT-01 WNOHANG does not consume a running child + * WAIT-02 WNOWAIT can be repeated before a consuming wait + * WAIT-03..06 waitid groups/auto-reap, signal status, and admission races + * Z-01..06 zombie retention, no-zombie dispositions, and SIGCHLD timing + * O-01..03 orphan adoption by PID 1 and a child subreaper + * O-04..05 reserved for separate parent-death/job-control follow-ups + * O-06 non-reaping PID 1 retains an adopted exit status + * O-07..08 blocking adoption wakeups, signal status, and adopted rusage + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" +#include "utils.h" + +int passes = 0, fails = 0; + +static int child_exited_cleanly(pid_t pid, int expected_code) +{ + int status = 0; + pid_t ret; + do { + ret = waitpid(pid, &status, 0); + } while (ret < 0 && errno == EINTR); + return ret == pid && WIFEXITED(status) && + WEXITSTATUS(status) == expected_code; +} + +struct nested_pids { + pid_t child; + pid_t grandchild; + int grandchild_ok; +}; + +static void test_nested_pid_uniqueness(void) +{ + TEST("PID-01 nested PIDs are unique"); + + int report[2]; + if (pipe(report) < 0) { + FAIL("PID-01 pipe failed"); + return; + } + + pid_t parent = getpid(); + pid_t child = fork(); + if (child < 0) { + close(report[0]); + close(report[1]); + FAIL("PID-01 first fork failed"); + return; + } + + if (child == 0) { + close(report[0]); + struct nested_pids observed = { + .child = getpid(), + .grandchild = -1, + .grandchild_ok = 0, + }; + + pid_t grandchild = fork(); + if (grandchild == 0) + _exit(37); + if (grandchild > 0) { + observed.grandchild = grandchild; + observed.grandchild_ok = child_exited_cleanly(grandchild, 37); + } + (void) write_all(report[1], &observed, sizeof(observed)); + close(report[1]); + _exit(grandchild < 0 ? 2 : 0); + } + + close(report[1]); + struct nested_pids observed; + memset(&observed, 0, sizeof(observed)); + int read_ok = read_all(report[0], &observed, sizeof(observed), true) >= 0; + close(report[0]); + int child_ok = child_exited_cleanly(child, 0); + + int unique = parent > 0 && child > 0 && observed.child > 0 && + observed.grandchild > 0 && parent != observed.child && + parent != observed.grandchild && + observed.child != observed.grandchild; + if (!read_ok || !child_ok || !observed.grandchild_ok || !unique) { + printf( + "[PID-01 parent=%d fork_child=%d child_self=%d " + "grandchild=%d child_ok=%d grandchild_ok=%d] ", + (int) parent, (int) child, (int) observed.child, + (int) observed.grandchild, child_ok, observed.grandchild_ok); + FAIL("nested process IDs were not unique/waitable"); + return; + } + PASS(); +} + +struct tid_probe { + int ready_fd; + int release_fd; + _Atomic int worker_tid; +}; + +static void *tid_probe_worker(void *opaque) +{ + struct tid_probe *probe = opaque; + int tid = (int) syscall(SYS_gettid); + atomic_store_explicit(&probe->worker_tid, tid, memory_order_release); + + char byte = 'R'; + if (write_all(probe->ready_fd, &byte, 1) < 0) + atomic_store_explicit(&probe->worker_tid, -1, memory_order_release); + close(probe->ready_fd); + + if (read_all(probe->release_fd, &byte, 1, true) < 0) + atomic_store_explicit(&probe->worker_tid, -1, memory_order_release); + close(probe->release_fd); + return NULL; +} + +struct pid_tid_report { + pid_t child_pid; + pid_t child_main_tid; + pid_t child_worker_tid; +}; + +static void test_pid_tid_namespace_uniqueness(void) +{ + TEST("PID-02 process PIDs and thread TIDs are unique"); + + int report_pipe[2]; + if (pipe(report_pipe) < 0) { + FAIL("PID-02 report pipe failed"); + return; + } + + pid_t parent_pid = getpid(); + pid_t child = fork(); + if (child < 0) { + close(report_pipe[0]); + close(report_pipe[1]); + FAIL("PID-02 fork failed"); + return; + } + + if (child == 0) { + close(report_pipe[0]); + int ready[2]; + int release[2]; + if (pipe(ready) < 0 || pipe(release) < 0) + _exit(2); + + struct tid_probe probe = { + .ready_fd = ready[1], + .release_fd = release[0], + .worker_tid = -1, + }; + pthread_t worker; + if (pthread_create(&worker, NULL, tid_probe_worker, &probe) != 0) + _exit(3); + + char byte; + int ready_ok = read_all(ready[0], &byte, 1, true) >= 0; + close(ready[0]); + struct pid_tid_report report = { + .child_pid = getpid(), + .child_main_tid = (pid_t) syscall(SYS_gettid), + .child_worker_tid = (pid_t) atomic_load_explicit( + &probe.worker_tid, memory_order_acquire), + }; + int report_ok = write_all(report_pipe[1], &report, sizeof(report)) == 0; + close(report_pipe[1]); + int release_ok = write_all(release[1], "G", 1) == 0; + close(release[1]); + int join_ok = pthread_join(worker, NULL) == 0; + _exit(ready_ok && report_ok && release_ok && join_ok ? 0 : 4); + } + + close(report_pipe[1]); + struct pid_tid_report report; + memset(&report, 0, sizeof(report)); + int report_ok = + read_all(report_pipe[0], &report, sizeof(report), true) >= 0; + close(report_pipe[0]); + int child_ok = child_exited_cleanly(child, 0); + int identity_ok = + report.child_pid == child && report.child_main_tid == report.child_pid; + int unique = parent_pid > 0 && report.child_pid > 0 && + report.child_worker_tid > 0 && + parent_pid != report.child_pid && + parent_pid != report.child_worker_tid && + report.child_pid != report.child_worker_tid; + + if (!report_ok || !child_ok || !identity_ok || !unique) { + printf( + "[PID-02 parent=%d fork_child=%d child_pid=%d main_tid=%d " + "worker_tid=%d report=%d child_ok=%d identity=%d unique=%d] ", + (int) parent_pid, (int) child, (int) report.child_pid, + (int) report.child_main_tid, (int) report.child_worker_tid, + report_ok, child_ok, identity_ok, unique); + FAIL("process/thread IDs collided or leader TID differed from PID"); + return; + } + PASS(); +} + +static void test_wnohang_running_child(void) +{ + TEST("WAIT-01 WNOHANG preserves child"); + + int release[2]; + int ready[2]; + if (pipe(release) < 0 || pipe(ready) < 0) { + FAIL("WAIT-01 pipe failed"); + return; + } + + pid_t child = fork(); + if (child < 0) { + FAIL("WAIT-01 fork failed"); + return; + } + if (child == 0) { + close(release[1]); + close(ready[0]); + char byte = 'R'; + if (write_all(ready[1], &byte, 1) < 0) + _exit(2); + close(ready[1]); + if (read_all(release[0], &byte, 1, true) < 0) + _exit(3); + close(release[0]); + _exit(41); + } + + close(release[0]); + close(ready[1]); + char byte; + int synchronized = read_all(ready[0], &byte, 1, true) >= 0; + close(ready[0]); + + int status = 0; + errno = 0; + pid_t first = synchronized ? waitpid(child, &status, WNOHANG) : -1; + int first_errno = errno; + byte = 'X'; + int released = write_all(release[1], &byte, 1) == 0; + close(release[1]); + int reaped = child_exited_cleanly(child, 41); + + if (!synchronized || first != 0 || !released || !reaped) { + printf( + "[WAIT-01 child=%d first=%d errno=%d synchronized=%d " + "released=%d reaped=%d] ", + (int) child, (int) first, first_errno, synchronized, released, + reaped); + FAIL("WNOHANG changed or lost a running child"); + return; + } + PASS(); +} + +static int64_t monotonic_milliseconds(void) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return -1; + return (int64_t) now.tv_sec * 1000 + now.tv_nsec / 1000000; +} + +/* Fork a child that holds @done's write end until process exit. A zero-byte + * read in the parent is an exit-side barrier: every writer has been closed by + * the kernel. It does not consume the child's wait status. + */ +static pid_t fork_exit_with_eof(int exit_code) +{ + int done[2]; + if (pipe(done) < 0) + return -1; + pid_t child = fork(); + if (child < 0) { + close(done[0]); + close(done[1]); + return -1; + } + if (child == 0) { + close(done[0]); + _exit(exit_code); + } + close(done[1]); + char byte; + ssize_t n; + do { + n = read(done[0], &byte, 1); + } while (n < 0 && errno == EINTR); + close(done[0]); + if (n != 0) { + int saved = errno; + kill(child, SIGKILL); + (void) child_exited_cleanly(child, 0); + errno = saved ? saved : EIO; + return -1; + } + return child; +} + +static int wait_for_pid_gone(pid_t pid, int timeout_ms) +{ + int64_t deadline = monotonic_milliseconds() + timeout_ms; + do { + errno = 0; + if (kill(pid, 0) < 0 && errno == ESRCH) + return 0; + usleep(10000); + } while (monotonic_milliseconds() < deadline); + errno = ETIMEDOUT; + return -1; +} + +static int waitid_wnowait_until_ready(pid_t child, siginfo_t *info) +{ + int64_t deadline = monotonic_milliseconds() + 5000; + do { + memset(info, 0, sizeof(*info)); + if (waitid(P_PID, (id_t) child, info, WEXITED | WNOWAIT | WNOHANG) < 0) + return -1; + if (info->si_pid == child) + return 0; + usleep(10000); + } while (monotonic_milliseconds() < deadline); + errno = ETIMEDOUT; + return -1; +} + +static void test_waitid_wnowait_repeat(void) +{ + TEST("WAIT-02 repeated WNOWAIT then reap"); + + pid_t child = fork(); + if (child < 0) { + FAIL("WAIT-02 fork failed"); + return; + } + if (child == 0) + _exit(42); + + siginfo_t first; + errno = 0; + int first_rc = waitid_wnowait_until_ready(child, &first); + int first_errno = errno; + + siginfo_t second; + memset(&second, 0, sizeof(second)); + errno = 0; + int second_rc = + waitid(P_PID, (id_t) child, &second, WEXITED | WNOWAIT | WNOHANG); + int second_errno = errno; + + int status = 0; + errno = 0; + pid_t consumed = waitpid(child, &status, 0); + int consume_errno = errno; + int consumed_status = status; + + errno = 0; + pid_t after = waitpid(child, &status, WNOHANG); + int after_errno = errno; + + int first_ok = first_rc == 0 && first.si_pid == child && + first.si_code == CLD_EXITED && first.si_status == 42; + int second_ok = second_rc == 0 && second.si_pid == child && + second.si_code == CLD_EXITED && second.si_status == 42; + int consume_ok = consumed == child && WIFEXITED(consumed_status) && + WEXITSTATUS(consumed_status) == 42; + int after_ok = after == -1 && after_errno == ECHILD; + + if (!first_ok || !second_ok || !consume_ok || !after_ok) { + printf( + "[WAIT-02 child=%d first=(rc=%d errno=%d pid=%d code=%d " + "status=%d) second=(rc=%d errno=%d pid=%d code=%d status=%d) " + "consume=(ret=%d errno=%d status=0x%x) " + "after=(ret=%d errno=%d)] ", + (int) child, first_rc, first_errno, (int) first.si_pid, + first.si_code, first.si_status, second_rc, second_errno, + (int) second.si_pid, second.si_code, second.si_status, + (int) consumed, consume_errno, consumed_status, (int) after, + after_errno); + FAIL("WNOWAIT did not preserve a repeatable wait status"); + return; + } + PASS(); +} + +static void test_waitid_pgid_matching(void) +{ + TEST("WAIT-03 waitid P_PGID matches group"); + + int release[2]; + if (pipe(release) < 0) { + FAIL("WAIT-03 pipe failed"); + return; + } + + pid_t group_child = fork(); + if (group_child == 0) { + close(release[1]); + char byte; + int ok = read_all(release[0], &byte, 1, true) >= 0; + close(release[0]); + _exit(ok ? 43 : 2); + } + close(release[0]); + if (group_child < 0) { + close(release[1]); + FAIL("WAIT-03 group child fork failed"); + return; + } + + int setpgid_ok = setpgid(group_child, group_child) == 0; + pid_t other_child = fork_exit_with_eof(44); + + siginfo_t empty; + memset(&empty, 0, sizeof(empty)); + errno = 0; + int empty_rc = + waitid(P_PGID, (id_t) group_child, &empty, WEXITED | WNOHANG); + int empty_errno = errno; + int empty_ok = empty_rc == 0 && empty.si_pid == 0; + + int other_ok = other_child > 0 && child_exited_cleanly(other_child, 44); + char byte = 'X'; + int released = write_all(release[1], &byte, 1) == 0; + close(release[1]); + + siginfo_t exited; + memset(&exited, 0, sizeof(exited)); + errno = 0; + int exited_rc = waitid(P_PGID, (id_t) group_child, &exited, WEXITED); + int exited_errno = errno; + int exited_ok = exited_rc == 0 && exited.si_pid == group_child && + exited.si_code == CLD_EXITED && exited.si_status == 43; + + if (!setpgid_ok || other_child < 0 || !empty_ok || !other_ok || !released || + !exited_ok) { + printf( + "[WAIT-03 group=%d other=%d setpgid=%d " + "empty=(rc=%d errno=%d pid=%d) other_ok=%d released=%d " + "exited=(rc=%d errno=%d pid=%d code=%d status=%d)] ", + (int) group_child, (int) other_child, setpgid_ok, empty_rc, + empty_errno, (int) empty.si_pid, other_ok, released, exited_rc, + exited_errno, (int) exited.si_pid, exited.si_code, + exited.si_status); + FAIL("waitid(P_PGID) selected a child outside the requested group"); + return; + } + PASS(); +} + +static void test_waitid_pgid_autoreap(void) +{ + TEST("WAIT-04 P_PGID sees live auto-reap child"); + + struct sigaction old_action; + struct sigaction action; + memset(&action, 0, sizeof(action)); + action.sa_handler = SIG_IGN; + sigemptyset(&action.sa_mask); + if (sigaction(SIGCHLD, &action, &old_action) < 0) { + FAIL("WAIT-04 installing SIGCHLD disposition failed"); + return; + } + + int release[2]; + if (pipe(release) < 0) { + (void) sigaction(SIGCHLD, &old_action, NULL); + FAIL("WAIT-04 pipe failed"); + return; + } + pid_t child = fork(); + if (child == 0) { + close(release[1]); + char byte; + int ok = read_all(release[0], &byte, 1, true) >= 0; + close(release[0]); + _exit(ok ? 45 : 2); + } + close(release[0]); + + int setpgid_ok = child > 0 && setpgid(child, child) == 0; + siginfo_t live; + memset(&live, 0xa5, sizeof(live)); + errno = 0; + int live_rc = + child > 0 ? waitid(P_PGID, (id_t) child, &live, WEXITED | WNOHANG) : -1; + int live_errno = errno; + int live_ok = live_rc == 0 && live.si_pid == 0 && live.si_signo == 0; + + char byte = 'X'; + int released = child > 0 && write_all(release[1], &byte, 1) == 0; + close(release[1]); + int gone_ok = child > 0 && wait_for_pid_gone(child, 5000) == 0; + + siginfo_t gone; + memset(&gone, 0, sizeof(gone)); + errno = 0; + int gone_rc = + child > 0 ? waitid(P_PGID, (id_t) child, &gone, WEXITED | WNOHANG) : -2; + int gone_errno = errno; + int gone_wait_ok = gone_rc == -1 && gone_errno == ECHILD; + int restore_ok = sigaction(SIGCHLD, &old_action, NULL) == 0; + + if (child < 0 || !setpgid_ok || !live_ok || !released || !gone_ok || + !gone_wait_ok || !restore_ok) { + printf( + "[WAIT-04 child=%d setpgid=%d " + "live=(rc=%d errno=%d signo=%d pid=%d) " + "released=%d gone=%d final=(rc=%d errno=%d pid=%d) " + "restore=%d] ", + (int) child, setpgid_ok, live_rc, live_errno, live.si_signo, + (int) live.si_pid, released, gone_ok, gone_rc, gone_errno, + (int) gone.si_pid, restore_ok); + FAIL("auto-reap waitid(P_PGID) did not track the requested group"); + return; + } + PASS(); +} + +static void test_signal_wait_status(void) +{ + TEST("WAIT-05 parent SIGKILL preserves signal status"); + + pid_t normal = fork(); + if (normal == 0) + _exit(137); + int normal_status = 0; + pid_t normal_ret = normal > 0 ? waitpid(normal, &normal_status, 0) : -1; + int normal_ok = normal_ret == normal && WIFEXITED(normal_status) && + WEXITSTATUS(normal_status) == 137; + + _Atomic int *stop = mmap(NULL, sizeof(*stop), PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + int ready[2]; + int setup_ok = stop != MAP_FAILED && pipe(ready) == 0; + if (stop != MAP_FAILED) + atomic_store_explicit(stop, 0, memory_order_relaxed); + + pid_t killed = setup_ok ? fork() : -1; + if (killed == 0) { + close(ready[0]); + char byte = 'R'; + if (write_all(ready[1], &byte, 1) < 0) + _exit(4); + close(ready[1]); + volatile uint64_t work = 1; + while (!atomic_load_explicit(stop, memory_order_acquire)) + work = work * 33u + 1u; + (void) work; + _exit(5); + } + if (setup_ok) + close(ready[1]); + char byte = 0; + int ready_ok = killed > 0 && read_all(ready[0], &byte, 1, true) >= 0; + if (setup_ok) + close(ready[0]); + /* Let the child return from write(2) and enter EL0 compute code. This + * specifically checks that the cross-process signal doorbell interrupts a + * running vCPU whose valid Hypervisor.framework handle may be zero. + */ + if (ready_ok) + usleep(20000); + errno = 0; + int kill_rc = ready_ok ? kill(killed, SIGKILL) : -1; + int kill_errno = errno; + int kill_ok = kill_rc == 0; + siginfo_t info; + memset(&info, 0, sizeof(info)); + int peek_rc = -1; + for (int i = 0; kill_ok && i < 2000; i++) { + memset(&info, 0, sizeof(info)); + peek_rc = + waitid(P_PID, (id_t) killed, &info, WEXITED | WNOWAIT | WNOHANG); + if (peek_rc < 0 || info.si_pid != 0) + break; + usleep(1000); + } + int peek_ok = peek_rc == 0 && info.si_pid == killed && + info.si_code == CLD_KILLED && info.si_status == SIGKILL; + if (!peek_ok && killed > 0) + atomic_store_explicit(stop, 1, memory_order_release); + int killed_status = 0; + pid_t killed_ret = killed > 0 ? waitpid(killed, &killed_status, 0) : -1; + int killed_ok = killed_ret == killed && WIFSIGNALED(killed_status) && + WTERMSIG(killed_status) == SIGKILL; + if (stop != MAP_FAILED) + munmap(stop, sizeof(*stop)); + + if (normal < 0 || !normal_ok || !setup_ok || killed < 0 || !ready_ok || + !kill_ok || !peek_ok || !killed_ok) { + printf( + "[WAIT-05 normal=%d ret=%d status=0x%x ok=%d killed=%d " + "setup=%d ready=%d kill=(rc=%d errno=%d) " + "peek=(rc=%d pid=%d code=%d status=%d) " + "reap=(ret=%d status=0x%x ok=%d)] ", + (int) normal, (int) normal_ret, normal_status, normal_ok, + (int) killed, setup_ok, ready_ok, kill_rc, kill_errno, peek_rc, + (int) info.si_pid, info.si_code, info.si_status, (int) killed_ret, + killed_status, killed_ok); + FAIL("guest wait status lost the signal termination cause"); + return; + } + PASS(); +} + +struct concurrent_wait_result { + _Atomic int stop; + _Atomic int reaped; + pid_t first_pid; + int first_status; + int error; +}; + +static void *concurrent_waiter(void *opaque) +{ + struct concurrent_wait_result *result = opaque; + while (!atomic_load_explicit(&result->stop, memory_order_acquire)) { + int status = 0; + errno = 0; + pid_t pid = waitpid(-1, &status, WNOHANG); + if (pid > 0) { + int count = atomic_fetch_add_explicit(&result->reaped, 1, + memory_order_acq_rel) + + 1; + if (count == 1) { + result->first_pid = pid; + result->first_status = status; + } + continue; + } + if (pid < 0 && errno != ECHILD && errno != EINTR) { + result->error = errno; + break; + } + usleep(100); + } + return NULL; +} + +static void test_concurrent_wait_during_fork_admission(void) +{ + TEST("WAIT-06 concurrent wait sees admitted child once"); + + struct concurrent_wait_result result; + memset(&result, 0, sizeof(result)); + pthread_t waiter; + int thread_ok = + pthread_create(&waiter, NULL, concurrent_waiter, &result) == 0; + if (thread_ok) + usleep(10000); + + pid_t child = thread_ok ? fork() : -1; + if (child == 0) + _exit(97); + + if (child > 0) { + for (int i = 0; i < 3000; i++) { + if (atomic_load_explicit(&result.reaped, memory_order_acquire) > 0) + break; + usleep(1000); + } + /* Keep the waiter active briefly after the first report: a phantom + * admission record would otherwise leave a second entry for this same + * guest PID, producing a duplicate consuming wait. + */ + usleep(50000); + } + + atomic_store_explicit(&result.stop, 1, memory_order_release); + if (thread_ok) + pthread_join(waiter, NULL); + + int count = atomic_load_explicit(&result.reaped, memory_order_acquire); + int status_ok = count == 1 && result.first_pid == child && + WIFEXITED(result.first_status) && + WEXITSTATUS(result.first_status) == 97; + errno = 0; + int cleanup_status = 0; + pid_t cleanup = child > 0 ? waitpid(child, &cleanup_status, WNOHANG) : -1; + int cleanup_errno = errno; + int cleanup_ok = cleanup < 0 && cleanup_errno == ECHILD; + + if (!thread_ok || child < 0 || result.error != 0 || !status_ok || + !cleanup_ok) { + printf( + "[WAIT-06 child=%d thread=%d reaped=%d first=(pid=%d " + "status=0x%x) error=%d cleanup=%d errno=%d] ", + (int) child, thread_ok, count, (int) result.first_pid, + result.first_status, result.error, (int) cleanup, cleanup_errno); + if (child > 0 && cleanup == 0) { + kill(child, SIGKILL); + (void) waitpid(child, NULL, 0); + } + FAIL("fork admission created a missing or duplicate wait record"); + return; + } + PASS(); +} + +static void test_delayed_zombie_reap(void) +{ + TEST("Z-01 delayed zombie retains status"); + + pid_t child = fork_exit_with_eof(51); + if (child < 0) { + FAIL("Z-01 fork/exit barrier failed"); + return; + } + usleep(50000); + if (!child_exited_cleanly(child, 51)) { + FAIL("delayed wait lost child exit status"); + return; + } + PASS(); +} + +static void test_reverse_zombie_reap(void) +{ + TEST("Z-02 reverse-order zombie reap"); + + enum { CHILDREN = 8 }; + pid_t children[CHILDREN]; + int created = 0; + for (int i = 0; i < CHILDREN; i++) { + children[i] = fork_exit_with_eof(20 + i); + if (children[i] < 0) + break; + created++; + } + + int ok = created == CHILDREN; + for (int i = created - 1; i >= 0; i--) + if (!child_exited_cleanly(children[i], 20 + i)) + ok = 0; + + if (!ok) { + printf("[Z-02 created=%d/%d] ", created, CHILDREN); + FAIL("reverse wait lost or changed a child status"); + return; + } + PASS(); +} + +static void test_zombie_table_pressure(void) +{ + TEST("Z-03 retain 65 zombie statuses"); + + enum { CHILDREN = 65 }; + pid_t children[CHILDREN]; + int created = 0; + for (int i = 0; i < CHILDREN; i++) { + children[i] = fork_exit_with_eof(i); + if (children[i] < 0) + break; + created++; + } + + int reaped = 0; + int first_failure = -1; + int first_errno = 0; + for (int i = 0; i < created; i++) { + errno = 0; + if (child_exited_cleanly(children[i], i)) { + reaped++; + } else if (first_failure < 0) { + first_failure = i; + first_errno = errno; + } + } + + if (created != CHILDREN || reaped != CHILDREN) { + printf("[Z-03 created=%d/%d reaped=%d first_failure=%d errno=%d] ", + created, CHILDREN, reaped, first_failure, first_errno); + FAIL("internal child-table pressure lost wait statuses"); + return; + } + PASS(); +} + +static void test_no_zombie_disposition(int use_no_cldwait) +{ + TEST(use_no_cldwait ? "Z-05 SA_NOCLDWAIT auto-reaps" + : "Z-04 SIGCHLD=SIG_IGN auto-reaps"); + + struct sigaction old_action; + struct sigaction action; + memset(&action, 0, sizeof(action)); + sigemptyset(&action.sa_mask); + if (use_no_cldwait) { + action.sa_handler = SIG_DFL; + action.sa_flags = SA_NOCLDWAIT; + } else { + action.sa_handler = SIG_IGN; + } + if (sigaction(SIGCHLD, &action, &old_action) < 0) { + FAIL("installing SIGCHLD disposition failed"); + return; + } + + pid_t child = fork_exit_with_eof(53 + use_no_cldwait); + int gone_ok = child > 0 && wait_for_pid_gone(child, 5000) == 0; + + int status = 0; + int restore_ok = sigaction(SIGCHLD, &old_action, NULL) == 0; + errno = 0; + pid_t waited = child < 0 ? -2 : waitpid(child, &status, 0); + int wait_errno = errno; + + if (child < 0 || !gone_ok || waited != -1 || wait_errno != ECHILD || + !restore_ok) { + printf( + "[Z-%02d child=%d gone=%d wait=%d errno=%d status=0x%x " + "restore=%d] ", + use_no_cldwait ? 5 : 4, (int) child, gone_ok, (int) waited, + wait_errno, status, restore_ok); + FAIL("no-zombie SIGCHLD disposition retained a waitable child"); + return; + } + PASS(); +} + +static volatile sig_atomic_t sigchld_count; + +static void sigchld_handler(int signum) +{ + (void) signum; + sigchld_count++; +} + +static void test_sigchld_before_wait(void) +{ + TEST("Z-06 SIGCHLD arrives before wait"); + + struct sigaction old_action; + struct sigaction action; + memset(&action, 0, sizeof(action)); + action.sa_handler = sigchld_handler; + sigemptyset(&action.sa_mask); + if (sigaction(SIGCHLD, &action, &old_action) < 0) { + FAIL("installing SIGCHLD handler failed"); + return; + } + + sigchld_count = 0; + pid_t child = fork(); + if (child == 0) + _exit(56); + + int64_t deadline = monotonic_milliseconds() + 3000; + while (child > 0 && sigchld_count == 0 && + monotonic_milliseconds() < deadline) + usleep(10000); + int before_wait = sigchld_count; + int reaped = child > 0 && child_exited_cleanly(child, 56); + int after_wait = sigchld_count; + int restore_ok = sigaction(SIGCHLD, &old_action, NULL) == 0; + + if (child < 0 || before_wait == 0 || !reaped || !restore_ok) { + printf( + "[Z-06 child=%d before_wait=%d after_wait=%d reaped=%d " + "restore=%d] ", + (int) child, before_wait, after_wait, reaped, restore_ok); + FAIL("SIGCHLD was not delivered when child became waitable"); + return; + } + PASS(); +} + +struct orphan_report { + pid_t child_pid; + pid_t original_ppid; + pid_t adopted_ppid; +}; + +static pid_t wait_for_ppid(pid_t expected, int timeout_ms) +{ + int64_t deadline = monotonic_milliseconds() + timeout_ms; + pid_t observed; + do { + observed = getppid(); + if (observed == expected) + return observed; + usleep(10000); + } while (monotonic_milliseconds() < deadline); + return observed; +} + +static void test_orphan_reparent_to_init(void) +{ + TEST("O-01 orphan reparents to PID 1"); + + int meta[2]; + int result[2]; + int leaf_ready[2]; + if (pipe(meta) < 0 || pipe(result) < 0 || pipe(leaf_ready) < 0) { + FAIL("O-01 pipe failed"); + return; + } + + pid_t middle = fork(); + if (middle < 0) { + FAIL("O-01 middle fork failed"); + return; + } + if (middle == 0) { + close(meta[0]); + close(result[0]); + pid_t leaf = fork(); + if (leaf == 0) { + close(meta[1]); + close(leaf_ready[0]); + struct orphan_report report = { + .child_pid = getpid(), + .original_ppid = getppid(), + .adopted_ppid = -1, + }; + char ready = 'R'; + (void) write_all(leaf_ready[1], &ready, 1); + close(leaf_ready[1]); + report.adopted_ppid = wait_for_ppid(1, 3000); + (void) write_all(result[1], &report, sizeof(report)); + close(result[1]); + _exit(report.adopted_ppid == 1 ? 0 : 2); + } + close(leaf_ready[1]); + char ready; + int ready_ok = read_all(leaf_ready[0], &ready, 1, true) >= 0; + close(leaf_ready[0]); + (void) write_all(meta[1], &leaf, sizeof(leaf)); + close(meta[1]); + close(result[1]); + _exit(ready_ok ? 0 : 3); + } + + close(meta[1]); + close(result[1]); + close(leaf_ready[0]); + close(leaf_ready[1]); + pid_t leaf = -1; + int meta_ok = read_all(meta[0], &leaf, sizeof(leaf), true) >= 0; + close(meta[0]); + int middle_ok = child_exited_cleanly(middle, 0); + struct orphan_report report; + memset(&report, 0, sizeof(report)); + int result_ok = read_all(result[0], &report, sizeof(report), true) >= 0; + close(result[0]); + + if (!meta_ok || !middle_ok || !result_ok || leaf <= 0 || + report.child_pid != leaf || report.original_ppid != middle || + report.adopted_ppid != 1) { + printf( + "[O-01 middle=%d leaf=%d report=(pid=%d old=%d new=%d) " + "meta=%d middle_ok=%d result=%d] ", + (int) middle, (int) leaf, (int) report.child_pid, + (int) report.original_ppid, (int) report.adopted_ppid, meta_ok, + middle_ok, result_ok); + FAIL("orphan PPID did not change to 1"); + return; + } + PASS(); +} + +static void test_orphan_reparent_to_subreaper(void) +{ + TEST("O-02 subreaper adopts live orphan"); + + if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) < 0) { + FAIL("O-02 setting subreaper failed"); + return; + } + + int meta[2]; + if (pipe(meta) < 0) { + FAIL("O-02 pipe failed"); + return; + } + pid_t subreaper = getpid(); + pid_t middle = fork(); + if (middle == 0) { + close(meta[0]); + pid_t leaf = fork(); + if (leaf == 0) { + close(meta[1]); + pid_t adopted = wait_for_ppid(subreaper, 3000); + _exit(adopted == subreaper ? 62 : 63); + } + (void) write_all(meta[1], &leaf, sizeof(leaf)); + close(meta[1]); + _exit(61); + } + + close(meta[1]); + pid_t leaf = -1; + int meta_ok = + middle > 0 && read_all(meta[0], &leaf, sizeof(leaf), true) >= 0; + close(meta[0]); + int middle_ok = middle > 0 && child_exited_cleanly(middle, 61); + int leaf_status = 0; + errno = 0; + pid_t leaf_reaped = leaf > 0 ? waitpid(leaf, &leaf_status, 0) : -1; + int leaf_errno = errno; + int leaf_ok = leaf_reaped == leaf && WIFEXITED(leaf_status) && + WEXITSTATUS(leaf_status) == 62; + int clear_ok = prctl(PR_SET_CHILD_SUBREAPER, 0, 0, 0, 0) == 0; + + if (!meta_ok || !middle_ok || !leaf_ok || !clear_ok) { + printf( + "[O-02 self=%d middle=%d leaf=%d meta=%d middle_ok=%d " + "leaf_reaped=%d leaf_status=0x%x leaf_errno=%d leaf_ok=%d " + "clear=%d] ", + (int) subreaper, (int) middle, (int) leaf, meta_ok, middle_ok, + (int) leaf_reaped, leaf_status, leaf_errno, leaf_ok, clear_ok); + FAIL("subreaper did not adopt and reap live orphan"); + return; + } + PASS(); +} + +static void test_subreaper_adopts_zombie(void) +{ + TEST("O-03 subreaper adopts exited children"); + + enum { ZOMBIE_CHILDREN = 16 }; + struct zombie_report { + pid_t leaves[ZOMBIE_CHILDREN]; + int created; + int exit_barrier_ok; + }; + + if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) < 0) { + FAIL("O-03 setting subreaper failed"); + return; + } + + int meta[2]; + if (pipe(meta) < 0) { + FAIL("O-03 pipe failed"); + return; + } + pid_t middle = fork(); + if (middle == 0) { + close(meta[0]); + int exited[2]; + if (pipe(exited) < 0) + _exit(2); + + struct zombie_report report; + memset(&report, 0, sizeof(report)); + for (int i = 0; i < ZOMBIE_CHILDREN; i++) { + pid_t leaf = fork(); + if (leaf == 0) { + close(exited[0]); + _exit(72 + i); + } + if (leaf < 0) + break; + report.leaves[report.created++] = leaf; + } + close(exited[1]); + char byte; + ssize_t n; + do { + n = read(exited[0], &byte, 1); + } while (n < 0 && errno == EINTR); + close(exited[0]); + report.exit_barrier_ok = n == 0; + (void) write_all(meta[1], &report, sizeof(report)); + close(meta[1]); + _exit(71); + } + + close(meta[1]); + struct zombie_report report; + memset(&report, 0, sizeof(report)); + int meta_ok = + middle > 0 && read_all(meta[0], &report, sizeof(report), true) >= 0; + close(meta[0]); + int middle_ok = middle > 0 && child_exited_cleanly(middle, 71); + int reaped = 0; + for (int i = 0; i < report.created; i++) + if (report.leaves[i] > 0 && + child_exited_cleanly(report.leaves[i], 72 + i)) + reaped++; + int clear_ok = prctl(PR_SET_CHILD_SUBREAPER, 0, 0, 0, 0) == 0; + + if (!meta_ok || report.created != ZOMBIE_CHILDREN || + !report.exit_barrier_ok || !middle_ok || reaped != ZOMBIE_CHILDREN || + !clear_ok) { + printf( + "[O-03 middle=%d created=%d/%d reaped=%d meta=%d barrier=%d " + "middle_ok=%d clear=%d errno=%d] ", + (int) middle, report.created, ZOMBIE_CHILDREN, reaped, meta_ok, + report.exit_barrier_ok, middle_ok, clear_ok, errno); + FAIL("subreaper did not inherit and reap exited child statuses"); + return; + } + PASS(); +} + +static void test_pid1_retains_adopted_exit(void) +{ + TEST("O-06 non-reaping PID 1 retains adopted exit status"); + + int meta[2]; + int result[2]; + int ready[2]; + int exit_barrier[2]; + if (pipe(meta) < 0 || pipe(result) < 0 || pipe(ready) < 0 || + pipe(exit_barrier) < 0) { + FAIL("O-06 pipe failed"); + return; + } + + pid_t observer = getpid(); + pid_t middle = fork(); + if (middle < 0) { + FAIL("O-06 middle fork failed"); + return; + } + if (middle == 0) { + close(meta[0]); + close(result[0]); + close(exit_barrier[0]); + + pid_t leaf = fork(); + if (leaf == 0) { + close(meta[1]); + close(ready[0]); + struct orphan_report report = { + .child_pid = getpid(), + .original_ppid = getppid(), + .adopted_ppid = -1, + }; + char byte = 'R'; + if (write_all(ready[1], &byte, 1) < 0) + _exit(85); + close(ready[1]); + report.adopted_ppid = wait_for_ppid(1, 3000); + (void) write_all(result[1], &report, sizeof(report)); + close(result[1]); + /* Keep exit_barrier[1] open until process teardown. EOF tells the + * observer that the leaf has finished, not merely that it sent + * the report above. + */ + _exit(report.adopted_ppid == 1 ? 83 : 84); + } + + close(ready[1]); + close(exit_barrier[1]); + char byte; + int ready_ok = leaf > 0 && read_all(ready[0], &byte, 1, true) >= 0; + close(ready[0]); + int meta_ok = write_all(meta[1], &leaf, sizeof(leaf)) == 0; + close(meta[1]); + close(result[1]); + _exit(ready_ok && meta_ok ? 82 : 86); + } + + close(meta[1]); + close(result[1]); + close(ready[0]); + close(ready[1]); + close(exit_barrier[1]); + + pid_t leaf = -1; + int meta_ok = read_all(meta[0], &leaf, sizeof(leaf), true) >= 0; + close(meta[0]); + int middle_ok = child_exited_cleanly(middle, 82); + + struct orphan_report report; + memset(&report, 0, sizeof(report)); + int result_ok = read_all(result[0], &report, sizeof(report), true) >= 0; + close(result[0]); + char byte; + ssize_t barrier_ret; + do { + barrier_ret = read(exit_barrier[0], &byte, 1); + } while (barrier_ret < 0 && errno == EINTR); + close(exit_barrier[0]); + int barrier_ok = barrier_ret == 0; + int adoption_ok = leaf > 0 && report.child_pid == leaf && + report.original_ppid == middle && + report.adopted_ppid == 1; + + int retained_ok = 0; + int cleanup_ok = 0; + int first_pid = -1, second_pid = -1; + int first_status = -1, second_status = -1; + int wait_errno = 0; + if (observer == 1 && leaf > 0) { + /* Deliberately leave the adopted terminal child unconsumed for a + * bounded interval. PID 1 is not magic in Linux: without wait(), + * SIG_IGN, or SA_NOCLDWAIT, the status must remain waitable. + */ + usleep(50000); + siginfo_t first; + siginfo_t second; + memset(&first, 0, sizeof(first)); + memset(&second, 0, sizeof(second)); + int first_rc = + waitid(P_PID, (id_t) leaf, &first, WEXITED | WNOHANG | WNOWAIT); + int second_rc = + waitid(P_PID, (id_t) leaf, &second, WEXITED | WNOHANG | WNOWAIT); + first_pid = first.si_pid; + second_pid = second.si_pid; + first_status = first.si_status; + second_status = second.si_status; + retained_ok = first_rc == 0 && second_rc == 0 && first.si_pid == leaf && + second.si_pid == leaf && first.si_code == CLD_EXITED && + second.si_code == CLD_EXITED && first.si_status == 83 && + second.si_status == 83; + + int status = 0; + errno = 0; + pid_t consumed = waitpid(leaf, &status, 0); + wait_errno = errno; + cleanup_ok = + consumed == leaf && WIFEXITED(status) && WEXITSTATUS(status) == 83; + } else if (leaf > 0) { + /* In the QEMU/Linux reference lane this test process is normally not + * PID 1; the system init owns the orphan, so this process must see + * ECHILD. Adoption and terminal exit are still verified above. + */ + errno = 0; + pid_t foreign = waitpid(leaf, NULL, WNOHANG); + wait_errno = errno; + retained_ok = foreign == -1 && wait_errno == ECHILD; + cleanup_ok = retained_ok; + } + + if (!meta_ok || !middle_ok || !result_ok || !barrier_ok || !adoption_ok || + !retained_ok || !cleanup_ok) { + printf( + "[O-06 observer=%d middle=%d leaf=%d report=(pid=%d old=%d " + "new=%d) meta=%d middle_ok=%d result=%d barrier=%d " + "first=(pid=%d status=%d) second=(pid=%d status=%d) " + "retained=%d cleanup=%d errno=%d] ", + (int) observer, (int) middle, (int) leaf, (int) report.child_pid, + (int) report.original_ppid, (int) report.adopted_ppid, meta_ok, + middle_ok, result_ok, barrier_ok, first_pid, first_status, + second_pid, second_status, retained_ok, cleanup_ok, wait_errno); + FAIL("PID 1 did not retain the adopted terminal status"); + return; + } + PASS(); +} + +struct blocking_wait_result { + _Atomic int done; + pid_t pid; + int status; + int error; +}; + +static void *blocking_wait_any(void *opaque) +{ + struct blocking_wait_result *result = opaque; + errno = 0; + do { + result->pid = waitpid(-1, &result->status, 0); + } while (result->pid < 0 && errno == EINTR); + result->error = errno; + atomic_store_explicit(&result->done, 1, memory_order_release); + return NULL; +} + +static void test_blocked_subreaper_imports_zombie(void) +{ + TEST("O-07 blocked subreaper wakes for adopted zombie"); + + if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) < 0) { + FAIL("O-07 setting subreaper failed"); + return; + } + + int meta[2], release_branch[2], release_parent[2]; + if (pipe(meta) < 0 || pipe(release_branch) < 0 || + pipe(release_parent) < 0) { + FAIL("O-07 pipe failed"); + return; + } + + pid_t parent = fork(); + if (parent == 0) { + close(meta[0]); + close(release_branch[1]); + close(release_parent[1]); + pid_t branch = fork(); + if (branch == 0) { + close(release_parent[0]); + int life[2]; + if (pipe(life) < 0) + _exit(2); + pid_t leaf = fork(); + if (leaf == 0) { + close(life[0]); + close(meta[1]); + close(release_branch[0]); + _exit(90); + } + close(life[1]); + char byte; + ssize_t n; + do { + n = read(life[0], &byte, 1); + } while (n < 0 && errno == EINTR); + close(life[0]); + int meta_ok = leaf > 0 && n == 0 && + write_all(meta[1], &leaf, sizeof(leaf)) == 0; + close(meta[1]); + int release_ok = read_all(release_branch[0], &byte, 1, true) >= 0; + close(release_branch[0]); + _exit(meta_ok && release_ok ? 91 : 3); + } + close(meta[1]); + close(release_branch[0]); + int branch_ok = branch > 0 && child_exited_cleanly(branch, 91); + char byte; + int release_ok = read_all(release_parent[0], &byte, 1, true) >= 0; + close(release_parent[0]); + _exit(branch_ok && release_ok ? 92 : 4); + } + + close(meta[1]); + close(release_branch[0]); + close(release_parent[0]); + pid_t leaf = -1; + int meta_ok = + parent > 0 && read_all(meta[0], &leaf, sizeof(leaf), true) >= 0; + close(meta[0]); + + /* Earlier cases may leave an asynchronously auto-reaped table entry long + * enough for one final nonblocking observation. Drain only statuses that + * are already ready; the live direct parent remains and makes the blocking + * wait below valid before the deeper zombie is adopted. + */ + int stale_status; + while (waitpid(-1, &stale_status, WNOHANG) > 0) + ; + + struct blocking_wait_result result; + memset(&result, 0, sizeof(result)); + result.pid = -1; + pthread_t waiter; + int thread_ok = meta_ok && pthread_create(&waiter, NULL, blocking_wait_any, + &result) == 0; + if (thread_ok) + usleep(20000); + char byte = 'X'; + int branch_released = + thread_ok && write_all(release_branch[1], &byte, 1) == 0; + close(release_branch[1]); + + int woke_for_leaf = 0; + if (thread_ok) { + for (int i = 0; i < 3000; i++) { + if (atomic_load_explicit(&result.done, memory_order_acquire)) + break; + usleep(1000); + } + woke_for_leaf = + atomic_load_explicit(&result.done, memory_order_acquire) && + result.pid == leaf && WIFEXITED(result.status) && + WEXITSTATUS(result.status) == 90; + } + + int parent_released = write_all(release_parent[1], &byte, 1) == 0; + close(release_parent[1]); + if (thread_ok) + pthread_join(waiter, NULL); + + int parent_ok; + if (result.pid == parent) + parent_ok = + WIFEXITED(result.status) && WEXITSTATUS(result.status) == 92; + else + parent_ok = parent > 0 && child_exited_cleanly(parent, 92); + if (result.pid != leaf && leaf > 0) + (void) waitpid(leaf, NULL, 0); + int clear_ok = prctl(PR_SET_CHILD_SUBREAPER, 0, 0, 0, 0) == 0; + + if (!meta_ok || !thread_ok || !branch_released || !woke_for_leaf || + !parent_released || !parent_ok || !clear_ok) { + printf( + "[O-07 parent=%d leaf=%d meta=%d thread=%d branch_release=%d " + "wait=(pid=%d status=0x%x errno=%d done=%d) woke_leaf=%d " + "parent_release=%d parent_ok=%d clear=%d] ", + (int) parent, (int) leaf, meta_ok, thread_ok, branch_released, + (int) result.pid, result.status, result.error, + atomic_load_explicit(&result.done, memory_order_acquire), + woke_for_leaf, parent_released, parent_ok, clear_ok); + FAIL("blocking wait missed the newly adopted zombie"); + return; + } + PASS(); +} + +static void burn_cpu_ms(long duration_ms) +{ + struct timespec start, now; + if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) + return; + volatile uint64_t work = 0; + do { + for (int i = 0; i < 10000; i++) + work = work * 33u + (uint64_t) i; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + } while ((now.tv_sec - start.tv_sec) * 1000L + + (now.tv_nsec - start.tv_nsec) / 1000000L < + duration_ms); + (void) work; +} + +static void test_adopted_signal_status_and_rusage(void) +{ + TEST("O-08 adopted signal status and rusage survive"); + + if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) < 0) { + FAIL("O-08 setting subreaper failed"); + return; + } + int meta[2]; + if (pipe(meta) < 0) { + FAIL("O-08 pipe failed"); + return; + } + + pid_t middle = fork(); + if (middle == 0) { + close(meta[0]); + int life[2]; + if (pipe(life) < 0) + _exit(2); + pid_t leaf = fork(); + if (leaf == 0) { + close(life[0]); + close(meta[1]); + burn_cpu_ms(75); + char byte = 'R'; + if (write_all(life[1], &byte, 1) < 0) + _exit(3); + raise(SIGKILL); + _exit(5); + } + close(life[1]); + char byte; + int ready_ok = leaf > 0 && read_all(life[0], &byte, 1, true) >= 0; + int kill_ok = ready_ok; + ssize_t n; + do { + n = read(life[0], &byte, 1); + } while (n < 0 && errno == EINTR); + close(life[0]); + int dead_ok = n == 0; + int meta_ok = write_all(meta[1], &leaf, sizeof(leaf)) == 0; + close(meta[1]); + _exit(ready_ok && kill_ok && dead_ok && meta_ok ? 93 : 4); + } + + close(meta[1]); + pid_t leaf = -1; + int meta_ok = + middle > 0 && read_all(meta[0], &leaf, sizeof(leaf), true) >= 0; + close(meta[0]); + int middle_ok = middle > 0 && child_exited_cleanly(middle, 93); + + int status = 0; + struct rusage usage; + memset(&usage, 0, sizeof(usage)); + errno = 0; + pid_t reaped = leaf > 0 ? wait4(leaf, &status, 0, &usage) : -1; + int wait_errno = errno; + int signal_ok = + reaped == leaf && WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL; + long long cpu_us = + (long long) usage.ru_utime.tv_sec * 1000000LL + usage.ru_utime.tv_usec + + (long long) usage.ru_stime.tv_sec * 1000000LL + usage.ru_stime.tv_usec; + int usage_ok = cpu_us > 0; + int clear_ok = prctl(PR_SET_CHILD_SUBREAPER, 0, 0, 0, 0) == 0; + + if (!meta_ok || !middle_ok || !signal_ok || !usage_ok || !clear_ok) { + printf( + "[O-08 middle=%d leaf=%d meta=%d middle_ok=%d " + "wait=(ret=%d status=0x%x errno=%d) signal=%d cpu_us=%lld " + "usage=%d clear=%d] ", + (int) middle, (int) leaf, meta_ok, middle_ok, (int) reaped, status, + wait_errno, signal_ok, cpu_us, usage_ok, clear_ok); + FAIL("adopted wait lost signal status or resource usage"); + return; + } + PASS(); +} + +int main(void) +{ + setvbuf(stdout, NULL, _IONBF, 0); + printf("test-process-lifecycle: Linux process lifecycle semantics\n"); + + test_nested_pid_uniqueness(); + test_pid_tid_namespace_uniqueness(); + test_wnohang_running_child(); + test_waitid_wnowait_repeat(); + test_waitid_pgid_matching(); + test_waitid_pgid_autoreap(); + test_signal_wait_status(); + test_concurrent_wait_during_fork_admission(); + test_delayed_zombie_reap(); + test_reverse_zombie_reap(); + test_zombie_table_pressure(); + test_no_zombie_disposition(0); + test_no_zombie_disposition(1); + test_sigchld_before_wait(); + test_orphan_reparent_to_init(); + test_orphan_reparent_to_subreaper(); + test_subreaper_adopts_zombie(); + test_pid1_retains_adopted_exit(); + test_blocked_subreaper_imports_zombie(); + test_adopted_signal_status_and_rusage(); + + SUMMARY("test-process-lifecycle"); + return fails == 0 ? 0 : 1; +}