Skip to content

Hosted and vendored rewrites replace poetry.lock with a file owned by the user running socket-patch, so after a root scan (e.g. a Docker bind mount) the lock's owner gets "Permission denied" from poetry add / poetry lock #1635

Open

Description

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

Summary

Hosted and vendored lock rewrites (and rollback / vendor --revert) write poetry.lock by staging a new file and renaming it over the old one (atomic_write_bytes_preserving_mode). The stage keeps the old file's permission bits but not its owner or group, so the rewritten poetry.lock belongs to whoever ran socket-patch.

  • Run as root on a project owned by a normal user (the usual docker run -v "$PWD:/app" … socket-patch scan setup, or sudo): poetry.lock turns into root:root 0644. Its real owner can still read it, but every Poetry command that writes the lock (poetry add, poetry lock, poetry remove, poetry update) then fails with [Errno 13] Permission denied: '…/poetry.lock'. Poetry writes the lock in place, so it can't replace a root-owned file. (Running git checkout poetry.lock or another socket-patch run as the user does work, because those replace the directory entry.)
  • Run as a normal user in a shared checkout whose files belong to a team group (ubuntu:devs 0664, directory not setgid): the lock is reassigned to the user's primary group (ubuntu:ubuntu 0664), so the other devs members lose write access to it.

Control: Poetry itself running as root (poetry add --lock idna==3.7) rewrites the same lock and keeps it owned by 1000:1000. Afterwards the owner's poetry add works.

The patch engine already restores uid/gid for files it patches (restore_file_permissions, mentioned in the doc comment at crates/socket-patch-core/src/utils/fs.rs:440). The lockfile writer doesn't.

Impact

After a containerised or sudo run, the next dependency change by the project's owner fails until they chown the lock back. Nothing tells them socket-patch changed the owner. pyproject.toml (which hosted and vendored don't write) keeps its owner, so the failure looks like a broken lock rather than a permissions change.

Repro (Linux, real Poetry 2.5.1; same with 1.8.5)

# as root; uid 1000 = ubuntu
mkdir /srv/own && cd /srv/own
cat > pyproject.toml <<'EOF'
[tool.poetry]
name = "demo"
version = "0.1.0"
description = ""
authors = ["x <x@example.com>"]
[tool.poetry.dependencies]
python = "^3.8"
six = "1.16.0"
EOF
poetry lock && chown -R 1000:1000 /srv/own
stat -c '%u:%g %a' poetry.lock          # 1000:1000 644
socket-patch scan --mode hosted --yes   # or --mode vendored; a patch for six@1.16.0
stat -c '%u:%g %a' poetry.lock          # 0:0 644
su ubuntu -c 'cd /srv/own && poetry add --lock attrs==23.2.0'
# [Errno 13] Permission denied: '/srv/own/poetry.lock'   (exit 1)
socket-patch rollback --yes; stat -c '%u:%g' poetry.lock   # still 0:0

The patch data came from a local mock of the patch API serving a patched copy of the real six-1.16.0 wheel (patches-api.socket.dev is unreachable from the sandbox). The repo's vex_pypi_real_common mock has the same shape. Ownership doesn't depend on the patch content.

Expected vs actual

  • Expected: an edit to a file the user owns keeps its owner and group, the same as its mode. The doc comment on atomic_write_bytes_preserving_mode says it's for "files the user owns and we merely edit … matching npm's write-file-atomic", and write-file-atomic also chowns the new file to the old file's uid/gid when it can. crates/socket-patch-cli/CLI_CONTRACT.md says nothing about ownership; it only promises that a rewrite's stage keeps the old permission bits.
  • Actual: the replacement file is owned by the euid/egid of the socket-patch process.

Matrix

OS Poetry Mode Run as Result
Linux 2.5.1 hosted scan root fail (0:0, owner's poetry add --lock exit 1), 2/2
Linux 2.5.1 vendored scan root fail (same)
Linux 1.8.5 vendored scan, then root rollback root fail (0:0 after both, owner's poetry add --lock exit 1)
Linux 2.5.1 hosted scan non-root member of the file's group fail (ubuntu:devs → ubuntu:ubuntu)
Linux 2.5.1 control: poetry add --lock root pass (stays 1000:1000)
macOS / Windows – – – untested (no probe branch this run; Windows has no POSIX owner, and macOS behaves like Linux for rename)

Release v5.0.1 (npm @socketsecurity/socket-patch) behaves the same as main 7d3086f, so this isn't a recent regression.

Suspect code

  • crates/socket-patch-core/src/utils/fs.rs:735 (stage_and_rename_blocking): only metadata(path).permissions() is carried over to the stage (set_permissions at ~:752). There's no fchown to the old uid/gid before the rename. A best-effort std::os::unix::fs::fchown(&file, Some(uid), Some(gid)) (ignoring EPERM, as write-file-atomic does) would fix it.
  • Callers that write poetry.lock: crates/socket-patch-core/src/vendor/pypi_poetry.rs:399, crates/socket-patch-core/src/patch/redirect/staged.rs:60 / :88, crates/socket-patch-cli/src/commands/scan/hosted.rs:2079 / :2133, crates/socket-patch-cli/src/commands/vendor.rs:270 / :1676.

This writer is shared, so every ecosystem's lockfile and manifest rewrite (package.json, .npmrc, Gemfile.lock, …) probably behaves the same way. I've only verified the Poetry path.

Activity

  1. mikolalysenko commented on Oct 17, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (Poetry; the writer is shared, so every ecosystem's lock/manifest rewrite is affected). Confirmed on main 7d3086f: stage_and_rename_blocking (crates/socket-patch-core/src/utils/fs.rs) carries only metadata(path).permissions() onto the stage and never chowns it to the old uid/gid before the rename, unlike restore_file_permissions in patch/apply.rs. No duplicate or existing fix PR found.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 17, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the atomic stage-and-rename writer keeps the old file's mode but not its uid/gid). Branch: agent/fix-atomic-write-keep-owner. Claim-ID: 2026-10-17T20:20:52Z-68b394


    Generated by Claude Code

  3. mikolalysenko commented on Oct 17, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1636


    Generated by Claude Code

  4. added 3 commits that reference this issue on Oct 17, 2026
    423c192
    a5d69e3
    1e945c5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions