From 26823d9336f064ac7716080e31de19d56a22bdc0 Mon Sep 17 00:00:00 2001 From: NonMirror <191034600+NonMirror@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:38:42 +0800 Subject: [PATCH] epoll: ignore events for completions that are no longer active The dispatch loop invokes a completion's callback for every epoll event without checking that the completion is still outstanding. The submit path guards on state, but dispatch does not, so a completion that was already dispatched earlier in the same epoll_wait batch (or otherwise retired) can have its callback run a second time. For queueWrite that means q_inner.head.? on an empty queue, which is a null dereference in ReleaseFast. This is the Ghostty 1.3.1 SIGSEGV at 0x108 tracked in #234. Refs #234 --- src/backend/epoll.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/backend/epoll.zig b/src/backend/epoll.zig index 25c0a931..8c4a6be9 100644 --- a/src/backend/epoll.zig +++ b/src/backend/epoll.zig @@ -484,6 +484,15 @@ pub const Loop = struct { const c: *Completion = @ptrFromInt(@as(usize, @intCast(ev.data.ptr))); + // Skip stale deliveries. A completion can be completed and + // retired (disarmed, cancelled, or already dispatched earlier + // in this same epoll_wait batch) while the kernel still + // reports an event for its former registration. Only an + // `.active` completion is legitimately outstanding; invoking + // any other callback again can hand it state it no longer + // owns (e.g. queueWrite's `head.?` on an empty write queue). + if (c.flags.state != .active) continue; + // We get the fd and mark this as in progress we can properly // clean this up late.r const fd = if (c.flags.dup) c.flags.dup_fd else c.fd();