Skip to content

With several Poetry envs for one project (poetry env use on two Python minors), agent mode patches the alphabetically first env instead of the one Poetry activated, and VEX attests not_affected #526

Description

[agent] Found by the scheduled Poetry bug-hunt routine (ledger #311).

Summary

Poetry keeps one out-of-tree virtualenv per Python minor you've used with a project (<name>-<hash>-py3.11, <name>-<hash>-py3.12, …). It runs and installs into the activated one, which is recorded in <virtualenvs.path>/envs.toml by poetry env use ([<name>-<hash>] minor = "3.12").

Since #330 (5678b76), poetry_virtualenv_site_packages returns every matching -py* env, sorted by directory name (crates/socket-patch-core/src/crawlers/python_crawler.rs:881, venvs.sort()). It never reads envs.toml. Agent mode then patches only one copy: the first in lexicographic order. When that isn't the activated env, the results are:

  • scan --mode agent reports added (exit 0) and patches the inactive env. The env that poetry run / poetry shell use stays unpatched.
  • A follow-up apply reports already_patched and does nothing.
  • vex attests not_affected / inline_mitigations_already_exist for the project.

Because the sort is lexicographic, the pick isn't even "lowest version": py3.10 sorts before py3.13, and py3.10 would sort before py3.9.

Impact

This is a common setup: anyone who has moved a project to a newer Python with poetry env use 3.12 keeps the old env around. Patches land in a dead env, and the VEX document says the live one is fixed. The run looks fully successful, with nothing printed to stderr.

Repro (real Poetry, Linux, main 61cfb9b)

Any agent patch for six@1.16.0 works. I used a local mock of the patch API (batch + view, the same shapes as crates/socket-patch-cli/tests/in_process_pypi_multi_release.rs).

SP=/path/to/target/release/socket-patch
A="--api-url $MOCK --api-token sktsec_..._api --org o"
mkdir envmulti && cd envmulti
cat > pyproject.toml <<'T'
[tool.poetry]
name = "envmulti"
version = "0.1.0"
description = ""
authors = ["x <x@x>"]
package-mode = false
[tool.poetry.dependencies]
python = ">=3.10"
six = "1.16.0"
T
poetry lock
poetry env use python3.11 && poetry install --no-root
poetry env use python3.12 && poetry install --no-root     # py3.12 is now the activated env
poetry env list --full-path
#   …/envmulti-HDRmhnzz-py3.11
#   …/envmulti-HDRmhnzz-py3.12 (Activated)
$SP scan --mode agent $A --ecosystems pypi --yes --json   # status success, action "added"
grep -c SOCKET-PATCHED ~/.cache/pypoetry/virtualenvs/envmulti-*/lib/*/site-packages/six.py
#   …-py3.11/lib/python3.11/site-packages/six.py:1   <- patched (inactive env)
#   …-py3.12/lib/python3.12/site-packages/six.py:0   <- NOT patched (activated env)
poetry run python -c "import six; print(six.__file__)"    # …-py3.12/…/six.py, unpatched
$SP apply $A --ecosystems pypi --json                       # skipped, already_patched
$SP vex $A --output vex.json                                # exit 0, statement: not_affected

Expected vs actual

  • Expected: the patch goes to the env Poetry uses for the project, or to every env that holds the package. Fix Poetry venv discovery to match Poetry (#327, #329) #330's stated goal is venv discovery that matches Poetry. For that, the activated env (envs.toml's minor, else the env matching the interpreter Poetry would pick) has to come first. vex should only attest copies that are actually patched.
  • Actual: the alphabetically first <name>-<hash>-py* env is patched. The activated env keeps upstream bytes, apply calls it already_patched, and vex attests not_affected.

OS × version matrix (Linux, main 61cfb9b)

There were 9 independent fresh-project runs. The first row ran twice. Every interpreter here is a real separate CPython (Ubuntu 3.10, 3.11, 3.12 and 3.13), with Poetry pip-installed into its own venv.

Poetry envs (first created → activated) virtualenvs.path Patched env Activated env patched? VEX
2.5.1 3.11 → 3.12 custom (POETRY_VIRTUALENVS_PATH) py3.11 no not_affected ✗
2.5.1 3.11 → 3.12 default cache dir, package-mode = false py3.11 no not_affected ✗
2.5.1 3.11 → 3.12 default cache dir, package mode py3.11 no not_affected ✗
2.5.1 3.10 → 3.13 custom py3.10 no not_affected ✗
1.8.5 3.11 → 3.12 custom py3.11 no not_affected ✗
2.5.1 3.12 → 3.11 custom py3.11 yes (by luck) ok
2.5.1 3.13 → 3.10 custom py3.10 yes (by luck) ok
1.8.5 3.12 → 3.11 custom py3.11 yes (by luck) ok

macOS and Windows aren't tested (this routine's probe branches are blocked), but the selection logic is platform-independent.

First bad version

This isn't a regression in a release. v4.0.0 and v3.3.0 don't discover these out-of-tree envs at all in this layout: package_not_installed, exit 0 (the #327 family). The wrong-env pick arrived with #330 (5678b76), which is unreleased.

Suspect code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions