Skip to content

fix(backend): strip mise shims from dependency_env PATH to prevent fork bomb#8475

Merged
jdx merged 1 commit into
jdx:mainfrom
pose:fix/go-backend-shim-recursion
Mar 9, 2026
Merged

fix(backend): strip mise shims from dependency_env PATH to prevent fork bomb#8475
jdx merged 1 commit into
jdx:mainfrom
pose:fix/go-backend-shim-recursion

Conversation

@pose

@pose pose commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Summary

When a go: backend tool is configured alongside a go version that is not installed, and the remote version cache has been cleared (e.g. mise cache clear), subsequent mise commands spawn infinite child processes until OOM.

Root cause

_list_remote_versions for the go: backend calls dependency_env to build the environment for go list. In --shims mode, PRISTINE_ENV contains the mise shims directory in PATH (shims are added to the shell PATH directly without setting __MISE_DIFF, so they survive the pristine-env reconstruction). With go@<configured-version> not installed, no real go binary is added to dependency_env's PATH — the only go available is its shim.

The shim calls mise exec -- go list ..., which spawns a new mise process. That process has the same config, calls ts.resolve() on all tools including the go: backend, hits _list_remote_versions (cache still empty), calls go list via the shim again → infinite fork bomb.

Fix

Strip dirs::SHIMS from the PATH in dependency_env before returning it. The dependency tool either resolves to a real installed binary or the command fails cleanly — no shim is ever invoked. This is the correct invariant: dependency envs should never rely on mise's own shim layer.

The fix applies to all backends using the default dependency_env (go, npm, gem, dotnet, spm, elixir).

Repro

The original Docker repro from the issue runs cleanly with this fix:

docker run --rm ubuntu:24.04 bash -c '
  apt-get update -qq && apt-get install -y -qq zstd curl git 2>/dev/null
  curl -fsSL https://mise.run | sh
  eval "$(/root/.local/bin/mise activate bash --shims)"

  mkdir -p /tmp/repro/.config
  cat > /tmp/repro/.config/mise.toml << TOML
[tools]
"go:github.com/pulumi/upgrade-provider" = "main"
go = "1.23.3"
TOML
  cd /tmp/repro && mise trust --yes && mise install go@1.26 && mise cache clear
  MISE_LOG_LEVEL=debug mise ls golangci-lint --json -c 2>&1 | head -30
'

Before the fix: fork bombs until OOM. After: exits cleanly with [].

Test plan

  • New e2e test e2e/backend/test_go_shim_recursion reproduces the bug (times out without fix, passes with fix)
  • cargo test — 515 unit tests pass
  • e2e/backend/test_backend_missing_deps, e2e/cli/test_cache_clear, e2e/cli/test_ls, e2e/cli/test_backends — all pass

This PR was authored with Claude Code.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a critical bug that leads to infinite process spawning in mise when using go: backend tools with specific configurations. The fix ensures that dependency environments do not rely on mise's shim layer, preventing recursive calls and OOM errors. An e2e test was added to verify the fix.

Highlights

  • Infinite Fork Bomb Prevention: Addresses an issue where mise commands could spawn infinite child processes due to a misconfiguration involving go: backend tools and uninstalled go versions.
  • Shim Stripping: Strips the dirs::SHIMS directory from the PATH in dependency_env to prevent the invocation of mise shims during dependency resolution.
  • E2E Test: Introduces a new end-to-end test (e2e/backend/test_go_shim_recursion) to reproduce and verify the fix for the fork bomb issue.
Changelog
  • backend
    • Fixed infinite process spawning bug with go backend tools.
    • Ensured dependency environments do not rely on mise's shim layer.
  • e2e
    • Added e2e test to reproduce and verify the fix for the fork bomb issue.
Activity
  • The PR introduces a fix to prevent infinite process spawning when a go: backend tool is configured alongside a go version that is not installed.
  • The root cause was identified as the _list_remote_versions function calling dependency_env which includes mise shims in the PATH, leading to recursive shim invocations.
  • The fix involves stripping dirs::SHIMS from the PATH in dependency_env to ensure dependency tools resolve to real installed binaries or fail cleanly.
  • A new e2e test (e2e/backend/test_go_shim_recursion) was added to reproduce the bug and verify the fix.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@greptile-apps

greptile-apps Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a critical fork-bomb bug where go: backend tools with an uncached remote version list and an uninstalled go dependency (in --shims mode) caused infinite process spawning until OOM, by stripping dirs::SHIMS from the PATH inside dependency_env() before returning it.

