Skip to content

Commit fee8b37

Browse files
authored
Signal EINTR + Asyncify updates (349 update) (#906)
* update PR to integrated signal EINTR with lind-boot * Remove spurious set_stack_pointer call in fork child path The set_stack_pointer call was carried over from the old asyncify branch but is not needed in the new architecture. The fork child's stack pointer is correctly managed by the asyncify rewind mechanism. Calling set_stack_pointer before the rewind interfered with the fork process, causing all fork-based tests to deadlock/timeout. * Rewrite eintr_fork_signal test to use spin loop instead of blocking read The original test used alarm(1) + blocking read(pipe), but RawPOSIX's read() calls host libc::read() directly, which blocks in the host kernel. Since SIGALRM is delivered via epoch callback (which requires wasm to be executing), the blocking read can never be interrupted. Rewrite to use a spin loop that allows the epoch to fire and deliver the signal.
1 parent f15676e commit fee8b37

File tree

4 files changed

+161
-3
lines changed

4 files changed

+161
-3
lines changed

src/wasmtime/crates/lind-common/src/lib.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use threei::threei::{
1010
};
1111
use threei::threei_const;
1212
use typemap::path_conversion::get_cstr;
13-
use wasmtime::Caller;
13+
use wasmtime::{AsContext, AsContextMut, AsyncifyState, Caller};
1414
use wasmtime_lind_multi_process::{get_memory_base, LindHost};
1515
// These syscalls (`clone`, `exec`, `exit`, `fork`) require special handling
1616
// inside Lind Wasmtime before delegating to RawPOSIX. For example, they may
@@ -126,6 +126,19 @@ fn add_syscall_to_linker<
126126
}
127127
}
128128

129+
// If we are reaching here at rewind state, that means fork was called within
130+
// a syscall-interrupted signal handler. We should restore the saved return value
131+
// of the syscall that was interrupted, rather than re-executing it.
132+
if let AsyncifyState::Rewind(_) = caller.as_context().get_asyncify_state() {
133+
let retval = caller
134+
.as_context_mut()
135+
.get_current_syscall_rewind_data()
136+
.unwrap();
137+
// let signal handler finish rest of the rewinding process
138+
wasmtime_lind_multi_process::signal::signal_handler(&mut caller);
139+
return retval;
140+
}
141+
129142
// Some thread-related operations must be executed against a specific thread's
130143
// VMContext (e.g., pthread_create/exit). Because syscalls may be interposed/routed
131144
// through 3i functionality and the effective thread instance cannot be reliably derived
@@ -144,7 +157,7 @@ fn add_syscall_to_linker<
144157
arg2
145158
};
146159

