From 099a95041054e456ebefbd75f6a4f9f6961002be Mon Sep 17 00:00:00 2001 From: mlugg Date: Fri, 19 Sep 2025 13:35:12 +0100 Subject: std.debug.SelfInfo: thread safety This has been a TODO for ages, but in the past it didn't really matter because stack traces are typically printed to stderr for which a mutex is held so in practice there was a mutex guarding usage of `SelfInfo`. However, now that `SelfInfo` is also used for simply capturing traces, thread safety is needed. Instead of just a single mutex, though, there are a couple of different mutexes involved; this helps make critical sections smaller, particularly when unwinding the stack as `unwindFrame` doesn't typically need to hold any lock at all. --- lib/std/debug/SelfInfo/DarwinModule.zig | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'lib/std/debug/SelfInfo/DarwinModule.zig') diff --git a/lib/std/debug/SelfInfo/DarwinModule.zig b/lib/std/debug/SelfInfo/DarwinModule.zig index fa28720357..29178b5068 100644 --- a/lib/std/debug/SelfInfo/DarwinModule.zig +++ b/lib/std/debug/SelfInfo/DarwinModule.zig @@ -252,6 +252,15 @@ fn loadMachO(module: *const DarwinModule, gpa: Allocator) !DebugInfo.LoadedMachO }; } pub fn getSymbolAtAddress(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, address: usize) Error!std.debug.Symbol { + // We need the lock for a few things: + // * loading the Mach-O module + // * loading the referenced object file + // * scanning the DWARF of that object file + // * building the line number table of that object file + // That's enough that it doesn't really seem worth scoping the lock more tightly than the whole function.. + di.mutex.lock(); + defer di.mutex.unlock(); + if (di.loaded_macho == null) di.loaded_macho = module.loadMachO(gpa) catch |err| switch (err) { error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| return e, else => return error.ReadFailed, @@ -341,8 +350,12 @@ pub fn unwindFrame(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, }; } fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize { - if (di.unwind == null) di.unwind = module.loadUnwindInfo(); - const unwind = &di.unwind.?; + const unwind: *const DebugInfo.Unwind = u: { + di.mutex.lock(); + defer di.mutex.unlock(); + if (di.unwind == null) di.unwind = module.loadUnwindInfo(); + break :u &di.unwind.?; + }; const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo; if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo; @@ -649,10 +662,17 @@ fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, return ret_addr; } pub const DebugInfo = struct { + /// Held while checking and/or populating `unwind` or `loaded_macho`. + /// Once a field is populated and the pointer `&di.loaded_macho.?` or `&di.unwind.?` has been + /// gotten, the lock is released; i.e. it is not held while *using* the loaded info. + mutex: std.Thread.Mutex, + unwind: ?Unwind, loaded_macho: ?LoadedMachO, pub const init: DebugInfo = .{ + .mutex = .{}, + .unwind = null, .loaded_macho = null, }; -- cgit v1.2.3