Skip to content

perf: use os.path.abspath() instead of Path.resolve() - #10821

Merged
radoering merged 1 commit into
python-poetry:mainfrom
radoering:wheel-installer-perf-regression
Apr 12, 2026
Merged

radoering merged 1 commit into
python-poetry:mainfrom
radoering:wheel-installer-perf-regression

Conversation

@radoering

Copy link
Copy Markdown
Member

Pull Request Check List

  • Added tests for changed code.
  • Updated documentation for changed code.

There is a significant performance regression due to #10792, which is mostly caused by the two calls of Path.resolve().

The time to install a sample project with some dependencies increased from 8 seconds (Poetry 2.3.2) to 13 seconds (Poetry 2.3.3).

With the fix, installation is almost as fast as before (less than half a second slower).

It should be safe to use os.path.abspath() instead of Path.resolve(). At least, I could not craft a wheel that triggers a path traversal via symlinks (see new test). Further, os.path.abspath() is recommended in https://docs.python.org/3/library/zipfile.html#zipfile.Path and (if I do not miss anything) pip also uses os.path.abspath() (and not Path.resolve()): https://github.com/pypa/pip/blob/8c5468dc9695c023c0a6428e630126aa14c9db4e/src/pip/_internal/utils/unpacking.py#L79-L87

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Previously is_relative_to from poetry.utils._compat had a specific workaround for Windows long-path prefix mismatches; switching to Path.is_relative_to may reintroduce those edge cases, so it would be good to confirm and, if necessary, preserve the compatibility helper or equivalent logic.
  • The new comment references using os.path.commonpath() for validation, but the implementation uses Path.is_relative_to() instead; consider aligning the comment with the actual approach or updating the code to match the documented strategy to avoid future confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Previously `is_relative_to` from `poetry.utils._compat` had a specific workaround for Windows long-path prefix mismatches; switching to `Path.is_relative_to` may reintroduce those edge cases, so it would be good to confirm and, if necessary, preserve the compatibility helper or equivalent logic.
- The new comment references using `os.path.commonpath()` for validation, but the implementation uses `Path.is_relative_to()` instead; consider aligning the comment with the actual approach or updating the code to match the documented strategy to avoid future confusion.

## Individual Comments

### Comment 1
<location path="tests/installation/test_wheel_installer.py" line_range="178-187" />
<code_context>
+    return wheel
+
+
+def test_no_path_traversal_via_symlink(
+    tmp_path: Path, env: MockEnv, wheel_with_symlink: Path
+) -> None:
+    target = tmp_path / "target"
+    target.mkdir()
+
+    installer = WheelInstaller(env)
+    with pytest.raises(FileNotFoundError if WINDOWS else NotADirectoryError):
+        installer.install(wheel_with_symlink)
+
+    traversal_link = Path(env.paths["purelib"]) / "symlink" / "traversal_link"
+    assert traversal_link.exists()
+    assert not traversal_link.is_symlink()  # not even extracted as symlink
</code_context>
<issue_to_address>
**suggestion (testing):** Add an assertion that no file is created at the symlink target path outside the installation directory.

The test already verifies that the symlink itself is not extracted and that installation fails. To more clearly prove traversal is blocked, also assert that the external target (e.g. `tmp_path / "target" / "traversal.txt"`) does not exist after installation, confirming no file is written outside the install root.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@radoering
radoering force-pushed the wheel-installer-perf-regression branch from d27575b to c321d9d Compare April 11, 2026 12:05

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/installation/test_wheel_installer.py" line_range="134-143" />
<code_context>


-def test_path_traversal(env: MockEnv, wheel_with_path_traversal: Path) -> None:
+@pytest.mark.parametrize("existing", [False, True])
+def test_no_path_traversal(
+    env: MockEnv, wheel_with_path_traversal: Path, existing: bool
+) -> None:
+    target = env.path.parent / "traversal.txt"
+    if existing:
+        target.write_text("original", encoding="utf-8")
     installer = WheelInstaller(env)
     with pytest.raises(ValueError):
         installer.install(wheel_with_path_traversal)
-    assert not (env.path.parent / "traversal.txt").exists()
+
+    if existing:
+        assert target.exists()
+        assert target.read_text(encoding="utf-8") == "original"
+    else:
+        assert not target.exists()
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a positive test for a safe traversal-like path that stays within the target directory

The current tests cover rejecting traversal attempts and preserving/omitting files outside the env. To fully exercise the `abspath`-based checks, add a case where a ZIP entry contains `..` but still resolves inside the target directory (e.g. `"pkg/../in_pkg.txt"`), and assert that it is successfully written. This demonstrates that the logic permits safe normalized paths while blocking unsafe ones.

Suggested implementation:

```python
from poetry.installation.wheel_installer import WheelInstaller
from poetry.utils._compat import WINDOWS
from poetry.utils.env import MockEnv



@pytest.fixture
def env(tmp_path: Path) -> MockEnv:
    return MockEnv(path=tmp_path / "env")


def test_safe_normalized_path_allowed(
    env: MockEnv, wheel_with_safe_traversal: Path
) -> None:
    """
    A ZIP entry containing '..' that still normalizes inside the target directory
    should be accepted and written to disk.
    """
    installer = WheelInstaller(env)
    installer.install(wheel_with_safe_traversal)

    # The wheel should contain an entry like "pkg/../in_pkg.txt" which
    # normalizes to "in_pkg.txt" within the target directory.
    # Adjust the expected target path to match the MockEnv layout.
    target = env.site_packages / "in_pkg.txt"  # type: ignore[attr-defined]
    assert target.exists()
    assert target.read_text(encoding="utf-8") == "safe traversal"

```

To fully wire this test in, you will also need to:

1. **Add a `wheel_with_safe_traversal` fixture** in this file, next to `wheel_with_path_traversal`, for example:
   - Use the same wheel-building helper used by `wheel_with_path_traversal` (e.g. `_build_wheel`, `_make_wheel`, or similar).
   - Construct the wheel file contents with at least:
     - A safe, traversal-like path such as `"pkg/../in_pkg.txt": b"safe traversal"`.
     - Minimal package metadata so `WheelInstaller` accepts it (similar to the other wheel fixtures).
   - Return the path to the wheel file from the fixture.

2. **Confirm the target path** in the assertion:
   - If `WheelInstaller` installs into `env.site_packages`, keep `env.site_packages / "in_pkg.txt"` as written.
   - If it installs elsewhere (e.g. `env.site_packages / "traversal" / "in_pkg.txt"` or directly under `env.path`), adjust the `target = ...` expression accordingly.

3. **Ensure imports are sufficient**:
   - If `Path` or `pytest` are not already imported at the top of this file (they likely are), add:
     - `from pathlib import Path`
     - `import pytest`

With these adjustments, the new test will exercise the positive case where a path containing `..` resolves inside the target directory and is allowed by the `abspath`-based checks.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@radoering
radoering merged commit a029a41 into python-poetry:main Apr 12, 2026
54 checks passed
radoering added a commit that referenced this pull request Apr 12, 2026
mwalbeck pushed a commit to mwalbeck/docker-python-poetry that referenced this pull request Apr 26, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [poetry](https://github.com/python-poetry/poetry) ([changelog](https://python-poetry.org/history/)) | patch | `2.3.3` → `2.3.4` |

---

### Release Notes

<details>
<summary>python-poetry/poetry (poetry)</summary>

### [`v2.3.4`](https://github.com/python-poetry/poetry/blob/HEAD/CHANGELOG.md#234---2026-04-12)

[Compare Source](python-poetry/poetry@2.3.3...2.3.4)

##### Fixed

- Fix a performance regression in the wheel installer that was introduced in Poetry 2.3.3 ([#&#8203;10821](python-poetry/poetry#10821)).
- Fix a path traversal vulnerability in sdist extraction on Python 3.10.0-3.10.12 and 3.11.0-3.11.4 that could allow malicious tarball files to write files outside the target directory ([#&#8203;10837](python-poetry/poetry#10837)).

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDQuNCIsInVwZGF0ZWRJblZlciI6IjQzLjEwNC40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://git.walbeck.it/mwalbeck/docker-python-poetry/pulls/1742
Co-authored-by: renovate-bot <bot@walbeck.it>
Co-committed-by: renovate-bot <bot@walbeck.it>
@github-actions

Copy link
Copy Markdown

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators May 13, 2026
@radoering
radoering deleted the wheel-installer-perf-regression branch August 22, 2026 05:04
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant