Skip to content

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

Description

@schickling

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions