diff --git a/docs/architecture.md b/docs/architecture.md index d48238352f..dba9a45768 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 @@ -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 diff --git a/examples/accept_signal_test.c b/examples/accept_signal_test.c new file mode 100644 index 0000000000..630b948732 --- /dev/null +++ b/examples/accept_signal_test.c @@ -0,0 +1,168 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/host/src/kernel-worker.ts b/host/src/kernel-worker.ts index b10d8481d5..3b5fa6bfbf 100644 --- a/host/src/kernel-worker.ts +++ b/host/src/kernel-worker.ts @@ -4639,7 +4639,16 @@ export class CentralizedKernelWorker { // callers remain parked in the same host-owned retry loop as EAGAIN. // The sockaddr-family guard deliberately excludes AF_UNIX from this // transport-specific retry rule. - if (this.handlePendingInetConnect(channel, syscallNr, origArgs, retVal, errVal)) { + if ( + this.handlePendingInetConnect( + channel, + syscallNr, + origArgs, + retVal, + errVal, + deliveredSignal, + ) + ) { return; } @@ -4757,19 +4766,30 @@ export class CentralizedKernelWorker { */ private dequeueSignalForDelivery(channel: ChannelInfo): number { const preparedSignals = this.resumePreparedSignals; - if (preparedSignals?.has(channel)) { - const existingSignal = new DataView( - channel.memory.buffer, - channel.channelOffset, - ).getUint32(CH_SIG_SIGNUM, true); - if (existingSignal > 0) return existingSignal; - // The channel was retired or the guest consumed the record without a - // normal publication path. Do not suppress a genuinely new signal. - preparedSignals.delete(channel); + const dequeueSignal = this.kernelInstance?.exports.kernel_dequeue_signal as + ((pid: number, tid: number, outPtr: KernelPointer) => number) | undefined; + if (!dequeueSignal && !preparedSignals?.has(channel)) { + // A kernel without the dequeue export cannot have transferred a signal + // into this channel. This also keeps host-only retry harnesses from + // needing a full ABI-sized process memory when signals are out of scope. + return 0; } + const existingSignal = new DataView( + channel.memory.buffer, + channel.channelOffset, + ).getUint32(CH_SIG_SIGNUM, true); + if (existingSignal > 0) { + // WHY: dequeuing transfers this signal from Rust into the channel. Until + // a completed mailbox wakes the guest and the libc glue clears signum, + // the channel is its only owner. A host-side EAGAIN retry must neither + // dequeue a second signal nor erase this still-undelivered record. + return existingSignal; + } + // The channel was retired or the guest consumed the resume-time record + // without a normal publication path. Do not suppress a genuinely new + // signal. + preparedSignals?.delete(channel); - const dequeueSignal = this.kernelInstance!.exports.kernel_dequeue_signal as - ((pid: number, tid: number, outPtr: KernelPointer) => number) | undefined; if (!dequeueSignal) return 0; // Use the signal area in kernel scratch as the output buffer @@ -6668,6 +6688,7 @@ export class CentralizedKernelWorker { origArgs: number[], retVal: number, errVal: number, + deliveredSignal: number, ): boolean { if ( syscallNr !== SYS_CONNECT || @@ -6703,6 +6724,15 @@ export class CentralizedKernelWorker { -1, errVal, ); + } else if (deliveredSignal > 0) { + this.completeChannel( + channel, + syscallNr, + origArgs, + undefined, + -1, + EINTR_ERRNO, + ); } else { this.handleBlockingRetry(channel, syscallNr, origArgs); } @@ -6777,6 +6807,32 @@ export class CentralizedKernelWorker { return true; } + /** + * Publish a caught signal before a host-owned blocking retry can re-park. + * + * Rust has already removed the signal from its pending queue and copied its + * handler record into CH_SIG. The guest cannot run that handler until this + * channel completes, so retaining the mailbox in an async wait would strand + * the signal and can leave SIGCHLD zombies unreaped indefinitely. + */ + private interruptBlockingRetryForCaughtSignal( + channel: ChannelInfo, + syscallNr: number, + origArgs: number[], + deliveredSignal: number, + ): boolean { + if (deliveredSignal <= 0) return false; + this.completeChannel( + channel, + syscallNr, + origArgs, + undefined, + -1, + EINTR_ERRNO, + ); + return true; + } + private handleBlockingRetry( channel: ChannelInfo, syscallNr: number, @@ -6790,10 +6846,28 @@ export class CentralizedKernelWorker { // parking a retry that no later cancellation dispatch can discover. if (this.interruptPendingFifoOpenCancellation(channel, syscallNr)) return; + // Every call site reaches this method only after the kernel reported its + // internal EAGAIN "would block" sentinel. Some marshalling paths already + // prepared a caught signal; others bypass the generic completion path and + // need to dequeue it here. dequeueSignalForDelivery is idempotent while a + // CH_SIG record remains owned by this uncompleted channel. + const deliveredSignal = this.dequeueSignalForDelivery(channel); + if (this.kernelInstance && this.finishSignalTermination(channel)) return; + // Futex wait: use Atomics.waitAsync on the target address in process memory if (syscallNr === SYS_FUTEX) { const futexOp = origArgs[1] & 0x7f; // mask out FUTEX_PRIVATE_FLAG if (futexOp === 0) { // FUTEX_WAIT + if ( + this.interruptBlockingRetryForCaughtSignal( + channel, + syscallNr, + origArgs, + deliveredSignal, + ) + ) { + return; + } const addr = origArgs[0]; // address in process memory const expectedVal = origArgs[2]; const i32View = new Int32Array(channel.memory.buffer); @@ -6849,6 +6923,16 @@ export class CentralizedKernelWorker { this.completeChannel(channel, syscallNr, origArgs, SYSCALL_ARGS[syscallNr], 0, 0); return; } + if ( + this.interruptBlockingRetryForCaughtSignal( + channel, + syscallNr, + origArgs, + deliveredSignal, + ) + ) { + return; + } const deadline = this.getReadinessDeadline(channel, timeoutMs); if (deadline > 0 && Date.now() >= deadline) { // Re-enter once with timeout=0. Besides checking readiness at the @@ -6934,6 +7018,16 @@ export class CentralizedKernelWorker { if (syscallNr === SYS_RT_SIGTIMEDWAIT) { const timeoutPtr = origArgs[2]; // pointer to timespec in process memory if (timeoutPtr === 0) { + if ( + this.interruptBlockingRetryForCaughtSignal( + channel, + syscallNr, + origArgs, + deliveredSignal, + ) + ) { + return; + } // NULL timeout = wait indefinitely. Use long retry interval since // signals arrive via kernel_kill, not organically. In the browser, // short retries starve the event loop when multiple threads are active @@ -6962,6 +7056,17 @@ export class CentralizedKernelWorker { this.signalWaitDeadlines.delete(key); this.completeChannel(channel, syscallNr, origArgs, SYSCALL_ARGS[syscallNr], -1, EAGAIN_ERRNO); } else { + if ( + this.interruptBlockingRetryForCaughtSignal( + channel, + syscallNr, + origArgs, + deliveredSignal, + ) + ) { + this.signalWaitDeadlines.delete(key); + return; + } const existingDeadline = this.signalWaitDeadlines.get(key); const deadline = existingDeadline?.deadline ?? performance.now() + timeoutMs; if (!existingDeadline) { @@ -7057,6 +7162,17 @@ export class CentralizedKernelWorker { } } + if ( + this.interruptBlockingRetryForCaughtSignal( + channel, + syscallNr, + origArgs, + deliveredSignal, + ) + ) { + return; + } + // Socket timeout check: if a read/write-like syscall blocks on a socket // with SO_RCVTIMEO or SO_SNDTIMEO set, schedule a timer for ETIMEDOUT. if ( @@ -11784,15 +11900,12 @@ export class CentralizedKernelWorker { // 2. Pending ppoll/poll retry — wake ALL threads for this pid. // Snapshot-and-skip-if-replaced: retrySyscall runs handleSyscall - // synchronously, and a non-interruptible blocking wait (notably - // accept(), which has no EINTR path) re-inserts the SAME - // exact-channel key via pendingPollRetries.set when it re-parks on - // EAGAIN. JS Map iterators are not snapshots — a deleted-then- - // reinserted key reappears at the tail and the raw for..of would - // revisit it forever, livelocking the whole kernel worker thread. - // Mirror wakeBlockedPoll / wakeAllBlockedRetries. (Regression: - // SIGCHLD to a forking daemon's master parked in accept() — - // e.g. msmtpd delivering WordPress mail — wedged the kernel.) + // synchronously, and a wait that remains blocked can reinsert the SAME + // exact-channel key via pendingPollRetries.set. JS Map iterators are not + // snapshots — a deleted-then-reinserted key reappears at the tail and + // the raw for..of would revisit it forever, livelocking the whole + // kernel worker thread. Mirror wakeBlockedPoll / + // wakeAllBlockedRetries. const pollMatches = Array.from(this.pendingPollRetries.entries()).filter( ([, e]) => e.channel.pid === targetPid, ); diff --git a/host/test/accept-signal-guest.test.ts b/host/test/accept-signal-guest.test.ts new file mode 100644 index 0000000000..9ab63f3372 --- /dev/null +++ b/host/test/accept-signal-guest.test.ts @@ -0,0 +1,25 @@ +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { runCentralizedProgram } from "./centralized-test-helper"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "../.."); +const program = join(repoRoot, "examples/accept_signal_test.wasm"); + +describe.skipIf(!existsSync(program))("accept signal guest", () => { + it("delivers SIGCHLD before restarting a blocked accept", async () => { + const result = await runCentralizedProgram({ + programPath: program, + argv: ["accept_signal_test"], + useDefaultRootfs: false, + timeout: 10_000, + }); + + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toContain( + "PASS accept signal interruption and SA_RESTART", + ); + expect(result.stderr).toBe(""); + }); +}); diff --git a/host/test/connect-pending-retry.test.ts b/host/test/connect-pending-retry.test.ts index 1fa3c7a385..e9fbc34d36 100644 --- a/host/test/connect-pending-retry.test.ts +++ b/host/test/connect-pending-retry.test.ts @@ -12,6 +12,7 @@ import { CentralizedKernelWorker } from "../src/kernel-worker"; const EINPROGRESS = 115; const EALREADY = 114; const ECONNREFUSED = 111; +const EINTR = 4; type KernelResult = { retVal: number; errVal: number }; @@ -21,7 +22,11 @@ function createSharedMemory(pages = 2): WebAssembly.Memory { function createConnectHarness( results: KernelResult[], - options: { nonblock?: boolean; family?: number } = {}, + options: { + nonblock?: boolean; + family?: number; + handlerSignal?: number; + } = {}, ) { const kernelMemory = createSharedMemory(); const processMemory = createSharedMemory(); @@ -84,7 +89,7 @@ function createConnectHarness( bindKernelTidForChannel: vi.fn(), highControlFloorForProcess: vi.fn(() => null), getProcessExitSignal: vi.fn(() => 0), - dequeueSignalForDelivery: vi.fn(() => 0), + dequeueSignalForDelivery: vi.fn(() => options.handlerSignal ?? 0), finishSignalTermination: vi.fn(() => false), completeChannel, completeChannelRaw: vi.fn(), @@ -136,6 +141,20 @@ describe("pending AF_INET connect routing", () => { expect(harness.worker.pendingPollRetries.size).toBe(0); }); + it("interrupts a blocking pending connect for a caught signal", () => { + const harness = createConnectHarness( + [{ retVal: -1, errVal: EINPROGRESS }], + { handlerSignal: 10 }, + ); + + harness.worker.handleSyscall(harness.channel); + + expect(harness.completeChannel).toHaveBeenCalledOnce(); + expect(harness.completeChannel.mock.calls[0].slice(-2)) + .toEqual([-1, EINTR]); + expect(harness.worker.pendingPollRetries.size).toBe(0); + }); + it("keeps a blocking EALREADY retry parked and then returns the failure", () => { vi.useFakeTimers(); const harness = createConnectHarness([ diff --git a/host/test/signal-accept-livelock.test.ts b/host/test/signal-accept-livelock.test.ts index 1dd35fce10..97a9246ddc 100644 --- a/host/test/signal-accept-livelock.test.ts +++ b/host/test/signal-accept-livelock.test.ts @@ -1,7 +1,6 @@ /** * Regression test for a kernel-worker deadlock: delivering a signal to a - * process that is blocked in a non-interruptible re-parking syscall (notably - * `accept()`, which has no EINTR path) must not livelock. + * process that is blocked in a re-parking syscall must not livelock. * * `sendSignalToProcess` / `notifyPipeReadable` iterate `pendingPollRetries` * and, for each matching entry, delete it and synchronously `retrySyscall`. @@ -16,17 +15,30 @@ * livelocked the kernel — the reset request (and every other request) hung * forever. Fix: snapshot the entries before iterating (mirrors the existing * `wakeBlockedPoll` / `wakeAllBlockedRetries` pattern). + * + * A second failure lived at the same boundary: retrying accept could dequeue + * SIGCHLD into CH_SIG and then re-park the mailbox. The guest could not run + * its handler, and a later retry erased the channel's only signal record. + * Forking daemons then accumulated zombies until their session limit was + * exhausted. The tests below protect both the retry iteration and signal + * ownership invariants. */ import { describe, expect, it, vi } from "vitest"; import { CentralizedKernelWorker } from "../src/kernel-worker"; import { + ABI_SYSCALLS, CH_ARGS, CH_ARG_SIZE, CH_ERRNO, CH_RETURN, + CH_SIG_FLAGS, + CH_SIG_SIGNUM, CH_SYSCALL, } from "../src/generated/abi"; +const EAGAIN = 11; +const EINTR = 4; +const SA_RESTART = 0x10000000; const SIGCHLD = 17; const SIGTERM = 15; const SYS_TKILL = 204; @@ -66,7 +78,165 @@ function createWorkerHarness(): any { return worker; } +function createAcceptSignalHarness(options: { nonblock?: boolean } = {}) { + const kernelMemory = createSharedMemory(); + const processMemory = createSharedMemory(); + const channel = { + pid: 61, + memory: processMemory, + channelOffset: 0, + i32View: new Int32Array(processMemory.buffer), + consecutiveSyscalls: 0, + }; + const args = [7, 0, 0, 0, 0, 0]; + const processView = new DataView(processMemory.buffer); + processView.setUint32(CH_SYSCALL, ABI_SYSCALLS.Accept, true); + args.forEach((arg, index) => { + processView.setBigInt64( + CH_ARGS + index * CH_ARG_SIZE, + BigInt(arg), + true, + ); + }); + + const handleChannel = vi.fn(() => { + const kernelView = new DataView(kernelMemory.buffer); + kernelView.setBigInt64(CH_RETURN, -1n, true); + kernelView.setUint32(CH_ERRNO, EAGAIN, true); + return 0; + }); + const dequeueSignal = vi.fn( + (_pid: number, _tid: number, outPtr: number) => { + const view = new DataView(kernelMemory.buffer); + view.setUint32(outPtr, SIGCHLD, true); + view.setUint32(outPtr + 8, SA_RESTART, true); + return SIGCHLD; + }, + ); + const completeChannel = vi.fn(); + const worker: any = Object.assign( + Object.create(CentralizedKernelWorker.prototype), + { + kernel: { + toKernelPtr: (value: number | bigint) => Number(value), + bos: { findBindingByAddr: vi.fn() }, + }, + kernelInstance: { + exports: { + kernel_dequeue_signal: dequeueSignal, + kernel_get_fd_accept_wake_idx: vi.fn(() => 8), + kernel_get_process_exit_signal: vi.fn(() => -1), + kernel_handle_channel: handleChannel, + kernel_is_fd_nonblock: vi.fn(() => options.nonblock ? 1 : 0), + }, + }, + kernelMemory, + scratchOffset: 0, + currentHandlePid: 0, + config: {}, + syscallRing: new Map(), + channelTids: new Map(), + syscallTraceEnabled: false, + sharedMmapBackings: new Map(), + hostReaped: new Set(), + pendingCancels: new Set(), + pendingPollRetries: new Map(), + pendingSelectRetries: new Map(), + pendingSleeps: new Map(), + pendingPipeReaders: new Map(), + pendingPipeWriters: new Map(), + pendingSignalWaits: new Map(), + signalWaitDeadlines: new Map(), + socketTimeoutTimers: new Map(), + processes: new Map([ + [ + channel.pid, + { + pid: channel.pid, + memory: processMemory, + channels: [channel], + ptrWidth: 4, + }, + ], + ]), + isRegisteredChannel: vi.fn(() => true), + deferChannelWhileStopped: vi.fn(() => false), + synchronizeSharedMemoryForBoundary: vi.fn(), + bindKernelTidForChannel: vi.fn(), + highControlFloorForProcess: vi.fn(() => null), + finishSignalTermination: vi.fn(() => false), + completeChannel, + }, + ); + + return { + args, + channel, + completeChannel, + dequeueSignal, + processMemory, + worker, + }; +} + describe("signal delivery to a process blocked in accept()", () => { + it("publishes EINTR instead of re-parking a caught signal", () => { + const harness = createAcceptSignalHarness(); + + harness.worker.handleSyscall(harness.channel); + + expect(harness.completeChannel).toHaveBeenCalledWith( + harness.channel, + ABI_SYSCALLS.Accept, + harness.args, + undefined, + -1, + EINTR, + ); + expect(harness.worker.pendingPollRetries.size).toBe(0); + const channelView = new DataView(harness.processMemory.buffer); + expect(channelView.getUint32(CH_SIG_SIGNUM, true)).toBe(SIGCHLD); + expect(channelView.getUint32(CH_SIG_FLAGS, true)).toBe(SA_RESTART); + }); + + it("retains the channel-owned signal record until the guest consumes it", () => { + const harness = createAcceptSignalHarness(); + + harness.worker.handleSyscall(harness.channel); + expect(harness.worker.dequeueSignalForDelivery(harness.channel)) + .toBe(SIGCHLD); + + expect(harness.dequeueSignal).toHaveBeenCalledOnce(); + expect( + new DataView(harness.processMemory.buffer).getUint32( + CH_SIG_SIGNUM, + true, + ), + ).toBe(SIGCHLD); + }); + + it("preserves public EAGAIN for a non-blocking accept", () => { + const harness = createAcceptSignalHarness({ nonblock: true }); + + harness.worker.handleSyscall(harness.channel); + + expect(harness.completeChannel).toHaveBeenCalledWith( + harness.channel, + ABI_SYSCALLS.Accept, + harness.args, + expect.anything(), + -1, + EAGAIN, + ); + expect(harness.worker.pendingPollRetries.size).toBe(0); + expect( + new DataView(harness.processMemory.buffer).getUint32( + CH_SIG_SIGNUM, + true, + ), + ).toBe(SIGCHLD); + }); + it("does not livelock when retrySyscall re-parks the same poll key", () => { const worker = createWorkerHarness(); const targetPid = 42; diff --git a/libc/glue/channel_syscall.c b/libc/glue/channel_syscall.c index 08b8d8c464..5a1209f9db 100644 --- a/libc/glue/channel_syscall.c +++ b/libc/glue/channel_syscall.c @@ -108,6 +108,8 @@ int *__errno_location(void); #define EINVAL 22 #define SYS_OPEN 1 #define SYS_OPENAT 69 +#define SYS_ACCEPT 53 +#define SYS_ACCEPT4 384 #define SYS_SIGACTION 36 #define SYS_WAIT4 139 #define SYS_WAITID 288 @@ -535,16 +537,21 @@ static long __do_syscall_impl(long n, long long a1, long long a2, long long a3, &delivered_signal ); - /* wait4()/waitid() and blocking FIFO open/openat are host-deferred, so a - * caught signal completes the channel with EINTR in order to run its handler. - * SA_RESTART makes that interruption transparent: after the handler and - * mask restoration finish, submit the same operation again. Keep the - * retry list deliberately narrow; several other EINTR-returning calls have - * timeout/cancellation rules that forbid this generic treatment. */ + /* These calls can remain parked in host-owned waits after the kernel has + * returned its internal EAGAIN sentinel. A caught signal completes the + * channel with EINTR so its handler can run. SA_RESTART makes that + * interruption transparent by submitting the operation again after the + * handler and mask restoration finish. + * + * Keep this list deliberately narrow: several other EINTR-returning calls + * have timeout, partial-I/O, or cancellation rules that forbid generic + * resubmission. accept/accept4 are safe here because the host interrupts + * only an EAGAIN attempt that did not remove a connection from the queue. */ if (err == EINTR && delivered_signal && (delivered_flags & SA_RESTART) != 0 && (n == SYS_WAIT4 || n == SYS_WAITID || - n == SYS_OPEN || n == SYS_OPENAT)) { + n == SYS_OPEN || n == SYS_OPENAT || + n == SYS_ACCEPT || n == SYS_ACCEPT4)) { /* __syscall_cp's outer cancellation check has not run yet. A signal * handler may have enabled a cancellation that was already pending, * or the host may have used this EINTR completion to wake a canceled