Skip to content

macOS shebang launcher (SHEBANG_TPL_MACOS) cd's away from caller cwd before exec'ing python, breaking relative paths #293

@digitalresistor

Description

@digitalresistor

What happened?

On macOS, relenv's polyglot shell+python launcher (SHEBANG_TPL_MACOS in relenv/common.py) cds into the script's own (post-symlink-resolution) directory while resolving REALPATH, and never returns to the caller's directory before exec-ing the bundled python. Python therefore inherits a cwd of the install dir (e.g. /opt/salt), not the directory the user invoked the command from.

Current template:

SHEBANG_TPL_MACOS = textwrap.dedent(
    """\
#!/bin/sh
"true" ''''
TARGET_FILE=$0
cd "$(dirname "$TARGET_FILE")" || return
TARGET_FILE=$(basename "$TARGET_FILE")
# Iterate down a (possible) chain of symlinks
while [ -L "$TARGET_FILE" ]
do
    TARGET_FILE=$(readlink "$TARGET_FILE")
    cd "$(dirname "$TARGET_FILE")" || return
    TARGET_FILE=$(basename "$TARGET_FILE")
done
PHYS_DIR=$(pwd -P)
REALPATH=$PHYS_DIR/$TARGET_FILE
# shellcheck disable=SC2093
"exec" "$(dirname "$REALPATH")"{} "$REALPATH" "$@"
' '''
"""
)

Note this is macOS-only: SHEBANG_TPL_LINUX uses readlink -f in a single command substitution and never cds, so the caller's cwd is preserved on Linux. The macOS template emulates readlink -f with a cd/readlink loop (since macOS lacks readlink -f pre-Big Sur era coreutils) but forgets to restore the original directory afterwards.

Real-world impact

Every entry point of the official Salt macOS onedir (salt, salt-ssh, salt-call, salt-run, ...) is generated from this template. Any relative path Salt resolves against os.getcwd() breaks: Saltfile auto-discovery from the current directory, config_dir: ./... inside a Saltfile, --config-dir=./..., root_dir: ./ in master config, ssh_pre_flight: ./... in a salt-ssh roster — all resolve against /opt/salt instead of the user's actual cwd.

Originally reported (with full reproducer against com.saltstack.salt 3007.14 / relenv 0.22.4) at saltstack/salt#69027 and re-filed here at the Salt maintainers' request, since the launcher template is generated by relenv and only consumed by Salt at install time.

Reproducer

$ mkdir -p /tmp/saltrepro && cd /tmp/saltrepro
$ cat > Saltfile <<'YAML'
salt-ssh:
  config_dir: ./salt_root
YAML
$ mkdir salt_root
$ cat > salt_root/master <<'YAML'
root_dir: ./
file_roots:
  base:
    - ./states
YAML
$ salt-ssh -l debug '*' test.ping
[DEBUG   ] Missing configuration file: /opt/salt/salt_root/master

./salt_root resolved against /opt/salt, not /tmp/saltrepro.

Expected behavior

Python's cwd at entry should be the directory the user invoked the command from, matching the Linux template's behavior. Concretely: capture pwd before the cd/readlink loop and cd back before the exec.

Suggested patch

SHEBANG_TPL_MACOS = textwrap.dedent(
    """\
#!/bin/sh
"true" ''''
ORIG_CWD=$(pwd)
TARGET_FILE=$0
cd "$(dirname "$TARGET_FILE")" || return
TARGET_FILE=$(basename "$TARGET_FILE")
# Iterate down a (possible) chain of symlinks
while [ -L "$TARGET_FILE" ]
do
    TARGET_FILE=$(readlink "$TARGET_FILE")
    cd "$(dirname "$TARGET_FILE")" || return
    TARGET_FILE=$(basename "$TARGET_FILE")
done
PHYS_DIR=$(pwd -P)
REALPATH=$PHYS_DIR/$TARGET_FILE
cd "$ORIG_CWD" || return
# shellcheck disable=SC2093
"exec" "$(dirname "$REALPATH")"{} "$REALPATH" "$@"
' '''
"""
)

Workaround

Invoke the bundled python on the launcher script directly from the desired cwd; the bash blob lives inside a Python triple-quoted string and is ignored when python parses the file:

cd "$REPO_ROOT" && exec /opt/salt/bin/python3.10 /opt/salt/salt-ssh "$@"

Versions

  • relenv 0.22.4 (as shipped in Salt 3007.14 official macOS .pkg)
  • macOS 15 / Darwin 25.4.0, Apple Silicon

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions