Skip to content

Commit 1bf7486

Browse files
committed
Redo GetFinalPathNameByHandle using DeviceIoControl
This commit reimagines `std.os.windows.GetFinalPathNameByHandle` using `DeviceIoControl` to query the OS mount manager for the DOS (symlink) paths for the given NT volume name. In particular, it uses `IOCTL_MOUNTMGR_QUERY_POINTS` ioctl opcode to query the manager for the available moount points.
1 parent e8abfef commit 1bf7486

File tree

4 files changed

+141
-176
lines changed

4 files changed

+141
-176
lines changed

lib/std/os.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4094,7 +4094,7 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat
40944094
defer w.CloseHandle(h_file);
40954095

40964096
var wide_buf: [w.PATH_MAX_WIDE]u16 = undefined;
4097-
const wide_slice = try w.GetFinalPathNameByHandle(h_file, wide_buf[0..]);
4097+
const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, wide_buf[0..]);
40984098

40994099
// Trust that Windows gives us valid UTF-16LE.
41004100
const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable;

lib/std/os/windows.zig

Lines changed: 125 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -902,135 +902,142 @@ pub const GetFinalPathNameByHandleError = error{
902902
Unexpected,
903903
};
904904

905-
/// Returns canonical (normalized) path of handle. The output path assumes
906-
/// Win32 namespace, however, '\\?\' prefix is *not* prepended to the result.
907-
/// TODO support other namespaces/volume names.
908-
pub fn GetFinalPathNameByHandle(hFile: HANDLE, out_buffer: []u16) GetFinalPathNameByHandleError![]u16 {
909-
// The implementation is based on implementation found in Wine sources, however,
910-
// we make the following tweaks. First of all, if `NtQueryInformationFile` supports
911-
// `FILE_INFORMATION_CLASS.FileNormalizedNameInformation` and `FileVolumeNameInformation`,
912-
// we use those two calls to generate a valid Win32/DOS path. Otherwise, we fallback to
913-
// more widely supported `NtQueryObject` generic routine.
914-
// Wine source: https://source.winehq.com/git/wine.git/blob/HEAD:/dlls/kernelbase/file.c#l1708
915-
var buffer: [PATH_MAX_WIDE]u16 = undefined;
916-
const object_path = blk: {
917-
var path_buffer: [PATH_MAX_WIDE * 2]u8 = undefined;
918-
var io: IO_STATUS_BLOCK = undefined;
919-
var rc = ntdll.NtQueryInformationFile(hFile, &io, &path_buffer, path_buffer.len, FILE_INFORMATION_CLASS.FileNormalizedNameInformation);
920-
switch (rc) {
921-
.SUCCESS => {
922-
var nt_volume_buffer: [MAX_PATH]u8 = undefined;
923-
rc = ntdll.NtQueryInformationFile(hFile, &io, &nt_volume_buffer, nt_volume_buffer.len, FILE_INFORMATION_CLASS.FileVolumeNameInformation);
924-
switch (rc) {
925-
.SUCCESS => {
926-
const file_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &path_buffer[0]));
927-
const file_name_u16 = @ptrCast([*]const u16, &file_name.FileName[0])[0..file_name.FileNameLength / 2];
928-
929-
if (file_name_u16.len > PATH_MAX_WIDE) return error.NameTooLong;
930-
931-
const volume_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &nt_volume_buffer[0]));
932-
const volume_name_u16 = @ptrCast([*]const u16, &volume_name.FileName[0])[0..volume_name.FileNameLength / 2];
933-
934-
std.mem.copy(u16, buffer[0..], volume_name_u16);
935-
std.mem.copy(u16, buffer[volume_name_u16.len..], file_name_u16);
936-
937-
break :blk buffer[0..volume_name_u16.len + file_name_u16.len];
938-
},
939-
.INVALID_PARAMETER => {}, // fall through
940-
else => return unexpectedStatus(rc),
941-
}
942-
},
943-
.INVALID_PARAMETER => {}, // fall through
944-
else => return unexpectedStatus(rc),
945-
}
905+
/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`.
906+
/// Defaults to DOS volume names.
907+
pub const GetFinalPathNameByHandleFormat = struct {
908+
volume_name: enum {
909+
/// Format as DOS volume name
910+
Dos,
911+
/// Format as NT volume name
912+
Nt,
913+
} = .Dos,
914+
};
946915

947-
var dummy: ULONG = undefined;
948-
rc = ntdll.NtQueryObject(hFile, OBJECT_INFORMATION_CLASS.ObjectNameInformation, &path_buffer, path_buffer.len, &dummy);
949-
switch (rc) {
950-
.SUCCESS => {},
951-
else => return unexpectedStatus(rc),
952-
}
916+
/// Returns canonical (normalized) path of handle.
917+
/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include
918+
/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`).
919+
/// If DOS volume name format is selected, note that this function does *not* prepend
920+
/// `\\?\` prefix to the resultant path.
921+
pub fn GetFinalPathNameByHandle(
922+
hFile: HANDLE,
923+
fmt: GetFinalPathNameByHandleFormat,
924+
out_buffer: []u16,
925+
) GetFinalPathNameByHandleError![]u16 {
926+
// Get normalized path; doesn't include volume name though.
927+
var path_buffer: [@sizeOf(FILE_NAME_INFORMATION) + PATH_MAX_WIDE * 2 + 2]u8 = undefined;
928+
var io: IO_STATUS_BLOCK = undefined;
929+
var rc = ntdll.NtQueryInformationFile(hFile, &io, &path_buffer, path_buffer.len, FILE_INFORMATION_CLASS.FileNormalizedNameInformation);
930+
switch (rc) {
931+
.SUCCESS => {},
932+
.INVALID_PARAMETER => unreachable,
933+
else => return unexpectedStatus(rc),
934+
}
953935

954-
const object_name = @ptrCast(*const OBJECT_NAME_INFORMATION, @alignCast(@alignOf(OBJECT_NAME_INFORMATION), &buffer));
936+
// Get NT volume name.
937+
var volume_buffer: [MAX_PATH]u8 = undefined; // MAX_PATH bytes should be enough since it's Windows-defined name
938+
rc = ntdll.NtQueryInformationFile(hFile, &io, &volume_buffer, volume_buffer.len, FILE_INFORMATION_CLASS.FileVolumeNameInformation);
939+
switch (rc) {
940+
.SUCCESS => {},
941+
.INVALID_PARAMETER => unreachable,
942+
else => return unexpectedStatus(rc),
943+
}
955944

956-
// Apparently, `NtQueryObject` has a bug when signalling an error condition.
957-
// To check for error, i.e., FileNotFound, we check if the result Buffer is non null
958-
// and Length is greater than zero.
959-
// Source: https://stackoverflow.com/questions/65170/how-to-get-name-associated-with-open-handle
960-
if (object_name.Name.Length == 0) return error.FileNotFound;
945+
const file_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &path_buffer[0]));
946+
const file_name_u16 = @ptrCast([*]const u16, &file_name.FileName[0])[0 .. file_name.FileNameLength / 2];
961947

