atproto relay implementation in zig zlay.waow.tech
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

add standalone fiber GPF repro with root cause analysis

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) <noreply@anthropic.com>

zzstoatzz e64d903e 4748b759

+95
+95
scripts/fiber_gpf_repro.zig
··· 1 + ///! Minimal reproduction: Io.Evented GPFs under ReleaseSafe on x86_64-linux. 2 + ///! No external dependencies — pure zig stdlib. 3 + ///! 4 + ///! zig version: 0.16.0-dev.3059+42e33db9d (and unchanged through dev.3091) 5 + ///! platform: x86_64-linux (io_uring required — Evented not available on macOS/darwin) 6 + ///! 7 + ///! Results: 8 + ///! Debug — PASS 9 + ///! ReleaseFast — PASS 10 + ///! ReleaseSmall — PASS 11 + ///! ReleaseSafe — GPF at fiber.zig:30 contextSwitch → Uring.zig:1142 mainIdle 12 + ///! 13 + ///! Root cause: LLVM register allocator bug. The inline asm in fiber.zig 14 + ///! contextSwitch uses an explicit register constraint: "{rsi}" (s). Under 15 + ///! ReleaseSafe, the stack probe (__zig_probe_stack) and canary (fs:0x28) 16 + ///! instrumentation changes the code layout in Uring.idle such that LLVM 17 + ///! fails to emit the `lea` that materializes the SwitchMessage address 18 + ///! into %rsi before the inline asm block. %rsi is left holding a stale 19 + ///! value from a prior function call (Thread.current), causing the context 20 + ///! switch to write to a garbage address. 21 + ///! 22 + ///! Comparison of Uring.idle disassembly at the context switch: 23 + ///! 24 + ///! ReleaseFast: ReleaseSafe: 25 + ///! lea -0x80(%rbp),%rsi ← CORRECT (missing — no lea before asm) 26 + ///! ... call Thread.current ← clobbers rsi 27 + ///! mov (%rsi),%rax ← valid ptr mov (%rsi),%rax ← stale value → GPF 28 + ///! 29 + ///! Build & run: 30 + ///! zig build-exe -OReleaseSafe fiber_gpf_repro.zig -lc && ./fiber_gpf_repro 31 + 32 + const std = @import("std"); 33 + const Io = std.Io; 34 + 35 + var evented: Io.Evented = undefined; 36 + 37 + var debug_threaded_io: Io.Threaded = undefined; 38 + pub const std_options_debug_threaded_io: ?*Io.Threaded = &debug_threaded_io; 39 + 40 + fn fiberReturn(_: Io) void { 41 + std.debug.print(" fiber: entered and returning\n", .{}); 42 + } 43 + 44 + fn fiberYield(io: Io) void { 45 + std.debug.print(" fiber: yielding\n", .{}); 46 + io.sleep(Io.Duration.fromMilliseconds(0), .awake) catch return; 47 + std.debug.print(" fiber: resumed\n", .{}); 48 + } 49 + 50 + fn fiberSleep(io: Io) void { 51 + std.debug.print(" fiber: sleeping 50ms\n", .{}); 52 + io.sleep(Io.Duration.fromMilliseconds(50), .awake) catch return; 53 + std.debug.print(" fiber: woke up\n", .{}); 54 + } 55 + 56 + pub fn main() !void { 57 + const allocator = std.heap.c_allocator; 58 + 59 + debug_threaded_io = Io.Threaded.init(allocator, .{}); 60 + try Io.Evented.init(&evented, allocator, .{}); 61 + const io = evented.io(); 62 + 63 + std.debug.print("zig version: {s}\n", .{@import("builtin").zig_version_string}); 64 + std.debug.print("optimize: {s}\n", .{@tagName(@import("builtin").mode)}); 65 + std.debug.print("Io backend: Evented\n\n", .{}); 66 + 67 + // test 1: immediate return — GPFs here under ReleaseSafe 68 + std.debug.print("test 1: fiber returns immediately\n", .{}); 69 + { 70 + var f = try io.concurrent(fiberReturn, .{io}); 71 + io.sleep(Io.Duration.fromMilliseconds(10), .awake) catch {}; 72 + f.cancel(io); 73 + } 74 + std.debug.print("test 1: PASSED\n\n", .{}); 75 + 76 + // test 2: yield once 77 + std.debug.print("test 2: fiber yields once\n", .{}); 78 + { 79 + var f = try io.concurrent(fiberYield, .{io}); 80 + io.sleep(Io.Duration.fromMilliseconds(50), .awake) catch {}; 81 + f.cancel(io); 82 + } 83 + std.debug.print("test 2: PASSED\n\n", .{}); 84 + 85 + // test 3: sleep 86 + std.debug.print("test 3: fiber sleeps 50ms\n", .{}); 87 + { 88 + var f = try io.concurrent(fiberSleep, .{io}); 89 + io.sleep(Io.Duration.fromMilliseconds(200), .awake) catch {}; 90 + f.cancel(io); 91 + } 92 + std.debug.print("test 3: PASSED\n\n", .{}); 93 + 94 + std.debug.print("all tests passed\n", .{}); 95 + }