Commit e36bf2baff

Ryan Liptak <squeek502@hotmail.com>
2024-04-22 16:46:09
windows.GetFinalPathNameByHandle: Support volumes mounted as paths
A volume can be mounted as a NTFS path, e.g. as C:\Mnt\Foo. In that case, IOCTL_MOUNTMGR_QUERY_POINTS gives us a mount point with a symlink value something like `\??\Volume{383da0b0-717f-41b6-8c36-00500992b58d}`. In order to get the `C:\Mnt\Foo` path, we can query the mountmgr again using IOCTL_MOUNTMGR_QUERY_DOS_VOLUME_PATH. Fixes #19731
1 parent 4babedf
Changed files (1)
lib
lib/std/os/windows.zig
@@ -1368,6 +1368,61 @@ pub fn GetFinalPathNameByHandle(
                         return error.BadPathName;
                     }
 
+                    return out_buffer[0..total_len];
+                } else if (mountmgrIsVolumeName(symlink)) {
+                    // If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d},
+                    // then it is a volume mounted as a path rather than a drive letter. We need to
+                    // query the mount manager again to get the DOS path for the volume.
+
+                    // 49 is the maximum length accepted by mountmgrIsVolumeName
+                    const vol_input_size = @sizeOf(MOUNTMGR_TARGET_NAME) + (49 * 2);
+                    var vol_input_buf: [vol_input_size]u8 align(@alignOf(MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size;
+                    // Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path,
+                    // and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>).
+                    // However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here.
+                    const min_output_size = @sizeOf(MOUNTMGR_VOLUME_PATHS) + (PATH_MAX_WIDE * 2);
+                    var vol_output_buf: [min_output_size]u8 align(@alignOf(MOUNTMGR_VOLUME_PATHS)) = undefined;
+
+                    var vol_input_struct: *MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]);
+                    vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2);
+                    @memcpy(@as([*]WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink);
+
+                    DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_QUERY_DOS_VOLUME_PATH, &vol_input_buf, &vol_output_buf) catch |err| switch (err) {
+                        error.AccessDenied => return error.Unexpected,
+                        else => |e| return e,
+                    };
+                    const volume_paths_struct: *const MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]);
+                    const volume_path = std.mem.sliceTo(@as(
+                        [*]const u16,
+                        &volume_paths_struct.MultiSz,
+                    )[0 .. volume_paths_struct.MultiSzLength / 2], 0);
+
+                    if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong;
+
+                    // `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where
+                    // we want to place the filename before returning. Here are the possible overlapping cases:
+                    //
+                    // out_buffer:       [filename]
+                    //       dest: [___(a)___] [___(b)___]
+                    //
+                    // In the case of (a), we need to copy forwards, and in the case of (b) we need
+                    // to copy backwards. We also need to do this before copying the volume path because
+                    // it could overwrite the file_name_u16 memory.
+                    const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len];
+                    const file_name_byte_offset = @intFromPtr(file_name_u16.ptr) - @intFromPtr(out_buffer.ptr);
+                    const file_name_index = file_name_byte_offset / @sizeOf(u16);
+                    if (volume_path.len > file_name_index)
+                        mem.copyBackwards(u16, file_name_dest, file_name_u16)
+                    else
+                        mem.copyForwards(u16, file_name_dest, file_name_u16);
+                    @memcpy(out_buffer[0..volume_path.len], volume_path);
+                    const total_len = volume_path.len + file_name_u16.len;
+
+                    // Validate that DOS does not contain any spurious nul bytes.
+                    if (mem.indexOfScalar(u16, out_buffer[0..total_len], 0)) |_| {
+                        return error.BadPathName;
+                    }
+
                     return out_buffer[0..total_len];
                 }
             }
@@ -1379,6 +1434,32 @@ pub fn GetFinalPathNameByHandle(
     }
 }
 
+/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h
+fn mountmgrIsVolumeName(name: []const u16) bool {
+    return (name.len == 48 or (name.len == 49 and name[48] == mem.nativeToLittle(u16, '\\'))) and
+        name[0] == mem.nativeToLittle(u16, '\\') and
+        (name[1] == mem.nativeToLittle(u16, '?') or name[1] == mem.nativeToLittle(u16, '\\')) and
+        name[2] == mem.nativeToLittle(u16, '?') and
+        name[3] == mem.nativeToLittle(u16, '\\') and
+        mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and
+        name[19] == mem.nativeToLittle(u16, '-') and
+        name[24] == mem.nativeToLittle(u16, '-') and
+        name[29] == mem.nativeToLittle(u16, '-') and
+        name[34] == mem.nativeToLittle(u16, '-') and
+        name[47] == mem.nativeToLittle(u16, '}');
+}
+
+test mountmgrIsVolumeName {
+    const L = std.unicode.utf8ToUtf16LeStringLiteral;
+    try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
+    try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
+    try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
+    try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
+    try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
+    try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo")));
+    try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}")));
+}
+
 test GetFinalPathNameByHandle {
     if (builtin.os.tag != .windows)
         return;
@@ -4845,6 +4926,8 @@ pub const SYMLINK_FLAG_RELATIVE: ULONG = 0x1;
 pub const SYMBOLIC_LINK_FLAG_DIRECTORY: DWORD = 0x1;
 pub const SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: DWORD = 0x2;
 
+pub const MOUNTMGRCONTROLTYPE = 0x0000006D;
+
 pub const MOUNTMGR_MOUNT_POINT = extern struct {
     SymbolicLinkNameOffset: ULONG,
     SymbolicLinkNameLength: USHORT,
@@ -4861,7 +4944,17 @@ pub const MOUNTMGR_MOUNT_POINTS = extern struct {
     NumberOfMountPoints: ULONG,
     MountPoints: [1]MOUNTMGR_MOUNT_POINT,
 };
-pub const IOCTL_MOUNTMGR_QUERY_POINTS: ULONG = 0x6d0008;
+pub const IOCTL_MOUNTMGR_QUERY_POINTS = CTL_CODE(MOUNTMGRCONTROLTYPE, 2, .METHOD_BUFFERED, FILE_ANY_ACCESS);
+
+pub const MOUNTMGR_TARGET_NAME = extern struct {
+    DeviceNameLength: USHORT,
+    DeviceName: [1]WCHAR,
+};
+pub const MOUNTMGR_VOLUME_PATHS = extern struct {
+    MultiSzLength: ULONG,
+    MultiSz: [1]WCHAR,
+};
+pub const IOCTL_MOUNTMGR_QUERY_DOS_VOLUME_PATH = CTL_CODE(MOUNTMGRCONTROLTYPE, 12, .METHOD_BUFFERED, FILE_ANY_ACCESS);
 
 pub const OBJECT_INFORMATION_CLASS = enum(c_int) {
     ObjectBasicInformation = 0,