From 74188359086b17801d21900a166ac39d008da286 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Fri, 19 Dec 2025 00:28:32 +0300 Subject: [PATCH] Prevent infinite loop if a shim is linked and called from a nonstandard location with "system" active --- libexec/pyenv-exec | 8 ++++++ libexec/pyenv-rehash | 4 +++ libexec/pyenv-which | 24 ++++++++++------- test/exec.bats | 23 ++++++++++++++++ test/rehash.bats | 25 +++++++++++++++++ test/shims-linked-from-elsewhere.bats | 39 +++++++++++++++++++++++++++ test/test_helper.bash | 12 ++++++--- test/which.bats | 26 ++++++++++++++++++ 8 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 test/shims-linked-from-elsewhere.bats diff --git a/libexec/pyenv-exec b/libexec/pyenv-exec index 6ed5ac20..2368bdd3 100755 --- a/libexec/pyenv-exec +++ b/libexec/pyenv-exec @@ -29,6 +29,14 @@ if [ -z "$PYENV_COMMAND" ]; then exit 1 fi +if [[ -n $_PYENV_SHIM_PATH ]]; then + PROGRAM="$(echo "$PYENV_COMMAND" | tr a-z- A-Z_ | sed 's/[^A-Z0-9_]/_/g')" + NR_PYENV_SHIM_PATHS_PROGRAM="_PYENV_SHIM_PATHS_${PROGRAM}" + export -- "$NR_PYENV_SHIM_PATHS_PROGRAM=$_PYENV_SHIM_PATH${!NR_PYENV_SHIM_PATHS_PROGRAM:+:${!NR_PYENV_SHIM_PATHS_PROGRAM}}" + unset PROGRAM NR_PYENV_SHIM_PATHS_PROGRAM + unset _PYENV_SHIM_PATH +fi + PYENV_COMMAND_PATH="$(pyenv-which "$PYENV_COMMAND")" PYENV_BIN_PATH="${PYENV_COMMAND_PATH%/*}" export PYENV_VERSION diff --git a/libexec/pyenv-rehash b/libexec/pyenv-rehash index 745cfb8e..61000ff6 100755 --- a/libexec/pyenv-rehash +++ b/libexec/pyenv-rehash @@ -82,6 +82,10 @@ set -e program="\${0##*/}" export PYENV_ROOT="$PYENV_ROOT" +SHIM_PATH=\${0%/*} +if [[ \$SHIM_PATH != "$PYENV_ROOT/shims" ]]; then + export _PYENV_SHIM_PATH="\$SHIM_PATH" +fi exec "$(command -v pyenv)" exec "\$program" "\$@" SH chmod +x "$PROTOTYPE_SHIM_PATH" diff --git a/libexec/pyenv-which b/libexec/pyenv-which index 9ff3ddae..0ebf8219 100755 --- a/libexec/pyenv-which +++ b/libexec/pyenv-which @@ -42,15 +42,19 @@ done remove_from_path() { - local path_to_remove="$1" - local path_before + local -a paths_to_remove + IFS=: paths_to_remove=($1) + local path_to_remove path_before local result=":${PATH//\~/$HOME}:" - while [ "$path_before" != "$result" ]; do - path_before="$result" - result="${result//:$path_to_remove:/:}" + for path_to_remove in "${paths_to_remove[@]}"; do + while true; do + path_before="$result" + result="${result//:$path_to_remove:/:}" + if [[ ${#path_before} == "${#result}" ]]; then break; fi + done done - result="${result%:}" - echo "${result#:}" + result="${result:1:${#result}-2}" + echo "$result" } if [ -z "$PYENV_COMMAND" ]; then @@ -66,8 +70,10 @@ declare -a nonexistent_versions for version in "${versions[@]}" "$system"; do if [ "$version" = "system" ]; then - PATH="$(remove_from_path "${PYENV_ROOT}/shims")" - PYENV_COMMAND_PATH="$(command -v "$PYENV_COMMAND" || true)" + PROGRAM="$(echo "$PYENV_COMMAND" | tr a-z- A-Z_ | sed 's/[^A-Z0-9_]/_/g')" + NR_CUSTOM_SHIM_PATHS="_PYENV_SHIM_PATHS_$PROGRAM" + SEARCH_PATH="$(remove_from_path "${PYENV_ROOT}/shims${!NR_CUSTOM_SHIM_PATHS:+:${!NR_CUSTOM_SHIM_PATHS}}")" + PYENV_COMMAND_PATH="$(PATH="$SEARCH_PATH" command -v "$PYENV_COMMAND" || true)" else # $version may be a prefix to be resolved by pyenv-latest version_path="$(pyenv-prefix "${version}" 2>/dev/null)" || \ diff --git a/test/exec.bats b/test/exec.bats index c1db5fbc..ff49912b 100644 --- a/test/exec.bats +++ b/test/exec.bats @@ -116,3 +116,26 @@ OUT PYENV_VERSION=system:custom run pyenv-exec python3 assert_success "$PATH" } + +@test "sets/adds to _PYENV_SHIM_PATHS_{PROGRAM} when _PYENV_SHIM_PATH is set, unsets _PYENV_SHIM_PATH" { + progname='123;wacky-prog.name ^%$#' + envvarname="_PYENV_SHIM_PATHS_123_WACKY_PROG_NAME_____" + create_path_executable "$progname" </dev/null || true" assert_success assert [ -x "${PYENV_ROOT}/shims/python" ] } + +@test "shim sets _PYENV_SHIM_PATH when linked from elsewhere" { + export PYENV_VERSION="custom" + create_alt_executable python3 + #must stub pyenv before rehash 'cuz the path is hardcoded into shims + create_stub pyenv </dev/null | grep ^XDG_ | cut -d= -f1`; do unset "$xdg_var"; done unset xdg_var @@ -128,7 +128,7 @@ path_without() { if [ "$found" != "${PYENV_ROOT}/shims" ]; then alt="${PYENV_TEST_DIR}/$(echo "${found#/}" | tr '/' '-')" mkdir -p "$alt" - for util in bash head cut readlink greadlink; do + for util in bash head cut readlink greadlink tr sed; do if [ -x "${found}/$util" ]; then ln -s "${found}/$util" "${alt}/$util" fi @@ -156,9 +156,13 @@ create_alt_executable_in_version() { create_executable "${PYENV_ROOT}/versions/$version/bin" "$@" } +create_stub() { + create_executable "${BATS_TEST_TMPDIR}/stubs" "$@" +} + create_executable() { - bin="${1:?}" - name="${2:?}" + local bin="${1:?}" + local name="${2:?}" shift 2 mkdir -p "$bin" local payload diff --git a/test/which.bats b/test/which.bats index 75601b0c..e54c1704 100644 --- a/test/which.bats +++ b/test/which.bats @@ -193,3 +193,29 @@ exit pyenv: py.test: command not found OUT } + +@test "excludes paths in _PYENV_SHIM_PATHS_{PROGRAM} from search only for PROGRAM" { + progname='123;wacky-prog.name ^%$#' + envvarname="_PYENV_SHIM_PATHS_123_WACKY_PROG_NAME_____" + create_path_executable "$progname" + + for dir_ in "$BATS_TEST_TMPDIR/alt-path"{1,2}; do + mkdir -p "$dir_" + ln -s "${PYENV_TEST_DIR}/bin/$progname" "$dir_/$progname" + eval 'export '"$envvarname"'="$dir_${'"$envvarname"':+:$'"$envvarname"'}"' + PATH="$dir_:$PATH" + done + create_executable "$dir_" "normal_program" + + run pyenv-which "$progname" + assert_success + assert_output <