Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,17 @@ Some syscalls (read from empty pipe, accept on socket, poll with timeout) cannot

This mechanism is critical: the process worker blocks on `Atomics.wait` while the host manages async retry via `Atomics.waitAsync`.

The retry boundary also owns caught-signal delivery. Once Rust dequeues a
caught signal into `CH_SIG`, that channel is the signal record's sole owner
until libc runs the handler and clears it. If the syscall is still blocked, the
host therefore completes the channel with `EINTR` before it can park again.
This lets libc run the handler and prevents a later retry from losing the
signal. The glue transparently retries only the narrow set of operations for
which `SA_RESTART` is safe, including `accept` and `accept4`; a public
nonblocking `EAGAIN` remains `EAGAIN`. The shared
`CentralizedKernelWorker` state machine provides the same behavior in Node.js
and browser hosts.

`F_SETLKW` uses the same parking mechanism with a narrower wake contract. A
conflict returns the internal retry result, and the host parks only that lock
request. Unlock, conversion, close, exit, and other Rust-side changes that may
Expand Down Expand Up @@ -1251,6 +1262,12 @@ Signals are delivered at syscall boundaries. When a process has a pending signal
4. After the handler returns, the glue calls `SYS_RT_SIGRETURN` to restore the signal mask
5. If the signal interrupted a blocking syscall, EINTR is returned

The host distinguishes the kernel's internal `EAGAIN` retry sentinel from a
completed nonblocking `EAGAIN`. When a caught signal is prepared while an
internal retry is still blocked, the host publishes `EINTR` without discarding
the prepared `CH_SIG` record. Libc runs the handler before deciding whether
`SA_RESTART` permits resubmitting that syscall.

Features: RT signal queuing with `si_value`, cross-process `kill`/`killpg`, `sigaltstack` with shadow stack swap, `sigsuspend`, `sigtimedwait`, `setitimer`/`alarm` via host timers.

Exact-thread delivery never degrades into process-wide delivery. `tkill` and
Expand Down
168 changes: 168 additions & 0 deletions examples/accept_signal_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#define _POSIX_C_SOURCE 200809L

#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

static volatile sig_atomic_t sigchld_count;

static void on_sigchld(int signum)
{
(void)signum;
sigchld_count++;
}

static void sleep_ms(long milliseconds)
{
struct timespec delay = {
.tv_sec = milliseconds / 1000,
.tv_nsec = (milliseconds % 1000) * 1000000,
};
while (nanosleep(&delay, &delay) != 0 && errno == EINTR)
;
}

static int connect_after_delay(uint16_t port)
{
sleep_ms(400);

int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
return 20;
struct sockaddr_in address = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
if (connect(fd, (struct sockaddr *)&address, sizeof(address)) != 0)
return 21;

/*
* WHY: keep this child alive until after the parent inspects the handler
* count. Otherwise the connector's own SIGCHLD could hide a lost signal
* from the child that was meant to interrupt accept().
*/
sleep_ms(100);
close(fd);
return 0;
}

static int run_case(uint16_t port, int restart)
{
struct sigaction action;
memset(&action, 0, sizeof(action));
action.sa_handler = on_sigchld;
action.sa_flags = restart ? SA_RESTART : 0;
sigemptyset(&action.sa_mask);
if (sigaction(SIGCHLD, &action, NULL) != 0)
return 2;
sigchld_count = 0;

int listener = socket(AF_INET, SOCK_STREAM, 0);
if (listener < 0)
return 3;
int reuse = 1;
if (setsockopt(
listener,
SOL_SOCKET,
SO_REUSEADDR,
&reuse,
sizeof(reuse)
) != 0)
return 4;
struct sockaddr_in address = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
if (bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0)
return 5;
if (listen(listener, 4) != 0)
return 6;

pid_t exiting_child = fork();
if (exiting_child < 0)
return 7;
if (exiting_child == 0) {
close(listener);
sleep_ms(100);
_exit(0);
}

pid_t connector = fork();
if (connector < 0)
return 8;
if (connector == 0) {
close(listener);
_exit(connect_after_delay(port));
}

errno = 0;
int accepted = accept(listener, NULL, NULL);
int accept_errno = errno;
if (!restart) {
if (accepted >= 0 || accept_errno != EINTR) {
fprintf(
stderr,
"accept without SA_RESTART returned %d, errno=%d\n",
accepted,
accept_errno
);
return 9;
}
accepted = accept(listener, NULL, NULL);
accept_errno = errno;
}

if (accepted < 0) {
fprintf(
stderr,
"accept with restart=%d returned errno=%d\n",
restart,
accept_errno
);
return 10;
}
if (sigchld_count != 1) {
fprintf(
stderr,
"accept with restart=%d observed %d handlers, expected 1\n",
restart,
(int)sigchld_count
);
return 11;
}

close(accepted);
close(listener);

int status;
if (waitpid(exiting_child, &status, 0) != exiting_child ||
!WIFEXITED(status) || WEXITSTATUS(status) != 0)
return 12;
if (waitpid(connector, &status, 0) != connector ||
!WIFEXITED(status) || WEXITSTATUS(status) != 0)
return 13;
return 0;
}

int main(void)
{
int result = run_case(25254, 0);
if (result != 0)
return result;
result = run_case(25255, 1);
if (result != 0)
return result;

puts("PASS accept signal interruption and SA_RESTART");
return 0;
}
Loading
Loading