atproto relay in zig zlay.waow.tech
relay zig atproto
zlay scripts fiber_gpf_issue.md
2.9 kB
Markdown

Io.Evented + ReleaseSafe: GPF in fiber.zig contextSwitch on x86_64-linux #

Io.Evented crashes with a general protection fault under ReleaseSafe on x86_64-linux. Debug, ReleaseFast, and ReleaseSmall all pass. Tested on unpatched zig — no modifications to the stdlib.

Reproduction #

const std = @import("std");
const Io = std.Io;

var evented: Io.Evented = undefined;

var debug_threaded_io: Io.Threaded = undefined;
pub const std_options_debug_threaded_io: ?*Io.Threaded = &debug_threaded_io;

fn fiberReturn(_: Io) void {}

pub fn main() !void {
    const allocator = std.heap.c_allocator;
    debug_threaded_io = Io.Threaded.init(allocator, .{});
    try Io.Evented.init(&evented, allocator, .{});
    const io = evented.io();

    var f = try io.concurrent(fiberReturn, .{io});
    io.sleep(Io.Duration.fromMilliseconds(10), .awake) catch {};
    f.cancel(io);
}
$ zig build-exe -OReleaseSafe repro.zig -lc && ./repro
General protection exception (no address available)
lib/std/Io/fiber.zig:30:20: 0x1079589 in contextSwitch
lib/std/Io/Uring.zig:1142:12: 0x109cc86 in mainIdle
Mode Result
Debug pass
ReleaseFast pass
ReleaseSmall pass
ReleaseSafe GPF

Environment #

  • zig 0.16.0-dev.3059+42e33db9d, unmodified
  • x86_64-linux, kernel 6.8.0-101-generic (Debian bookworm, glibc)

What we've observed #

The crash is at the inline asm in fiber.zig contextSwitch (x86_64 path, line 244). This gets inlined into Uring.idle.

Comparing the disassembly of Uring.idle between ReleaseFast and ReleaseSafe, the difference we see is in how %rsi is set up before the inline asm block. The asm uses "{rsi}" (s) as an input constraint:

ReleaseFast — there's a lea -0x80(%rbp),%rsi that loads the SwitchMessage address into %rsi before the asm block.

ReleaseSafe — the corresponding lea appears to be absent. %rsi seems to hold a stale value from a prior function call (Thread.current, which is caller-saved). The ReleaseSafe prologue includes __zig_probe_stack and a stack canary load (fs:0x28) that aren't present in ReleaseFast.

We're not certain this is the full picture — we may be misreading the disassembly or missing something about how the register constraint interacts with the surrounding code. But the crash is consistent and the repro is deterministic.

Context #

We're building an AT Protocol relay (zlay) that runs ~2,800 concurrent fibers on Io.Evented. We've been working around this by building with ReleaseFast, but recently a separate bug (a websocket off-by-one) manifested as a silent SIGSEGV under ReleaseFast for days. ReleaseSafe would have caught it immediately as a bounds-check panic with a stack trace.

We understand Evented is experimental. Happy to provide more information or test patches.