From aad9870a284410b9b7fa4d5c5d84f4a4d697c71b Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Sun, 19 Jul 2026 14:26:35 -0700 Subject: [PATCH] Io.Threaded: Decouple follow_symlinks and asynchronous IO on Windows The logic around follow_symlinks and asynchronous IO was first introduced in 66bbe4ec4c11839217d6a9d65771d60d45cd6bc1, and then cemented in 390194431e7fa439d67686e5a0e9efceed2d898a with the added comment: > If we're not following symlinks, we need to ensure we don't pass in any synchronization flags such as FILE_SYNCHRONOUS_IO_NONALERT. As far as I can tell, this comment is erroneous. There is no documented incompatibility between OPEN_REPARSE_POINT and SYNCHRONOUS_IO_NONALERT, and empirically everything works fine when using SYNCHRONOUS_IO_NONALERT. In fact, in 68ed787751ed36af3db0c52031741a5c31413034 the ReadLink implementation (a main usage of OPEN_REPARSE_POINT) was specifically switched to *not* use asynchronous IO (although that was effectively reverted during the move to std.Io). This also fixes a bug with `.follow_symlinks = false` since, before this commit, the returned File would always have `.nonblocking = false` even though that was not correct when `.follow_symlinks` was false. --- lib/std/Io/Threaded.zig | 8 ++++---- lib/std/fs/test.zig | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 5b230064199c4d90df6c9e5ce7ecf155dcd46313..04384b3d68e4e8c26f610731479f4ec435c7cc44 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -5053,7 +5053,7 @@ pub fn dirOpenFileWtf16( .VALID_FLAGS, .OPEN, .{ - .IO = if (flags.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS, + .IO = .SYNCHRONOUS_NONALERT, .NON_DIRECTORY_FILE = !allow_directory, .OPEN_REPARSE_POINT = !flags.follow_symlinks, }, @@ -8136,7 +8136,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink .{ .DIRECTORY_FILE = false, .NON_DIRECTORY_FILE = false, - .IO = .ASYNCHRONOUS, + .IO = .SYNCHRONOUS_NONALERT, .OPEN_REPARSE_POINT = true, }, null, @@ -8202,7 +8202,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(windows.REPARSE_DATA_BUFFER)) = undefined; switch ((try deviceIoControl(&.{ - .file = .{ .handle = result_handle, .flags = .{ .nonblocking = true } }, + .file = .{ .handle = result_handle, .flags = .{ .nonblocking = false } }, .code = .GET_REPARSE_POINT, .out = &reparse_buf, })).u.Status) { @@ -18995,7 +18995,7 @@ fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!windows .{ .DIRECTORY_FILE = options.filter == .dir_only, .NON_DIRECTORY_FILE = options.filter == .non_directory_only, - .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS, + .IO = .SYNCHRONOUS_NONALERT, .OPEN_REPARSE_POINT = !options.follow_symlinks, }, null, diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index bf16a7c216b4f9087195c7a98160e6b765687960..fc613ccdaa313d46dab643f849149293f422a9a5 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -758,6 +758,43 @@ test "readFileAlloc" { ); } +test "file operations with follow_symlinks=false" { + const io = testing.io; + + var tmp_dir = tmpDir(.{}); + defer tmp_dir.cleanup(); + + const contents = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n"; + try tmp_dir.dir.writeFile(io, .{ + .sub_path = "test_file", + .data = contents, + }); + + // Without lock + { + var file = try tmp_dir.dir.openFile(io, "test_file", .{ .follow_symlinks = false }); + defer file.close(io); + + var file_reader = file.reader(io, &.{}); + const actual_contents = try file_reader.interface.allocRemaining(testing.allocator, .unlimited); + defer testing.allocator.free(actual_contents); + + try std.testing.expectEqualSlices(u8, contents, actual_contents); + } + + // With lock + { + var file = try tmp_dir.dir.openFile(io, "test_file", .{ .follow_symlinks = false, .lock = .exclusive }); + defer file.close(io); + + var file_reader = file.reader(io, &.{}); + const actual_contents = try file_reader.interface.allocRemaining(testing.allocator, .unlimited); + defer testing.allocator.free(actual_contents); + + try std.testing.expectEqualSlices(u8, contents, actual_contents); + } +} + test "Dir.statFile" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { -- 2.54.0