Threats affecting pkg:brew/conda-lock
70 threats
Most recently published first
| Severity | CVSS | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|
| Low | 1.0 | Low1.0GCVE Database ### Summary On Windows, attacker-controlled content in a wheel's `.dist-info/namespace_packages.txt` can cause setuptools' legacy wheel installation path to create an `__init__.py` outside the requested `destination_eggdir`. The attacker can influence the destination path, but not the contents of the created file. ### Details Tested setuptools commit: `6ead555c5fb29bc57fe6105b1bffc163f56fd558` Relevant source: `setuptools/wheel.py` SHA-256: `359b7e0d5dd8c3135653abe08a8ed5dc0352eadfd7df22cb5e2b5303b7c3cafd` The relevant call path is: `Wheel(wheel_path).install_as_egg(destination)` → `_install_as_egg` → `_convert_metadata` → `_fix_namespace_packages` A wheel can provide attacker-controlled content in: `<distribution>.dist-info/namespace_packages.txt` The namespace value is eventually used approximately as: ```python mod_dir = os.path.join(destination_eggdir, *mod.split('.')) mod_init = os.path.join(mod_dir, '__init__.py') If mod is a Windows drive-qualified absolute path containing no dots, mod.split('.') preserves it as a single component. On Windows, os.path.join(destination_eggdir, absolute_component) discards the original base directory and resolves to the attacker-supplied absolute path. The existing wheel filename and ZIP traversal checks do not validate this metadata value before it is subsequently interpreted as a filesystem path. ```markdown ### PoC The reproduction was performed entirely inside a disposable temporary directory. Layout: ```text sandbox/ destination/ outside/ input/ canary.txt ``` A synthetic wheel was created containing: `demo_pkg-1.0.dist-info/namespace_packages.txt` whose content was the absolute path of: `sandbox\outside` The real acquired setuptools implementation was then invoked: ```python setuptools.wheel.Wheel(synthetic_wheel).install_as_egg( sandbox_destination ) ``` The operation completed without exception. Expected metadata was created under: `sandbox\destination\EGG-INFO` but setuptools also created: `sandbox\outside\__init__.py` The file did not exist before execution. Its contents were: ```python __import__('pkg_resources').declare_namespace(__name__) ``` The unrelated canary remained byte-identical. The sandbox was deleted after recording the evidence. ``` ### Impact A crafted wheel can cause setuptools to create a fixed-content `__init__.py` outside the intended installation directory at a filesystem path influenced by the wheel, with the permissions of the process performing the installation. The demonstrated reproduction required the target parent directory to already exist. | GCVE Database | ||
| High | 7.5 | CVE-2026-114613MessagePack · Unbounded memory leak in msgpack C extension on strict_map_key validation failure High7.5GCVE DatabaseCVE-2026-114613 ### Impact The msgpack-python C extension (`msgpack._cmsgpack`) leaks a decoded map value when deserialization rejects a key with `strict_map_key=True` (the default). Repeated malformed inputs can cause unbounded process memory growth and denial of service in applications that deserialize attacker-controlled MessagePack data. Both `unpackb()` and the streaming `Unpacker` API are affected. The pure-Python fallback does not exhibit this leak in the reported tests. The report measured approximately 4 MiB retained per failed parse and 100 MiB after 25 failed parses on Windows with Python 3.14 and msgpack 1.2.2. These are reporter-provided results. ### Additional impact: stale parser-stack references The reference-ownership audit performed while fixing this leak found an additional memory-safety issue in the streaming `Unpacker`. After `unpack()` completed a nested container, an inactive parser-stack slot could retain a non-owning pointer to the decoded object. A subsequent `skip()` could reactivate that slot. If parsing then failed, context cleanup treated the stale pointer as an owned reference and decremented it. This corrupts the object's reference count and can result in a use-after-free or double `Py_DECREF` when its actual owners later release it. Although few users use this API, but those who do may experience crashes or unexpected errors due to a use-after-free. ### Details In `msgpack/unpack.h`, `unpack_callback_map_item()` rejects keys that are neither `str` nor `bytes`. At this point the map value has already been constructed. The early error return does not release the value reference. In `msgpack/unpack_template.h`, the value is held in the local `obj` in `unpack_execute()`. The `CT_MAP_VALUE` error path jumps to `_failed` without decrementing that reference. Cleanup of the parser stack does not recover the value because it is not stored in the stack. The fallback checks the key before decoding the value. ### Affected versions The report reproduces the issue on **1.2.2** and reports it on development commit `475c1b7`. The affected-product field lists the reproduced release only; it is not a claim that earlier releases are unaffected. The earliest affected release needs confirmation: the report's summary says 1.1.x through 1.2.2, while its root-cause analysis attributes introduction to commit `cd81370` (PR #673). Resolve this discrepancy before publishing a definitive affected-version range. ### Reproduction Run in a disposable local process with the C extension installed: ```python import msgpack payload = msgpack.packb({42: b"A" * (4 * 1024 * 1024)}, use_bin_type=True) msgpack.unpackb(payload) # Raises ValueError; the report observes a 4 MiB leak. ``` Streaming API trigger: ```python import msgpack payload = msgpack.packb({42: b"A" * (4 * 1024 * 1024)}, use_bin_type=True) u = msgpack.Unpacker() u.feed(payload) list(u) # Same strict_map_key error path. ``` ### Workarounds The pure-Python fallback is reported not to leak on this path and may be used as a temporary workaround after application compatibility and performance checks. Request-size and rate limits may reduce the impact but do not eliminate cumulative leakage. | GCVE Database | ||
| High | 7.5 | CVE-2026-114145Conda lock · GitPython validates a submodule's ".git" path only after cloning into it (RCE) High7.5GCVE DatabaseCVE-2026-114145 ### Summary `create_submodule(name, path, url)` clones a submodule into the repo's own `.git` directory before it rejects the path. A `path` of ".git/hooks" is accepted by the pre-clone check, so the submodule is cloned straight into the live hooks directory, and a file named `post-checkout` in the submodule becomes an executable `.git/hooks/post-checkout`. On 3.2.0+ the later index check does raise `ValueError`, but only after the clone already wrote to disk, and it does not remove the planted hook; on 3.1.x there is no such check and the call succeeds silently. Either way the hook runs on the next git operation (e.g. checkout), giving code execution. ### Details The checkout path is validated too late, after the clone has already mutated disk. This breaks the "validate before mutation" invariant that commit 43a43cd8 ("Validate submodule checkout and metadata paths before mutation") set out to enforce. Submodule.add() calls the permissive path check before cloning, and the strict check only reaches the ".git" alias at index time, after _clone_repo has written to disk: git/objects/submodule/base.py (Submodule.add) 599: path = cls._to_relative_path(repo, path) # accepts ".git/hooks" 673: mrepo = cls._clone_repo(repo, url, path, name, ...) # clones into <worktree>/.git/hooks 714: index.add([sm], write=True) # strict check only triggers here git/index/base.py (IndexFile._preprocess_add_items) 732: _validate_repo_path(entry.path) # rejects ".git", too late _to_relative_path only blocks `..`/absolute/out-of-tree paths; `.git` aliases are rejected only by _validate_repo_path, which was added in 3.2.0 (commit a75aedf3) and runs at the index boundary. The ValueError it raises at line 732 does not roll back what _clone_repo already wrote. Version-dependent behavior: - 3.2.0 .. tip bda18e08: _validate_repo_path raises ValueError at index.add, but the hook is already on disk and is not removed (incorrect order of validation vs. clone). - 3.1.x (e.g. 3.1.59): _validate_repo_path does not exist and _to_relative_path does not reject `.git`; create_submodule(path=".git/hooks", ...) returns with no error at all and the hook is planted silently. ### Proof of concept Setup: evilsub/ : a git repo whose ROOT contains an executable file named post-checkout (the payload) victim/ : a git repo with at least 1 commit and a second branch, whose .git/hooks has NO sample hooks (the state of a minimal/CI checkout, or `git init` with an empty template dir, or GIT_TEMPLATE_DIR/init.templateDir empty) import git, os.path as osp try: git.Repo("victim").create_submodule(name="x", path=".git/hooks", url=osp.abspath("evilsub")) except ValueError: pass # 3.2.0+: ".git" is rejected, but only AFTER the clone, so the plant already happened. # 3.1.x: no exception is raised at all; the call succeeds and still plants the hook. #### victim/.git/hooks/post-checkout now exists, mode 0755, attacker-controlled, on every #### affected version. Any later checkout runs it: #### git -C victim checkout <other-branch> -> the hook runs -> code execution Verified live: - tip bda18e08 (3.2.0 + 3 commits): create_submodule raised ValueError from git/index/base.py:732, yet .git/hooks/post-checkout was present and executable, and `git checkout` executed it. - 3.1.59: create_submodule returned with no error; .git/hooks/post-checkout was planted and executed by `git checkout`. ### Impact Arbitrary code execution. A library/app that creates a submodule from an attacker-influenced `path` ends up with an attacker-controlled executable in `.git/hooks`, which git runs on ordinary later operations. This affects any service that derives a submodule checkout path from untrusted data, e.g. one that ingests a submodule spec (name/path/url) from a user-supplied manifest or API request and feeds `path` into create_submodule. On 3.2.0+ the library's own validation is the thing that fails: it correctly rejects the `.git` path, but only after the submodule has already been cloned into place, so an app that traps the ValueError reasonably believes the unsafe path was blocked. Preconditions (hence AC:H): (1) the app passes an attacker-influenced submodule `path`; (2) the target hook name is free in `.git/hooks`, true for minimal/CI/container checkouts and whenever init.templateDir/GIT_TEMPLATE_DIR is empty. With the default sample hooks present, `git clone` refuses the non-empty destination and no plant occurs. Affected: all tested versions from 3.1.x (3.1.59) through the current tip bda18e08 (3.2.0 + 3 commits). 3.1.x plants silently; 3.2.0+ plants despite raising ValueError. | GCVE Database | ||
| High | 7.8 | CVE-2026-113776Conda lock · virtualenv activation scripts run code from a path that contains a placeholder name High7.8GCVE DatabaseCVE-2026-113776 ### Impact `ViaTemplateActivator.instantiate_template` replaces the placeholders in an activation template one key at a time, each across the whole text. When a value that it already substituted and quoted contains the name of a later placeholder, the later pass rewrites that name inside the quoted literal. The inserted value brings its own quotes, which close the outer literal, and the rest of the path runs as code when a user activates the environment. `__VIRTUAL_NAME__` holds the environment's directory name, so a destination such as `A__VIRTUAL_NAME__B/x'$(touch PWNED)'y` is enough. In the default `activate` for bash, the `VIRTUAL_ENV` line renders as: ```sh VIRTUAL_ENV='/tmp/t/A'x'"'"'$(touch PWNED)'"'"'y'B/x'"'"'$(touch PWNED)'"'"'y' ``` Sourcing that script runs `touch PWNED`. The same payload in a path without the placeholder name runs nothing. `activate.fish` runs the command the same way, and the reporter also showed Python code execution through `activate.xsh` and `bin/activate_this.py`. In `activate.csh`, a directory named after a prompt display placeholder, such as `__VIRTUAL_PROMPT_DISPLAY_TCSH__`, breaks the script with `Unmatched '''`. No change to `quote()` can stop this, because the rewrite happens after `quote()` ran; it defeats `shlex.quote` and `repr()` alike. An attacker needs to influence the destination path or `--prompt`: a CI job or wrapper tool that builds the path from a branch or project name, or a directory in a cloned repository or unpacked archive. The bug affects versions 20.26.6 through 21.14.1. Version 20.26.6 first quoted template values (#2768) and introduced the per-key loop. ### Patches Fixed in 21.14.2. Activation scripts now keep a placeholder name in the environment's path or prompt as plain text, and activating the example above runs nothing. Fix: https://github.com/pypa/virtualenv/commit/602b26623a724827ff171bf11fef4ad186d01f74 ### Workarounds Do not create an environment whose path or `--prompt` contains a placeholder name such as `__VIRTUAL_NAME__`, `__VIRTUAL_ENV__` or `__VIRTUAL_PROMPT__`, and inspect the generated activation script before you source one you did not create. ### Credits Kevin Stubbings (@Kwstubbs) of GitHub Security Lab reported this issue. | GCVE Database | ||
| High | 7.8 | CVE-2026-113732Conda lock: virtualenv activate.fish runs commands from a path or prompt with a backslash before a quote High7.8GCVE DatabaseCVE-2026-113732 ### Impact `FishActivator` defines no `quote()`, so it inherits `ViaTemplateActivator.quote`, which returns `shlex.quote(string)`. That function follows POSIX `sh` rules, where a backslash inside single quotes stays a literal character. fish reads `\'` inside single quotes as an escaped quote and `\\` as an escaped backslash. A value with a backslash before a quote therefore ends the literal somewhere other than where `shlex.quote` put the end, and fish expands the command substitution that follows. The environment path reaches `set -gx VIRTUAL_ENV`, and `--prompt` or `VIRTUALENV_PROMPT` reaches `set -gx VIRTUAL_ENV_PROMPT`. For `--prompt "x\'\$(touch PWNED)'\'"`, the generated `activate.fish` contains: ```fish set -gx VIRTUAL_ENV_PROMPT 'x\'"'"'$(touch PWNED)'"'"'\'"'"'' ``` `source bin/activate.fish` runs `touch PWNED`, and fish prints no error. The same payload as a directory name runs through `VIRTUAL_ENV`. `--prompt .` takes the current directory's name, so running virtualenv inside a directory someone else named is enough. Without the backslash the payload runs nothing, and under bash it runs nothing either. virtualenv generates `activate.fish` by default. An attacker needs to influence the environment path or prompt: a CI job or wrapper tool that passes a branch, project or job name through, or a directory in a cloned repository or unpacked archive. The bug affects every release up to 21.14.1 that ships `activate.fish`. The GHSA-p58f-9548-mpm2 fix in 21.7.13 moved the placeholders outside their quotes but kept `shlex.quote`. ### Patches Fixed in 21.14.2. `activate.fish` now escapes backslashes and single quotes the way fish reads them, so the example above runs nothing and shows the prompt as given. Fix: https://github.com/pypa/virtualenv/commit/3bddeaf8d02c3ee7f576503efb082f8f494cfcab ### Workarounds Do not create an environment whose path or prompt contains a backslash, and inspect `activate.fish` before you source one you did not create. ### Credits Kevin Stubbings (@Kwstubbs) of GitHub Security Lab reported this issue. | GCVE Database | ||
| High | 7.8 | CVE-2026-113726Conda lock · virtualenv activate runs commands from the environment name in zsh with PROMPT_SUBST High7.8GCVE DatabaseCVE-2026-113726 ### Impact zsh users activate an environment with the bash `activate` script, which puts the environment name into `PS1`. Since 21.13.0 the script escapes backslashes, dollar signs and backticks in that name, but only when `BASH_VERSION` is set, so zsh gets the raw value. With the `PROMPT_SUBST` option set, zsh runs parameter expansion, command substitution and arithmetic expansion on `PS1` each time it draws the prompt. A name that contains `$(...)`, backticks or `${...}` then runs as commands in the user's shell, once per prompt. The name comes from the environment directory, or from `--prompt` or `VIRTUALENV_PROMPT`. After you create an environment with: ```console $ virtualenv 'x$(touch PWNED)y' ``` `source 'x$(touch PWNED)y/bin/activate'` in a zsh session with `PROMPT_SUBST` set creates `PWNED`, and the prompt shows `(xy)`. Passing that value as `--prompt` to an environment with a plain name has the same effect. Without `PROMPT_SUBST`, zsh shows the name as text and runs nothing. bash runs nothing either. A plain zsh starts with `PROMPT_SUBST` off. oh-my-zsh users have it on: oh-my-zsh loads [`lib/theme-and-appearance.zsh`](https://github.com/ohmyzsh/ohmyzsh/blob/4d4cfc287e9d887b81242c0e431b5f49f9cec5c1/lib/theme-and-appearance.zsh#L5) at startup, and that file sets it. An attacker needs to influence the environment path or prompt: a CI job or wrapper tool that passes a branch, project or job name through, or a directory in a cloned repository or unpacked archive. `--prompt .` takes the current directory's name. zsh also reads `%` in `PS1` as a prompt escape, so a name with `%n` showed the user name in place of those characters, though without running commands. The bug is in releases up to and including 21.14.3. The payload runs on 20.26.6 and 21.14.3, and the reporter confirmed 21.9.1. ### Patches Fixed in 21.14.4. Under zsh, `activate` now escapes the name the way it does under bash and doubles `%`, so the example above runs nothing and the prompt shows the name as given. zsh gets the escaping even when `PROMPT_SUBST` is off, because a user can set the option after activation; a name with a backslash, dollar sign or backtick then shows the added backslashes. Fix: https://github.com/pypa/virtualenv/commit/5b915367da82bd024b2a0d28d2f98007034b4aa2 ### Workarounds Set `VIRTUAL_ENV_DISABLE_PROMPT=1` before you source `activate`, which leaves `PS1` alone, or run `unsetopt prompt_subst` if your prompt does not need it. Check the name of an environment you did not create before you source its `activate`. ### Credits Kaya Emre Arikan (@kemrec) reported this issue. | GCVE Database | ||
| High | 7.8 | CVE-2026-113725Conda lock · virtualenv bash activate runs commands from an environment name with a backslash before $ High7.8GCVE DatabaseCVE-2026-113725 ### Summary The bash `activate` script escapes the environment name for only one of the two passes bash makes over `PS1`, so a backslash in the name undoes the escaping and a name containing `\$(...)` runs as commands in the user's shell each time bash draws the prompt. ### Affected area `src/virtualenv/activation/bash/activate.sh`, the block that prepends the environment name to `PS1`. ### Attacker-controlled input The environment name: the environment directory's name, or the `--prompt` or `VIRTUALENV_PROMPT` value. An attacker needs to influence it, for example through a CI job or wrapper tool that passes a branch, project or job name through, or a directory in a cloned repository or unpacked archive. `--prompt .` takes the current directory's name. ### Details bash handles `PS1` in two passes each time it draws the prompt: it first decodes backslash prompt escapes, turning `\\` into `\` and `\$` into `$` (or `#` for root), and then runs parameter expansion, command substitution and arithmetic expansion on the result. Since 21.13.0 the script escapes backslashes, dollar signs and backticks in the name, but only for the second pass, so the decode pass strips the escaping that a backslash in the name precedes. Releases before 21.13.0 do not escape the name at all. A bash running as root decodes `\$` to `#`, so the payload shows the wrong text there but runs nothing. zsh does no decode pass and is not affected. ### Proof of concept ```console $ virtualenv --prompt 'a\$(touch PWNED)' env $ bash --norc --noprofile -c 'source env/bin/activate; printf "%s" "${PS1@P}"' $ ls PWNED PWNED ``` ### Impact Arbitrary command execution in the shell of a user who sources the `activate` script of an environment whose name an attacker influenced, with that user's privileges, once per prompt draw. ### Versions tested 21.14.6 with bash 5.2.15 on Debian bookworm, as an unprivileged user and as root. ### Known mitigation Set `VIRTUAL_ENV_DISABLE_PROMPT=1` before you source `activate`, which leaves `PS1` alone, or run `shopt -u promptvars` if your prompt does not need expansion. Check the name of an environment you did not create before you source its `activate`. ### Latest release - [x] The problem reproduces on the latest virtualenv release. 21.14.6 ### Patches Fixed in 21.14.7. Under bash, `activate` now escapes backslashes, dollar signs and backticks for both passes, so the example above runs nothing and the prompt shows the name as given. zsh keeps the single escaping it needs. Fix: https://github.com/pypa/virtualenv/commit/c3104e02474b652832a9bbaae79f6e1696296932 ### Credits Found while running the virtualenv test suite as root inside the Anthropic OSS Scanner build image. | GCVE Database | ||
| High | 7.1 | High7.1GCVE DatabaseCVE-2026-113723 ## Summary Caller-supplied prompt text can become executable syntax in the bash activation prompt. An attacker must influence the value passed to virtualenv, and a user must subsequently activate the environment and draw the prompt. ## Affected area Bash runs with compatibility level 4.2 and draws the environment prompt. Its replacement-string behavior differs from newer compatibility modes, leaving prompt text subject to a second interpretation. The report concerns POSIX activation. It does not establish exposure through unrelated activators. ## Attacker-controlled input The attacker controls prompt or destination data passed as a virtualenv argument. The user later sources the generated activation script. ## Affected code The relevant 21.14.8 source is in [bash prompt assembly](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/bash/activate.sh). ## Mechanism `bin/activate` copies the prompt into `PS1`. A shell expands `PS1` again when it draws a prompt, so the value must survive both the assignment and that later expansion. The bash and zsh branches escape for their usual prompt rules. Three other paths still interpret prompt data as commands: dash sourcing `activate` directly, a child dash inheriting an exported `PS1` from bash, and bash running at compatibility level 4.2 or lower. A value such as `x$(touch MARKER)y` then runs `touch` on prompt display. Creating the environment does not run the payload. ## Reproduction and evidence The scanner's child-shell reproducer creates an environment with `--prompt 'x$(touch MARKER)y'`, sources `bin/activate` in interactive bash, and starts interactive `sh` (dash). Its control leaves `PS1` unexported: neither shell creates `MARKER`. With `PS1` exported, bash displays the prompt without executing the value, but the child dash creates `MARKER`; the reproducer ends in `BUG`. The report gives separate console reproductions for direct dash activation and bash compatibility level 4.2. The child-shell path can arise after another environment's activation has exported `PS1`. The scanner linked this finding to [#3326](https://github.com/pypa/virtualenv/pull/3326) and its follow-up #3390. Those changes cover the usual bash path but leave the three paths above. The proposed change in [#3418](https://github.com/pypa/virtualenv/pull/3418) keeps a variable reference in `PS1`, so prompt text is expanded as data when drawn. The private report's reproducer was run by the scanner; the later command-execution result was supplied by the maintainer review. This advisory does not claim that every POSIX shell or every bash mode is affected. ## Introduced The report traces direct dash sourcing to the first 20.x activator (20.0.0b1), and inherited `PS1` to #3390 (first released in 21.14.7). It demonstrates the compatibility-level case on bash 5.2. Those historical boundaries come from the report; the release confirmed by the maintainer is 21.14.8. ## Attack sequence ```mermaid sequenceDiagram participant A as Prompt supplier participant V as virtualenv participant B as Interactive bash participant D as Child dash A->>V: --prompt x$(touch MARKER)y V->>B: generated bin/activate B->>B: assign escaped prompt to PS1 alt PS1 is not exported B->>D: start sh with its own PS1 D-->>B: marker absent else PS1 is exported B->>D: start sh with inherited PS1 D->>D: expand $(touch MARKER) on prompt draw D-->>B: marker created end ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-1MOI2X3T`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import shutil import subprocess from pathlib import Path work = Path("/tmp/repro-inherited-ps1") shutil.rmtree(work, ignore_errors=True) work.mkdir() marker = work / "MARKER" print("sh is", Path("/bin/sh").resolve()) # attacker: only the text a tool such as tox or hatch relays as --prompt hostile_prompt = f"x$(touch {marker})y" # set up: the environment, made with the virtualenv command subprocess.run( ["virtualenv", "--no-periodic-update", "--without-pip", "--activators", "bash", "--prompt", hostile_prompt, str(work / "env")], check=True, stdout=subprocess.DEVNULL, ) assert not marker.exists(), "creating the environment must not run the prompt" # user: in an interactive bash, source bin/activate, then start sh. # -i makes both shells draw their prompt although stdin is not a terminal; sh draws one, reads EOF and returns session = Path("/tmp/repro.d/user_session.sh").read_text() interactive_bash = ["bash", "--norc", "--nopr | GCVE Database | ||
| High | 7.8 | CVE-2026-113722virtualenv · Generated POSIX console launchers interpret destination paths as shell syntax High7.8GCVE DatabaseCVE-2026-113722 ## Summary A destination path supplied to virtualenv can be interpreted as shell syntax by a generated POSIX console launcher. The app-data seeder and pip delegate launcher generation to distlib but supply the interpreter path in a form that does not preserve it as literal data in the shell wrapper. ## Affected area POSIX console scripts generated by the app-data seeder or pip when the interpreter path requires a shell wrapper. Windows executable launchers are not affected by this path. ## Attacker-controlled input An attacker must influence the environment destination passed to virtualenv, and the user must subsequently run a generated launcher. The attacker does not need to modify an installed executable or control the user's app-data cache. ## Affected code The relevant 21.14.8 source is in [ScriptMakerCustom](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/seed/embed/via_app_data/pip_install/base.py). ## Mechanism The app-data seeder passes the environment's interpreter path through `enquote_executable()` to distlib's `ScriptMaker`. It adds double quotes for a path with a space but does not escape shell expansions inside those quotes. When the path has a space or the shebang exceeds 127 bytes, distlib emits a `#!/bin/sh` wrapper with a line shaped like `'''exec' "<path>" "$0" "$@"`. The shell expands `$(...)` and backticks in that path when the launcher is run. The same wrapper must also parse as Python, which exposes additional quote and backslash cases. Environment creation and activation do not trigger the shell payload. ## Reproduction and evidence The scanner created three environments: a control under `my -touch PWNED- project`, one under `my $(touch PWNED) project`, and one under a path of more than 127 bytes ending in `$(touch${IFS}PWNED)`. Each was created without producing `PWNED`. Running the generated `bin/pip --version` created `PWNED` in both attack cases; the control created none. The long-path case reached the shell wrapper without a space. This reproduces execution by the generated launcher, with the user's privileges, rather than during seeding. The report dates the app-data seeder's distlib launcher integration to #1579, first released in 20.0.2. [PR #3417](https://github.com/pypa/virtualenv/pull/3417) quotes the raw interpreter path for the POSIX wrapper and repairs launchers written by the pip seeder. Pip's own distlib can write a vulnerable wrapper again during a later pip install or upgrade; the proposed virtualenv fix does not change pip's vendored code. The maintainer-provided follow-up review reports that later path separately. ## Attack sequence ```mermaid sequenceDiagram participant A as Destination supplier participant V as virtualenv seeder participant D as distlib ScriptMaker participant S as Generated bin/pip participant U as User A->>V: path contains $(touch PWNED) V->>D: pass interpreter path with incomplete shell quoting D->>S: write /bin/sh wrapper Note over V,S: no payload runs during creation or activation U->>S: pip --version S->>S: shell expands path and runs touch S-->>U: PWNED appears with user's privileges ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-5DZVLGL1`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import shutil import subprocess from pathlib import Path WORK = Path("/tmp/repro-console-script") # The attacker's whole input: the name of a directory above the environment (a repository, a project folder). DIRECTORY_NAMES = { "CONTROL": "my -touch PWNED- project", "OBSERVED": "my $(touch PWNED) project", # no space: distlib also picks the /bin/sh launcher once "#!<dest>/bin/python" is longer than 127 bytes "OBSERVED_LONG": "p" * 120 + "$(touch${IFS}PWNED)", } shutil.rmtree(WORK, ignore_errors=True) pip_printed = {} created_by_pip = {} for label, name in DIRECTORY_NAMES.items(): # the payload can hold no "/", so it touches a relative PWNED: each case gets its own empty working directory cwd = WORK / label / "cwd" venv = WORK / label / name / "venv" cwd.mkdir(parents=True) # set up: the user creates an environment there with default options (--no-periodic-update keeps it offline) subprocess.run(["virtualenv", "--quiet", "--no-periodic-update", str(venv)], cwd=cwd, check=True) assert list(cwd.iterdir()) == [], "environment creation must not have created anything" # trigger: the user runs the seeded pip; argv is a list, so the only shell that reads the name is bin/pip's own pip = subprocess.run([venv / "bin" | GCVE Database | ||
| Medium | 4.5 | Medium4.5GCVE DatabaseCVE-2026-113721 ## Summary Generated activation files and pyvenv.cfg can follow existing links and write outside the requested environment. The template writer tests exists() before unlinking, which misses dangling symlinks; the configuration writer opens the existing path directly. Anthropic's OSS Scanner reported this as `ANT-2026-XBEOCBS1` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Final file entries written during environment creation and seeding. ## Attacker-controlled input Another account must be able to prepare file links in the destination before creation. The invoking user must have write permission at the linked target. The destination parent must remain controlled during creation; concurrent parent-directory replacement is a separate concern. ## Affected code The relevant 21.14.8 source is in [template writer](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/via_template.py), [configuration writer](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/create/pyenv_cfg.py). ## Mechanism `ViaTemplateActivator._generate()` removes an existing output only when `Path.exists()` is true. A dangling symlink therefore survives, and `write_bytes()` creates its target outside the environment. Other generated files have related final-entry behavior. `PyEnvCfg.write()` opens `pyvenv.cfg` without first replacing an existing link; a symlink or hard link there can overwrite an existing target. These paths require a prepared destination tree and the invoking user's permission to write the linked target. ## Reproduction and evidence The scanner's project tree contains `venv/bin/activate -> ../../../.bash_aliases` and an executable `venv/bin/basename`. Before creation, a new interactive bash does not create the marker. A plain `virtualenv venv` writes the activation script through the dangling link, creating `~/.bash_aliases` as a regular file. Debian's stock `.bashrc` sources that file; the next interactive bash adds `venv/bin` to `PATH` and the planted `basename` writes the marker. The reproducer checks that the link remains, `~/.bash_aliases` appears, and the second shell executes the planted file. A separate manual check links `pyvenv.cfg` to `~/.bashrc` and observes the latter replaced with generated configuration text. The link lets creation write outside the requested environment. The attacker chooses the target path but the generated bytes come from virtualenv. The executable in the example was already in the prepared tree; this finding is not arbitrary code injection into an otherwise trusted destination. The scanner traces the template behavior to #1454, first released in 20.0.0b1. [PR #3431](https://github.com/pypa/virtualenv/pull/3431) replaces pre-existing output links and uses exclusive creation for generated text. It does not make a destination safe if another account can replace parent directories during creation. ## Attack sequence ```mermaid sequenceDiagram participant A as Prepared project tree participant V as virtualenv writer participant H as User home participant B as Next interactive bash A->>V: venv/bin/activate links to missing ~/.bash_aliases V->>V: Path.exists() follows link and returns false V->>H: write activation bytes through link H-->>B: ~/.bash_aliases now exists B->>H: .bashrc sources ~/.bash_aliases B->>A: PATH reaches planted venv/bin/basename A-->>B: marker created Note over V,H: pyvenv.cfg can also overwrite an existing linked target ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-XBEOCBS1`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import os import shutil import subprocess import sys import tempfile # set up: a fresh home with Debian's stock ~/.bashrc, which sources ~/.bash_aliases when that file exists home = tempfile.mkdtemp(prefix="home-") shutil.copy("/etc/skel/.bashrc", f"{home}/.bashrc") bash_aliases = f"{home}/.bash_aliases" marker = f"{home}/attacker-ran" open_terminal = ["bash", "-i", "-c", "true"] terminal_env = {"HOME": home, "PATH": "/usr/bin:/bin", "TERM": "dumb"} # the attacker's repository, cloned by the user to ~/proj checkout = f"{home}/proj" subprocess.run([sys.executable, "/tmp/repro.d/make_hostile_checkout.py", checkout], check=True) activate = f"{checkout}/venv/bin/activate" print("shipped: venv/bin/activate ->", os.readlink(activate), flush=True) # a terminal opened before virtualenv runs does not touch the checkout (stderr: bash -i without a tty warns) subprocess.run(open_terminal, e | GCVE Database | ||
| Medium | 4.4 | Medium4.4GCVE DatabaseCVE-2026-113720 ## Summary The app-data copy seeder uses shutil.copy, which preserves cached file modes independently of the current umask. A cache populated under permissive permissions can therefore produce files with wider access in later environments. Anthropic's OSS Scanner reported this as `ANT-2026-487MH6OW` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Permissions of files copied from app-data into a new environment. ## Attacker-controlled input The cache must contain files created with broader permissions than the current environment creator intends. Another account must also be able to traverse the new environment and reach those files. A private parent directory prevents that access. No control of a private live cache is assumed. ## Affected code The relevant 21.14.8 source is in [copy helpers](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/util/path/_sync.py). ## Mechanism The default app-data seeder extracts a wheel into a reusable cache and copies its files into later environments with `shutil.copy()`. That function applies the cached source mode to each destination file after creating it, overriding the current run's umask. If the cache image was built under umask `000`, a later environment created under umask `022` can receive mode-`0666` pip files. The cache may be private while the new environment is reachable by other local accounts. ## Reproduction and evidence The scanner created two histories with separate private app-data caches. In the control, the owner's first run used umask `022`; a later environment had no files writable by another account, and that account could not append to `pip/__init__.py`. In the attack history, the first run used umask `000` and the later run still used `022`. The report counted 478 of 493 environment files writable by others. A second unprivileged uid appended a line to pip, and `bin/pip --version` ran it as the environment owner. The cache itself remained under a mode-`0700` home in both histories. An attacker cannot force the owner's earlier umask through this finding; the attack also needs a traversable destination. A mode-`002` cache can expose group write access under suitable group permissions. The scanner dates app-data copy seeding to #1578, first released in 20.0.2. [PR #3425](https://github.com/pypa/virtualenv/pull/3425) creates copied files under a fixed non-writable-by-others policy further restricted by the current umask. ## Attack sequence ```mermaid sequenceDiagram participant U as Environment owner participant C as Private app-data cache participant E as Later environment participant A as Other local account U->>C: first seed extraction under umask 000 C-->>U: cached pip files have mode 0666 U->>E: create environment under umask 022 C->>E: shutil.copy reapplies mode 0666 A->>E: append code to pip/__init__.py U->>E: run bin/pip --version E-->>U: appended code runs as owner ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-487MH6OW`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import glob import os import shutil import stat import subprocess import sys from pathlib import Path sys.stdout.reconfigure(line_buffering=True) # keep these lines in order with those of the child processes os.umask(0o022) # for the directories made here as root OWNER, OTHER_USER = 1000, 65534 # root is only needed to switch to these two unprivileged uids VIRTUALENV = ["virtualenv", "--quiet", "--no-periodic-update"] # default seeder; no network for the update check # read here, as root: /tmp/repro.d need not be readable by the other user ATTACK = Path("/tmp/repro.d/other_user_appends_to_pip.py").read_text() def run_as_owner(command, home, umask): environ = {"HOME": str(home), "PATH": os.environ["PATH"]} subprocess.run(command, user=OWNER, group=OWNER, extra_groups=[], umask=umask, env=environ, cwd=home, check=True) def mode(path): return stat.filemode(os.stat(path).st_mode) work = Path("/tmp/seed-mode-repro") shutil.rmtree(work, ignore_errors=True) work.mkdir() proofs = work / "proofs" proofs.mkdir() proofs.chmod(0o777) # the other user's line writes the uid it runs as in here writable_by_others, appended_line_ran_as = {}, {} # the two histories differ only in the umask of the owner's earlier run, the one that fills ~/.cache/virtualenv for case, umask_of_earlier_run in [("CONTROL", 0o022), ("OBSERVED", 0o000)]: # set up: the owner's home, and with it the cache, is private; the project folder is reachable by path home = work / f"{ | GCVE Database | ||
| Medium | 4.5 | Medium4.5GCVE DatabaseCVE-2026-113719 ## Summary The app-data seeder joins paths from existing distribution metadata to site-packages and deletes the resulting paths without checking containment within the environment. Anthropic's OSS Scanner reported this as `ANT-2026-SXOEHWP7` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Removal of a previously installed seed package. ## Attacker-controlled input An environment being reused must contain distribution metadata influenced by another account. The invoking user must have deletion rights over the paths named by that metadata. Installing a new environment without existing package metadata does not enter this cleanup path. ## Affected code The relevant 21.14.8 source is in [seed uninstall](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/seed/embed/via_app_data/pip_install/base.py). ## Mechanism Before reinstalling a seed wheel, `PipInstall._uninstall_previous_version()` accepts an existing `pip-*.dist-info` directory in the destination. `_uninstall_dist()` reads its `top_level.txt` and `RECORD`, joins each entry to `site-packages`, and deletes the result. It does not check whether `..`, an absolute entry, or a linked parent takes the path outside the environment. The default app-data seeder reaches this path when it reuses a tree containing seed-package metadata; `--clear` removes that tree first. ## Reproduction and evidence The scanner prepared a project with `.venv/lib/python3.14/site-packages/pip-1.0.dist-info/`. Its `top_level.txt` contained `../../../../../Documents`; its `RECORD` named `../../../../../notes.txt,,`. The user's home initially held `Documents/thesis/chapter1.txt` and `notes.txt`. Running `virtualenv .venv` exited successfully but removed both paths. A control tree with `top_level.txt` set to `pip` and a normal `pip/__init__.py` RECORD entry left the home files in place. The reproducer prints `BUG` only when the control files survive and both attack targets disappear. The deletion uses the invoking user's permissions and runs during creation, before any use of the environment. It requires an existing destination that another account could populate; a fresh empty environment does not supply hostile metadata. The report traces this cleanup to #2186, first released in 20.8.1. [PR #3428](https://github.com/pypa/virtualenv/pull/3428) checks each deletion path against the resolved environment root and refuses to delete the root itself. ## Attack sequence ```mermaid sequenceDiagram participant A as Prepared .venv metadata participant V as App-data seeder participant P as site-packages participant H as User home A->>P: top_level.txt names ../../../../../Documents A->>P: RECORD names ../../../../../notes.txt V->>P: read pip-1.0.dist-info before reinstall P->>H: join entries without containment check V->>H: remove Documents and notes.txt V-->>A: virtualenv exits successfully ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-SXOEHWP7`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import os import shutil import subprocess import sys from pathlib import Path work = Path("/tmp/repro-seed-uninstall") shutil.rmtree(work, ignore_errors=True) USER_FILES = ("Documents/thesis/chapter1.txt", "notes.txt") def make_home(name): home = work / name (home / "Documents" / "thesis").mkdir(parents=True) (home / "Documents" / "thesis" / "chapter1.txt").write_text("months of work\n") (home / "notes.txt").write_text("my notes\n") return home def unpack_attacker_project(home, top_level_line, record_line): subprocess.run( [sys.executable, "/tmp/repro.d/make_project_tree.py", home / "project", top_level_line, record_line], check=True, ) def user_runs_virtualenv(home): # HOME only gives each run its own ~ and app-data; --no-periodic-update keeps the run offline result = subprocess.run( ["virtualenv", "--no-periodic-update", ".venv"], cwd=home / "project", env={**os.environ, "HOME": str(home)}, stdout=subprocess.PIPE, text=True, check=True, ) print(result.stdout, end="") def files_left(home): return [f for f in USER_FILES if (home / f).exists()] # control: the project ships well-formed pip metadata control_home = make_home("control") unpack_attacker_project(control_home, "pip", "pip/__init__.py,,") user_runs_virtualenv(control_home) control_left = files_left(control_home) print(f"CONTROL user files still in ~ after virtualenv: {control_left}") # attack: site-packages is ~/project | GCVE Database | ||
| Low | 2.3 | Low2.3GCVE DatabaseCVE-2026-113717 ## Summary The pip seeder acquires bundled wheels with download=False and lets pip access the index when --download is selected. Index candidates on that path do not pass through virtualenv's wheel digest verification. Anthropic's OSS Scanner reported this as `ANT-2026-74ALJBUF` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Wheel selection by the pip seeder when downloads are enabled. ## Attacker-controlled input The pip seeder and download option must be selected. The finding concerns the default-index digest-verification policy. Explicit custom-index settings already opt out of comparison against PyPI metadata; this patch retains that policy. ## Affected code The relevant 21.14.8 source is in [pip seeder](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/seed/embed/pip_invoke.py). ## Mechanism With `--seeder pip --download`, `PipInvoke.get_pip_install_cmd()` first calls `get_wheel(download=False)`, so virtualenv's `download_wheel()` and `verify_wheel_digest()` are skipped. It then lets `pip install` consult the configured index. An index wheel with the same package and version as the bundled wheel can be selected without virtualenv comparing its SHA-256 with PyPI metadata. The default app-data seeder takes the verification path for the same option. ## Reproduction and evidence The scanner built a local index named in `pip.conf` that served a modified copy of the bundled pip wheel under the same filename. In the control run, `virtualenv --download env-default` rejected the wheel with a digest mismatch. In the second run, adding `--seeder pip` produced an environment whose installed `pip/__init__.py` matched the index wheel; `env-pip/bin/pip --version` ran the added line. The reproducer checked the installed bytes and marker before reporting `BUG`. Both `--seeder pip` and `--download` are needed. The report used a non-PyPI index from `pip.conf` and left `PIP_INDEX_URL`, `PIP_EXTRA_INDEX_URL`, and `PIP_INDEX` unset, so virtualenv's default-seeder PyPI comparison remained active. This does not establish index control in the default PyPI setup. The unchecked pip-seeder download path is older, while the comparison with the default seeder arose when digest verification was added in 21.7.12. [PR #3430](https://github.com/pypa/virtualenv/pull/3430) acquires and verifies selected wheels before invoking pip with index lookup disabled. ## Attack sequence ```mermaid sequenceDiagram participant I as Index in pip.conf participant V as virtualenv PipInvoke participant P as pip install participant E as Seeded environment I->>V: serve modified pip wheel V->>V: get_wheel(download=False) V->>P: install with --download permitting index lookup P->>I: select same-version index wheel I->>E: modified pip installed without virtualenv digest check E->>E: later pip invocation runs added code Note over V,E: Default app-data seeder rejects the same digest ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-74ALJBUF`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import glob import hashlib import json import os import shutil import subprocess import sys import zipfile from virtualenv.seed.wheels.embed import get_embed_wheel companions = "/tmp/repro.d" work = "/tmp/repro-pip-seeder-download" shutil.rmtree(work, ignore_errors=True) os.makedirs(work) # set up: the pip wheel virtualenv bundles, and PyPI's record of its sha256 (served by virtualenv_offline.py: no network) bundled = get_embed_wheel("pip", f"{sys.version_info.major}.{sys.version_info.minor}") pypi_sha256 = hashlib.sha256(open(bundled.path, "rb").read()).hexdigest() pypi_release = {"filename": bundled.name, "digests": {"sha256": pypi_sha256}} os.makedirs(f"{work}/pypi.org/pypi/pip") with open(f"{work}/pypi.org/pypi/pip/json", "w") as pypi_json: json.dump({"releases": {bundled.version: [pypi_release]}}, pypi_json) # attacker: the mirror serves other bytes under the bundled wheel's own file name marker = f"{work}/MIRROR_CODE_RAN" subprocess.run([sys.executable, f"{companions}/make_mirror.py", str(bundled.path), f"{work}/mirror", marker], check=True) mirror_wheel = f"{work}/mirror/pip/{bundled.name}" mirror_sha256 = hashlib.sha256(open(mirror_wheel, "rb").read()).hexdigest() print(f"{bundled.name}: PyPI publishes sha256 {pypi_sha256}") print(f"{bundled.name}: the mirror serves sha256 {mirror_sha256}") # user: pip.conf names the mirror; PIP_INDEX_URL, PIP_EXTRA_INDEX_URL and PIP_INDEX (which turn the check off) are unset with open(f"{work}/pip.conf", "w") a | GCVE Database | ||
| Medium | 4.5 | Medium4.5GCVE DatabaseCVE-2026-113716 ## Summary The builtin creator accepts existing directory symlinks through ensure_dir() and writes environment files through those directories. This permits writes outside the requested destination. Anthropic's OSS Scanner reported this as `ANT-2026-MUFJEG11` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Directory preparation before environment files are created. ## Attacker-controlled input Another account must be able to prepare linked entries below the chosen destination before creation. The invoking user must have access to the linked destination. This does not require write access to the target itself under the other account. ## Affected code The relevant 21.14.8 source is in [builtin creator](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py), [ensure_dir()](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/util/path/_sync.py). ## Mechanism The builtin creator calls `ensure_dir()` for `bin`, `lib`, and package directories. `ensure_dir()` accepts an existing symlink to a directory because `exists()` follows it. Interpreter creation, seed installation, and activation-script generation then write through that parent link. A final-file check cannot prevent a write through a linked parent. `--clear` removes the planted tree first; ordinary creation over a prepared destination reaches it. ## Reproduction and evidence The scanner's project tree contains `venv/bin -> ../../.local/bin` and a `.pth` file in the environment's package tree. With the user's initially empty `~/.local/bin` first on `PATH`, `virtualenv venv` exited successfully and wrote `python*`, `pip*`, and `activate*` into `~/.local/bin`. The marker was absent immediately after creation; a later `pip --version` on the user's `PATH` loaded the seeded environment and the planted `.pth` code wrote the marker. A second case began with the `bin` link resolving inside the environment; replacement of a planted package link during seeding changed where it resolved, and writes again reached `~/.local/bin`. The attacker must prepare entries in an existing destination. The `.pth` content is already attacker-controlled and would run if the user used that environment directly; the added effect is creation-time writing or replacement outside the requested tree and an executable on the user's `PATH` pointing at it. The scanner traces this default builtin path to 20.0.0b1. [PR #3429](https://github.com/pypa/virtualenv/pull/3429) replaces linked directory components before creation. Concurrent parent replacement remains a separate case. ## Attack sequence ```mermaid sequenceDiagram participant A as Prepared destination participant V as Builtin creator participant L as venv/bin symlink participant H as ~/.local/bin participant U as User A->>L: point to ../../.local/bin V->>L: ensure_dir accepts existing directory link V->>H: write python, pip and activate files through link V-->>U: creation reports success U->>H: run pip found on PATH H->>A: interpreter loads planted .pth A-->>U: marker created ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-MUFJEG11`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import os import shutil import subprocess import sys from pathlib import Path WORK = Path("/tmp/planted-bin-link") shutil.rmtree(WORK, ignore_errors=True) def user_creates_venv_in_checkout_then_runs_pip(home, make_checkout): local_bin = home / ".local/bin" user_env = {"HOME": str(home), "PATH": f"{local_bin}:{os.environ['PATH']}"} local_bin_before = set(os.listdir(local_bin)) payload_marker = home / "PWNED" # the attacker's checkout arrives at ~/project subprocess.run([sys.executable, f"/tmp/repro.d/{make_checkout}", home / "project"], check=True) venv = home / "project/venv" print(f"CONTROL venv/bin before the run resolves to {os.path.realpath(venv / 'bin')}") # default options; the flag only keeps the run off the network subprocess.run(["virtualenv", "--no-periodic-update", "--quiet", "venv"], cwd=home / "project", env=user_env, check=True) written = sorted(set(os.listdir(local_bin)) - local_bin_before) payload_ran_at_creation = payload_marker.exists() print(f"OBSERVED venv/bin after the run resolves to {os.path.realpath(venv / 'bin')}") print(f"OBSERVED virtualenv wrote to {local_bin}: {written}") print(f"OBSERVED ~/PWNED exists after virtualenv: {payload_ran_at_creation}") # lat | GCVE Database | ||
| Low | 2.0 | Low2.0GCVE DatabaseCVE-2026-113715 ## Summary The venv creator reads the standard library's generated pyvenv.cfg with splitlines() and merges all resulting keys over virtualenv's configuration. Line-boundary characters from destination data can therefore become configuration structure before output sanitization. Anthropic's OSS Scanner reported this as `ANT-2026-6ZQHJ6VI` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Configuration readback by the stdlib venv creator. ## Attacker-controlled input An untrusted caller must influence the destination pathname passed to the venv creator. That pathname must contain a character that Python treats as a line boundary. The builtin creator follows a different configuration-writing path. ## Affected code The relevant 21.14.8 source is in [venv readback](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/create/via_global_ref/venv.py), [configuration parser](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/create/pyenv_cfg.py). ## Mechanism With `--creator venv`, virtualenv asks the standard library to create an environment, reads its `pyvenv.cfg` with `str.splitlines()`, and merges the parsed keys over virtualenv's chosen settings. CPython writes the destination into a `command =` value without escaping every character that `splitlines()` recognizes as a line boundary. A destination containing U+2028, for example, can therefore supply new `home` and `include-system-site-packages` keys during readback. Virtualenv writes those keys as real configuration lines. The builtin creator does not use this readback path. ## Reproduction and evidence The scanner used one destination value with U+2028 between `home = /attacker/chosen`, `include-system-site-packages = true`, and an absorbing `x =` key. A `python -m venv --without-pip` control kept the value inside its `command` line and reported the base interpreter's `sys._home`. `virtualenv --creator venv --no-seed` on the same value wrote separate injected configuration lines and its interpreter reported `sys._home = /attacker/chosen`. The path did not require a literal newline byte. The control establishes that the extra keys came from virtualenv's readback, not from the standard library's initial file alone. The altered `home` can change which standard-library files the interpreter loads if an attacker also controls that location; `include-system-site-packages = true` weakens environment isolation. Choosing the venv creator is required, either explicitly or through a rare fallback. The report traces the readback to the 20.x rewrite, first released in 20.0.0b1. [PR #3426](https://github.com/pypa/virtualenv/pull/3426) rejects line-boundary characters before creation and rebuilds the final configuration from selected settings. ## Attack sequence ```mermaid sequenceDiagram participant A as Destination supplier participant S as stdlib venv participant V as virtualenv readback participant C as Final pyvenv.cfg participant P as Environment Python A->>S: path contains U+2028 and home = /attacker/chosen S->>C: write path inside command value V->>C: read with splitlines() V->>V: parse suffix as separate keys V->>C: rewrite home and include-system-site-packages P->>C: read injected keys on startup C-->>P: sys._home changes and isolation is disabled ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-6ZQHJ6VI`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import os import shutil import subprocess work = "/tmp/repro-venv-creator-cfg" shutil.rmtree(work, ignore_errors=True) # the attacker's input: the text of a destination path. # U+2028 ends a line for str.splitlines() but not for CPython's own pyvenv.cfg reader. # The slashes only make nested directories; the last key, x, absorbs the rest of the path. LS = "\u2028" dest = f"{work}/proj{LS}home = /attacker/chosen{LS}include-system-site-packages = true{LS}x = /env" base_python = os.path.realpath(shutil.which("python")) real_home = os.path.dirname(base_python) def pyvenv_cfg_lines(env): # split on "\n" only, as the interpreter does return open(f"{env}/pyvenv.cfg", encoding="utf-8").read().split("\n") def sys_home(env): # site.py sets sys._home from the `home` key of the environment's pyvenv.cfg probe = [f"{env}/bin/python", "-c", "import sys; print(sys._home)"] return subprocess.run(probe, capture_output=True, text=True, check=True).stdout.strip() # control: the same path through the standard library alone stdlib_env = dest + "-stdlib" | GCVE Database | ||
| High | 7.1 | CVE-2026-112940conda-lock · filelock ReadWriteLock: SQLite reopens the lock database by name, so a planted symlink or journal file makes the lock High7.1GCVE DatabaseCVE-2026-112940 `ReadWriteLock` and `AsyncReadWriteLock` (the SQLite-backed reader/writer lock) hand SQLite a name they do not control. On Linux the lock validates its database through a descriptor (the number the kernel gives a process for an open file). It then connects SQLite to `/proc/self/fd/<n>`, a Linux path that names the process's own open file `n`. SQLite turns that path back into a plain path and reopens it by name, following symlinks (a symlink is a file that points at another path). On every POSIX route SQLite also acts on `-journal`, `-wal` and `-shm` files it finds beside that name. Those are SQLite's side files: the rollback journal (pages saved to undo an interrupted write), the write-ahead log or WAL (changes kept outside the main file), and the shared-memory index for the WAL. Another local user who can create or rename entries in the directory that holds the lock database can turn this against the user running the lock (below: the lock user). Such a directory is a `0777` or group-writable cache, build or project directory without the sticky bit (a directory flag that lets only a file's owner remove or rename it). That other user can make the lock user's ordinary `read_lock()` or `write_lock()` write bytes of the other user's choosing into the lock user's own files. The same position lets the other user make that call create or unlink files of the lock user, admit a reader beside a writer, or kill the holder. All of this runs with the lock user's privileges. In a sticky directory such as `/tmp` a planted `-wal` file makes every acquire fail. Affected versions are filelock 3.20.4 through 4.2.0 (the `/proc/self/fd` redirect since 4.0.9, the side-file variants since 3.20.4). Anthropic's OSS Scanner reported this in private as `ANT-2026-KMJ2575J` [high, CWE-363]: ReadWriteLock: SQLite still follows planted symlinks, overwriting files. ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H` scores 7.1 (High).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `UI:N` (no user interaction). `AC:L` (low attack complexity): the planted-journal variant needs no race, and the redirect won 77 of 77 attempts. `I:H` (high integrity impact): the other user writes bytes of their choosing into files of the lock user and unlinks files of their choosing. `A:H` (high availability impact): the other user makes every acquire fail or kills the lock holder. `C:N` (no confidentiality impact): the other user learns nothing. `S:U` (scope unchanged): the effect stays within the lock user's own privileges. Classification from the external review: **conditional security**, a cross-user effect that exists only when another user can change names in the lock database's directory. The review notes that the 77/77 inotify result (inotify is a Linux interface that reports file events) is stronger evidence than a theoretical TOCTOU (a time-of-check to time-of-use race). The scanner rated it high. The review accepts the mechanism and narrows it to that precondition. ## Scope and preconditions - The other party is a different local account that can create or rename entries in the directory that holds the lock database. That is a `0777` directory or a group-writable one (including a setgid `2775` project directory shared with that account), in each case without the sticky bit. The lock user's lock must be able to open the database read-write, so a database the other user creates is `0666`. filelock itself creates the database `0600`. - Sticky directories (`/tmp`, `/var/tmp`, any `1777` directory) narrow the finding rather than exclude it. The redirect and the journal replay do not reach a database the lock user owns there (tested with `fs.protected_regular=2`). What remains is a planted non-empty `<db>-wal` (any mode; a directory or FIFO works too) that the lock user cannot unlink. It makes every acquire raise `sqlite3.OperationalError: disk I/O error`. A hard link (a second name for the same file) to a file of the lock user at the lock path also remains, if `fs.protected_hardlinks=0`. - Two kernel settings matter. `fs.protected_regular` (1 or 2 is the systemd default on most distributions; the kernel's own default is 0) keeps a user from opening a file another user owns in a sticky world-writable directory. Where it is 0, the other user can own the database in `/tmp`, and SQLite follows a planted symlink itself (not tested). `fs.protected_hardlinks=1` (most distributions; kernel default 0) blocks a hard link of a lock user's file onto the lock path. Neither setting affects a non-sticky shared directory. - Platform and route. The `/proc/self/fd` redirect is Linux with `/proc` mounted, 4.0.9 and later. The side-file variants (`-journal`, `-wal`, `-shm`) exist on every POSIX route. On the hard-link route (macOS since 4.1.1 for every connection, Linux without `/proc`) they need the link-directory swap described under "Mechanism", so a non-sticky parent. N | GCVE Database | ||
| Medium | 4.7 | CVE-2026-112939conda-lock · filelock: SoftFileLock removes a live holder's marker when another user forges the PID in a writable marker Medium4.7GCVE DatabaseCVE-2026-112939 `SoftFileLock`, `MarkerSoftFileLock`, `SoftFileLease`, their async variants and a `FileLock` that has fallen back to a soft lock on `ENOSYS` decide from the record inside the marker file whether its owner is gone. The marker is the file whose presence says a process holds the lock. They do not check who could have written that record. A local user who can write the marker but cannot remove it rewrites the holder's record to name a dead PID (process id). That position arises with an explicit `mode` with group or other write bits, or the default mode under a lax umask (the process setting that strips permission bits from new files), in a sticky or otherwise non-removable directory. The sticky bit is a directory flag that lets only a file's owner remove or rename it. The next contender running as the holder's account, or root, judges the lock stale, removes the live marker on that user's behalf and acquires it. Two processes then run the critical section together and neither gets an error. Affected versions are filelock 3.22.0 through 4.2.0 (finding `ANT-2026-W1H9NXUH`). ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N` scores 4.7 (Medium).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `UI:N` (no user interaction). `AC:H` (high attack complexity): the marker must carry group or other write bits (an explicit permissive `mode`, or umask `002`/`000` with a shared group), and a contender of the holder's account must ask for the lock while the forged record is in place. `I:H` (high integrity impact): two holders then write whatever the lock protects, and the holder is not told. `C:N` and `A:N` (no confidentiality or availability impact). `S:U` (scope unchanged). Classification from the review: **security, conditional** on another user being able to write the live marker. The scanner rated it high (CWE-345). The default mode under umask `022` is not affected. ## Scope and preconditions - The other user can write the marker. The application passes a `mode` with group or other write bits, which `fchmod` applies as given since 4.1.0. `0o666` is a common choice for a lock shared between accounts, although a soft-lock contender of another account waits as well on a `0o644` marker, since contenders only read and rename. Or the application uses the default mode (`0o666` through the umask) under umask `002` in a directory whose group the other user belongs to, or under umask `000`. The default mode under umask `022` is not affected. - The other user cannot remove the marker itself: the directory is sticky, or the other user cannot write it at all (a `0755` directory owned by the lock's user gives the same overlap). In a non-sticky shared directory the other user can unlink the marker without filelock, and filelock adds nothing. - A contender that may rename the marker asks for the lock: another process of the holder's account (several workers of one user) or root. A contender of another unprivileged account cannot rename the marker in a sticky directory and times out. - The finding reaches every class that publishes a soft marker: `SoftFileLock`, `MarkerSoftFileLock`, `SoftFileLease`, their async variants, and `FileLock(mode=0o666)` on a filesystem where `flock` (a kernel lock on an open file) fails with `ENOSYS`. These protections against another local user hold only when no other user can replace the lock's directory or any of its ancestors. That holds when each ancestor belongs to the user or root and is either not writable by others or sticky. A user who can rename entries in a non-sticky directory above the lock controls every name below it, which no file-based lock can defend against. ## Affected code At the scanned commit b831496: the create mode in [`SoftFileLock._acquire`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L67-L71), the explicit-mode `fchmod` in [`_apply_explicit_mode`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L100-L105), the trusting verdict in [`_try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L107-L123) and the [`owner_is_stale`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_identity.py#L64-L79) it relies on; the same call in [`MarkerSoftFileLock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_marker.py#L74-L81) and [`SoftFileLease`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_lease.py#L229-L256). ## Introduced The stale verdict that trusts the record came in with e35c3afac473 ("feat(soft): detect and break stale locks", #476, 2026-02-13), first released in 3.22.0. From then on a marker left group- or other-writable by the umask is forgeable. The re | GCVE Database | ||
| Medium | 5.8 | CVE-2026-112938conda-lock · filelock: StrictSoftFileLock follows a symbolic link swapped in at <path>.filelock and creates claim files in a Medium5.8GCVE DatabaseCVE-2026-112938 `StrictSoftFileLock` and `AsyncStrictSoftFileLock` check `<path>.filelock` (the coordination directory that holds the lock's state) with one `lstat` call on its name. `lstat` reports what a name is without following a symbolic link (a file that redirects to another path). After that check, the code reaches `<path>.filelock/claims` by pathname for every later operation (`mkdir`, scan, publish, link, unlink). `_open_directory` passes `O_NOFOLLOW` (a flag that makes an open fail when the final name is a symbolic link), but the flag covers only the final `claims` component. The kernel follows a symbolic link in any earlier component. Another local user (the other user below) who can rename `<path>.filelock` exchanges it for a symbolic link after the check. In a shared directory without the sticky bit (a directory flag that lets only a file's owner rename or remove it), any user with write access can do this. Where the shared directory is sticky, such as `/tmp`, only the user who created `<path>.filelock` before the user running the lock (the lock user below) first used it can. The lock user's process (root's, if root takes the lock) then creates a `claims` directory and claim files (the files whose presence says a process wants or holds the lock) inside a directory the other user chooses but only the lock user can write. When the link points at another strict lock's `.filelock` directory, the stranded claims make that lock time out for every process until an operator finds and force-breaks each one. The `claims` property and `force_break()` resolve the same path with no check at all. Affected versions: filelock >= 3.30.0 and <= 4.2.0 (finding `ANT-2026-SVFEYX89`). ## Severity **CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H, 5.8 (Medium).** `AV:L` (local): the other user needs an account on the same host. `AC:H` (high attack complexity): the swap must land between the `lstat` and the next `mkdir`. The other user does not aim at that window. It flips the name in a loop with `renameat2(RENAME_EXCHANGE)` (a system call that swaps two names in one step) across repeated acquires, and won after 1 to 31 runs of the lock user's program in the scanner's trials. A sticky directory also requires it to have created `<path>.filelock` first. `PR:L` (low privileges): an unprivileged account is enough. `UI:N` (no user interaction): the lock user's process acts on its own. `C:L` (low confidentiality impact): through `claims`, one file name per planted link from a directory only the lock user can list. `I:L` (low integrity impact): a directory named `claims` and files with filelock's fixed claim names and contents appear, created with the lock user's privileges, in a directory the other user chooses but cannot write. The process overwrites or truncates nothing that exists. `A:H` (high availability impact): aimed at another strict lock of the lock user, the stranded claims deny that lock to every process until an operator finds and force-breaks each one. Classification from the review: security, conditional on the other user being able to rename the intermediate component. The scanner rated it high (CWE-363). ## Scope and preconditions - The other user can rename `<path>.filelock`. In a writable directory without the sticky bit (a `0777` or group-writable project or cache directory), any user with write permission on the directory can, even though the lock user created and owns `<path>.filelock`. In a sticky directory such as `/tmp`, only the user who created `<path>.filelock` before the lock user's first use can, because that user then owns both the directory and the symbolic link (the planting precondition of GHSA-9c4x-gm8v-wgc4). - The link's target is a directory the lock user can write. The effect that matters is on that directory. An acquire that completes through the link also holds the shared lock with its claims elsewhere, but a user who can rename `<path>.filelock` can defeat that lock's exclusion anyway. - `fs.protected_symlinks=1` (a Linux setting that refuses to follow another user's symbolic link in a world-writable sticky directory) does not interfere. The link is an intermediate path component, not the last one. In the sticky-directory case it also sits in a directory the other user owns. - Missed attempts show up to the lock user as `SoftFileLockProtocolError` or `FileNotFoundError`. The swap therefore works against a lock that a program takes more than once: a periodic job, a worker pool, or a caller that retries or waits with a timeout (each poll of a waiting acquisition is a new attempt). - The two entry points that need no race (`claims` and `force_break()` on a resting symbolic link) depend on the lock user inspecting or recovering the lock. The planted link invites this by making `acquire()` fail with a protocol error. The `force_break()` variant works only when the lock user breaks claims that name a live process on its own host without checking them first, as the recovery loop in `docs/tutorials.rst | GCVE Database | ||
| Medium | 6.1 | Medium6.1GCVE DatabaseCVE-2026-112936 ### Summary `ReadWriteLock` and `AsyncReadWriteLock` resolve the lock path with `pathlib.Path.resolve()` and pass it to `sqlite3.connect()` without refusing a symlink. A local user who can create names in the lock's directory, such as `/tmp` or a shared cache, can plant a symlink at the lock path and make the victim's process create, lock, or write the file it points to, or break mutual exclusion between the victim's own processes. ### Details `ReadWriteLock.__init__` stores `pathlib.Path(lock_file).resolve()`, which follows a final symlink, and every connection opens that path through `sqlite3.connect()` with no `O_NOFOLLOW` or regular-file check. SQLite's own VFS also resolves the final symlink in user space, so `fs.protected_symlinks` and the sticky bit do not help: the kernel never sees a symlink to refuse. The other lock classes do not follow a final symlink: `FileLock` opens with `O_NOFOLLOW`, and `SoftReadWriteLock` coordinates through `<path>.rw/`. ### Proof of concept On Linux, with the victim and attacker as different users and `/shared` mode 1777 with `fs.protected_symlinks=1`: 1. The attacker runs `ln -s /home/victim/private/pwned.flag /shared/app.rwlock.db`. 2. The victim constructs `ReadWriteLock("/shared/app.rwlock.db")`. A plain `os.open(..., O_NOFOLLOW)` on the same path fails, but the lock creates `/home/victim/private/pwned.flag`, owned by the victim, inside a directory the attacker cannot write. 3. With `/shared/cache.rwlock.db -> /shared/a.db`, one victim process holds the write lock. The attacker repoints the link to `/shared/b.db`, and a second victim process acquires the write lock while the first still holds it. The same behavior reproduces on macOS, on Windows 11, and through `AsyncReadWriteLock`. ### Impact A local attacker with write access to the lock directory can make the victim create empty files it owns at paths the attacker cannot write, make the victim hold a SQLite lock on another application's database so that application gets `database is locked`, and give two victim processes the write lock at once, letting them corrupt the resource the lock protects. ### Workarounds Keep `ReadWriteLock` paths in directories that only trusted users can write. | GCVE Database | ||
| Medium | 4.7 | CVE-2026-112935conda-lock · filelock: SoftFileLock stale-lock break removes a live lock file on a reused inode number and admits two holders Medium4.7GCVE DatabaseCVE-2026-112935 The stale-lock break shared by `SoftFileLock`, `MarkerSoftFileLock` and `SoftFileLease` (and their async variants, and a `FileLock` that has fallen back to a soft lock on `ENOSYS`, the "not implemented" error) recognises the marker it judged stale by two values. The marker is the lock file at `<path>`, whose presence says a process holds the lock. The values are its inode number (the kernel's identity for a file, independent of its name) and its modification time (mtime) not being newer. The check runs after the contender has already closed the marker, so nothing keeps the inode allocated. On a filesystem that hands a freed inode number straight to the next file (ext4, xfs, overlayfs on them) the next holder's marker can carry that number. A contender still between its read and its break then renames and unlinks the live marker. Another local user (the other user below) who can create files in the lock's directory, a sticky `/tmp` included (the sticky bit is a directory flag that lets only a file's owner remove it), forces this at will. It plants a marker with a stale owner record and a modification time in the future and unlinks it as soon as a contender has read it. Two processes of the user running the lock (the lock user below) then hold the same default-configured lock at once, and nothing raises or logs. Affected versions: filelock >= 3.22.0 and <= 4.2.0 (finding `ANT-2026-KIIS96IX`). ## Severity **CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N, 4.7 (Medium).** `AV:L` (local): the other user needs an account on the same host. `AC:H` (high attack complexity): a race, though one the other user starts, lengthens (by making the lock user's `/proc` read slow) and retries at every release. The scanner measured 10 to 26 overlaps per 20-second run. `PR:L` (low privileges): an unprivileged account is enough. `UI:N` (no user interaction): the lock user's processes act on their own. `C:N` (no confidentiality impact): the other user reads nothing. `I:H` (high integrity impact): two holders write the data the lock protects, and neither learns of the other. `A:N` (no availability impact): the lock keeps working. Classification from the review: security, conditional on a sticky shared directory, ext4-style inode reuse and at least two contenders of the lock user. Our review verified it across UIDs (a UID is a user account's numeric id): the other user as uid 1001, the lock user's processes as uid 1000, BUG on 4.2.0 over ext4. The scanner rated it high (CWE-367). ## Scope and preconditions - The other user can create files in the lock's directory; a root-owned `1777` directory such as `/tmp` or `/var/tmp` is enough. It creates, `utime`s (sets the timestamps of) and unlinks only its own file. The sticky bit stops it removing the lock user's marker (checked: `EPERM`, operation not permitted), so the lock user's own stale break does the deletion. - The directory is on a filesystem that hands a freed inode number straight to the next file. ext4 does (six create/unlink rounds returned one number six times, in the image's overlayfs `/tmp` and on a loop-mounted ext4), and xfs does. tmpfs returned six increasing numbers and the same run there gave no overlap, and btrfs does not hand a freed number back. - The lock user has at least two processes contending for the lock, and the displaced holder is still inside when the next contender polls (the breaker itself comes back one `poll_interval`, 50 ms by default, later). - The lock is `SoftFileLock`, `MarkerSoftFileLock` or `SoftFileLease` (or async forms). The default `flock`-based `FileLock` joins them only after it has fallen back to `SoftFileLock` on `ENOSYS`. `StrictSoftFileLock` does not call `break_lock_file`, so it is not affected. - The same overlap also occurs with no other user involved, under heavy same-account contention, when a contender breaks a dead holder's marker and a successor creates its marker within the same timestamp tick (about once in 750 crashes with 64 workers on a 2-CPU machine, not on tmpfs). filelock's checks against another local user hold only when no other user can replace the lock's directory or any of its ancestors. That is the case when each ancestor belongs to the lock user or root and is either not writable by others or sticky. A user who can rename entries in a non-sticky directory above the lock controls every name below it. No file-based lock can defend against that. ## Affected code At the scanned commit b831496, the re-check sits in [`break_lock_file`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_util.py#L113-L118). Its callers are [`SoftFileLock._try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L107-L123), [`MarkerSoftFileLock._try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_marker.py#L74-L81) and [`SoftFileLease._try_break_stale_l | GCVE Database |
Page 1 of 4