Skip to content

Windows: add fallback if canonicalize fails - #161951

Open
ChrisDenton wants to merge 3 commits into
rust-lang:mainfrom
ChrisDenton:canonicalize-boogaloo
Open

Windows: add fallback if canonicalize fails#161951
ChrisDenton wants to merge 3 commits into
rust-lang:mainfrom
ChrisDenton:canonicalize-boogaloo

Conversation

@ChrisDenton

@ChrisDenton ChrisDenton commented Aug 28, 2026

Copy link
Copy Markdown
Member

This attempts a partial workaround for issues such as: #59392, #79449, #59107, #54875, #52440, #52377, #48249, #74327, #55812

This may require a bit of explanation depending on how familiar you are with Windows paths, I'll try to keep it brief. The short version is that the above issues are cases where third party devices don't integrate with the system sufficiently so Windows isn't aware of the canonical drive for a particular path, causing our canonicalize function to fail. This PR works around it by manually search for the drive letter that corresponds to the root of the path. This only works in cases where there is a drive letter assigned but that is the majority of cases. It won't work when the device is only mounted to a directory in another filesystem or isn't mounted at all.

To explain the implementation of this PR you should be aware that Windows on Windows NT is more like WINE on Linux then many people realise. You have a kernel (NT) and then you have an implementation of the Win32 APIs on top (this is why kernel32.dll is nothing to do with the real kernel, it's like an implementation of Win95's kernel API on top of another OS). Admittedly the boundaries have become fuzzier over the years but there still remains a clear distinction between the Win32 API and the NT kernel API in many places.

Paths are one place where this distinction is made clear. You have the familiar Win32 paths like C:\path\to\file that date back to the time of DOS. And then you have the low-level NT kernel paths that look like \Device\HarddiskVolume6\path\to\file (which aren't really meant to be user-visible). To bridge the gap, the NT namespace has a special ?? directory containing mappings (i.e. symlinks) from drives like C: to paths like \Device\HarddiskVolume6. In that way translating between Win32 and NT paths is made simpler as you can replace C: with \??\C: and it'll get resolved to the right path and vice versa (the actual translation from win32 to NT is more complicated but I've already spent too many words on this).

So back to canonicalisation. Resolving the canonical NT path should always succeed. The problem comes when mapping that to a Win32 drive path. When the drive is managed by the system then when resolving paths it knows which drive to pick. However, if the drive mapping is added manually then it doesn't.

So the way to workaround this is to manually look at the drive mappings and see which one is the root of the NT path we have.

Get the NT path then search for a drive that links to a prefix of it.
@rustbot rustbot added O-windows Operating system: Windows S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Aug 28, 2026
@rustbot

rustbot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

r? @clarfonthey

rustbot has assigned @clarfonthey.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @ChrisDenton, libs
  • @ChrisDenton, libs expanded to 13 candidates
  • Random selection from JohnTitor, Mark-Simulacrum, clarfonthey, nia-e

@ChrisDenton

Copy link
Copy Markdown
Member Author

This will probably need a libs discussion. But it isn't urgent.

)
}

/// Fallback in case `get_path_canonical` fails.

@clarfonthey clarfonthey Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I know it's going to be a pain, but it would be worth adding some of the extra context that you shared on Zulip (+ more if you've got it) on when exactly this might fail. Knowing that it generally is a driver issue for the specific drive (whether it's physical hardware or virtual) is helpful here, and it would also be helpful to know why the normal method can fail while this would still succeed.

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now added a brief explanation

Comment thread library/std/src/sys/fs/windows.rs Outdated
|buf, sz| unsafe { c::GetFinalPathNameByHandleW(handle, buf, sz, c::VOLUME_NAME_NT) },
|nt_path| {
// SAFETY: `GetLogicalDrives` only returns information.
let drives = unsafe { c::GetLogicalDrives() };

@clarfonthey clarfonthey Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have safe externs now, how much of a pain would it be to edit the script to add safety to bindings.txt and maybe mark this function as safe?

If the answer is too much, that's okay, just thinking it might be helpful.

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have much control of the script. What I can do is write a safe wrapper function that. That could also handle the point below of wanting a nice iterator.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've add a couple of safe wrappers that hopefully address your over comments too. The api:: module is like the c:: except it contains safe(r) wrappers instead of direct calls to the Windows API.

Comment thread library/std/src/sys/fs/windows.rs Outdated
Comment on lines +1640 to +1642
let result = unsafe {
c::QueryDosDeviceW(device_name.as_ptr(), buf.as_mut_ptr(), c::MAX_PATH)
} as usize;

@clarfonthey clarfonthey Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the safety invariant here is just buffer validity, but worth adding an explicit comment.

View changes since the review

Comment thread library/std/src/sys/fs/windows.rs Outdated
Comment on lines +1634 to +1638
let drives = unsafe { c::GetLogicalDrives() };
let mut buf = [0_u16; c::MAX_PATH as usize];
for letter in b'A'..=b'Z' {
let enabled = drives >> (letter - b'A') & 1;
if enabled == 1 {

@clarfonthey clarfonthey Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small preference for making a wrapper around GetLogicalDrives() that returns an impl Iterator here, instead of inlining this logic into this function, since it is kind-of sandwiched between code with this unrelated buffer too.

Something like:

fn get_drive_letters() -> impl Iterator<Item = u8> {
    let drives = unsafe { c::GetLogicalDrives() };
    (b'A'..=b'Z').filter(|letter| drives >> (letter - b'A') & 1 == 1)
}

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: doing an impl Iterator also allows for an option where you make the original iterator 0..26 and then add b'A' to the result, if you so wish. The possibilities are endless, but it will probably be one of these two options.

Comment thread library/std/src/sys/fs/windows.rs Outdated
let result = unsafe {
c::QueryDosDeviceW(device_name.as_ptr(), buf.as_mut_ptr(), c::MAX_PATH)
} as usize;
if result < buf.len() {

@clarfonthey clarfonthey Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading this correctly, wouldn't this always be true?

https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-querydosdevicew#return-value

Says here that the return value is zero when it fails.

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh oops. I was confusing it with another API. I mean, it should usually succeed but there are definitely failure modes.

Comment thread library/std/src/sys/fs/windows.rs Outdated
c::QueryDosDeviceW(device_name.as_ptr(), buf.as_mut_ptr(), c::MAX_PATH)
} as usize;
if result < buf.len() {
let drive_path = buf[..result].trim_suffix(&[0, 0]);

@clarfonthey clarfonthey Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trim_suffix is confusing me. I get one trailing zero because C, but why two?

View changes since the review

@ChrisDenton ChrisDenton Aug 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's because QueryDosDeviceW can return a list of null-terminated paths and the list is also null terminated.

The function fills this buffer with one or more null-terminated strings. The final null-terminated string is followed by an additional NULL.

We give it a device name so it'll only return one path but for consistency it still returns such a list.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth commenting then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

O-windows Operating system: Windows S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants