Skip to content

Commit f15676e

Browse files
authored
Implement structured exit status handling and correct POSIX wait status encoding (#910)
* Fix exit code handling to include signal termination * Revert fs_call * Remove redundant bit check * Not using sleep to sync in test
1 parent f135811 commit f15676e

File tree

6 files changed

+191
-5
lines changed

6 files changed

+191
-5
lines changed

skip_test_cases.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
file_tests/deterministic/popen.c
2-
process_tests/deterministic/exit_failure.c
2+
signal_tests/deterministic/kill.c
33
signal_tests/deterministic/signal.c
44
signal_tests/deterministic/signal_int_thread.c
55
signal_tests/deterministic/signal_longjmp.c

src/cage/src/cage.rs

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,100 @@ pub use std::sync::Arc;
1414
use sysdefs::constants::lind_platform_const::MAX_CAGEID;
1515
use sysdefs::data::fs_struct::SigactionStruct;
1616

17+
/// Represents how a cage terminated, mirroring the two primary POSIX
18+
/// process termination modes.
19+
///
20+
/// A process may either:
21+
/// - exit normally via `exit()` with an exit code, or
22+
/// - be terminated by a signal.
23+
///
24+
/// This enum stores the termination information in a structured form
25+
/// before it is encoded into the traditional POSIX wait status returned
26+
/// by `waitpid`.
27+
///
28+
/// TODO: Currently, Lind-Wasm only supports normal exit and signal
29+
/// termination. Job-control states such as `Stopped` and `Continued`
30+
/// are not yet implemented.
31+
#[derive(Debug, Clone, Copy)]
32+
pub enum ExitStatus {
33+
/// Process exited normally with the given exit code.
34+
/// The exit code will later be truncated to 8 bits when encoded
35+
/// into a POSIX wait status.
36+
Exited(i32),
37+
/// Process was terminated by a signal.
38+
/// The boolean indicates whether a core dump occurred.
39+
Signaled(i32, bool), // (signal, core_dump)
40+
}
41+
42+
/// A zombie child process.
43+
///
44+
/// A zombie represents a child cage that has already terminated but whose
45+
/// termination status has not yet been collected by the parent via
46+
/// `waitpid` or a related wait syscall.
47+
///
48+
/// The runtime stores the cage identifier together with the termination
49+
/// status so the parent can later retrieve it.
1750
#[derive(Debug, Clone, Copy)]
1851
pub struct Zombie {
1952
pub cageid: u64,
20-
pub exit_code: i32,
53+
pub exit_code: ExitStatus,
54+
}
55+
56+
/// Encode a structured `ExitStatus` into the traditional POSIX
57+
/// `waitpid` status integer.
58+
///
59+
/// The encoding follows the standard Unix wait status layout:
60+
///
61+
/// Normal exit:
62+
/// status = (exit_code & 0xff) << 8
63+
///
64+
/// Signal termination:
65+
/// bits 0–6 : signal number
66+
/// bit 7 : core dump flag
67+
///
68+
/// Exit codes are truncated to 8 bits to match POSIX semantics.
69+
/// This ensures that `WIFEXITED`, `WEXITSTATUS`, and related libc
70+
/// macros behave correctly.
71+
pub fn encode_wait_status(st: ExitStatus) -> i32 {
72+
match st {
73+
ExitStatus::Exited(code) => ((code & 0xff) << 8),
74+
ExitStatus::Signaled(sig, core) => {
75+
let mut s = sig & 0x7f;
76+
if core {
77+
s |= 0x80;
78+
} // core dump flag in traditional encoding
79+
s
80+
}
81+
}
82+
}
83+
84+
/// Record the final termination status of a cage.
85+
///
86+
/// This function stores the exit status that will later be reported to the
87+
/// parent when the cage becomes a zombie (e.g., via `waitpid`). The status
88+
/// may represent either a normal exit (`Exited`) or signal-based termination
89+
/// (`Signaled`).
90+
///
91+
/// The recorded status is later consumed when inserting a `Zombie` entry
92+
/// into the parent's zombie list.
93+
///
94+
/// This function is currently used on signal-based termination to record
95+
/// the signal number.
96+
///
97+
/// # Panics
98+
///
99+
/// Panics if the specified cage does not exist in the cage table.
100+
pub fn cage_record_exit_status(cageid: u64, status: ExitStatus) {
101+
let cage = get_cage(cageid).unwrap_or_else(|| {
102+
panic!(
103+
"Attempted to record exit status for non-existent cage ID {}",
104+
cageid
105+
)
106+
});
107+
let mut final_status = cage.final_exit_status.write();
108+
if final_status.is_none() {
109+
*final_status = Some(status);
110+
}
21111
}
22112

23113
#[derive(Debug)]
@@ -73,6 +163,28 @@ pub struct Cage {
73163
pub child_num: AtomicU64,
74164
// vmmap represents the virtual memory mapping for this cage. More details on `memory::vmmap`
75165
pub vmmap: RwLock<Vmmap>,
166+
// final_exit_status stores the terminal status of the cage once a
167+
// termination condition has been determined.
168+
//
169+
// This field is used as a temporary cache for the cage's final exit
170+
// status (either `Exited(code)` or `Signaled(signo, core_dump)`).
171+
// The status is recorded when the cage enters a terminal state
172+
// (e.g., exit syscall or signal-triggered termination), but before
173+
// the cage is fully cleaned up.
174+
//
175+
// The recorded value is later consumed when inserting a `Zombie`
176+
// entry into the parent cage's `zombies` list, which is what the
177+
// parent observes through `wait()` / `waitpid()`.
178+
//
179+
// This field cannot be replaced by the `exit_code` stored in
180+
// `Zombie`. A `Zombie` object only exists in the parent's zombie
181+
// list and is created during the final cleanup phase of the exiting
182+
// cage. However, the cage's termination reason may need to be
183+
// determined earlier (for example during signal handling), before
184+
// the zombie entry is created. Therefore, the cage must temporarily
185+
// store its final termination status until the zombie entry is
186+
// generated.
187+
pub final_exit_status: RwLock<Option<ExitStatus>>,
76188
}
77189

78190
/// We achieve an O(1) complexity for our cage map implementation through the following three approaches:
@@ -225,6 +337,7 @@ mod tests {
225337
zombies: RwLock::new(vec![]),
226338
child_num: AtomicU64::new(0),
227339
vmmap: RwLock::new(crate::memory::vmmap::Vmmap::new()),
340+
final_exit_status: RwLock::new(None),
228341
};
229342

230343
add_cage(2, test_cage);

src/rawposix/src/init.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ pub fn rawposix_start(verbosity: isize) {
240240
zombies: RwLock::new(vec![]),
241241
child_num: AtomicU64::new(0),
242242
vmmap: RwLock::new(Vmmap::new()),
243+
final_exit_status: RwLock::new(None),
243244
};
244245

245246
// Add cage to cagetable

src/rawposix/src/sys_calls.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use cage::memory::vmmap::{VmmapOps, *};
55
use cage::signal::signal::{convert_signal_mask, lind_send_signal, signal_check_trigger};
66
use cage::timer::IntervalTimer;
7-
use cage::{add_cage, get_cage, remove_cage, Cage, Zombie};
7+
use cage::{add_cage, encode_wait_status, get_cage, remove_cage, Cage, ExitStatus, Zombie};
88
use dashmap::DashMap;
99
use fdtables;
1010
use libc::sched_yield;
@@ -141,6 +141,7 @@ pub extern "C" fn fork_syscall(
141141
zombies: RwLock::new(vec![]),
142142
child_num: AtomicU64::new(0),
143143
vmmap: RwLock::new(new_vmmap),
144+
final_exit_status: RwLock::new(None),
144145
};
145146

146147
// increment child counter for parent
@@ -346,6 +347,9 @@ pub extern "C" fn exit_syscall(
346347
// in the cage (0 = no, 1 = yes).
347348
let mut is_last_thread = 0;
348349

350+
// Set the normal exit code
351+
let exit_st = ExitStatus::Exited(status);
352+
349353
// Perform thread exit inside RawPOSIX.
350354
//
351355
// `lind_thread_exit` returns true if this thread was the last
@@ -371,9 +375,22 @@ pub extern "C" fn exit_syscall(
371375
if let Some(parent) = parent_cage {
372376
parent.child_num.fetch_sub(1, SeqCst);
373377
let mut zombie_vec = parent.zombies.write();
378+
// Determine the final termination status for this cage.
379+
//
380+
// If a termination status was previously recorded (e.g., due to
381+
// signal-based termination), use that value. Otherwise fall back
382+
// to the normal exit status derived from the `exit()` syscall.
383+
//
384+
// This ensures that signal-triggered termination (which may have
385+
// recorded `ExitStatus::Signaled`) is not overwritten by the
386+
// default exit status path.
387+
let zombie_status = {
388+
let recorded = *selfcage.final_exit_status.read();
389+
recorded.unwrap_or(exit_st)
390+
};
374391
zombie_vec.push(Zombie {
375392
cageid: selfcageid,
376-
exit_code: status,
393+
exit_code: zombie_status,
377394
});
378395
} else {
379396
// if parent already exited
@@ -596,7 +613,7 @@ pub extern "C" fn waitpid_syscall(
596613
let zombie = zombie_opt.unwrap();
597614
// update the status
598615
if let Some(status) = status {
599-
*status = zombie.exit_code;
616+
*status = encode_wait_status(zombie.exit_code);
600617
}
601618

602619
// return child's cageid

src/wasmtime/crates/lind-multi-process/src/signal.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ pub fn signal_handler<
6767
// look up the signal's default handler
6868
match sysdefs::constants::signal_default_handler_dispatcher(signo) {
6969
sysdefs::constants::SignalDefaultHandler::Terminate => {
70+
// Set the exit status of the cage to signaled with the signal number and core dump flag
71+
// (currently set to false)
72+
cage::cage_record_exit_status(cageid, cage::ExitStatus::Signaled(signo, false));
7073
// if we are supposed to be terminated, switch the epoch state of all other threads
7174
// to "killed" state and perform a suicide
7275
cage::signal::epoch_kill_all(cageid);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <unistd.h>
4+
#include <signal.h>
5+
#include <sys/wait.h>
6+
7+
int main(void) {
8+
pid_t pid = fork();
9+
if (pid < 0) {
10+
perror("fork");
11+
return 1;
12+
}
13+
14+
if (pid == 0) {
15+
// child: wait for parent to kill us
16+
while (1) {
17+
18+
}
19+
_exit(123);
20+
}
21+
22+
sleep(1);
23+
24+
if (kill(pid, SIGKILL) != 0) {
25+
perror("kill(SIGKILL)");
26+
return 1;
27+
}
28+
29+
int status = 0;
30+
if (waitpid(pid, &status, 0) < 0) {
31+
perror("waitpid");
32+
return 1;
33+
}
34+
35+
printf("raw wait status = 0x%x\n", status);
36+
37+
if (!WIFSIGNALED(status)) {
38+
printf("FAIL: child not reported as signaled\n");
39+
return 2;
40+
}
41+
42+
int sig = WTERMSIG(status);
43+
printf("child terminated by signal %d\n", sig);
44+
45+
if (sig != SIGKILL) {
46+
printf("FAIL: expected SIGKILL (%d), got %d\n", SIGKILL, sig);
47+
return 3;
48+
}
49+
50+
printf("PASS\n");
51+
return 0;
52+
}

0 commit comments

Comments
 (0)