From 61656d0e0e8ec894d72232c21c5933d396ef8501 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Fri, 9 Oct 2026 19:20:27 -0700 Subject: [PATCH 1/3] Threaded.windowsCreatePipe: Set READ_ATTRIBUTES flags Without this, the child trying to read the attributes of the stdout/stderr/stdin file (NtQueryInformationFile, NtQueryVolumeInformationFile, etc) would get ACCESS_DENIED. It's not strictly necessary on the server side of the pipe but I don't see a reason not to set it there, too. --- lib/std/Io/Threaded.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f719bb02a336382a0a8843db52c4ca3fd3406ddf..69cd17326c34178162b08ac6bbf58bdb331f10bc 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -17818,6 +17818,7 @@ pub fn windowsCreatePipe(t: *Threaded, options: CreatePipeOptions) ![2]windows.H .READ_DATA = options.inbound, .WRITE_DATA = options.outbound, .WRITE_ATTRIBUTES = true, + .READ_ATTRIBUTES = true, } }, .STANDARD = .{ .SYNCHRONIZE = true }, }, @@ -17860,6 +17861,7 @@ pub fn windowsCreatePipe(t: *Threaded, options: CreatePipeOptions) ![2]windows.H .READ_DATA = options.outbound, .WRITE_DATA = options.inbound, .WRITE_ATTRIBUTES = true, + .READ_ATTRIBUTES = true, } }, .STANDARD = .{ .SYNCHRONIZE = true }, }, -- 2.54.0 From 1c6a9d75df27564387764ee178d48666156039d4 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Fri, 9 Oct 2026 22:28:11 -0700 Subject: [PATCH 2/3] Threaded.fileStatWindows: fix for character device files Tested with a program that calls `stdin().stat(io)`. With this change, the following all work and return `.kind = .character_device` whereas before they would hit error.Unexpected or panic: stdin-stat.exe stdin-stat.exe < NUL stdin-stat.exe < COM1 stdin-stat.exe < AUX --- lib/std/Io/Threaded.zig | 50 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 69cd17326c34178162b08ac6bbf58bdb331f10bc..c6bcd0a456d94d6e2e7658424e228d1a6f216398 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4110,6 +4110,30 @@ fn fileStatLinux(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { } } +fn windowsGetFileDevice(file: File) error{ Canceled, AccessDenied, Unexpected }!windows.CTL_CODE.FILE_DEVICE { + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var device_info: windows.FILE.FS_DEVICE_INFORMATION = undefined; + { + const syscall: Syscall = try .start(); + while (true) switch (windows.ntdll.NtQueryVolumeInformationFile( + file.handle, + &io_status_block, + &device_info, + @sizeOf(windows.FILE.FS_DEVICE_INFORMATION), + .Device, + )) { + .SUCCESS => break syscall.finish(), + .CANCELLED => { + try syscall.checkCancel(); + continue; + }, + .ACCESS_DENIED => return syscall.fail(error.AccessDenied), + else => |s| return syscall.unexpectedNtstatus(s), + }; + } + return device_info.DeviceType.FileDevice; +} + fn fileStatWindows(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); @@ -4134,7 +4158,31 @@ fn fileStatWindows(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { // size provided. This is treated as success because the type of variable-length information that this would be relevant for // (name, volume name, etc) we don't care about. .BUFFER_OVERFLOW => break syscall.finish(), - .INVALID_PARAMETER => |err| return syscall.ntstatusBug(err), + .INVALID_PARAMETER, // SERIAL_PORT devices like AUX, COM1, etc + .INVALID_DEVICE_REQUEST, // CONSOLE, etc devices + .INVALID_INFO_CLASS, // NULL, etc devices + => { + syscall.finish(); + const device = try windowsGetFileDevice(file); + return .{ + .inode = @intCast(@intFromPtr(file.handle)), + .size = 0, + .permissions = .default_file, + .kind = switch (device) { + // These are classified as FILE_TYPE_CHAR by GetFileType + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfiletype + // (GetFileType calls NtQueryVolumeInformationFile with FS_DEVICE_INFORMATION) + .CONSOLE, .NULL, .SERIAL_PORT => .character_device, + else => .unknown, + }, + // Match the times that NtQueryInformationFile returns for named pipes. + .atime = windows.fromSysTime(0), + .mtime = windows.fromSysTime(0), + .ctime = windows.fromSysTime(0), + .nlink = 1, + .block_size = block_size, + }; + }, .ACCESS_DENIED => return syscall.fail(error.AccessDenied), .CANCELLED => { try syscall.checkCancel(); -- 2.54.0 From 55d429e0ba37c0609f8c713952113078a11bb593 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Fri, 9 Oct 2026 22:29:11 -0700 Subject: [PATCH 3/3] Threaded.fileStatWindows: disambiguate between files and named pipes --- lib/std/Io/Threaded.zig | 6 ++++-- lib/std/fs/test.zig | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c6bcd0a456d94d6e2e7658424e228d1a6f216398..1e0b94c86e1e074ac4dee4ad86ddb7a095552102 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4221,8 +4221,10 @@ fn fileStatWindows(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { break :reparse_point .unknown; } else if (info.BasicInformation.FileAttributes.DIRECTORY) .directory - else - .file, + else switch (try windowsGetFileDevice(file)) { + .NAMED_PIPE => .named_pipe, + else => .file, + }, .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime), diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 75a1dfe70305f73fdc0c1e8245ad2d3fb43db5b0..a1bddd0bf6c33ffa1e071719af767a01acfb9b50 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -298,6 +298,43 @@ test "File.stat on a File that is a symlink returns Kind.sym_link" { }.impl); } +test "File.stat on a File that is a named pipe returns Kind.named_pipe" { + // Creating pipes is Io.Threaded-specific + var threaded: std.Io.Threaded = .init(std.testing.allocator, .{ + .argv0 = .empty, + .environ = .empty, + }); + defer threaded.deinit(); + const io = threaded.io(); + + var read_end: Io.File = undefined; + var write_end: Io.File = undefined; + switch (builtin.target.os.tag) { + .wasi => return error.SkipZigTest, + .windows => { + const pipe = try threaded.windowsCreatePipe(.{ + .server = .{ .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, + .client = .{ .mode = .{ .IO = .SYNCHRONOUS_NONALERT } }, + .inbound = true, + }); + read_end = .{ .handle = pipe[0], .flags = .{ .nonblocking = false } }; + write_end = .{ .handle = pipe[1], .flags = .{ .nonblocking = false } }; + }, + else => { + const pipe = try std.Io.Threaded.pipe2(.{ .CLOEXEC = true }); + read_end = .{ .handle = pipe[0], .flags = .{ .nonblocking = false } }; + write_end = .{ .handle = pipe[1], .flags = .{ .nonblocking = false } }; + }, + } + defer { + read_end.close(io); + write_end.close(io); + } + + try expectEqual(.named_pipe, (try read_end.stat(io)).kind); + try expectEqual(.named_pipe, (try write_end.stat(io)).kind); +} + test "Dir.statFile on a symlink" { const io = testing.io; -- 2.54.0