– Programming – Windows
In itself, the title of this post is a true piece of trivia, which can be verified with the built-in subst tools (among other methods).
Here is an example of creating a drive +:\ as an alias for a directory C:\foo,
subst +: C:\foo
+:\ Then the drive works normally (at least in cmd.exe, this will be discussed later):
> cd /D +:\
+:\> tree .
Folder PATH listing
Volume serial number is 00000001 12AB:23BC
+:\
└───bar
However, understanding Why It’s true that it explains a lot about how Windows works under the hood, and brings up some curious behaviors.
What is a drive letter anyway?🔗
The paths most people are familiar with are Win32 namespace paths, for example something like this C:\foo which is a drive-absolute Win32 path. However, higher-level APIs that take Win32 paths CreateFileW will eventually convert to a path C:\foo in the NT namespace path before calling a lower level API. ntdll.dll Like NtCreateFile,
This can be confirmed with NtTrace, where a call can be made CreateFileW with C:\foo eventually leads to a call NtCreateFile with \??\C:\foo,
NtCreateFile( FileHandle=0x40c07ff640 [0xb8], DesiredAccess=SYNCHRONIZE|GENERIC_READ|0x80, ObjectAttributes="\??\C:\foo", IoStatusBlock=0x40c07ff648 [0/1], AllocationSize=null, FileAttributes=0, ShareAccess=7, CreateDisposition=1, CreateOptions=0x4000, EaBuffer=null, EaLength=0 ) => 0
NtClose( Handle=0xb8 ) => 0
test code, reproduction information
createfilew.zig,
const std = @import("std");
const windows = std.os.windows;
const L = std.unicode.wtf8ToWtf16LeStringLiteral;
pub extern "kernel32" fn CreateFileW(
lpFileName: windows.LPCWSTR,
dwDesiredAccess: windows.DWORD,
dwShareMode: windows.DWORD,
lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES,
dwCreationDisposition: windows.DWORD,
dwFlagsAndAttributes: windows.DWORD,
hTemplateFile: ?windows.HANDLE,
) callconv(.winapi) windows.HANDLE;
pub fn main() !void windows.FILE_FLAG_OVERLAPPED,
null,
);
if (dir_handle == windows.INVALID_HANDLE_VALUE) return error.FailedToOpenDir;
defer windows.CloseHandle(dir_handle);
Built with:
zig build-exe createfilew.zig
To run with NtTrace:
nttrace createfilew.exe > createfilew.log
He \??\C:\foo is an NT namespace path, which is what NtCreateFile hopefully. However, to understand this path, we need to talk about the Object Manager, which is responsible for handling NT paths.
Object Manager🔗
The object manager is responsible for keeping track of named objects, which we can explore using the WinObj tool. \?? part of \??\C:\foo The path is actually a special virtual folder within the Object Manager that adds \GLOBAL?? Folder and one per-user DosDevices folders together.
for me, object C: is within \GLOBAL??And indeed it is a symbolic link \Device\HarddiskVolume4,
so, \??\C:\foo is finally resolved \Device\HarddiskVolume4\fooAnd then dealing with it is up to the actual device foo Part of the path.
However, the important thing here is \??\C:\foo it’s enough one way Referencing the device path \Device\HarddiskVolume4\fooFor example, volumes will also be made a named object using their GUID with the format Volume That’s also a symlink like this
.access_mask = windows.SYNCHRONIZE \Device\HarddiskVolume4so like a path \??\Volume windows.FILE_SHARE_WRITE \foo is effectively equivalent to \??\C:\foo,
That is to say, there is nothing inherently special about the named object. C:The object manager treats it just like any other symbolic link and resolves it accordingly.
So what Is Really a drive letter?🔗
How I see it, drive letters are essentially a convention resulting from the conversion of Win32 paths to NT paths. Specifically, it will depend on the implementation RtlDosPathNameToNtPathName_U,
In other words, since RtlDosPathNameToNtPathName_U converts C:\foo To \??\C:\fooThen an object named C: Will behave like a drive letter. To give an example of what I mean by this: In an alternate universe, RtlDosPathNameToNtPathName_U can change path FOO:\bar To \??\FOO:\bar And then FOO: Can behave like a drive letter.
So, coming back to the title, how does RtlDosPathNameToNtPathName_U behave like this +:\fooWell, exactly the same C:\foo,
> paths.exe C:\foo
path type: .DriveAbsolute
nt path: \??\C:\foo
> paths.exe +:\foo
path type: .DriveAbsolute
nt path: \??\+:\foo
test program code
paths.zig,
const std = @import("std");
const windows = std.os.windows;
pub fn main() !void {
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const args = try std.process.argsAlloc(arena);
if (args.len <= 1) return error.ExpectedArg;
const path = try std.unicode.wtf8ToWtf16LeAllocZ(arena, args[1]);
const path_type = RtlDetermineDosPathNameType_U(path);
std.debug.print("path type: windows.GENERIC_READ \n", . windows.FILE_SHARE_DELETE,
.creation = windows.FILE_OPEN,
);
const nt_path = try RtlDosPathNameToNtPathName_U(path);
std.debug.print(" nt path: {f}\n", .{std.unicode.fmtUtf16Le(nt_path.span())});
}
const RTL_PATH_TYPE = enum(c_int) {
Unknown,
UncAbsolute,
DriveAbsolute,
DriveRelative,
Rooted,
Relative,
LocalDevice,
RootLocalDevice,
};
pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
Path: [*:0]const u16,
) callconv(.winapi) RTL_PATH_TYPE;
fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !windows.PathSpace {
var out: windows.UNICODE_STRING = undefined;
const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
if (rc != windows.TRUE) return error.BadPathName;
defer windows.ntdll.RtlFreeUnicodeString(&out);
var path_space: windows.PathSpace = undefined;
const out_path = out.Buffer.?[0 .. out.Length / 2];
@memcpy(path_space.data[0..out_path.len], out_path);
path_space.len = out.Length / 2;
path_space.data[path_space.len] = 0;
return path_space;
}
Therefore, if an object with name +: is in virtual folder \??We can expect Win32 path +:\ Behaving like any other drive-absolute path, which is exactly what we see.
Some exploration of implications🔗
This section just focused on a few things that were relevant to what I was working on. I encourage others to further investigate its implications if they feel like it.
explorer.exe doesn’t play ball🔗
Drives with any drive-letter other than AZ do not appear in File Explorer, and cannot be navigated to in File Explorer.

