Repository navigation
coreutils: use the executable's name on disk on Windows - #16229
KrushnXGIT wants to merge 1 commit into
Conversation
| #[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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
This comment was marked as low quality.
Sorry, something went wrong.
There was a problem hiding this comment.
Actually, symlink is not allowed by default system config. Thus, it would not work on the CI. We should explicitly use proper API...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
OK. Then unwrap() should be avoided for local testing.
There was a problem hiding this comment.
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.
This comment was marked as resolved.
This comment was marked as resolved.
It was the windows-sys dependency I added, it made the |
| let argv0 = args.next().unwrap(); | ||
| // Backend is `GetModuleFileNameW` which does not resolve symlink. | ||
| std::env::current_exe().unwrap_or_else(|_| argv0.into()) |
There was a problem hiding this comment.
We need to unwrap() args.next() if and only if current_exe failed. No?
There was a problem hiding this comment.
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.
|
|
||
| #[test] | ||
| #[cfg(windows)] | ||
| fn test_binary_path_without_argv0() { |
There was a problem hiding this comment.
Is this really needed? We already have many tests with real binaries.
This comment was marked as resolved.
This comment was marked as resolved.
e9bdb35 to
d801e92
Compare
Squashed. |
94e8518 to
a527627
Compare
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>
a527627 to
c3c5785
Compare
| /// `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. |
There was a problem hiding this comment.
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. 😄
There was a problem hiding this comment.
It is expected because we don't consider BaseName as valid name.
There was a problem hiding this comment.
By the way, we already use AT_EXECFN to reject hijacked argv[0] . Is lpApplicationName commonly used?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]?
There was a problem hiding this comment.
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.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Thanks @lhecker, @oech3, that settles it for me. On Windows, |
On Windows, cmd.exe passes argv[0] as typed, so running
Falsefindsfalse.exebut the multicall binary seesFalseand fails with"unknown program". This uses
current_exe()instead, which has the nameas 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