From 2085096f50fe235e9b6ddff8abc04daa4e5e7667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Sat, 1 Aug 2026 11:29:32 +0200 Subject: [PATCH] std.Thread: fix freeAndExit() on sparc[64]-linux hardware On SPARC, the kernel needs to be able to restore the current register window from the stack when returning from a syscall. That presents a bit of a problem in freeAndExit() since we're deallocating the stack! The good news is that, since we do not care about the contents of the incoming and local registers at that point, we can just tell the kernel that our stack is an undefined global buffer. This didn't manifest in QEMU but does reproduce deterministically on a real kernel/machine. --- lib/std/Thread.zig | 69 +++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 6eb76719a55d2787d04f124e58fa8829cbc7e8e9..05125adbe852a3586b4d77fa2d0f8f7dd4d4bb94 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -1146,6 +1146,13 @@ const LinuxThreadImpl = struct { parent_tid: i32 = undefined, mapped: []align(std.heap.page_size_min) u8, + // On SPARC, the kernel needs to be able to restore the current register window from the + // stack when returning from a syscall. That presents a bit of a problem in `freeAndExit` + // since we're deallocating the stack! The good news is that, since we do not care about + // the contents of the incoming and local registers at that point, we can just tell the + // kernel that our stack is this undefined global buffer. + var sparc_exit_stack: [192]u8 align(16) = undefined; + /// Calls `munmap(mapped.ptr, mapped.len)` then `exit(1)` without touching the stack (which lives in `mapped.ptr`). /// Ported over from musl libc's pthread detached implementation: /// https://github.com/ifduyue/musl/search?q=__unmapself @@ -1365,51 +1372,39 @@ const LinuxThreadImpl = struct { [len] "{r5}" (self.mapped.len), ), .sparc => asm volatile ( - \\ # See sparc64 comments below. - \\ 1: - \\ cmp %%fp, 0 - \\ beq 2f - \\ nop - \\ ba 1b - \\ restore - \\ 2: - \\ mov %%g1, %%o0 // ptr - \\ mov %%g2, %%o1 // len - \\ mov 73, %%g1 // SYS_munmap - \\ t 0x3 // ST_FLUSH_WINDOWS - \\ t 0x10 - \\ mov 1, %%g1 // SYS_exit - \\ mov 0, %%o0 - \\ t 0x10 + \\ // See sparc64 comments below. + \\ t 0x3 // ST_FLUSH_WINDOWS + \\ mov %%g3, %%sp + \\ mov %%g1, %%o0 + \\ mov %%g2, %%o1 + \\ mov 73, %%g1 // SYS_munmap + \\ t 0x10 + \\ mov 1, %%g1 // SYS_exit + \\ mov 0, %%o0 + \\ t 0x10 : : [ptr] "{g1}" (@intFromPtr(self.mapped.ptr)), [len] "{g2}" (self.mapped.len), + [stack] "{g3}" (&sparc_exit_stack), : .{ .memory = true }), .sparc64 => asm volatile ( - \\ # SPARCs really don't like it when active stack frames - \\ # is unmapped (it will result in a segfault), so we - \\ # force-deactivate it by running `restore` until - \\ # all frames are cleared. - \\ 1: - \\ cmp %%fp, 0 - \\ beq 2f - \\ nop - \\ ba 1b - \\ restore - \\ 2: - \\ mov %%g1, %%o0 // ptr - \\ mov %%g2, %%o1 // len - \\ mov 73, %%g1 // SYS_munmap - \\ # Flush register window contents to prevent background - \\ # memory access before unmapping the stack. - \\ flushw - \\ t 0x6d - \\ mov 1, %%g1 // SYS_exit - \\ mov 0, %%o0 - \\ t 0x6d + \\ // Ensure that the kernel only has to flush the current register window. + \\ flushw + \\ // Set up a fake stack for the syscall to restore l/i registers from. Local + \\ // and incoming registers must be treated as effectively garbage past this + \\ // instruction! + \\ sub %%g3, 2047, %%sp + \\ mov %%g1, %%o0 + \\ mov %%g2, %%o1 + \\ mov 73, %%g1 // SYS_munmap + \\ t 0x6d + \\ mov 1, %%g1 // SYS_exit + \\ mov 0, %%o0 + \\ t 0x6d : : [ptr] "{g1}" (@intFromPtr(self.mapped.ptr)), [len] "{g2}" (self.mapped.len), + [stack] "{g3}" (&sparc_exit_stack), : .{ .memory = true }), .loongarch32, .loongarch64 => asm volatile ( \\ ori $a7, $zero, 215 # SYS_munmap -- 2.54.0