Skip to content

Fix std.os.windows.GetFinalPathNameByHandle for os versions before win10_rs4 #7379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

43 changes: 43 additions & 0 deletions lib/std/mem.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2402,3 +2402,46 @@ test "freeing empty string with null-terminated sentinel" {
const empty_string = try dupeZ(testing.allocator, u8, "");
testing.allocator.free(empty_string);
}

/// Returns a slice with the given new alignment,
/// all other pointer attributes copied from `AttributeSource`.
fn AlignedSlice(comptime AttributeSource: type, comptime new_alignment: u29) type {
const info = @typeInfo(AttributeSource).Pointer;
return @Type(.{
.Pointer = .{
.size = .Slice,
.is_const = info.is_const,
.is_volatile = info.is_volatile,
.is_allowzero = info.is_allowzero,
.alignment = new_alignment,
.child = info.child,
.sentinel = null,
},
});
}

/// Returns the largest slice in the given bytes that conforms to the new alignment,
/// or `null` if the given bytes contain no conforming address.
pub fn alignInBytes(bytes: []u8, comptime new_alignment: usize) ?[]align(new_alignment) u8 {
const begin_address = @ptrToInt(bytes.ptr);
const end_address = begin_address + bytes.len;

const begin_address_aligned = mem.alignForward(begin_address, new_alignment);
const new_length = std.math.sub(usize, end_address, begin_address_aligned) catch |e| switch (e) {
error.Overflow => return null,
};
const alignment_offset = begin_address_aligned - begin_address;
return @alignCast(new_alignment, bytes[alignment_offset .. alignment_offset + new_length]);
}

/// Returns the largest sub-slice within the given slice that conforms to the new alignment,
/// or `null` if the given slice contains no conforming address.
pub fn alignInSlice(slice: anytype, comptime new_alignment: usize) ?AlignedSlice(@TypeOf(slice), new_alignment) {
const bytes = sliceAsBytes(slice);
const aligned_bytes = alignInBytes(bytes, new_alignment) orelse return null;

const Element = @TypeOf(slice[0]);
const slice_length_bytes = aligned_bytes.len - (aligned_bytes.len % @sizeOf(Element));
const aligned_slice = bytesAsSlice(Element, aligned_bytes[0..slice_length_bytes]);
return @alignCast(new_alignment, aligned_slice);
}
132 changes: 101 additions & 31 deletions lib/std/os/windows.zig
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,56 @@ pub fn SetFilePointerEx_CURRENT_get(handle: HANDLE) SetFilePointerError!u64 {
return @bitCast(u64, result);
}

pub fn QueryObjectName(
handle: HANDLE,
out_buffer: []u16,
) ![]u16 {
const out_buffer_aligned = mem.alignInSlice(out_buffer, @alignOf(OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong;

const info = @ptrCast(*OBJECT_NAME_INFORMATION, out_buffer_aligned);
//buffer size is specified in bytes
const out_buffer_len = std.math.cast(ULONG, out_buffer_aligned.len * 2) catch |e| switch (e) {
error.Overflow => std.math.maxInt(ULONG),
};
//last argument would return the length required for full_buffer, not exposed here
const rc = ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null);
switch (rc) {
.SUCCESS => {
// info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0)
// if the object was "unnamed", not sure if this can happen for file handles
if (info.Name.MaximumLength == 0) return error.Unexpected;
// resulting string length is specified in bytes
const path_length_unterminated = @divExact(info.Name.Length, 2);
return info.Name.Buffer[0..path_length_unterminated];
},
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_HANDLE => return error.InvalidHandle,
// triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH),
// or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL)
.INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => return error.NameTooLong,
else => |e| return unexpectedStatus(e),
}
}
test "QueryObjectName" {
if (comptime builtin.os.tag != .windows)
return;

//any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const handle = tmp.dir.fd;
var out_buffer: [PATH_MAX_WIDE]u16 = undefined;

var result_path = try QueryObjectName(handle, &out_buffer);
const required_len_in_u16 = result_path.len + @divExact(@ptrToInt(result_path.ptr) - @ptrToInt(&out_buffer), 2) + 1;
//insufficient size
std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1]));
//exactly-sufficient size
_ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]);
}