Root cause correctly identified: In --shims mode, PRISTINE_ENV retains the mise shims directory in PATH (since shims are added directly without __MISE_DIFF). When go is not installed, dependency_env would have the shim as the only go in PATH, causing infinite recursion through mise exec.

Fix is minimal and correct: The new path-stripping logic in dependency_env mirrors the identical pattern already used in src/cli/exec.rs. The Windows branch correctly hoists shims_normalized outside the filter closure and the guard if filtered.len() != original_len avoids unnecessary PATH reconstruction.

Broad coverage: The fix applies to all backends using the default dependency_env (go, npm, gem, dotnet, spm, elixir).

Test is well-designed: The e2e test uses the portable run_with_timeout helper (matching test_exec_wrapper_recursion), sets up the reproduction scenario, and asserts that the output contains "go" (correctly checking that the tool appears in the listing).

Confidence Score: 5/5

  • This PR is safe to merge: it fixes a critical fork-bomb bug with a minimal, well-tested change that mirrors an existing proven pattern in the codebase.
  • The fix is a faithful port of the already-reviewed shim-stripping logic from exec.rs into dependency_env. It correctly handles both Unix and Windows paths, includes proper guards to avoid unnecessary PATH reconstruction, and is backed by a new e2e regression test that uses established test patterns (run_with_timeout helper). All implementation details are correct, and the test properly validates both timeout behavior and output assertion.
  • No files require special attention

Last reviewed commit: 19fe4ed

Comment thread e2e/backend/test_go_shim_recursion Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request aims to address a critical fork bomb issue that occurs when using a go: backend tool with an uninstalled go version by stripping the mise shims directory from the PATH within dependency_env to prevent infinite recursion of mise exec calls. While the fix is effective for Unix-like systems and validated by the new e2e/backend/test_go_shim_recursion test, the path comparison used for filtering is fragile on Windows. This fragility could allow the fork bomb to persist on Windows, leading to a Denial of Service (DoS). Additionally, there is a minor suggestion for a small performance improvement.

Comment thread src/backend/mod.rs
@pose

pose commented Mar 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews!

Greptile / portability: Fixed in cbdc866 — switched from assert "timeout 15 ..." (which uses the system binary via bash -c) to run_with_timeout called directly in the sourced test script, matching the pattern in test_exec_wrapper_recursion.

Gemini / Windows path comparison: Also fixed in cbdc866 — the filter now uses the same case-insensitive, separator-normalised comparison with file::replace_path that already exists in src/cli/exec.rs's Windows implementation. On Unix the simple p.as_path() != *dirs::SHIMS is kept.

This comment was generated by Claude Code.

@jdx

jdx commented Mar 5, 2026

Copy link
Copy Markdown
Owner

@greptileai

Comment thread src/backend/mod.rs Outdated
@pose

pose commented Mar 6, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, it seems I broke the code by accepting the greptile suggestion. Fixing it.

@pose pose force-pushed the fix/go-backend-shim-recursion branch from 0b4838f to 2afb4bc Compare March 9, 2026 12:13
Comment thread src/backend/mod.rs
Comment thread e2e/backend/test_go_shim_recursion Outdated
…rk bomb

When a go: backend tool is configured alongside a go version that is not
installed, and the remote version cache has been cleared, _list_remote_versions
calls dependency_env to build the environment for `go list`. In --shims mode,
PRISTINE_ENV contains the mise shims directory in PATH (since shims are added
to the shell PATH without setting __MISE_DIFF). With go not installed, the
only `go` on PATH is its shim, which calls `mise exec`, which resolves all
tools in config including the go: backend, which calls _list_remote_versions
again — an infinite fork bomb until OOM.

Fix by stripping dirs::SHIMS from the PATH in dependency_env, so the shim
cannot be invoked. The dependency tool either resolves to a real installed
binary or fails cleanly. This affects all backends that use the default
dependency_env (go, npm, gem, dotnet, spm, elixir).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@pose pose force-pushed the fix/go-backend-shim-recursion branch from 2afb4bc to 19fe4ed Compare March 9, 2026 12:34
@jdx jdx merged commit 7d740a3 into jdx:main Mar 9, 2026
34 checks passed
jdx pushed a commit that referenced this pull request Mar 9, 2026
### 🐛 Bug Fixes

