Skip to content

Commit 4729d26

Browse files
committed
Don't match libexec/git-core in usr; refactor
- Allowing `usr` as a `<platform>` prefix is more likely to produce desired than undesired behavior. But due to the ambiguity of the name `usr` on non-Unix systems, maybe this would lead to problems that are relevant to security. The concern is described and the `usr` prefix, which is never needed to find the shell in a Git for Windows installation, is no longer matched. Note that this only affects it as a path component that `libexec/git-core` is found initially to be in. We do still use <prefix>/libexec/git-core/../../../usr/bin/sh.exe if we don't find something we can plausibly use at: `<prefix>/libexec/git-core/../../../bin/sh.exe The helper docstring explains why a security model under which this is reasonable does necessarily entail that it is reasonable to allow a `<prefix>` of `usr`, *even though* in the path with `usr` that we form, it is `usr` in the `<prefix>` position. With that said, and even though it does not help find `sh` from Git for Windows, hopefully future research can establish that it is safe to treat `usr/libexec/git-core` the same as platform prefixes like `mingw64/libexec/git-core` and it can be enabled. - Start refactoring by extracting and renaming the recently introduced helper constants and functions. - Extract most of the `cfg!(windows)` branch in `shell()` itself, even with its helpers already extracted, to make it a helper function as well. - Document the recently introduced helper constants.
1 parent 6cc75c3 commit 4729d26

File tree

1 file changed

+79
-37
lines changed

1 file changed

+79
-37
lines changed

gix-path/src/env/mod.rs

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,84 @@ pub fn installation_config_prefix() -> Option<&'static Path> {
2828
installation_config().map(git::config_to_base_path)
2929
}
3030

