Skip to content

fix(event): fall back to a byte-copy receipt when capability linkat is unsupported (macOS) - #309

Merged
schickling merged 1 commit into
mainfrom
fix/linkat-devfd-eperm-darwin
Aug 22, 2026
Merged

fix(event): fall back to a byte-copy receipt when capability linkat is unsupported (macOS)#309
schickling merged 1 commit into
mainfrom
fix/linkat-devfd-eperm-darwin

Conversation

@schickling

Copy link
Copy Markdown
Contributor

Fixes #308

Problem

archive_validated_file (src/event.rs) archives a validated predecessor by hardlinking through the open-file descriptor path — /proc/self/fd/N on Linux, /dev/fd/N elsewhere — with linkat(AT_SYMLINK_FOLLOW). Linux procfs materializes that link against the underlying inode. macOS fdescfs does not support it and answers EPERM, so since #300 every Darwin host fails publication, and cargo test -p st2 --lib cannot pass there:

ding::tests::a_producer_supersede_of_a_staged_event_never_repastes_and_the_successor_delivers
ding::tests::a_superseded_but_still_retained_staged_event_keeps_ownership_without_repasting
→ called `Result::unwrap()` on an `Err` value: archive the validated predecessor capability
  Caused by: Operation not permitted (os error 1)

Minimal repro (verified Linux OK / macOS EPERM): https://github.com/schickling-repros/2026-08-st2-linkat-devfd-eperm-on-macos

Goal

Publication must succeed on platforms where capability linkat is unsupported, without changing supersession semantics where it works today.

Decisions

  • Classify only EPERM/ENOSYS/EOPNOTSUPP from the capability linkat as platform-unsupported; everything else remains a hard error so real failures surface instead of being silently degraded.
  • Fallback = staged byte-copy receipt: create_new temp in the archive dir (.st2-archive-{pid}-{n}, O_NOFOLLOW), write validated bytes from the fd, fsync, install via existing rename_noreplace; an install race resolves like the existing AlreadyExists path. The caller's readback byte-comparison proves whichever receipt won carries exactly the validated bytes, so downstream ownership logic is unchanged.
  • Tradeoff vs the hardlink fast path is inode identity: a crash between the copy and the conditional unlink leaves the retained inbox entry in place until revalidation — the same-inode checks read that as "still present", never as data loss. Documented on the helper.
  • Test hook is a debug-only TEST_FORCE_ARCHIVE_RECEIPT_COPY atomic rather than an env var: process-global env mutation is unsafe under edition 2024 and racy across parallel tests; the atomic keeps the fallback deterministically exercisable on every platform.

Verification

  • Linux x86_64 (nixpkgs cargo 1.95): cargo test -p st2 --lib343 passed; 0 failed, including the new archive_copy_fallback_preserves_supersede_ownership_without_staging_leftovers.
  • macOS arm64 (same suite): 336 passed; 1 failed — the previously-failing ding supersession tests and the new fallback test all pass:
test ding::tests::a_producer_supersede_of_a_staged_event_never_repastes_and_the_successor_delivers ... ok
test ding::tests::a_superseded_but_still_retained_staged_event_keeps_ownership_without_repasting ... ok
test ding::tests::archive_copy_fallback_preserves_supersede_ownership_without_staging_leftovers ... ok
  • The one remaining darwin failure (codex_app_server::process_group_cleanup_reaps_a_native_launcher_descendant, ParseIntError { kind: Empty }) does not touch this code path; evidence that it fails identically on unfixed 9b1a3b4b follows in a comment.

Complexity

One fallback helper + one classification helper; no new dependencies, no API changes.

References

…s unsupported

archive_validated_file hardlinks the validated predecessor through
/proc/self/fd/N (Linux) or /dev/fd/N (elsewhere) with AT_SYMLINK_FOLLOW.
Linux procfs permits materializing that link; macOS fdescfs answers with
EPERM, so publication failed on every Darwin host and cargo test -p st2
--lib could not pass there since #300.

Classify EPERM/ENOSYS/EOPNOTSUPP from the capability linkat as
platform-unsupported and install the receipt as a staged, fsynced,
rename_noreplace byte copy instead; every other error stays a hard
failure. The caller's readback proves whichever receipt won carries the
validated bytes, so supersession semantics are unchanged. The tradeoff is
inode identity: a crash between copy and conditional unlink leaves the
retained inbox entry in place until revalidation.

A debug-only TEST_FORCE_ARCHIVE_RECEIPT_COPY switch exercises the
fallback on every platform; the new ding supersession test asserts receipt
bytes match and no staging files leak into the archive.

Fixes #308

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff31f6fb1c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/event.rs
Comment on lines +706 to +708
#[cfg(debug_assertions)]
if TEST_FORCE_ARCHIVE_RECEIPT_COPY.load(Ordering::Relaxed) {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bypass linkat when the copy fallback is forced

When capability linkat succeeds, as it normally does on Linux CI, this flag is never consulted because capability_link_unsupported is called only inside the result < 0 branch. Consequently, archive_copy_fallback_preserves_supersede_ownership_without_staging_leftovers passes through the original hardlink path rather than exercising write_archive_receipt_copy, leaving the new fallback untested on supported platforms; check the force flag before attempting linkat or otherwise force that call to take the copy path.

Useful? React with 👍 / 👎.

@schickling

Copy link
Copy Markdown
Contributor Author

Correction to the Verification section: codex_app_server::process_group_cleanup_reaps_a_native_launcher_descendant is a pre-existing darwin flake under full-suite parallel load, not a regression of this PR — in isolation it passes on unfixed 9b1a3b4b (1 passed; 0 failed). It parses external process output and hit an empty read mid-suite. A rerun of the complete suite on this branch is running; result will be attached.

@schickling

Copy link
Copy Markdown
Contributor Author

Rerun of the complete suite on this branch, macOS arm64:

test result: ok. 337 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.44s

Fully green — the earlier single failure was the documented pre-existing parallel-load flake, which passes here and on unfixed main in isolation.

@schickling
schickling requested a review from myobie August 22, 2026 08:56
@schickling
schickling merged commit 4629aeb into main Aug 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

archive_validated_file: linkat on /dev/fd/N returns EPERM on macOS — crate cannot build/test on Darwin

1 participant