fix(aws-control): open lock files non-truncating before the acquire - #9316
Conversation
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS The contract scan claims fleet-wide enforcement but its detector shape misses live in-class offenders, so the invariant is believed enforced while still open. Watch
Suggestions
[DESIGN-REVIEWED] cf7de1c |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsThe diff is a clean, mechanical fix: No findings. [OPUS-REVIEWED] cf7de1c |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of I have everything needed. Verified findings: (1) the two exempted deploy files are already fixed on base (touch + First-Principles-Verdict: CONCERNS The "fleet-wide" scanner misses the one live sibling ( Not justified as shipped
What this change shipsIntent: stop Windows lock acquisition from truncating the lock file it serializes on — a FIX (linked #9248, #9264).
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] cf7de1c |
f957356 to
ca306f4
Compare
A lock file opened with open(path, "w") is truncated before any lock is held; on Windows the acquire is msvcrt.locking on the already-truncated file, so a contending process can observe or produce an empty lock file and crash out of the critical section (kirodotdevGH-9248). Adds platform_compat.open_lock_file — a create-or-open (O_RDWR|O_CREAT, never truncating) opener yielding a raw fd for file_lock/flock_exclusive — and converts the four aws_control lock sites (backup, library, shares x2), the last unclaimed offenders after kirodotdev#9250/kirodotdev#9275/kirodotdev#9279/kirodotdev#9237. Pins the property fleet-wide: a contract test scans the source tree for truncating opens handed to a lock acquire, so a new offender fails with its file and line. deploy/pending.py and deploy/profiles.py are exempted while PR kirodotdev#9269 (which owns those sites) is in flight. Fixes kirodotdev#9248 (remaining sites).
ca306f4 to
cf7de1c
Compare
|
Disposition ( Finding (" What WAS failing on the Windows shard — and is a good catch to arrive at, just not the stated one: Also: PR Hygiene wanted an explicit |
iamwhatever
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (5 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: root cause is open(lock_path, "w") truncating a lock file before the lock is held (GH-9248); a new open_lock_file helper uses O_RDWR|O_CREAT and the three aws_control lock sites switch to it. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.
…9267) (#9647) Migrate the four with-scoped copies of the non-truncating lock-open preamble onto platform_compat.open_lock_file (merged in #9316): session_pid._session_pid_file_lock, session_pid._pid_file_lock, webhooks.locked, and mcp_tools.control.register_hook. Each site keeps its own parent mkdir (the helper does not create dirs), drops its touch + "r+" preamble, and replaces its rationale comment with a one-line pointer to the helper docstring, which now holds the reasoning once. Deliberately left inline, each with a pointer comment stating why: - work_ledger._open_lock: PR #9374 (open, MERGEABLE) edits the same file; it follows after that lands so two of my PRs do not chain on one file. - _McpFileLock / _McpFileLockSync: acquire-now/release-later fd stored on self._fd, so it must outlive __aenter__/__enter__; the with-scoped helper would close it at method return. - session_pid._periodic_pid_sweep: takes a SHARED read lock; the helper serves exclusive with-scoped acquisition only. The repo-wide contract scan (test_no_lock_site_opens_truncating) already guarantees every site opens non-truncating, so what is left here is deduplication, not a bug fix. ## Pattern harvest A duplication issue's stated copy count is a lower bound, and the SHAPES hiding inside it decide whether consolidation is even possible. #9267 named four copies; inspection found seven open-sites across three shapes (with-scoped, acquire-later, shared-read), and the shared helper serves only one, so five sites could never collapse onto it regardless of who tried. Rule candidate: when consolidating N hand-rolled copies of an idiom, first enumerate the copies by GREP (the stated count is a floor) and classify each by the calling shape it needs (lifetime of the resource, exclusive vs shared acquire). Migrate only the shape the shared helper serves; leave the others inline with a pointer comment naming the shape mismatch, and put the coverage gap on the issue rather than widening the helper to fit every shape. Refs #9267
Problem / Motivation
#9248: a lock file opened with
open(path, "w")is truncated before any lock is held. On Windows the acquire routes tomsvcrt.lockingon the already-truncated file, so a contending process can observe or produce an empty lock file and crash out of the critical section. The overnight sweep fixed most subsystems (#9250 session_pid, #9275 issue_radar, #9279 webhooks + mcp_tools/control, #9237 work_ledger), but the four aws_control sites were still live on main, and every merged fix re-derived its own local touch-then-"r+"preamble — nothing stops the class growing back.This also covers the scope of #9264 (aws_control sites) and #9267 (consolidate a shared helper).
Why it matters
Intermittent-by-interleaving Windows corruption: this exact class reddened Backend Tests (Windows) shards across four unrelated PRs before #9248 named it. The aws_control sites guard the backup/library/shares stores — user data writes serialized by locks that could flicker empty under contention.
What changed
Motivation → approach → change:
platform_compat.open_lock_file(path)(new, next tofile_lock): create-or-open viaos.open(..., O_RDWR | O_CREAT)— one syscall, never truncates — yielding a raw fd ready forfile_lock/flock_exclusive. Future lock sites get the non-truncating shape by construction instead of by convention (Consolidate the non-truncating lock-open preamble into one shared helper #9267's ask).backup.py,library.py,shares.py×2) to it (aws_control lock sidecars are opened truncating before the acquire (4 sites, #9248 sweep) #9264's ask).open(..., "w")whose descriptor is handed to afile_lock-family acquire within the next two lines, and fails naming the file and line.deploy/pending.py/deploy/profiles.pyare exempted while PR fix: avoid truncating lock files before acquiring lock on Windows #9269 (which owns those sites) is in flight; the exemption comment says to drop them once it merges.Tests
TestOpenLockFile::test_preserves_existing_content— the defect shape: pre-existing lock-file bytes survive open + exclusive lock.TestOpenLockFile::test_creates_missing_file_and_is_lockable— create-if-absent, lockable viaflock_exclusive.TestOpenLockFile::test_no_lock_site_opens_truncating— the repo-wide contract scan. Detector verified against the pre-fix tree, where it named exactly the expected offenders and nothing else.Manual verification
Ran the contract scanner standalone against current main: the only hits are the two exempted deploy sites (#9269's scope). No behavioral change on POSIX beyond the open mode; lock-file content is never meaningful to the lock itself.
Screenshots / video
N/A — backend-only, no UI surface.
Related Issues
Pattern harvest
A lock-file open must never truncate: acquiring a lock cannot destroy the thing it serializes on. Codified as
open_lock_file+ a source-scanning contract test so the invariant is enforced, not remembered.Rule candidate: any file descriptor handed to a
file_lock-family acquire must come from a non-truncating open (open_lock_file,"r+","a+", orO_RDWR|O_CREAT) — neveropen(path, "w"); enforced repo-wide by the contract scan intest_platform_compat.py::TestOpenLockFile::test_no_lock_site_opens_truncating.Checklist
Contribution License Agreement
By submitting this pull request, I confirm that my contribution is made under the terms of the project's license.