962-
break :blk @as([*]const u16, object_name.Name.Buffer)[0..object_name.Name.Length / 2];
963-
};
948+
const volume_name = @ptrCast(*const FILE_NAME_INFORMATION, @alignCast(@alignOf(FILE_NAME_INFORMATION), &volume_buffer[0]));
964949

965-
// By now, we got a a fully-qualified NT path, which we need to translate
966-
// into a Win32/DOS path, for instance:
967-
// \Device\HarddiskVolume4\foo => C:\foo
968-
//
969-
// NOTE:
970-
// I couldn't figure out a better way of doing this unfortunately...
971-
// This snippet below is in part based around `QueryDosDeviceW` implementation
972-
// found in Wine. The trick with `NtQueryDirectoryObject` for some reason
973-
// only lists `\DosDevices\Global` as the only SymblinkObject available, which
974-
// means it's not possible to query the kernel for all available DOS volume name
975-
// symlinks.
976-
// TODO investigate!
977-
// Wine source: https://source.winehq.com/git/wine.git/blob/HEAD:/dlls/kernelbase/volume.c#l1009
978-
const dos_drive_letters = &[_]u16{ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };
979-
var query_path = [_]u16{ '\\', 'D', 'o', 's', 'D', 'e', 'v', 'i', 'c', 'e', 's', '\\', 'C', ':' };
980-
for (dos_drive_letters) |drive_letter| {
981-
const drive = &[_]u16{ drive_letter, ':' };
982-
std.mem.copy(u16, query_path[query_path.len - 2..], drive[0..]);
983-
984-
var sym_handle: HANDLE = undefined;
985-
const len_bytes = @intCast(u16, query_path.len) * 2;
986-
var nt_name = UNICODE_STRING{
987-
.Length = len_bytes,
988-
.MaximumLength = len_bytes,
989-
.Buffer = @intToPtr([*]u16, @ptrToInt(&query_path)),
990-
};
991-
var attr = OBJECT_ATTRIBUTES{
992-
.Length = @sizeOf(OBJECT_ATTRIBUTES),
993-
.RootDirectory = null,
994-
.Attributes = 0,
995-
.ObjectName = &nt_name,
996-
.SecurityDescriptor = null,
997-
.SecurityQualityOfService = null,
998-
};
999-
var rc = ntdll.NtOpenSymbolicLinkObject(&sym_handle, SYMBOLIC_LINK_QUERY, attr);
1000-
switch (rc) {
1001-
.SUCCESS => {},
1002-
.OBJECT_NAME_NOT_FOUND => continue,
1003-
else => return unexpectedStatus(rc),
1004-
}
950+
switch (fmt.volume_name) {
951+
.Nt => {
952+
// Nothing to do, we simply copy the bytes to the user-provided buffer.
953+
const volume_name_u16 = @ptrCast([*]const u16, &volume_name.FileName[0])[0 .. volume_name.FileNameLength / 2];
1005954

1006-
var link_buffer: [MAX_PATH]u8 = undefined;
1007-
var link = UNICODE_STRING{
1008-
.Length = 0,
1009-
.MaximumLength = MAX_PATH,
1010-
.Buffer = @intToPtr([*]u16, @ptrToInt(&link_buffer[0])),
1011-
};
1012-
rc = ntdll.NtQuerySymbolicLinkObject(sym_handle, &link, null);
1013-
CloseHandle(sym_handle);
1014-
switch (rc) {
1015-
.SUCCESS => {},
1016-
else => return unexpectedStatus(rc),
1017-
}
955+
if (out_buffer.len < volume_name_u16.len + file_name_u16.len) return error.NameTooLong;
956+
957+
std.mem.copy(u16, out_buffer[0..], volume_name_u16);
958+
std.mem.copy(u16, out_buffer[volume_name_u16.len..], file_name_u16);
959+
960+
return out_buffer[0 .. volume_name_u16.len + file_name_u16.len];
961+
},
962+
.Dos => {
963+
// Get DOS volume name. DOS volume names are actually symbolic link objects to the
964+
// actual NT volume. For example:
965+
// (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
966+
const MIN_SIZE = @sizeOf(MOUNTMGR_MOUNT_POINT) + MAX_PATH;
967+
// We initialize the input buffer to all zeros for convenience since
968+
// `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this.
969+
var input_buf = [_]u8{0} ** MIN_SIZE;
970+
var output_buf: [MIN_SIZE * 4]u8 = undefined;
971+
972+
// This surprising path is a filesystem path to the mount manager on Windows.
973+
// Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
974+
const mgmt_path = "\\MountPointManager";
975+
const mgmt_path_u16 = sliceToPrefixedFileW(mgmt_path) catch unreachable;
976+
const mgmt_handle = OpenFile(mgmt_path_u16.span(), .{
977+
.access_mask = SYNCHRONIZE,
978+
.share_access = FILE_SHARE_READ | FILE_SHARE_WRITE,
979+
.creation = FILE_OPEN,
980+
.io_mode = .blocking,
981+
}) catch |err| switch (err) {
982+
error.IsDir => unreachable,
983+
error.NotDir => unreachable,
984+
error.NoDevice => unreachable,
985+
error.AccessDenied => unreachable,
986+
error.PipeBusy => unreachable,
987+
error.PathAlreadyExists => unreachable,
988+
error.WouldBlock => unreachable,
989+
else => |e| return e,
990+
};
991+
defer CloseHandle(mgmt_handle);
992+
993+
var input_struct = @ptrCast(*MOUNTMGR_MOUNT_POINT, @alignCast(@alignOf(MOUNTMGR_MOUNT_POINT), &input_buf[0]));
994+
input_struct.DeviceNameOffset = @sizeOf(MOUNTMGR_MOUNT_POINT);
995+
input_struct.DeviceNameLength = @intCast(USHORT, volume_name.FileNameLength);
996+
@memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..], @ptrCast([*]const u8, &volume_name.FileName[0]), volume_name.FileNameLength);
997+
998+
try DeviceIoControl(
999+
mgmt_handle,
1000+
IOCTL_MOUNTMGR_QUERY_POINTS,
1001+
input_buf[0 .. @sizeOf(MOUNTMGR_MOUNT_POINT) + volume_name.FileNameLength],
1002+
output_buf[0..],
1003+
);
1004+
const mount_points_struct = @ptrCast(*const MOUNTMGR_MOUNT_POINTS, @alignCast(@alignOf(MOUNTMGR_MOUNT_POINTS), &output_buf[0]));
10181005

1019-
const link_path = @as([*]const u16, link.Buffer)[0..link.Length / 2];
1020-
const idx = std.mem.indexOf(u16, object_path, link_path) orelse continue;
1006+
const mount_points = @ptrCast(
1007+
[*]const MOUNTMGR_MOUNT_POINT,
1008+
@alignCast(@alignOf(MOUNTMGR_MOUNT_POINT), &mount_points_struct.MountPoints[0]),
1009+
)[0..mount_points_struct.NumberOfMountPoints];
10211010

1022-
// TODO is this the most appropriate error here?
1023-
if (out_buffer.len < drive.len + object_path.len) return error.NameTooLong;
1011+
var found: bool = false;
1012+
for (mount_points) |mount_point| {
1013+
const symlink = @ptrCast(
1014+
[*]const u16,
1015+
@alignCast(@alignOf(u16), &output_buf[mount_point.SymbolicLinkNameOffset]),
1016+
)[0 .. mount_point.SymbolicLinkNameLength / 2];
10241017

1025-
std.mem.copy(u16, out_buffer[0..], drive[0..]);
1026-
std.mem.copy(u16, out_buffer[2..], object_path[link_path.len..]);
1018+
// Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
1019+
// with traditional DOS drive letters, so pick the first one available.
1020+
const prefix = &[_]u16{ '\\', 'D', 'o', 's', 'D', 'e', 'v', 'i', 'c', 'e', 's', '\\' };
10271021

1028-
return out_buffer[0..object_path.len - link_path.len + 2];
1029-
}
1022+
if (std.mem.indexOf(u16, symlink, prefix)) |idx| {
1023+
if (idx != 0) continue;
1024+
1025+
const drive_letter = std.mem.trimLeft(u16, symlink, prefix);
1026+
1027+
if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;
1028+
1029+
std.mem.copy(u16, out_buffer[0..], drive_letter);
1030+
std.mem.copy(u16, out_buffer[drive_letter.len..], file_name_u16);
10301031

1031-
// If we're here, that means there was no match so we panic!
1032-
// TODO should we actually panic here or return an error instead?
1033-
unreachable;
1032+
return out_buffer[0 .. drive_letter.len + file_name_u16.len];
1033+
}
1034+
}
1035+
1036+
// If we've ended up here, then something went wrong/is corrupted in the OS,
1037+
// so error out!
1038+
return error.FileNotFound;
1039+
},
1040+
}
10341041
}
10351042