31+
/// `usr`-like directory component names that MSYS2 may provide, other than for `/usr` itself.
32+
///
33+
/// These are the values of the "Prefix" column of the "Environments" and "Legacy Environments"
34+
/// tables in the [MSYS2 Environments](https://www.msys2.org/docs/environments/) documentation,
35+
/// with the leading `/` separator removed, except that this does not list `usr` itself.
36+
///
37+
/// On Windows, we prefer to use `sh` as provided by Git for Windows, when present. To find it, we
38+
/// run `git --exec-path` to get a path that is usually `<platform>/libexec/git-core` in the Git
39+
/// for Windows installation, where `<platform>` is something like `mingw64`. It is also acceptable
40+
/// to find `sh` in an environment not provided by Git for Windows, such as an independent MSYS2
41+
/// environment in which a `git` package has been installed. However, in an unusual installation,
42+
/// or if the user has set a custom value of `GIT_EXEC_PATH`, the output of `git --exec-path` may
43+
/// take a form other than `<platform>/libexec/git-core`, such that finding shell at a location
44+
/// like `../../../bin/sh.exe` relative to it should not be attempted. We lower the risk by
45+
/// checking that `<platform>` is a plausible value that is not likely to have any other meaning.
46+
///
47+
/// This involves two tradeoffs. First, it may be reasonable to find `sh.exe` in an environment
48+
/// that is not MSYS2 at all, for which in principle the prefix could be different. But listing
49+
/// more prefixes or matching a broad pattern of platform-like strings might be too broad. So only
50+
/// prefixes that have been used in MSYS2 are considered.
51+
///
52+
/// Second, we don't recognize `usr` itself here, even though is a plausible prefix. In MSYS2, it
53+
/// is the prefix for MSYS2 non-native programs, i.e. those that use `msys-2.0.dll`. But unlike the
54+
/// `<platform>` names we recognize, `usr` also has an effectively unbounded range of plausible
55+
/// meanings on non-Unix systems, which may occasionally relate to subdirectories whose contents
56+
/// are controlled by different user accounts.
57+
///
58+
/// If we start with a `libexec/git-core` directory that we already use and trust, and it is in a
59+
/// directory with a name like `mingw64`, we infer that this `mingw64` directory has the expected
60+
/// meaning and that its `usr` sibling, if present, is acceptable to treat as though it is a
61+
/// first-level directory inside an MSYS2-like tree. So we are willing to traverse down to
62+
/// `usr/sh.exe` and attempt to use it. But if the `libexec/git-core` we use and trust is inside a
63+
/// directory named `usr`, that `usr` directory may still not have the meaning we expect of `usr`.
64+
///
65+
/// The conditions for a privilege escalation attack or other serious malfunction seem unlikely. If
66+
/// research indicates the risk is low enough, `usr` may be added. But for now it is omitted.
67+
const MSYS_USR_VARIANTS: &[&str] = &["mingw64", "mingw32", "clangarm64", "clang64", "clang32", "ucrt64"];
68+
69+
/// Shell path fragments to concatenate to the root of a Git for Windows or MSYS2 installation.
70+
///
71+
/// These look like absolute Unix-style paths, but the leading `/` separators are present because
72+
/// they simplify forming paths like `C:/Program Files/Git` obtained by removing trailing
73+
/// components from the output of `git --exec-path`.
74+
const RAW_SH_EXE_PATH_SUFFIXES: &[&str] = &[
75+
"/bin/sh.exe", // Usually a shim, which currently we prefer, if available.
76+
"/usr/bin/sh.exe",
77+
];
78+
79+
///
80+
fn raw_join(path: &Path, raw_suffix: &str) -> OsString {
81+
let mut raw_path = OsString::from(path);
82+
raw_path.push(raw_suffix);
83+
raw_path
84+
}
85+
86+
///
87+
fn find_sh_on_windows() -> Option<OsString> {
88+
core_dir()
89+
.filter(|core| core.is_absolute() && core.ends_with("libexec/git-core"))
90+
.and_then(|core| core.ancestors().nth(2))
91+
.filter(|prefix| {
92+
// Only use `libexec/git-core` from inside something `usr`-like, such as `mingw64`.
93+
MSYS_USR_VARIANTS.iter().any(|name| prefix.ends_with(name))
94+
})
95+
.and_then(|prefix| prefix.parent())
96+
.into_iter()
97+
.flat_map(|git_root| {
98+
// Enumerate locations where `sh.exe` usually is. To avoid breaking scripts that assume the
99+
// shell's own path contains no `\`, and so messages are more readable, append literally
100+
// with `/` separators. The path from `git --exec-path` already uses `/` separators (and no
101+
// trailing `/`) unless explicitly overridden to an unusual value via `GIT_EXEC_PATH`.
102+
RAW_SH_EXE_PATH_SUFFIXES
103+
.iter()
104+
.map(|raw_suffix| raw_join(git_root, raw_suffix))
105+
})
106+
.find(|raw_path| Path::new(raw_path).is_file())
107+
}
108+
31109
/// Return the shell that Git would use, the shell to execute commands from.
32110
///
33111
/// On Windows, this is the full path to `sh.exe` bundled with Git for Windows if we can find it.
@@ -39,43 +117,7 @@ pub fn installation_config_prefix() -> Option<&'static Path> {
39117
pub fn shell() -> &'static OsStr {
40118
static PATH: Lazy<OsString> = Lazy::new(|| {
41119
if cfg!(windows) {
42-
const MSYS_PREFIX_NAMES: &[&str] = &[
43-
"mingw64",
44-
"mingw32",
45-
"clangarm64",
46-
"clang64",
47-
"clang32",
48-
"ucrt64",
49-
"usr",
50-
];
51-
const RAW_SUFFIXES: &[&str] = &[
52-
"/bin/sh.exe", // Usually a shim, which currently we prefer, if available.
53-
"/usr/bin/sh.exe",
54-
];
55-
fn raw_join(path: &Path, raw_suffix: &str) -> OsString {
56-
let mut raw_path = OsString::from(path);
57-
raw_path.push(raw_suffix);
58-
raw_path
59-
}
60-
core_dir()
61-
.filter(|core| core.is_absolute() && core.ends_with("libexec/git-core"))
62-
.and_then(|core| core.ancestors().nth(2))
63-
.filter(|prefix| {
64-
// Only use `libexec/git-core` from inside something `usr`-like, such as `mingw64`.
65-
MSYS_PREFIX_NAMES.iter().any(|name| prefix.ends_with(name))
66-
})
67-
.and_then(|prefix| prefix.parent())
68-
.into_iter()
69-
.flat_map(|git_root| {
70-
// Enumerate the locations where `sh.exe` usually is. To avoid breaking shell
71-
// scripts that assume the shell's own path contains no `\`, and to produce
72-
// more readable messages, append literally with `/` separators. The path from
73-
// `git --exec-path` will already have all `/` separators (and no trailing `/`)
74-
// unless it was explicitly overridden to an unusual value via `GIT_EXEC_PATH`.
75-
RAW_SUFFIXES.iter().map(|raw_suffix| raw_join(git_root, raw_suffix))
76-
})
77-
.find(|raw_path| Path::new(raw_path).is_file())
78-
.unwrap_or_else(|| "sh.exe".into())
120+
find_sh_on_windows().unwrap_or_else(|| "sh.exe".into())
79121
} else {
80122
"/bin/sh".into()
81123
}

0 commit comments

Comments
 (0)