Skip to content

Isolate salt-pip's PYTHONPATH from the parent environment - #70157

Open
twangboy wants to merge 5 commits into
saltstack:3008.xfrom
twangboy:fix/70151/3008.x
Open

Isolate salt-pip's PYTHONPATH from the parent environment#70157
twangboy wants to merge 5 commits into
saltstack:3008.xfrom
twangboy:fix/70151/3008.x

Conversation

@twangboy

@twangboy twangboy commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

_pip_environment() in salt/scripts.py prepended salt's own extras directory onto whatever PYTHONPATH the calling process already had, rather than replacing it. Anything inherited there (e.g. leaked from an unrelated build/init process) stayed visible to salt's pip subprocess.

Combined with --force-reinstall, pip uninstalls whatever satisfies a requirement wherever it finds it on sys.path, not just in the target directory. An inherited PYTHONPATH pointing at an unrelated Python installation could therefore cause salt-pip to delete a package belonging to that unrelated environment.

PYTHONPATH is now set to just the extras directory by default, matching the isolation salt-pip already documents. Extras packages remain importable at runtime via the onedir's .pth file mechanism, which is independent of PYTHONPATH, so this doesn't affect normal usage.

Five new minion config options make this configurable rather than hardcoded, so a site can opt back into old behavior or lock salt-pip down further, instead of relying on every caller to pass the right flags by hand:

  • saltpip_use_pythonpath (default False): restores the old prepend-onto-inherited-PYTHONPATH behavior for sites that specifically need it. Isolated behavior stays the default.
  • saltpip_no_deps, saltpip_no_index, saltpip_disable_pip_version_check (all default False, i.e. unchanged behavior): force PIP_NO_DEPS / PIP_NO_INDEX / PIP_DISABLE_PIP_VERSION_CHECK on salt-pip's subprocess, so an operator can guarantee salt-pip never resolves dependencies, queries an index, or checks for a newer pip release. Implemented as environment variables rather than CLI flags, since salt-pip proxies arbitrary pip subcommands (list, show, uninstall, ...) and pip silently ignores these env vars on subcommands that don't use them, whereas the equivalent CLI flags would error out on those subcommands.
  • saltpip_allow_find_links (default True, matching current behavior): --find-links is independent of --no-index by pip's own design (confirmed empirically), so saltpip_no_index alone doesn't stop pip from fetching via an inherited/leaked PIP_FIND_LINKS. Set to False to strip it as well. Kept as a separate, independent option rather than folded into saltpip_no_index, since --find-links alongside a disabled index is pip's own documented air-gapped-install pattern — forcing it off whenever saltpip_no_index is on would break that legitimate use.

Known, deliberately deferred gap: an inherited PIP_INDEX_URL / PIP_EXTRA_INDEX_URL is left untouched by every option here. pip already ignores both once PIP_NO_INDEX=1 is set, so stripping them in that case has no effect; filtering them when saltpip_no_index is off is a separate gap, documented as out of scope rather than addressed with more options (weighed against adding saltpip_allow_index_url / saltpip_allow_extra_index_url, which would be silent no-ops whenever saltpip_no_index is enabled — not implemented). Direct URL/VCS requirements (pip install https://..., git+https://...) also bypass --no-index inherently and can't be blocked by any pip flag.

Documentation updates:

  • doc/topics/packaging/index.rst and salt/modules/pip.py: the pip execution module/state defaults to the same salt-pip / extras behavior on a onedir minion, how to target the system Python instead via bin_env, and an unsupported stop-gap procedure for patching a vulnerable bundled dependency directly.
  • doc/ref/configuration/minion.rst and doc/topics/packaging/index.rst: all five new config options, including why the network options are env-var-based and the saltpip_allow_find_links / saltpip_no_index independence rationale.

Test coverage added:

  • Unit tests in tests/pytests/unit/test_scripts.py and tests/pytests/unit/test_salt_pip_user.py covering the isolated-vs-opt-in PYTHONPATH behavior, each network-lockdown option, saltpip_allow_find_links (including that it's unaffected by saltpip_no_index's state), and each option flowing through salt_pip() end-to-end into the subprocess environment.
  • Functional, onedir-gated tests in tests/pytests/functional/cli/test_salt_pip.py confirming installing a salt extension that depends on a package already bundled in the onedir doesn't duplicate it into extras, and that salt-pip doesn't see or touch an unrelated ("system") Python's packages even under --force-reinstall.

What issues does this PR fix or reference?

Fixes #70151

Merge requirements satisfied?

[NOTICE] Bug fixes or features added to Salt require tests.

Commits signed with GPG?

Yes

@twangboy twangboy added this to the Argon v3008.3 milestone Aug 27, 2026
@twangboy twangboy self-assigned this Aug 27, 2026
@twangboy
twangboy requested a review from a team as a code owner August 27, 2026 16:22
@twangboy twangboy added the test:full Run the full test suite label Aug 27, 2026
_pip_environment() only ever adds env vars to the pip subprocess it
builds; it never strips anything already present. --find-links is
independent of --no-index by pip's own design (confirmed: `pip install
--no-index --find-links <url> pkg` still fetches via find-links), so
an inherited PIP_FIND_LINKS remained a live network path even with
saltpip_no_index enabled.

Add saltpip_allow_find_links (default True, matching current
behavior) to strip an inherited PIP_FIND_LINKS when set to False.
Deliberately independent of saltpip_no_index rather than folded into
it: --find-links alongside a disabled index is pip's own documented
air-gapped-install pattern, so forcibly stripping it whenever
saltpip_no_index is on would break that legitimate use.

PIP_INDEX_URL/PIP_EXTRA_INDEX_URL are left untouched by design: pip
already ignores both once PIP_NO_INDEX=1 is set, so stripping them in
that case has no effect, and filtering them outside that mode is a
separate, unaddressed gap - noted in the docstring and docs rather
than papered over with more options.

Related to saltstack#70151
The test didn't mock os.environ.copy() like its sibling test does, so
it picked up the real CI job's ambient environment. GitHub Actions'
onedir test jobs already set PIP_DISABLE_PIP_VERSION_CHECK=1 at the
job level for their own unrelated reasons, and _pip_environment()
correctly leaves pre-existing env vars alone when the corresponding
option is off - so that ambient value passed straight through and
failed the test's "must not be present" assertion.

Mock os.environ.copy to a clean, empty dict so the test controls its
own baseline environment instead of depending on the host/CI runner
being free of PIP_* vars. Reproduced the failure locally by setting
PIP_DISABLE_PIP_VERSION_CHECK=1 before running pytest, confirmed the
fix resolves it either way.

Related to saltstack#70151
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:full Run the full test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant