Problem
src/event.rs's archive_validated_file (introduced in #300, rev 9b1a3b4bd4275a0afef207008f9bde7e70a35854) archives a validated inbox message by hardlinking the open fd through its magic symlink:
#[cfg(target_os = "linux")]
let capability = format!("/proc/self/fd/{}", file.as_raw_fd());
#[cfg(not(target_os = "linux"))]
let capability = format!("/dev/fd/{}", file.as_raw_fd());
libc::linkat(libc::AT_FDCWD, capability.as_ptr(), archive_dir.as_raw_fd(), filename_c.as_ptr(), libc::AT_SYMLINK_FOLLOW)
This works on Linux: /proc/self/fd/N resolves to the underlying regular file and linkat + AT_SYMLINK_FOLLOW links it. On macOS it does not: /dev/fd/N lives on fdescfs and linkat(..., AT_SYMLINK_FOLLOW) fails with EPERM. The function then returns Err with the context string "archive the validated predecessor capability".
Reproduction
Standalone repro (single ~80-line C program mirroring the exact call, plus a Python fallback): https://github.com/schickling-repros/2026-08-st2-linkat-devfd-eperm-on-macos
Linux x86_64:
$ ./run.sh
OK: linkat(/proc/self/fd/3, "archived", AT_SYMLINK_FOLLOW) succeeded
arm64 macOS:
$ ./run.sh
FAIL: linkat(/dev/fd/3, "archived", AT_SYMLINK_FOLLOW) returned errno=1 (Operation not permitted)
Regression window
Impact
On aarch64-darwin, cargo test -p st2 --lib fails deterministically — e.g.:
ding::tests::a_producer_supersede_of_a_staged_event_never_repastes_and_the_successor_delivers ... FAILED
called `Result::unwrap()` on an `Err` value: archive the validated predecessor capability
Caused by:
Operation not permitted (os error 1)
(same for ding::tests::a_superseded_but_still_retained_staged_event_keeps_ownership_without_repastes, occasionally also a third ding test). The crate is effectively unbuildable/untestable on Macs.
Suggested fix direction
Keep the capability linkat as the fast path (Linux unchanged), but treat EPERM from the non-Linux branch as "platform doesn't support linking through the fd" and fall back to still archiving the validated bytes — e.g. verify via fstat that the inbox path still names the same inode as the validated fd before linking the real path, or byte-copy from the fd into the archive entry. The function already readback-verifies archived bytes and fsyncs afterwards, so either fallback stays consistent with the existing guarantees.
Happy to follow up with a PR if useful.
Problem
src/event.rs'sarchive_validated_file(introduced in #300, rev9b1a3b4bd4275a0afef207008f9bde7e70a35854) archives a validated inbox message by hardlinking the open fd through its magic symlink:This works on Linux:
/proc/self/fd/Nresolves to the underlying regular file andlinkat+AT_SYMLINK_FOLLOWlinks it. On macOS it does not:/dev/fd/Nlives on fdescfs andlinkat(..., AT_SYMLINK_FOLLOW)fails withEPERM. The function then returnsErrwith the context string "archive the validated predecessor capability".Reproduction
Standalone repro (single ~80-line C program mirroring the exact call, plus a Python fallback): https://github.com/schickling-repros/2026-08-st2-linkat-devfd-eperm-on-macos
Linux x86_64:
arm64 macOS:
Regression window
9b1a3b4bd4(current main tip).e4e9d564builds and tests clean on the same machine.Impact
On aarch64-darwin,
cargo test -p st2 --libfails deterministically — e.g.:(same for
ding::tests::a_superseded_but_still_retained_staged_event_keeps_ownership_without_repastes, occasionally also a third ding test). The crate is effectively unbuildable/untestable on Macs.Suggested fix direction
Keep the capability linkat as the fast path (Linux unchanged), but treat
EPERMfrom the non-Linux branch as "platform doesn't support linking through the fd" and fall back to still archiving the validated bytes — e.g. verify viafstatthat the inbox path still names the same inode as the validated fd before linking the real path, or byte-copy from the fd into the archive entry. The function already readback-verifies archived bytes and fsyncs afterwards, so either fallback stays consistent with the existing guarantees.Happy to follow up with a PR if useful.