Skip to content

coreutils: use the executable's name on disk on Windows - #16229

Closed
KrushnXGIT wants to merge 1 commit into
uutils:mainfrom
KrushnXGIT:windows-case-insensitive-dispatch
Closed

KrushnXGIT wants to merge 1 commit into
uutils:mainfrom
KrushnXGIT:windows-case-insensitive-dispatch

Conversation

@KrushnXGIT

@KrushnXGIT KrushnXGIT commented Nov 6, 2026 •

Copy link
Copy Markdown

On Windows, cmd.exe passes argv[0] as typed, so running False finds
false.exe but the multicall binary sees False and fails with
"unknown program". This uses current_exe() instead, which has the name
as found on disk. It doesn't resolve symlinks on Windows, so symlinks,
hard links and copies keep working. A path given explicitly in another
case keeps that spelling.

Closes #16228

Comment thread src/bin/coreutils.rs Resolved
@KrushnXGIT KrushnXGIT changed the title coreutils: match the binary name case-insensitively on Windows coreutils: use the executable's name on disk on Windows Nov 7, 2026
Comment thread src/common/validation.rs Resolved
Comment thread src/common/validation.rs Outdated
#[cfg(windows)]
pub fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf {
let argv0 = args.next().unwrap();
std::env::current_exe().map_or_else(|_| argv0.into(), |exe| name_on_disk(&exe).unwrap_or(exe))

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 Rust std team decided to resolve symlink instead of using GetModuleFileNameW as backend in the future, this will be broken. On the other hand, I don't want to add unsafe for it.
@ChrisDenton Any thought?

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.

It is possible that std will change here. There are two main ways of getting the exe (other than arg0): GetModuleFileNameW or QueryFullProcessImageNameW. The latter does follow symlinks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks! I added a symlink case to the test, so if std switches to QueryFullProcessImageNameW, CI fails instead of this breaking silently. @oech3 is that enough for you, or would you rather call GetModuleFileNameW directly?

This comment was marked as low quality.

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.

Actually, symlink is not allowed by default system config. Thus, it would not work on the CI. We should explicitly use proper API...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Symlinks do work on the Windows CI runners since they run as admin: test_touch_reference_dangling already creates one with symlink_file(...).unwrap() on Windows, and Build/stable (windows-latest) passes on main. Locally it needs Developer Mode. But if you prefer, I can call GetModuleFileNameW directly through uucore::windows_sys, it's one unsafe call.

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.

OK. Then unwrap() should be avoided for local testing.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, it now skips locally when symlinks can't be created, but still fails on CI so the check can't silently go away there.

@oech3

This comment was marked as resolved.

@KrushnXGIT

Copy link
Copy Markdown
Author

cargo clippy CI is sad.

It was the windows-sys dependency I added, it made the uucore::windows_sys:: paths in test_mv an unnecessary qualification. Removed it, clippy passes locally now.

Comment thread src/common/validation.rs Resolved
Comment thread src/common/validation.rs Outdated
Comment on lines +125 to +127
let argv0 = args.next().unwrap();
// Backend is `GetModuleFileNameW` which does not resolve symlink.
std::env::current_exe().unwrap_or_else(|_| argv0.into())

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.

We need to unwrap() args.next() if and only if current_exe failed. No?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Right, changed it. args.next() still runs so argv[0] is skipped for the caller, it's only unwrapped when current_exe() fails. Added a small test with empty args.

Comment thread src/common/validation.rs Outdated

#[test]
#[cfg(windows)]
fn test_binary_path_without_argv0() {

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.

Is this really needed? We already have many tests with real binaries.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not really, removed it.

@oech3

This comment was marked as resolved.

@KrushnXGIT
KrushnXGIT force-pushed the windows-case-insensitive-dispatch branch from e9bdb35 to d801e92 Compare November 7, 2026 08:01
@KrushnXGIT

Copy link
Copy Markdown
Author

Would you squash commits?

Squashed.

@KrushnXGIT
KrushnXGIT force-pushed the windows-case-insensitive-dispatch branch 2 times, most recently from 94e8518 to a527627 Compare November 7, 2026 09:38
cmd.exe passes argv[0] the way it was typed, so running `False` finds
`false.exe` but the multicall binary sees `False` and fails with
"unknown program". Use current_exe() instead, which has the name as
found on disk and does not resolve symlinks on Windows.

Co-authored-by: oech3 <79379754+oech3@users.noreply.github.com>
@KrushnXGIT
KrushnXGIT force-pushed the windows-case-insensitive-dispatch branch from a527627 to c3c5785 Compare November 7, 2026 09:56
Comment thread src/common/validation.rs
Comment on lines +119 to +121
/// `false.exe`, while this function has the name as found on disk without resolving symlink,
/// so links named after a utility keep working. A path given explicitly in another case
/// keeps that spelling.

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.

There are 2 problems with this:

If your basename is on disk as BaseName.exe, this will fail to match any commands, because GetModuleFileNameW returns BaseName.exe and not basename.exe.

More importantly, the comment is incorrect. GetModuleFileNameW actually returns the value stored in the PE loader and that can be whatever. Technically, it can be truly whatever, but in practice it's whatever you passed to CreateProcess as lpApplicationName.

For instance, this is an entirely feasible way to use coreutils:

subprocess.run(
    ["basename.exe", "a/b"],
    executable="coreutils.exe",
)

This PR breaks name lookup, because GetModuleFileNameW returns coreutils.exe.


The correct solution is to simply do lowercase hashing which can be trivially implemented. But I would personally be opposed to this. Coreutils aren't just some random Windows tools, and I think it's in the best interest of everyone if they get their (if I may) damn path casing correct. 😄

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.

It is expected because we don't consider BaseName as valid name.

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.

By the way, we already use AT_EXECFN to reject hijacked argv[0] . Is lpApplicationName commonly used?

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.

It is expected because we don't consider BaseName as valid name.

I thought this is about insensitive filesystem support? In that frame of mind, storing a BaseName.exe on disk would be equivalent to basename.exe, would it not?

By the way, we already use AT_EXECFN to reject hijacked argv[0] . Is lpApplicationName commonly used?

Huh. Yeah, there are actually THREE different issues with the comment. 😄

It is common. In fact, it's the only reason this PR even seems like it would work: CMD sets it to the normalized file path (and so do other Windows launchers).

If you call CreateProcess("BaseName.exe") naively, then GetModuleFileNameW simply returns whatever is in argv[0] that matched a path on disk. It's not normalized.

So, actually this PR only fixes CMD (and some others), but then also breaks the inverse scenario, creating problems elsewhere.

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.

In my thought, user should symlink/hardlink as basename.exe, but it can be called as BaseName.

Is there any API doing same things with AT_EXECFN? If not, I don't agree with this change too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Then the current version might fit that: as lhecker said, GetModuleFileNameW is the closest thing to AT_EXECFN, so dispatching on it is the same kind of protection as on Linux. lpApplicationName / executable= with a different name gets rejected the same way exec -a is there, and cmd's casing working is only a side effect. I'd fix the comment, lhecker is right that it isn't the name on disk.

@lhecker does that framing work for you, or is there a reason Windows should keep trusting argv[0]?

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.

As I said, I'm not really familiar with AT_EXECFN and what it's supposed to do here. If I understood the first 2 paragraphs of #15785 correctly (it's just way way way too long to read in full), I think it's for ensuring that a multicall ls.exe cannot be invoked as echo.exe by manipulating argv[0]. I'm not sure how valuable that is on Windows, since AppArmor does not exist like that and I suspect the Windows equivalents don't care about lpApplicationName vs lpCommandLine anyway.

However, if we want to fix that, GetModuleFileNameW would be a good replacement for argv[0]. It would not fix the case-sensitivity issue, however.

This comment was marked as outdated.

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.

lpApplicationName is a place to set the binary we want to exec. So lpApplicationName=coreutils.exe is invalid if we installed individual binaries on the system. So I think we should specify lpApplicationName=/path/to/hardlink and using GetModuleFileNameW is making a sense.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That makes sense to me: with individual binaries installed, lpApplicationName=coreutils.exe plus a basename.exe argv[0] is the same thing exec -a does on Linux, which we already reject. If @lhecker is fine with that use being rejected, I can reopen this framed as AT_EXECFN parity on Windows, with the comment fixed and no claim about case-insensitivity.

@KrushnXGIT

Copy link
Copy Markdown
Author

Thanks @lhecker, @oech3, that settles it for me. On Windows, GetModuleFileNameW would only add the #10773 protection, which doesn't buy much there. The case fix would only work for launchers that pass the on-disk name, like cmd. That isn't worth breaking the executable= use, so I'm closing this. If someone ever wants the #10773 protection on Windows, the branch is still there.

@KrushnXGIT KrushnXGIT closed this Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

coreutils: utility name in a different case is not recognized on Windows

4 participants