- **(activate)** reorder shims to front of PATH on re-source in fish by
@jdx in [#8534](#8534)
- **(backend)** strip mise shims from dependency_env PATH to prevent
fork bomb by @pose in [#8475](#8475)
- **(github)** resolve "latest" version correctly via GitHub API by @jdx
in [#8532](#8532)
- **(lock)** set env tags and clarify lockfile docs by @jdx in
[#8519](#8519)
- **(lock)** use separate mise.<env>.lock files instead of env tags by
@jdx in [#8523](#8523)
- **(task)** include args in task output prefix and truncate long
prefixes by @jdx in [#8533](#8533)
- **(task)** only include args in task prefix when disambiguating
duplicates by @jdx in [#8536](#8536)
- **(test)** pin goreleaser version in attestation e2e test by @jdx in
[#8518](#8518)
- **(windows)** env._.source needs to run bash.exe on Windows (fix
#6513) by @pjeby in [#8520](#8520)
- handle locked .exe shims on Windows during reshim by @davireis in
[#8517](#8517)

### 🚜 Refactor

- **(prepare)** remove touch_outputs and update docs to reflect blake3
hashing by @jdx in [#8535](#8535)

### 📚 Documentation

- **(docker)** replace jdxcode/mise image with curl install, update to
debian:13-slim by @jdx in [#8526](#8526)
- fix "gzip: stdin is encrypted" error in shell tricks cookbook by
@pjeby in [#8512](#8512)

### 📦 Registry

- add tigerbeetle
([github:tigerbeetle/tigerbeetle](https://github.com/tigerbeetle/tigerbeetle))
by @risu729 in [#8514](#8514)

### New Contributors

- @pjeby made their first contribution in
[#8520](#8520)
- @davireis made their first contribution in
[#8517](#8517)
- @Aurorxa made their first contribution in
[#8511](#8511)

## 📦 Aqua Registry Updates

#### New Packages (6)

-
[`betterleaks/betterleaks`](https://github.com/betterleaks/betterleaks)
- [`majorcontext/moat`](https://github.com/majorcontext/moat)
- [`princjef/gomarkdoc`](https://github.com/princjef/gomarkdoc)
- [`remko/age-plugin-se`](https://github.com/remko/age-plugin-se)
- [`sudorandom/fauxrpc`](https://github.com/sudorandom/fauxrpc)
- [`swanysimon/mdlint`](https://github.com/swanysimon/mdlint)

#### Updated Packages (1)

- [`moonrepo/moon`](https://github.com/moonrepo/moon)
antonioacg added a commit to antonioacg/mise that referenced this pull request Mar 28, 2026
get_credential_command_token() and get_git_credential_token() spawn
subprocesses (sh -c <cmd> and git credential fill) that inherit the
current PATH, including mise shims. When the credential tool (e.g. gh)
or git is managed by mise, the shim calls mise exec, which may
re-enter token resolution, spawning the same command indefinitely.

This causes a recursive fork bomb that can produce thousands of processes
and make the system unresponsive (observed load average >1800 on an ARM
SBC running k3s).

Apply the same shim-stripping pattern used in dependency_env()
(PR jdx#8475) and exec_program() (PR jdx#8276): filter dirs::SHIMS out of
PATH before spawning the subprocess.
antonioacg added a commit to antonioacg/mise that referenced this pull request Mar 28, 2026
get_credential_command_token() and get_git_credential_token() spawn
subprocesses (sh -c <cmd> and git credential fill) that inherit the
current PATH, including mise shims. When the credential tool (e.g. gh)
or git is managed by mise, the shim calls mise exec, which may
re-enter token resolution, spawning the same command indefinitely.

This causes a recursive fork bomb that can produce thousands of processes
and make the system unresponsive (observed load average >1800 on an ARM
SBC running k3s).

Apply the same shim-stripping pattern used in dependency_env()
(PR jdx#8475) and exec_program() (PR jdx#8276): filter dirs::SHIMS out of
PATH before spawning the subprocess.
antonioacg added a commit to antonioacg/mise that referenced this pull request Mar 28, 2026
get_credential_command_token() and get_git_credential_token() spawn
subprocesses (sh -c <cmd> and git credential fill) that inherit the
current PATH, including mise shims. When the credential tool (e.g. gh)
or git is managed by mise, the shim calls mise exec, which may
re-enter token resolution, spawning the same command indefinitely.

This causes a recursive fork bomb that can produce thousands of processes
and make the system unresponsive (observed load average >1800 on an ARM
SBC running k3s).

Apply the same shim-stripping pattern used in dependency_env()
(PR jdx#8475) and exec_program() (PR jdx#8276): filter dirs::SHIMS out of
PATH before spawning the subprocess.
antonioacg added a commit to antonioacg/mise that referenced this pull request Mar 28, 2026
get_credential_command_token() and get_git_credential_token() spawn
subprocesses (sh -c <cmd> and git credential fill) that inherit the
current PATH, including mise shims. When the credential tool (e.g. gh)
or git is managed by mise, the shim calls mise exec, which may
re-enter token resolution, spawning the same command indefinitely.

This causes a recursive fork bomb that can produce thousands of processes
and make the system unresponsive (observed load average >1800 on an ARM
SBC running k3s).

Apply the same shim-stripping pattern used in dependency_env()
(PR jdx#8475) and exec_program() (PR jdx#8276): filter dirs::SHIMS out of
PATH before spawning the subprocess.
antonioacg added a commit to antonioacg/mise that referenced this pull request Mar 28, 2026
get_credential_command_token() and get_git_credential_token() spawn
subprocesses (sh -c <cmd> and git credential fill) that inherit the
current PATH, including mise shims. When the credential tool (e.g. gh)
or git is managed by mise, the shim calls mise exec, which may
re-enter token resolution, spawning the same command indefinitely.

This causes a recursive fork bomb that can produce thousands of processes
and make the system unresponsive (observed load average >1800 on an ARM
SBC running k3s).

Apply the same shim-stripping pattern used in dependency_env()
(PR jdx#8475) and exec_program() (PR jdx#8276): filter dirs::SHIMS out of
PATH before spawning the subprocess.
antonioacg added a commit to antonioacg/mise that referenced this pull request Mar 28, 2026
get_credential_command_token() and get_git_credential_token() spawn
subprocesses (sh -c <cmd> and git credential fill) that inherit the
current PATH, including mise shims. When the credential tool (e.g. gh)
or git is managed by mise, the shim calls mise exec, which may
re-enter token resolution, spawning the same command indefinitely.

This causes a recursive fork bomb that can produce thousands of processes
and make the system unresponsive (observed load average >1800 on an ARM
SBC running k3s).

Apply the same shim-stripping pattern used in dependency_env()
(PR jdx#8475) and exec_program() (PR jdx#8276): filter dirs::SHIMS out of
PATH before spawning the subprocess.
antonioacg added a commit to antonioacg/mise that referenced this pull request Mar 28, 2026
get_credential_command_token() and get_git_credential_token() spawn
subprocesses (sh -c <cmd> and git credential fill) that inherit the
current PATH, including mise shims. When the credential tool (e.g. gh)
or git is managed by mise, the shim calls mise exec, which may
re-enter token resolution, spawning the same command indefinitely.

This causes a recursive fork bomb that can produce thousands of processes
and make the system unresponsive (observed load average >1800 on an ARM
SBC running k3s).

Apply the same shim-stripping pattern used in dependency_env()
(PR jdx#8475) and exec_program() (PR jdx#8276): filter dirs::SHIMS out of
PATH before spawning the subprocess.
antonioacg added a commit to antonioacg/mise that referenced this pull request Mar 28, 2026
get_credential_command_token() and get_git_credential_token() spawn
subprocesses (sh -c <cmd> and git credential fill) that inherit the
current PATH, including mise shims. When the credential tool (e.g. gh)
or git is managed by mise, the shim calls mise exec, which may
re-enter token resolution, spawning the same command indefinitely.

This causes a recursive fork bomb that can produce thousands of processes
and make the system unresponsive (observed load average >1800 on an ARM
SBC running k3s).

Apply the same shim-stripping pattern used in dependency_env()
(PR jdx#8475) and exec_program() (PR jdx#8276): filter dirs::SHIMS out of
PATH before spawning the subprocess.
jdx pushed a commit that referenced this pull request Mar 30, 2026
…8802)

## Summary

Three code paths spawn subprocesses that can invoke mise-managed tools
(e.g. `gh`, `git`) without stripping mise shims from PATH. When the tool
resolves to a mise shim, it re-enters mise, which may trigger the same
subprocess again — causing **infinite recursion (fork bomb)**.

Observed: load average >1800 on an ARM SBC, system unresponsive. Also
reproduced on macOS.

## Root Cause

Three subprocess-spawning paths inherit shims in PATH:

### 1. `credential_command` in `src/github.rs`
`get_credential_command_token()` runs `sh -c <cmd>` (e.g. `gh auth
token`). If `gh` is a mise shim → recursion.

### 2. `git credential fill` in `src/github.rs`
`get_git_credential_token()` runs `git credential fill`. If `git` is a
mise shim, or git's credential helper invokes `gh` (via `gh auth
setup-git`) → recursion.

### 3. `exec()` template function in `src/tera.rs` (primary trigger)
When a `.mise.toml` contains:
```toml
[env]
GITHUB_TOKEN = "{{exec(command='gh auth token')}}"
```
Every `mise hook-env` in that directory runs `gh auth token` via
`tera_exec()` with `PRISTINE_ENV`, which includes shims in PATH → `gh`
shim → `mise exec` → evaluates env → runs `gh auth token` again →
infinite recursion.

This is the most common trigger because `exec()` in `[env]` is the
idiomatic way to derive env vars from CLI tools.

## Fix

Add a shared `file::path_env_without_shims()` helper (next to the
existing `which_no_shims()`) that filters `dirs::SHIMS` out of PATH and
returns an `OsString` suitable for `.env("PATH", ...)`. Used in all
three call sites:

- **`src/github.rs`**: `get_credential_command_token()` and
`get_git_credential_token()`
- **`src/tera.rs`**: `tera_exec()`

Follows the same shim-stripping pattern established in:
- PR #8475 (`dependency_env()`)
- PR #8276 / #8560 (`exec_program()`)
- PR #8189 (Windows variant)
- PR #8402 (`uv` venv creation via `which_no_shims()`)

Related: Discussion #6374 — same user-facing symptom ("Cannot fork")
from `exec()` path.

## Reproduction

### Tera exec path (most common)
1. Install `gh` via mise (`gh = "latest"`)
2. Create a project `.mise.toml` with `[env] GITHUB_TOKEN =
"{{exec(command='gh auth token')}}"`
3. `cd` into that directory — `mise hook-env` fires, evaluates the
template, runs `gh auth token` through the shim → fork bomb

### Credential command path
1. Install `gh` via mise, set `credential_command = "gh auth token"` in
mise settings
2. Trigger any GitHub API call → fork bomb

## Changes

- **`src/file.rs`**: New `path_env_without_shims()` public helper —
shared by all call sites
- **`src/github.rs`**: Use shared helper in
`get_credential_command_token()` and `get_git_credential_token()`
- **`src/tera.rs`**: Use shared helper in `tera_exec()`
- **`e2e/cli/test_github_credential_shim_recursion`**: e2e test for
credential_command path
- **`e2e/cli/test_tera_exec_shim_recursion`**: e2e test for tera exec()
path

## Note on env var workaround

Setting `MISE_GITHUB_TOKEN=""` does **not** prevent the hang —
`resolve_token()` filters empty strings with `.filter(|t|
!t.is_empty())`, so it falls through to subprocess-based fallbacks. A
non-empty sentinel value (e.g. `MISE_GITHUB_TOKEN="none"`) works for the
github.rs paths but the tera `exec()` path is independent of token
resolution entirely.

## Test plan

- [x] `cargo check` passes
- [ ] CI: `cargo test` passes  
- [ ] CI: e2e test `test_github_credential_shim_recursion` passes
- [ ] CI: e2e test `test_tera_exec_shim_recursion` passes
- [x] Manual test: project with `exec(command='gh auth token')` in
`.mise.toml`, `cd` does not fork-bomb

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
andrewjamesbrown added a commit to andrewjamesbrown/mise that referenced this pull request May 25, 2026
jdx#8475 fixed the fork-bomb where backend version-resolution subprocesses
re-entered mise through a shim, by filtering `dirs::SHIMS` (the user
shims dir) out of `dependency_env`'s PATH. Devcontainer / Docker setups
that use `mise install --system` also have a *system* shims dir at
`$MISE_SYSTEM_DATA_DIR/shims`, also on PATH by convention. That dir
wasn't filtered, so the same recursion still fired through it.

Concrete repro: a workspace `mise.toml` pinning `"npm:yarn" = "1"`
together with an uninstalled node trips the npm backend's
`npm view yarn versions time --json` call. The user shim is stripped
(per jdx#8475), but the system shim still matches PATH lookup → mise
re-enters as `npm` → sees the same uninstalled `npm:yarn` →
auto-installs → calls `npm view` again — until the container OOMs.

Filter both shim dirs in `dependency_env`, mirroring the dual-dir guard
that `shims::which_shim` already applies after jdx#8816. Adds a regression
test under `e2e/backend/` that plants a shim in
`$MISE_SYSTEM_DATA_DIR/shims` and asserts no fork-bomb when an `npm:`
backend tool resolves remote versions.
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.

2 participants