Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions e2e/cli/test_system_shims_no_recursion
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash

# Regression test: when a tool is installed --system (or any scenario where
# BOTH the user shims dir AND a system shims dir are on PATH), invoking a
# shim for a tool that is NOT in any config file must NOT recurse infinitely.
#
# Root cause: which_shim's PATH fallback skipped only dirs::SHIMS (user shims).
# If a second mise shims dir (e.g. /usr/local/share/mise/shims) was also on
# PATH it would be found as the "fallback binary" and exec'd, re-entering the
# same shim logic — infinite loop.

shimdir="$MISE_DATA_DIR/shims"
mkdir -p "$shimdir"

# Simulate a second (system-level) shims directory on PATH.
# The binary inside it is a symlink to the mise binary, exactly like a real
# system shim created by `mise install --system`.
sys_shimdir="$(mktemp -d)"

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.

medium

To ensure the temporary directory is always cleaned up, even if the script exits unexpectedly, it's a good practice to use trap for cleanup. This makes the test more robust. The explicit rm -rf at the end of the script can then be removed.

sys_shimdir="$(mktemp -d)"
trap 'rm -rf "$sys_shimdir"' EXIT

trap 'rm -rf "$sys_shimdir"' EXIT
mise_bin="$(command -v mise)"
ln -s "$mise_bin" "$sys_shimdir/dummy"

# Put both shims dirs on PATH — the user shims dir first, then the fake
# system shims dir. This is the exact PATH layout that triggered the bug.
export PATH="$shimdir:$sys_shimdir:$PATH"

# dummy is not in any config file. The shim must fail fast with a clear
# error rather than looping forever.
output="$(run_with_timeout 10 dummy 2>&1)" && {
echo "ERROR: dummy should have failed but exited 0"
exit 1
} || true

# Should contain a helpful error, not be empty (which would indicate a hang
# that was killed) or a recursion-induced crash.
assert_contains "echo '$output'" "dummy"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
14 changes: 11 additions & 3 deletions src/cli/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ where
// User-configured paths (_.path/venv) maintain their position
// relative to tool paths since both are "mise-added".
// The child process still inherits the full unmodified PATH.
let shims_dir = &*crate::dirs::SHIMS;
let user_shims = &*crate::dirs::SHIMS;
let sys_shims = crate::env::MISE_SYSTEM_DATA_DIR.join("shims");
let is_shims_dir = |p: &std::path::PathBuf| p == user_shims || p == &sys_shims;
let pristine: std::collections::HashSet<_> = crate::env::PATH.iter().collect();
let all_paths: Vec<_> = std::env::split_paths(&OsString::from(path_val)).collect();
// Mise-added paths first (preserving relative order)
Expand All @@ -216,7 +218,7 @@ where
// Then original system paths (minus shims)
let original: Vec<_> = all_paths
.iter()
.filter(|p| pristine.contains(p) && *p != shims_dir)
.filter(|p| pristine.contains(p) && !is_shims_dir(p))
.cloned()
.collect();
std::env::join_paths(mise_added.iter().chain(original.iter())).unwrap()
Expand Down Expand Up @@ -249,9 +251,15 @@ where
.to_string_lossy()
.to_lowercase()
.replace('/', "\\");
let sys_shims_normalized = crate::env::MISE_SYSTEM_DATA_DIR
.join("shims")
.to_string_lossy()
.to_lowercase()
.replace('/', "\\");
Comment on lines +254 to +258

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.

medium

The path normalization logic (.to_string_lossy().to_lowercase().replace('/', "\\")) is repeated multiple times in this function (here, for shims_normalized, in is_shims, pristine, mise_added, and original).

To improve maintainability and reduce duplication, consider extracting this logic into a local helper closure or function within exec_program.

let is_shims = |p: &std::path::PathBuf| {
let expanded = crate::file::replace_path(p);
expanded.to_string_lossy().to_lowercase().replace('/', "\\") == shims_normalized
let normalized = expanded.to_string_lossy().to_lowercase().replace('/', "\\");
normalized == shims_normalized || normalized == sys_shims_normalized
};
let pristine: std::collections::HashSet<_> = crate::env::PATH
.iter()
Expand Down
19 changes: 16 additions & 3 deletions src/shims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,27 @@ async fn which_shim(config: &mut Arc<Config>, bin_name: &str) -> Result<PathBuf>
}
}
// fallback for "system"
let mise_bin = fs::canonicalize(&*env::MISE_BIN).unwrap_or_else(|_| env::MISE_BIN.clone());
let user_shims = fs::canonicalize(*dirs::SHIMS).unwrap_or_default();
let sys_shims = {
let p = env::MISE_SYSTEM_DATA_DIR.join("shims");
if p.exists() {
fs::canonicalize(&p).unwrap_or(p)
} else {
PathBuf::new()
}
};
for path in &*env::PATH {
if fs::canonicalize(path).unwrap_or_default()
== fs::canonicalize(*dirs::SHIMS).unwrap_or_default()
{
let canon_path = fs::canonicalize(path).unwrap_or_default();
if canon_path == user_shims || canon_path == sys_shims {
continue;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
let bin = path.join(bin_name);
if bin.exists() {
// Skip if this binary is a mise shim (symlink pointing to the mise binary)
if fs::canonicalize(&bin).unwrap_or_default() == mise_bin {
continue;
}
trace!("shim[{bin_name}] SYSTEM {bin}", bin = display_path(&bin));
return Ok(bin);
}
Expand Down
Loading