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 = .{},
6969
7070csprng: 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
7285pub const Csprng = struct {
7386 rng: std.Random.DefaultCsprng = .{
7487 .state = undefined,
......@@ -1315,6 +1328,7 @@ pub fn setAsyncLimit(t: *Threaded, new_limit: Io.Limit) void {
13151328}
13161329
13171330pub fn deinit(t: *Threaded) void {
1331 const gpa = t.allocator;
13181332 t.join();
13191333 if (is_windows and t.wsa.status == .initialized) {
13201334 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
......@@ -1325,6 +1339,22 @@ pub fn deinit(t: *Threaded) void {
13251339 }
13261340 t.null_file.deinit();
13271341 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 }
13281358 t.* = undefined;
13291359}
13301360
......@@ -3863,9 +3893,35 @@ fn dirOpenFilePosix(
38633893 }
38643894 }
38653895
3896 trackOpenFile(t, fd, @returnAddress());
38663897 return .{ .handle = fd };
38673898}
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
38693925fn dirOpenFileWindows(
38703926 userdata: ?*anyopaque,
38713927 dir: Dir,
......@@ -3873,14 +3929,14 @@ fn dirOpenFileWindows(
38733929 flags: File.OpenFlags,
38743930) File.OpenError!File {
38753931 const t: *Threaded = @ptrCast(@alignCast(userdata));
3876 _ = t;
38773932 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
38783933 const sub_path_w = sub_path_w_array.span();
38793934 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);
38813936}
38823937
38833938pub fn dirOpenFileWtf16(
3939 t: *Threaded,
38843940 dir_handle: ?windows.HANDLE,
38853941 sub_path_w: [:0]const u16,
38863942 flags: File.OpenFlags,
......@@ -4022,6 +4078,7 @@ pub fn dirOpenFileWtf16(
40224078 .ACCESS_VIOLATION => |err| return syscall.ntstatusBug(err), // bad io_status_block pointer
40234079 else => |status| return syscall.unexpectedNtstatus(status),
40244080 };
4081 trackOpenFile(t, handle, @returnAddress());
40254082 return .{ .handle = handle };
40264083}
40274084
......@@ -4114,6 +4171,7 @@ fn dirOpenFileWasi(
41144171 if (is_dir) return error.IsDir;
41154172 }
41164173
4174 trackOpenFile(t, fd, @returnAddress());
41174175 return .{ .handle = fd };
41184176}
41194177
......@@ -7856,8 +7914,10 @@ fn dirHardLink(
78567914
78577915fn fileClose(userdata: ?*anyopaque, files: []const File) void {
78587916 const t: *Threaded = @ptrCast(@alignCast(userdata));
7859 _ = t;
7860 for (files) |file| posix.close(file.handle);
7917 for (files) |file| {
7918 trackCloseFile(t, file.handle);
7919 posix.close(file.handle);
7920 }
78617921}
78627922
78637923const fileReadStreaming = switch (native_os) {