Repository navigation
fix(shim): stop a shim from launching itself as its PATH fallback - #280
Merged
Merged
Conversation
Installs downloaded and extracted into os.TempDir() and then used os.Rename to move the result into the versions directory. When the dtvem root lives on a different drive than the OS temp directory (e.g. DTVEM_ROOT on H: with Windows on C:), the rename fails with "The system cannot move the file to a different disk drive". The same happens on Unix when /tmp is a separate filesystem such as tmpfs. Stage installs in <root>/tmp/<runtime>-<version> instead, so the final move is always a same-volume rename. Leftovers from an interrupted install are cleared before reuse since the OS no longer cleans them.
When a shim cannot resolve a version it falls back to the next match on PATH, skipping only ShimsDir(). ShimsDir() is derived from DTVEM_ROOT or the home directory, so when a process runs with a different home (a sandbox redirecting USERPROFILE, or no USERPROFILE at all) the real shims directory was not skipped. The shim found itself and launched itself, one process per hop, until the machine ran out of memory. The fallback lookup now also skips the directory of the running shim and any candidate that is the same file, whatever the environment says the home is. Directory comparison also matches different spellings of the same directory (trailing separator, 8.3 short names, symlinks) via os.SameFile. As a backstop, a shim records the path it hands off to in DTVEM_SHIM_FALLBACK. A shim that starts and finds itself named there was launched by another shim's fallback and exits with an explanation instead of looping. The marker names a file rather than a shim name so a real executable reached this way can still start the same command again.
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a shim can't resolve a version, it falls back to the next match on PATH. That lookup skipped only
ShimsDir(), which comes fromDTVEM_ROOTor the home directory. When a process ran with a different home than the one the shims on PATH were installed under, the real shims directory wasn't skipped, so the shim found itself and launched itself. That chain grew to thousands of processes and exhausted memory.This PR applies both fixes the issue suggests.
1. The fallback lookup excludes the running shim
path.LookPathExcludingSelf(execName, self)skips the directory of the running executable (os.Executable(), symlinks resolved) and any candidate thatos.SameFiles it. This works whatever the environment says the home is.LookPathExcludingShimskeeps its behavior for migration detection; it does not skip the dtvem binary's own directory, which may be a shared bin dir. Its directory comparison now also matches different spellings of the shims directory: trailing separator,.segments, case, and, throughos.SameFile, 8.3 short names and symlinked paths.2. Recursion guard
DTVEM_SHIM_FALLBACK=<path it is launching>. A shim that starts and finds itself named there was launched by another shim's fallback, so it exits with an explanation instead of looping.DTVEM_SHIM_ACTIVE=node). A name-based marker is inherited by every descendant, so a real systemnodereached through the fallback that runs a script spawningnodeagain would be wrongly refused. With a path, that nested shim is not the file named, so it runs normally.On the MCP trigger in the issue
A likely cause, not verified: the launch environment had no
USERPROFILE.os.UserHomeDir()then fails,ShimsDir()returns""so nothing is skipped, andgetRootDir()falls back to./.dtvemin the working directory, so resolution fails too. Fix 1 covers that case because it doesn't depend on the home directory.Tests
path:LookPathExcludingSelf(own dir, hard link to self, the old self-finding lookup when no self path is given), shims dir written differently (trailing separator, dot segment, case on Windows, symlink where supported), andSameFile.cmd/shim/fallback_test.go: end-to-end tests that run copies of the test binary as real shim processes.TestMaincaps the chain at 8 hops, so a regression fails the test instead of fork-bombing CI. Cases:nodeagain: not mistaken for the loopResolves #279