pub const GetFinalPathNameByHandleError = error{
AccessDenied,
BadPathName,
FileNotFound,
NameTooLong,
Expand Down Expand Up @@ -980,32 +1029,31 @@ pub fn GetFinalPathNameByHandle(
fmt: GetFinalPathNameByHandleFormat,
out_buffer: []u16,
) GetFinalPathNameByHandleError![]u16 {
// Get normalized path; doesn't include volume name though.
var path_buffer: [@sizeOf(FILE_NAME_INFORMATION) + PATH_MAX_WIDE * 2]u8 align(@alignOf(FILE_NAME_INFORMATION)) = undefined;
try QueryInformationFile(hFile, .FileNormalizedNameInformation, path_buffer[0..]);

// Get NT volume name.
var volume_buffer: [@sizeOf(FILE_NAME_INFORMATION) + MAX_PATH]u8 align(@alignOf(FILE_NAME_INFORMATION)) = undefined; // MAX_PATH bytes should be enough since it's Windows-defined name
try QueryInformationFile(hFile, .FileVolumeNameInformation, volume_buffer[0..]);

const file_name = @ptrCast(*const FILE_NAME_INFORMATION, &path_buffer[0]);
const file_name_u16 = @ptrCast([*]const u16, &file_name.FileName[0])[0 .. file_name.FileNameLength / 2];

const volume_name = @ptrCast(*const FILE_NAME_INFORMATION, &volume_buffer[0]);
const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) {
// we assume InvalidHandle is close enough to FileNotFound in semantics
// to not further complicate the error set
error.InvalidHandle => return error.FileNotFound,
else => |e| return e,
};

switch (fmt.volume_name) {
.Nt => {
// Nothing to do, we simply copy the bytes to the user-provided buffer.
const volume_name_u16 = @ptrCast([*]const u16, &volume_name.FileName[0])[0 .. volume_name.FileNameLength / 2];
// the returned path is already in .Nt format
return final_path;
},
.Dos => {
// parse the string to separate volume path from file path
const expected_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");

if (out_buffer.len < volume_name_u16.len + file_name_u16.len) return error.NameTooLong;
// TODO find out if a path can start with something besides `\Device\<volume name>`,
// and if we need to handle it differently
// (i.e. how to determine the start and end of the volume name in that case)
if (!mem.eql(u16, expected_prefix, final_path[0..expected_prefix.len])) return error.Unexpected;

std.mem.copy(u16, out_buffer[0..], volume_name_u16);
std.mem.copy(u16, out_buffer[volume_name_u16.len..], file_name_u16);
const file_path_begin_index = mem.indexOfPos(u16, final_path, expected_prefix.len, &[_]u16{'\\'}) orelse unreachable;
const volume_name_u16 = final_path[0..file_path_begin_index];
const file_name_u16 = final_path[file_path_begin_index..];

return out_buffer[0 .. volume_name_u16.len + file_name_u16.len];
},
.Dos => {
// Get DOS volume name. DOS volume names are actually symbolic link objects to the
// actual NT volume. For example:
// (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
Expand Down Expand Up @@ -1038,10 +1086,10 @@ pub fn GetFinalPathNameByHandle(

var input_struct = @ptrCast(*MOUNTMGR_MOUNT_POINT, &input_buf[0]);
input_struct.DeviceNameOffset = @sizeOf(MOUNTMGR_MOUNT_POINT);
input_struct.DeviceNameLength = @intCast(USHORT, volume_name.FileNameLength);
@memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..], @ptrCast([*]const u8, &volume_name.FileName[0]), volume_name.FileNameLength);
input_struct.DeviceNameLength = @intCast(USHORT, volume_name_u16.len * 2);
@memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..], @ptrCast([*]const u8, volume_name_u16.ptr), volume_name_u16.len * 2);

DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_QUERY_POINTS, input_buf[0..], output_buf[0..]) catch |err| switch (err) {
DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_QUERY_POINTS, &input_buf, &output_buf) catch |err| switch (err) {
error.AccessDenied => unreachable,
else => |e| return e,
};
Expand All @@ -1061,22 +1109,20 @@ pub fn GetFinalPathNameByHandle(

// Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
// with traditional DOS drive letters, so pick the first one available.
const prefix_u8 = "\\DosDevices\\";
var prefix_buf_u16: [prefix_u8.len]u16 = undefined;
const prefix_len_u16 = std.unicode.utf8ToUtf16Le(prefix_buf_u16[0..], prefix_u8[0..]) catch unreachable;
const prefix = prefix_buf_u16[0..prefix_len_u16];
var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\");
const prefix = prefix_buf[0..prefix_buf.len];

if (std.mem.startsWith(u16, symlink, prefix)) {
if (mem.startsWith(u16, symlink, prefix)) {
const drive_letter = symlink[prefix.len..];

if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;

std.mem.copy(u16, out_buffer[0..], drive_letter);
std.mem.copy(u16, out_buffer[drive_letter.len..], file_name_u16);
mem.copy(u16, out_buffer, drive_letter);
mem.copy(u16, out_buffer[drive_letter.len..], file_name_u16);
const total_len = drive_letter.len + file_name_u16.len;

// Validate that DOS does not contain any spurious nul bytes.
if (std.mem.indexOfScalar(u16, out_buffer[0..total_len], 0)) |_| {
if (mem.indexOfScalar(u16, out_buffer[0..total_len], 0)) |_| {
return error.BadPathName;
}

Expand All @@ -1091,6 +1137,30 @@ pub fn GetFinalPathNameByHandle(
}
}

test "GetFinalPathNameByHandle" {
if (comptime builtin.os.tag != .windows)
return;

//any file will do
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const handle = tmp.dir.fd;
var buffer: [PATH_MAX_WIDE]u16 = undefined;

//check with sufficient size
const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer);
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer);

const required_len_in_u16 = nt_path.len + @divExact(@ptrToInt(nt_path.ptr) - @ptrToInt(&buffer), 2) + 1;
//check with insufficient size
std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1]));
std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1]));

//check with exactly-sufficient size
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]);
_ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]);
}

pub const QueryInformationFileError = error{Unexpected};