10361043
pub const GetFileSizeError = error{Unexpected};

lib/std/os/windows/bits.zig

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,28 +1574,20 @@ pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x1;
15741574
pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1;
15751575
pub const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2;
15761576

1577-
pub const OBJECT_INFORMATION_CLASS = extern enum {
1578-
ObjectBasicInformation,
1579-
ObjectNameInformation,
1580-
ObjectTypeInformation,
1581-
ObjectAllInformation,
1582-
ObjectDataInformation,
1577+
pub const MOUNTMGR_MOUNT_POINT = extern struct {
1578+
SymbolicLinkNameOffset: ULONG,
1579+
SymbolicLinkNameLength: USHORT,
1580+
Reserved1: USHORT,
1581+
UniqueIdOffset: ULONG,
1582+
UniqueIdLength: USHORT,
1583+
Reserved2: USHORT,
1584+
DeviceNameOffset: ULONG,
1585+
DeviceNameLength: USHORT,
1586+
Reserved3: USHORT,
15831587
};
1584-
pub const OBJECT_NAME_INFORMATION = extern struct {
1585-
Name: UNICODE_STRING,
1586-
};
1587-
1588-
pub const DIRECTORY_QUERY: DWORD = 0x0001;
1589-
pub const DIRECTORY_TRAVERSE: DWORD = 0x0002;
1590-
pub const DIRECTORY_CREATE_OBJECT: DWORD = 0x0004;
1591-
pub const DIRECTORY_CREATE_SUBDIRECTORY: DWORD = 0x0008;
1592-
pub const DIRECTORY_ALL_ACCESS: DWORD = STANDARD_RIGHTS_REQUIRED | 0xF;
1593-
1594-
pub const OBJDIR_INFORMATION = extern struct {
1595-
ObjectName: UNICODE_STRING,
1596-
ObjectTypeName: UNICODE_STRING,
1597-
Data: [1]BYTE,
1588+
pub const MOUNTMGR_MOUNT_POINTS = extern struct {
1589+
Size: ULONG,
1590+
NumberOfMountPoints: ULONG,
1591+
MountPoints: [1]MOUNTMGR_MOUNT_POINT,
15981592
};
1599-
1600-
pub const SYMBOLIC_LINK_QUERY: DWORD = 0x0001;
1601-
pub const SYMBOLIC_LINK_ALL_ACCESS: DWORD = STANDARD_RIGHTS_REQUIRED | 0x1;
1593+
pub const IOCTL_MOUNTMGR_QUERY_POINTS: ULONG = 0x6d0008;

lib/std/os/windows/ntdll.zig

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -106,37 +106,3 @@ pub extern "NtDll" fn NtWaitForKeyedEvent(
106106
Alertable: BOOLEAN,
107107
Timeout: ?*LARGE_INTEGER,
108108
) callconv(.Stdcall) NTSTATUS;
109-
110-
pub extern "NtDll" fn NtQueryObject(
111-
Handle: HANDLE,
112-
ObjectInformationClass: OBJECT_INFORMATION_CLASS,
113-
ObjectInformation: *c_void,
114-
ObjectInformationLength: ULONG,
115-
ReturnLength: *ULONG,
116-
) callconv(.Stdcall) NTSTATUS;
117-
118-
pub extern "NtDll" fn NtOpenSymbolicLinkObject(
119-
pHandle: *HANDLE,
120-
DesiredAccess: DWORD,
121-
ObjectAttributes: OBJECT_ATTRIBUTES,
122-
) callconv(.Stdcall) NTSTATUS;
123-
pub extern "NtDll" fn NtQuerySymbolicLinkObject(
124-
SymbolicLinkHandle: HANDLE,
125-
pLinkName: *UNICODE_STRING,
126-
pDataWritten: ?*ULONG,
127-
) callconv(.Stdcall) NTSTATUS;
128-
129-
pub extern "NtDll" fn NtOpenDirectoryObject(
130-
DirectoryObjectHandle: *HANDLE,
131-
DesiredAccess: DWORD,
132-
ObjectAttributes: OBJECT_ATTRIBUTES,
133-
) callconv(.Stdcall) NTSTATUS;
134-
pub extern "NtDll" fn NtQueryDirectoryObject(
135-
DirectoryHandle: HANDLE,
136-
Buffer: ?*c_void,
137-
Length: ULONG,
138-
ReturnSingleEntry: BOOLEAN,
139-
RestartScan: BOOLEAN,
140-
Context: *ULONG,
141-
ReturnLength: *ULONG,
142-
) callconv(.Stdcall) NTSTATUS;

0 commit comments

Comments
 (0)