Error while trying to navigate
+:\ in file explorer
As for the “do not appear” part, my guess is what’s happening explorer.exe going on \?? and looking for specifically named items A: Through Z:As for the “can’t navigate” part, it’s a little more mysterious, but that’s my guess, explorer.exe There’s a lot of special logic to handle paths typed in the location bar, and part of it restricts drive letters. A,Z (ie it’s short-circuited before actually trying to open the path).
Even powershell doesn’t do this
It appears that Powershell rejects non-A,Z Drive also:
PS C:\> cd +:\
cd : Cannot find drive. A drive with the name '+' does not exist.
At line:1 char:1
+ cd +:\
+ ~~~~~~
+ CategoryInfo : ObjectNotFound: (+:String) [Set-Location], DriveNotFoundException
+ FullyQualifiedErrorId : DriveNotFound,Microsoft.PowerShell.Commands.SetLocationCommand
Non-ASCII drive letters🔗
Drive letters do not have to be within the ASCII range; They can also be non-ASCII characters.
> subst €: C:\foo
> cd /D €:\
€:\> tree .
Folder PATH listing
Volume serial number is 000000DE 12AB:23BC
€:\
└───bar
Non-ASCII drive letters are also case-insensitive A,Z Are:
> subst Λ: C:\foo
> cd /D λ:\
λ:\> tree .
Folder PATH listing
Volume serial number is 000000DE 12AB:23BC
λ:\
└───bar
However, drive-letters cannot be arbitrary Unicode graphemes or even arbitrary code points; They are restricted to a single WTF-16 code unit (a u16so <= U+FFFFThe equipment we are using so far (subst.exeErrors with ) Invalid parameter If you try to use a drive letter with a code point larger than U+FFFFBut you can get through it and get through it MountPointManager directly:
code used to create 𤭢: symlink
const std = @import("std");
const windows = std.os.windows;
const L = std.unicode.wtf8ToWtf16LeStringLiteral;
const MOUNTMGR_CREATE_POINT_INPUT = extern struct {
SymbolicLinkNameOffset: windows.USHORT,
SymbolicLinkNameLength: windows.USHORT,
DeviceNameOffset: windows.USHORT,
DeviceNameLength: windows.USHORT,
};
pub fn main() !void {
const mgmt_handle = try windows.OpenFile(L("\\??\\MountPointManager"), .{
.access_mask = windows.SYNCHRONIZE | windows.GENERIC_READ | windows.GENERIC_WRITE,
.share_access = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE,
.creation = windows.FILE_OPEN,
});
defer windows.CloseHandle(mgmt_handle);
const volume_name = L("\\Device\\HarddiskVolume4");
const mount_point = L("\\DosDevices\\𤭢:");
const buf_size = @sizeOf(MOUNTMGR_CREATE_POINT_INPUT) + windows.MAX_PATH * 2 + windows.MAX_PATH * 2;
var input_buf: [buf_size]u8 align(@alignOf(MOUNTMGR_CREATE_POINT_INPUT)) = [_]u8{0} ** buf_size;
var input_struct: *MOUNTMGR_CREATE_POINT_INPUT = @ptrCast(&input_buf[0]);
input_struct.SymbolicLinkNameOffset = @sizeOf(MOUNTMGR_CREATE_POINT_INPUT);
input_struct.SymbolicLinkNameLength = mount_point.len * 2;
input_struct.DeviceNameOffset = input_struct.SymbolicLinkNameOffset + input_struct.SymbolicLinkNameLength;
input_struct.DeviceNameLength = volume_name.len * 2;
@memcpy(input_buf[input_struct.SymbolicLinkNameOffset..][0..input_struct.SymbolicLinkNameLength], @as([*]const u8, @ptrCast(mount_point)));
@memcpy(input_buf[input_struct.DeviceNameOffset..][0..input_struct.DeviceNameLength], @as([*]const u8, @ptrCast(volume_name)));
const IOCTL_MOUNTMGR_CREATE_POINT = windows.CTL_CODE(windows.MOUNTMGRCONTROLTYPE, 0, .METHOD_BUFFERED, windows.FILE_READ_ACCESS | windows.FILE_WRITE_ACCESS);
try windows.DeviceIoControl(mgmt_handle, IOCTL_MOUNTMGR_CREATE_POINT, &input_buf, null);
}
(The compiled executable must be run as administrator)
However, the presence of a symlink doesn’t solve anything on its own:
> cd /D 𤭢:\
The filename, directory name, or volume label syntax is incorrect.
This is because there is no way to get the drive-absolute Win32 path 𤭢:\ To end up as the relevant NT path. As mentioned earlier, the behavior of RtlDosPathNameToNtPathName_U That’s what matters, and we can verify that it will not convert drive-absolute paths with a drive letter larger than this U+FFFF For relevant NT paths:
C:\foo> paths.exe 𤭢:\foo
path type: .Relative
nt path: \??\C:\foo\𤭢:\foo
path classification mismatch
It is very common for path-related functions to be written without the use of system-specific APIs, which means there is a high chance of mismatch between how RtlDosPathNameToNtPathName_U Treats a file path and how something like a particular implementation path.isAbsolute Treats file paths.
As a random example, Rust only considers paths A,Z Play letters in absolute form:
use std::path::Path;
fn main() {
println!("C:\\ {}", Path::new("C:\\foo").is_absolute());
println!("+:\\ {}", Path::new("+:\\foo").is_absolute());
println!("€:\\ {}", Path::new("€:\\foo").is_absolute());
}
> rustc test.rs
> test.exe
C:\ true
+:\ false
€:\ false
Whether or not this problem is worth fixing is left as an exercise for the reader (I don’t actually know if this is a problem or not), but there is a second problem (hinted at earlier) associated with text encoding that could create something like this. isAbsolute Implementations return different results for the same path. This wrinkle is the reason I noticed this whole thing in the first place, when I was recently doing some work on zig path functions I realized that path[0], path[1]And path[2] like for a pattern C:\ Will look at different parts of the path depending on the encoding. i.e. for something like this €:\ (which is made up of code points ,
- where encoded as WTF-16
U+20ACcan be encoded as singleu16code unit0x20ACit would meanpath[0]will be0x20AC,path[1]will be0x3A,:), Andpath[2]will be0x5C,\), which looks like a drive-absolute path - where encoded as WTF-8
U+20ACis encoded as threeu8code units (0xE2 0x82 0xAC), this would meanpath[0]will be0xE2,path[1]will be0x82Andpath[2]will be0xACWhich means it won’t look anything like the drive-absolute path
So, to write an implementation that treats paths the same regardless of encoding, Some? Decision has to be taken:
- If with strict compatibility
RtlDetermineDosPathNameType_U,RtlDosPathNameToNtPathName_Uis desired, decode the first code point and check<= 0xFFFFWhen working with WTF-8 (this is the option I chose for the Zig standard library, but I’m not very happy with it) - If you want you can always check
path[0],path[1],path[2]And don’t care about non-ASCII drive letters, checkpath[0] <= 0x7Fregardless of encoding - If you don’t care about anything other than the standard
A,ZDrive the characters, then check it explicitly (this is what Rust does)
That’s not Euro Drive
What I find strange about this whole thing is that kernel32.dll API SetVolumeMountPointW It has its own unique quirk when dealing with non-ASCII drive letters. Specifically, this code (attempting to create the drive €:\) will succeed:
const std = @import("std");
const windows = std.os.windows;
const L = std.unicode.wtf8ToWtf16LeStringLiteral;
extern "kernel32" fn SetVolumeMountPointW(
VolumeMountPoint: windows.LPCWSTR,
VolumeName: windows.LPCWSTR,
) callconv(.winapi) windows.BOOL;
pub fn main() !void {
const volume_name = L("\\\\?\\Volume{18123456-abcd-efab-cdef-1234abcdabcd}\\");
const mount_point = L("€:\\");
if (SetVolumeMountPointW(mount_point, volume_name) == 0) {
const err = windows.GetLastError();
std.debug.print("{any}\n", .{err});
return error.Failed;
}
}
However, when we look at the Object Manager, €: The symlink will not exist… but ¬: Desire:
My time spent dealing extensively with Windows quirks helped me identify what’s going on here: 0x20AC it may have been shortened 0xAC By SetVolumeMountPointWAnd U+00AC happens to be ¬If that’s really what’s happening, then shortening the drive letter instead of disavowing the path seems very strange, but it also makes sense given that non-ASCII drive letters are a matter that no one has really thought about,
closing🔗
I don’t know if anything I’ve written here is new, although my cursory searches didn’t turn up much. The only mention of non-A,Z The drive letters I currently know about are from the article The Definitive Guide on Win32 to NT Path Conversion which says:
It is natural to assume that drive “letters” can only range from A to Z.
RtlGetFullPathName_UThe API does not enforce this requirement, although the Explorer shell and command prompt almost certainly do. So as long as the second character of the path is a colon, the conversion will treat it as a drive absolute or drive relative path. Of course it won’t do you much good if the DosDevices object directory doesn’t have the appropriate symbolic link.
Well, it turns out that the command prompt doesn’t even enforce the requirement, and I think there’s at least a few more quirks around this quirk waiting to be discovered.
<a href