pub fn QueryInformationFile(
Expand Down
15 changes: 15 additions & 0 deletions lib/std/os/windows/bits.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1606,3 +1606,18 @@ pub const IOCTL_MOUNTMGR_QUERY_POINTS: ULONG = 0x6d0008;
pub const SD_RECEIVE = 0;
pub const SD_SEND = 1;
pub const SD_BOTH = 2;

pub const OBJECT_INFORMATION_CLASS = extern enum {
ObjectBasicInformation = 0,
ObjectNameInformation = 1,
ObjectTypeInformation = 2,
ObjectTypesInformation = 3,
ObjectHandleFlagInformation = 4,
ObjectSessionInformation = 5,
MaxObjectInfoClass,
};

pub const OBJECT_NAME_INFORMATION = extern struct {
Name: UNICODE_STRING,
};
pub const POBJECT_NAME_INFORMATION = *OBJECT_NAME_INFORMATION;
8 changes: 8 additions & 0 deletions lib/std/os/windows/ntdll.zig
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,11 @@ pub extern "NtDll" fn NtWaitForKeyedEvent(
) callconv(WINAPI) NTSTATUS;

pub extern "NtDll" fn RtlSetCurrentDirectory_U(PathName: *UNICODE_STRING) callconv(WINAPI) NTSTATUS;

pub extern "NtDll" fn NtQueryObject(
Handle: HANDLE,
ObjectInformationClass: OBJECT_INFORMATION_CLASS,
ObjectInformation: PVOID,
ObjectInformationLength: ULONG,
ReturnLength: ?*ULONG,
) callconv(WINAPI) NTSTATUS;
29 changes: 26 additions & 3 deletions lib/std/target.zig
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,42 @@ pub const Target = struct {
win7 = 0x06010000,
win8 = 0x06020000,
win8_1 = 0x06030000,
win10 = 0x0A000000,
win10 = 0x0A000000, //aka win10_th1
win10_th2 = 0x0A000001,
win10_rs1 = 0x0A000002,
win10_rs2 = 0x0A000003,
win10_rs3 = 0x0A000004,
win10_rs4 = 0x0A000005,
win10_rs5 = 0x0A000006,
win10_19h1 = 0x0A000007,
win10_20h1 = 0x0A000008,
win10_vb = 0x0A000008, //aka win10_19h2
win10_mn = 0x0A000009, //aka win10_20h1
win10_fe = 0x0A00000A, //aka win10_20h2
_,

/// Latest Windows version that the Zig Standard Library is aware of
pub const latest = WindowsVersion.win10_20h1;
pub const latest = WindowsVersion.win10_fe;

/// Compared against build numbers reported by the runtime to distinguish win10 versions,
/// where 0x0A000000 + index corresponds to the WindowsVersion u32 value.
pub const known_win10_build_numbers = [_]u32{
10240, //win10 aka win10_th1
10586, //win10_th2
14393, //win10_rs1
15063, //win10_rs2
16299, //win10_rs3
17134, //win10_rs4
17763, //win10_rs5
18362, //win10_19h1
18363, //win10_vb aka win10_19h2
19041, //win10_mn aka win10_20h1
19042, //win10_fe aka win10_20h2
};

/// Returns whether the first version `self` is newer (greater) than or equal to the second version `ver`.
pub fn isAtLeast(self: WindowsVersion, ver: WindowsVersion) bool {
return @enumToInt(self) >= @enumToInt(ver);
}

pub const Range = struct {
min: WindowsVersion,
Expand Down
42 changes: 4 additions & 38 deletions lib/std/zig/system.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const process = std.process;
const Target = std.Target;
const CrossTarget = std.zig.CrossTarget;
const macos = @import("system/macos.zig");
pub const windows = @import("system/windows.zig");

const is_windows = Target.current.os.tag == .windows;

Expand Down Expand Up @@ -223,44 +224,9 @@ pub const NativeTargetInfo = struct {
}
},
.windows => {
var version_info: std.os.windows.RTL_OSVERSIONINFOW = undefined;
version_info.dwOSVersionInfoSize = @sizeOf(@TypeOf(version_info));

switch (std.os.windows.ntdll.RtlGetVersion(&version_info)) {
.SUCCESS => {},
else => unreachable,
}

// Starting from the system infos build a NTDDI-like version
// constant whose format is:
// B0 B1 B2 B3
// `---` `` ``--> Sub-version (Starting from Windows 10 onwards)
// \ `--> Service pack (Always zero in the constants defined)
// `--> OS version (Major & minor)
const os_ver: u16 =
@intCast(u16, version_info.dwMajorVersion & 0xff) << 8 |
@intCast(u16, version_info.dwMinorVersion & 0xff);
const sp_ver: u8 = 0;
const sub_ver: u8 = if (os_ver >= 0x0A00) subver: {
// There's no other way to obtain this info beside
// checking the build number against a known set of
// values
const known_build_numbers = [_]u32{
10240, 10586, 14393, 15063, 16299, 17134, 17763,
18362, 19041,
};
var last_idx: usize = 0;
for (known_build_numbers) |build, i| {
if (version_info.dwBuildNumber >= build)
last_idx = i;
}
break :subver @truncate(u8, last_idx);
} else 0;

const version: u32 = @as(u32, os_ver) << 16 | @as(u32, sp_ver) << 8 | sub_ver;

os.version_range.windows.max = @intToEnum(Target.Os.WindowsVersion, version);
os.version_range.windows.min = @intToEnum(Target.Os.WindowsVersion, version);
const detected_version = windows.detectRuntimeVersion();
os.version_range.windows.min = detected_version;
os.version_range.windows.max = detected_version;
},
.macos => {
var scbuf: [32]u8 = undefined;
Expand Down
Loading