create_staging_path() minted the stage's main file with mkstemp and only then
took its sidecar .lock via stage_owner_register(). A second, concurrent
cbm_pipeline_run() against the same final_path (auto_index, or the
stale-rendezvous-recovery retry path) could land its sweep_orphan_stages() in
that window: the main file was visible but not yet locked, so the sweep treated
it like a genuinely orphaned stage and deleted it out from under the live writer
(POSIX), or collided on the not-yet-created sidecar and surfaced EACCES/errno=13
(Windows). That is the #2111 red on test-windows-guards and both test-windows
CLANG64 x86_64 shards (index_format / simhash / index_resilience, with
pipeline.stage action=lock_failed errno=13).
Fix: mint the six-char alphanumeric suffix in create_staging_path() itself,
create and hold the sidecar lock FIRST, then create the main file with O_EXCL.
The stage's main file is therefore never present on disk without its lock
already held, so a racing sweep can only ever find the stage lock-held: it
reaches the stage through the .lock entry too (stage_entry_stage_length()
matches it), probes the lock, sees a live holder, and keeps it. The
create->register TOCTOU is closed by construction.
#1839 is preserved: sweep_one_stage() still removes a stage whose sidecar is
absent (ENOENT). With live stages now always carrying their sidecar, ENOENT is
only ever a genuine pre-lock-era orphan minted by an older binary, which #1839
requires be swept.
stage_owner_register() becomes stage_owner_adopt(), recording the already-held
descriptor; re-taking the lock would self-conflict, since flock() and Windows
_SH_DENYRW deny a second acquire of the same sidecar even from this process.
A new after_stage_created test seam and pipeline_concurrent_sweep_must_not_-
remove_inflight_stage drive a real concurrent sweep in that window; the test is
RED without the lock-first ordering.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>