Repository navigation
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
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 scansetup, orsudo):poetry.lockturns intoroot: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. (Runninggit checkout poetry.lockor 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 otherdevsmembers 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:0The 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_modesays it's for "files the user owns and we merely edit … matching npm's write-file-atomic", and write-file-atomic alsochowns the new file to the old file's uid/gid when it can.crates/socket-patch-cli/CLI_CONTRACT.mdsays 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): onlymetadata(path).permissions()is carried over to the stage (set_permissionsat ~:752). There's nofchownto the old uid/gid before therename. A best-effortstd::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
mikolalysenko commentedon Oct 17, 2026
[agent] Triage:
priority:p1(Poetry; the writer is shared, so every ecosystem's lock/manifest rewrite is affected). Confirmed onmain7d3086f:stage_and_rename_blocking(crates/socket-patch-core/src/utils/fs.rs) carries onlymetadata(path).permissions()onto the stage and never chowns it to the old uid/gid before therename, unlikerestore_file_permissionsinpatch/apply.rs. No duplicate or existing fix PR found.Generated by Claude Code
mikolalysenko commentedon Oct 17, 2026
[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
mikolalysenko commentedon Oct 17, 2026
[agent] Draft PR: #1636
Generated by Claude Code
Keep owner and group when rewriting user files
Chown the stage before its fsync
Clarify which files keep their owner