From e64d903e67bd861e26f1fe1d7584986f893fb578 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Sun, 5 Apr 2026 23:01:57 -0500 Subject: [PATCH] add standalone fiber GPF repro with root cause analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LLVM register allocator bug: under ReleaseSafe, the stack probe and canary instrumentation cause LLVM to skip materializing the SwitchMessage address into %rsi before fiber.zig's inline asm context switch. %rsi is left holding a stale value from Thread.current(), causing a GPF. Debug, ReleaseFast, and ReleaseSmall all pass. Only ReleaseSafe triggers it — the combination of optimization + safety instrumentation changes the code layout enough to expose the miscompilation. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/fiber_gpf_repro.zig | 95 +++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 scripts/fiber_gpf_repro.zig 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", .{}); +} -- 2.51.2