authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-11 00:40:46-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-11 00:40:46-08:00
log479e62e8ee18c958de50bfb5d09e934522c8435d
tree64925809feb82e5341ec8c32bad9f8ff8460f726
parent4e806f25210abd17f7092f40619b248e8b73def5

std.Io.Threaded: debug file handle leaks


1 files changed, 64 insertions(+), 4 deletions(-)

lib/std/Io/Threaded.zig+64-4
...@@ -69,6 +69,19 @@ random_file: RandomFile = .{},...@@ -69,6 +69,19 @@ random_file: RandomFile = .{},
6969
70csprng: Csprng = .{},70csprng: Csprng = .{},
7171
72/// Tracks open file handles for debugging purposes.
73open_file_map: OpenFileMap = if (OpenFileMap != void) .{} else {},
74
75pub const OpenFileMap = switch (builtin.mode) {
76 .Debug => struct {
77 map: std.AutoArrayHashMapUnmanaged(File.Handle, StackTrace) = .empty,
78 oom: bool = false,
79 },
80 else => void,
81};
82
83pub const StackTrace = [6]usize;
84
72pub const Csprng = struct {85pub const Csprng = struct {
73 rng: std.Random.DefaultCsprng = .{86 rng: std.Random.DefaultCsprng = .{
74 .state = undefined,87 .state = undefined,
...@@ -1315,6 +1328,7 @@ pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {...@@ -1315,6 +1328,7 @@ pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
1315}1328}
13161329
1317pub fn deinit(t: *Threaded) void {1330pub fn deinit(t: *Threaded) void {
1331 const gpa = t.allocator;
1318 t.join();1332 t.join();
1319 if (is_windows and t.wsa.status == .initialized) {1333 if (is_windows and t.wsa.status == .initialized) {
1320 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();1334 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
...@@ -1325,6 +1339,22 @@ pub fn deinit(t: *Threaded) void {...@@ -1325,6 +1339,22 @@ pub fn deinit(t: *Threaded) void {
1325 }1339 }
1326 t.null_file.deinit();1340 t.null_file.deinit();
1327 t.random_file.deinit();1341 t.random_file.deinit();
1342 if (OpenFileMap != void) {
1343 for (t.open_file_map.map.keys(), t.open_file_map.map.values()) |handle, *value| {
1344 const st: std.builtin.StackTrace = .{
1345 .instruction_addresses = value,
1346 .index = value.len,
1347 };
1348 std.log.err("file handle {any} leaked: {f}", .{
1349 handle,
1350 std.debug.FormatStackTrace{
1351 .stack_trace = st,
1352 .terminal_mode = std.log.terminalMode(),
1353 },
1354 });
1355 }
1356 t.open_file_map.map.deinit(gpa);
1357 }
1328 t.* = undefined;1358 t.* = undefined;
1329}1359}
13301360
...@@ -3863,9 +3893,35 @@ fn dirOpenFilePosix(...@@ -3863,9 +3893,35 @@ fn dirOpenFilePosix(
3863 }3893 }
3864 }3894 }
38653895
3896 trackOpenFile(t, fd, @returnAddress());
3866 return .{ .handle = fd };3897 return .{ .handle = fd };
3867}3898}
38683899
3900fn trackOpenFile(t: *Threaded, handle: File.Handle, ra: usize) void {
3901 if (OpenFileMap == void) return;
3902 t.mutex.lock();
3903 defer t.mutex.unlock();
3904 if (t.open_file_map.oom) return;
3905 const gop = t.open_file_map.map.getOrPut(t.allocator, handle) catch |err| switch (err) {
3906 error.OutOfMemory => {
3907 t.open_file_map.oom = true;
3908 t.open_file_map.map.clearAndFree(t.allocator);
3909 return;
3910 },
3911 };
3912 assert(!gop.found_existing);
3913 const st = std.debug.captureCurrentStackTrace(.{ .first_address = ra }, gop.value_ptr);
3914 @memset(gop.value_ptr[@min(st.index, gop.value_ptr.len)..], 0);
3915}
3916
3917fn trackCloseFile(t: *Threaded, handle: File.Handle) void {
3918 if (OpenFileMap == void) return;
3919 t.mutex.lock();
3920 defer t.mutex.unlock();
3921 if (t.open_file_map.oom) return;
3922 assert(t.open_file_map.map.swapRemove(handle));
3923}
3924
3869fn dirOpenFileWindows(3925fn dirOpenFileWindows(
3870 userdata: ?*anyopaque,3926 userdata: ?*anyopaque,
3871 dir: Dir,3927 dir: Dir,
...@@ -3873,14 +3929,14 @@ fn dirOpenFileWindows(...@@ -3873,14 +3929,14 @@ fn dirOpenFileWindows(
3873 flags: File.OpenFlags,3929 flags: File.OpenFlags,
3874) File.OpenError!File {3930) File.OpenError!File {
3875 const t: *Threaded = @ptrCast(@alignCast(userdata));3931 const t: *Threaded = @ptrCast(@alignCast(userdata));
3876 _ = t;
3877 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);3932 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
3878 const sub_path_w = sub_path_w_array.span();3933 const sub_path_w = sub_path_w_array.span();
3879 const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;3934 const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;
3880 return dirOpenFileWtf16(dir_handle, sub_path_w, flags);3935 return dirOpenFileWtf16(t, dir_handle, sub_path_w, flags);
3881}3936}
38823937
3883pub fn dirOpenFileWtf16(3938pub fn dirOpenFileWtf16(
3939 t: *Threaded,
3884 dir_handle: ?windows.HANDLE,3940 dir_handle: ?windows.HANDLE,
3885 sub_path_w: [:0]const u16,3941 sub_path_w: [:0]const u16,
3886 flags: File.OpenFlags,3942 flags: File.OpenFlags,
...@@ -4022,6 +4078,7 @@ pub fn dirOpenFileWtf16(...@@ -4022,6 +4078,7 @@ pub fn dirOpenFileWtf16(
4022 .ACCESS_VIOLATION => |err| return syscall.ntstatusBug(err), // bad io_status_block pointer4078 .ACCESS_VIOLATION => |err| return syscall.ntstatusBug(err), // bad io_status_block pointer
4023 else => |status| return syscall.unexpectedNtstatus(status),4079 else => |status| return syscall.unexpectedNtstatus(status),
4024 };4080 };
4081 trackOpenFile(t, handle, @returnAddress());
4025 return .{ .handle = handle };4082 return .{ .handle = handle };
4026}4083}
40274084
...@@ -4114,6 +4171,7 @@ fn dirOpenFileWasi(...@@ -4114,6 +4171,7 @@ fn dirOpenFileWasi(
4114 if (is_dir) return error.IsDir;4171 if (is_dir) return error.IsDir;
4115 }4172 }
41164173
4174 trackOpenFile(t, fd, @returnAddress());
4117 return .{ .handle = fd };4175 return .{ .handle = fd };
4118}4176}
41194177
...@@ -7856,8 +7914,10 @@ fn dirHardLink(...@@ -7856,8 +7914,10 @@ fn dirHardLink(
78567914
7857fn fileClose(userdata: ?*anyopaque, files: []const File) void {7915fn fileClose(userdata: ?*anyopaque, files: []const File) void {
7858 const t: *Threaded = @ptrCast(@alignCast(userdata));7916 const t: *Threaded = @ptrCast(@alignCast(userdata));
7859 _ = t;7917 for (files) |file| {
7860 for (files) |file| posix.close(file.handle);7918 trackCloseFile(t, file.handle);
7919 posix.close(file.handle);
7920 }
7861}7921}
78627922
7863const fileReadStreaming = switch (native_os) {7923const fileReadStreaming = switch (native_os) {