diff --git a/scripts/fiber_gpf_repro.zig b/scripts/fiber_gpf_repro.zig new file mode 100644 index 0000000..9c01e23 --- /dev/null +++ b/scripts/fiber_gpf_repro.zig @@ -0,0 +1,95 @@ +///! Minimal reproduction: Io.Evented GPFs under ReleaseSafe on x86_64-linux. +///! No external dependencies — pure zig stdlib. +///! +///! zig version: 0.16.0-dev.3059+42e33db9d (and unchanged through dev.3091) +///! platform: x86_64-linux (io_uring required — Evented not available on macOS/darwin) +///! +///! Results: +///! Debug — PASS +///! ReleaseFast — PASS +///! ReleaseSmall — PASS +///! ReleaseSafe — GPF at fiber.zig:30 contextSwitch → Uring.zig:1142 mainIdle +///! +///! Root cause: LLVM register allocator bug. The inline asm in fiber.zig +///! contextSwitch uses an explicit register constraint: "{rsi}" (s). Under +///! ReleaseSafe, the stack probe (__zig_probe_stack) and canary (fs:0x28) +///! instrumentation changes the code layout in Uring.idle such that LLVM +///! fails to emit the `lea` that materializes the SwitchMessage address +///! into %rsi before the inline asm block. %rsi is left holding a stale +///! value from a prior function call (Thread.current), causing the context +///! switch to write to a garbage address. +///! +///! Comparison of Uring.idle disassembly at the context switch: +///! +///! ReleaseFast: ReleaseSafe: +///! lea -0x80(%rbp),%rsi ← CORRECT (missing — no lea before asm) +///! ... call Thread.current ← clobbers rsi +///! mov (%rsi),%rax ← valid ptr mov (%rsi),%rax ← stale value → GPF +///! +///! Build & run: +///! zig build-exe -OReleaseSafe fiber_gpf_repro.zig -lc && ./fiber_gpf_repro + +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 { + std.debug.print(" fiber: entered and returning\n", .{}); +} + +fn fiberYield(io: Io) void { + std.debug.print(" fiber: yielding\n", .{}); + io.sleep(Io.Duration.fromMilliseconds(0), .awake) catch return; + std.debug.print(" fiber: resumed\n", .{}); +} + +fn fiberSleep(io: Io) void { + std.debug.print(" fiber: sleeping 50ms\n", .{}); + io.sleep(Io.Duration.fromMilliseconds(50), .awake) catch return; + std.debug.print(" fiber: woke up\n", .{}); +} + +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(); + + std.debug.print("zig version: {s}\n", .{@import("builtin").zig_version_string}); + std.debug.print("optimize: {s}\n", .{@tagName(@import("builtin").mode)}); + std.debug.print("Io backend: Evented\n\n", .{}); + + // test 1: immediate return — GPFs here under ReleaseSafe + std.debug.print("test 1: fiber returns immediately\n", .{}); + { + var f = try io.concurrent(fiberReturn, .{io}); + io.sleep(Io.Duration.fromMilliseconds(10), .awake) catch {}; + f.cancel(io); + } + std.debug.print("test 1: PASSED\n\n", .{}); + + // test 2: yield once + std.debug.print("test 2: fiber yields once\n", .{}); + { + var f = try io.concurrent(fiberYield, .{io}); + io.sleep(Io.Duration.fromMilliseconds(50), .awake) catch {}; + f.cancel(io); + } + std.debug.print("test 2: PASSED\n\n", .{}); + + // test 3: sleep + std.debug.print("test 3: fiber sleeps 50ms\n", .{}); + { + var f = try io.concurrent(fiberSleep, .{io}); + io.sleep(Io.Duration.fromMilliseconds(200), .awake) catch {}; + f.cancel(io); + } + std.debug.print("test 3: PASSED\n\n", .{}); + + std.debug.print("all tests passed\n", .{}); +}