Skip to content

fix: never generate a shim named "asdf" during reshim - #2307

Open
ankit090701 wants to merge 4 commits into
asdf-vm:masterfrom
ankit090701:fix/reshim-self-shim-recursion
Open

fix: never generate a shim named "asdf" during reshim#2307
ankit090701 wants to merge 4 commits into
asdf-vm:masterfrom
ankit090701:fix/reshim-self-shim-recursion

Conversation

@ankit090701

Copy link
Copy Markdown

Summary

asdf reshim can generate a shim literally named asdf, which then shadows the real asdf binary on PATH and causes every subsequent asdf invocation to hang forever in infinite recursion:

> which asdf
/usr/local/bin/asdf
> asdf reshim
> which asdf
/home/kedare/.asdf/shims/asdf
> asdf &
> ps faux | grep asdf
... bash /home/kedare/.asdf/shims/asdf exec asdf exec asdf exec asdf exec asdf ...

Root cause

GenerateForVersion (internal/shims/shims.go) scans every installed tool version's bin directory (via ToolExecutables) and calls Write for each executable found, with no exclusion list. Write derives the shim's name purely from filepath.Base(executablePath) and writes it unconditionally.

If a plugin-managed toolchain happens to produce a binary literally named asdf in its bin directory - the comment on the issue traced this to a Go toolchain having go install-ed asdf's own source at some point, landing an asdf binary under ~/.asdf/installs/golang/*/go/bin/ - reshim will generate shims/asdf for it. Since the shims directory precedes the real asdf binary on PATH, every asdf call afterward resolves to that shim instead, whose generated body is:

exec asdf exec "asdf" "$@"

which itself resolves to the same shim, recursing forever.

Fix

Added a small reserved-names check at the top of shims.Write: if the derived shim name is asdf, skip writing a shim for it entirely, regardless of which plugin/version/bin-directory it was found in. asdf should never be shimmed, since asdf itself must always remain directly callable.

Test plan

  • go build ./...
  • Added a subtest to the existing TestWrite in internal/shims/shims_test.go: calls Write with an executable path whose basename is asdf and asserts no shim file is created at shims/asdf
  • go test ./internal/shims/... — all pass (this package doesn't build on native Windows due to golang.org/x/sys/unix symbols like Access/X_OK not existing for that GOOS, so I validated using a Linux container: docker run --rm -v $(pwd):/src -w /src golang:1.26 go test ./internal/shims/... -v)
  • Also ran the full repo test suite in the same container for extra confidence; internal/shims is fully green (11/11 tests). Three unrelated packages (cmd/asdf, internal/execute, internal/help) show failures on my Windows dev machine, but they're plainly CRLF line-ending artifacts from my local checkout (visible \r\n bytes leaking into fixture/help-text comparisons in the failure output) - unrelated to internal/shims and not something I touched

Fixes #2166

A plugin-managed toolchain can end up installing a binary that
happens to be named "asdf" - for example, if asdf's own source is
built via `go install` under an asdf-managed Go, the resulting
$GOPATH/bin/asdf binary sits in a directory reshim scans for
executables. `asdf reshim` doesn't special-case this and generates a
shim for it like any other executable.

Since the shims directory is placed ahead of the real asdf binary on
PATH, every subsequent `asdf` invocation resolves to that shim
instead. The shim's generated body is `exec asdf exec "asdf" "$@"`,
which itself resolves to the same shim again, recursing into itself
forever and hanging every asdf command.

Add a small reserved-names check in shims.Write, so a shim is never
generated for "asdf" regardless of which installed tool's bin
directory it came from.

Fixes asdf-vm#2166
@ankit090701
ankit090701 requested a review from a team as a code owner July 31, 2026 19:41
@Stratus3D
Stratus3D requested a lite review from Copilot August 5, 2026 00:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents asdf reshim from generating a shim named asdf, which can shadow the real asdf binary on PATH and cause infinite recursion/hangs.

Changes:

  • Add a reserved shim-name check in shims.Write to skip generating a shim named asdf.
  • Add a regression subtest ensuring Write does not create shims/asdf when given an executable whose basename is asdf.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
internal/shims/shims.go Introduces a reserved-name guard in Write to prevent generating a shim named asdf.
internal/shims/shims_test.go Adds a regression test to assert no shims/asdf is created.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/shims/shims.go
Comment on lines +327 to +330
shimName := filepath.Base(executablePath)
if reservedShimNames[shimName] {
return nil
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in a3ad2e1Write now removes any pre-existing shim under the reserved name (ignoring os.IsNotExist) before returning, instead of just skipping creation. This matters specifically for asdf reshim <tool> <version>, which calls Write directly without a prior RemoveAll (unlike bare asdf reshim), so a stale shims/asdf from before this guard existed would otherwise persist indefinitely on that path.

Comment on lines +399 to +406
t.Run("never creates a shim named asdf, no matter what executable is passed", func(t *testing.T) {
fakeAsdfExecutable := filepath.Join(filepath.Dir(executable), "asdf")
err := Write(conf, plugin, version, fakeAsdfExecutable)
assert.Nil(t, err)

shimPath := Path(conf, "asdf")
_, statErr := os.Stat(shimPath)
assert.True(t, os.IsNotExist(statErr), "expected no shim to be created for the reserved name \"asdf\"")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test for this in a3ad2e1 — "removes a pre-existing shim named asdf left over from before this guard existed". It pre-creates a stale shims/asdf, calls Write, and asserts the file is gone afterward, so it covers the removal behavior from the first comment as well as the upgrade scenario you're describing.

ankit090701 and others added 3 commits August 5, 2026 12:22
…w one

asdf reshim <tool> <version> calls Write directly without RemoveAll,
so a shims/asdf left over from before this guard existed would
otherwise persist (and keep recursing) until a bare `asdf reshim`
happened to run.

Addresses Copilot review feedback on the guard added in 313194e.
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.

bug: asdf shimming itself causing infinite recursion

3 participants