147-
make_syscall(
160+
let retval = make_syscall(
148161
self_cageid,
149162
call_number as u64,
150163
call_name,
@@ -161,7 +174,23 @@ fn add_syscall_to_linker<
161174
arg5cageid,
162175
arg6,
163176
arg6cageid,
164-
)
177+
);
178+
179+
// If the syscall was interrupted by a signal (EINTR), invoke the signal handler.
180+
// If fork is called within the signal handler, asyncify will unwind the stack;
181+
// we save the syscall return value so it can be restored on rewind.
182+
if -retval == sysdefs::constants::Errno::EINTR as i32 {
183+
caller.as_context_mut().append_syscall_asyncify_data(retval);
184+
wasmtime_lind_multi_process::signal::signal_handler(&mut caller);
185+
186+
if caller.as_context().get_asyncify_state() == AsyncifyState::Unwind {
187+
return 0;
188+
} else {
189+
caller.as_context_mut().pop_syscall_asyncify_data();
190+
}
191+
}
192+
193+
retval
165194
},
166195
)?;
167196
Ok(())

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ impl<
305305
// set up unwind callback function
306306
let store = caller.as_context_mut().0;
307307
let signal_asyncify_data = store.get_signal_asyncify_data();
308+
let syscall_asyncify_data = store.get_syscall_asyncify_data();
308309
let is_parent_thread = store.is_thread();
309310
store.set_on_called(Box::new(move |mut store| {
310311
// unwind finished and we need to stop the unwind
@@ -462,6 +463,9 @@ impl<
462463
store
463464
.as_context_mut()
464465
.set_signal_asyncify_data(signal_asyncify_data);
466+
store
467+
.as_context_mut()
468+
.set_syscall_asyncify_data(syscall_asyncify_data);
465469

466470
let invoke_res = child_start_func.call(&mut store, &values, &mut results);
467471

src/wasmtime/crates/wasmtime/src/runtime/store.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,15 @@ pub struct StoreOpaque {
338338
host_globals: Vec<StoreBox<VMHostGlobalContext>>,
339339

340340
asyncify_state: AsyncifyState,
341+
// signal asyncify data, holds the sequence of the parameters for signal handler
341342
signal_asyncify_data: Vec<SignalAsyncifyData>,
342343
signal_asyncify_counter: u64,
344+
// syscall asyncify data, holds the sequence of syscall return values
345+
// used to preserve syscall results across asyncify unwind/rewind cycles
346+
// (e.g., when fork is called within a signal handler that interrupted a syscall)
347+
syscall_asyncify_data: Vec<i32>,
348+
syscall_asyncify_counter: u64,
349+
343350
// stack top
344351
stack_top: u64,
345352
// stack bottom
@@ -548,6 +555,8 @@ impl<T> Store<T> {
548555
asyncify_state: super::AsyncifyState::Normal,
549556
signal_asyncify_data: Vec::new(),
550557
signal_asyncify_counter: 0,
558+
syscall_asyncify_data: Vec::new(),
559+
syscall_asyncify_counter: 0,
551560
#[cfg(feature = "component-model")]
552561
num_component_instances: 0,
553562
signal_handler: None,
@@ -660,6 +669,8 @@ impl<T> Store<T> {
660669
asyncify_state: super::AsyncifyState::Normal,
661670
signal_asyncify_data: Vec::new(),
662671
signal_asyncify_counter: 0,
672+
syscall_asyncify_data: Vec::new(),
673+
syscall_asyncify_counter: 0,
663674
#[cfg(feature = "component-model")]
664675
num_component_instances: 0,
665676
signal_handler: None,
@@ -1428,6 +1439,39 @@ impl<'a, T> StoreContextMut<'a, T> {
14281439
self.0.signal_asyncify_counter = 0;
14291440
}
14301441

1442+
// append the syscall retval information
1443+
pub fn append_syscall_asyncify_data(&mut self, retval: i32) {
1444+
self.0.syscall_asyncify_data.push(retval);
1445+
}
1446+
1447+
// pop the syscall retval information
1448+
pub fn pop_syscall_asyncify_data(&mut self) {
1449+
self.0.syscall_asyncify_data.pop();
1450+
}
1451+
1452+
// get the current syscall retval information
1453+
pub fn get_current_syscall_rewind_data(&mut self) -> Option<i32> {
1454+
let data = self
1455+
.0
1456+
.syscall_asyncify_data
1457+
.get(self.0.syscall_asyncify_counter as usize)
1458+
.cloned();
1459+
let length = self.0.syscall_asyncify_data.len();
1460+
if self.0.syscall_asyncify_counter == (length - 1) as u64 {
1461+
self.0.syscall_asyncify_counter = 0;
1462+
} else {
1463+
self.0.syscall_asyncify_counter += 1;
1464+
}
1465+
data
1466+
}
1467+
1468+
// set the syscall retval information
1469+
// used by fork to copy asyncify information
1470+
pub fn set_syscall_asyncify_data(&mut self, data: Vec<i32>) {
1471+
self.0.syscall_asyncify_data = data;
1472+
self.0.syscall_asyncify_counter = 0;
1473+
}
1474+
14311475
/// get stack top
14321476
pub fn get_stack_top(&self) -> u64 {
14331477
self.0.stack_top
@@ -2988,6 +3032,10 @@ impl<T> StoreInner<T> {
29883032
self.signal_asyncify_data.clone()
29893033
}
29903034

3035+
pub fn get_syscall_asyncify_data(&mut self) -> Vec<i32> {
3036+
self.syscall_asyncify_data.clone()
3037+
}
3038+
29913039
pub fn is_thread(&self) -> bool {
29923040
self.is_thread
29933041
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Test: fork() inside a signal handler (via epoch-delivered SIGALRM).
3+
*
4+
* Scenario:
5+
* 1. Register a SIGALRM handler that calls fork()
6+
* 2. Set alarm(1) then spin-wait (wasm execution allows epoch to fire)
7+
* 3. SIGALRM fires via epoch callback, signal handler runs
8+
* 4. Signal handler forks: child exits 42, parent saves child pid
9+
* 5. Parent verifies child exited with status 42
10+
*
11+
* This exercises the asyncify unwind/rewind cycle that fork triggers
12+
* inside a signal handler. The signal_asyncify_data must be preserved
13+
* correctly for the child to start and the parent to resume.
14+
*
15+
* NOTE: Testing the EINTR-specific code path (fork inside a signal
16+
* handler that interrupted a *blocking* syscall) requires RawPOSIX to
17+
* support interrupting host blocking calls, which is not yet implemented.
18+
*/
19+
20+
#include <assert.h>
21+
#include <signal.h>
22+
#include <stdio.h>
23+
#include <stdlib.h>
24+
#include <string.h>
25+
#include <sys/wait.h>
26+
#include <unistd.h>
27+
28+
static volatile pid_t child_pid = -1;
29+
static volatile sig_atomic_t handler_ran = 0;
30+
31+
static void alarm_handler(int sig)
32+
{
33+
(void)sig;
34+
pid_t pid = fork();
35+
if (pid == 0) {
36+
/* child: exit with a recognizable status */
37+
_exit(42);
38+
}
39+
/* parent: save child pid for later waitpid */
40+
child_pid = pid;
41+
handler_ran = 1;
42+
}
43+
44+
int main(void)
45+
{
46+
int ret;
47+
48+
struct sigaction sa;
49+
memset(&sa, 0, sizeof(sa));
50+
sa.sa_handler = alarm_handler;
51+
sa.sa_flags = 0;
52+
sigemptyset(&sa.sa_mask);
53+
ret = sigaction(SIGALRM, &sa, NULL);
54+
assert(ret == 0);
55+
56+
/* fire SIGALRM in 1 second */
57+
alarm(1);
58+
59+
/* spin until handler fires — epoch will deliver SIGALRM
60+
* during wasm execution */
61+
while (!handler_ran)
62+
;
63+
64+
/* signal handler should have forked */
65+
assert(child_pid > 0);
66+
67+
/* wait for the child */
68+
int status;
69+
pid_t waited = waitpid(child_pid, &status, 0);
70+
assert(waited == child_pid);
71+
assert(WIFEXITED(status));
72+
assert(WEXITSTATUS(status) == 42);
73+
74+
printf("fork-in-signal-handler test passed\n");
75+
fflush(stdout);
76+
return 0;
77+
}

0 commit comments

Comments
 (0)