5.7 KiB
Linux journal publication: refuse replacement
Scope: the transition from a private staged journal directory to the pending
.arcadia-save-ENTRY.journal name. This is an ownership boundary before model
files can be replaced. It does not establish multi-file atomic visibility,
cross-process compare-and-swap, or power-loss durability.
Evidence and decision
The actual Ubuntu 24.04.4 / kernel 6.8.0-139 / x86_64 / glibc 2.39 / ext4 probe
in .local/linux-qa/rename-probe.py showed ordinary directory rename replacing
an existing empty destination. Its output
.local/linux-qa/runtime/linux-rename-probe.log records replacement=true,
RENAME_NOREPLACE refusal errno 17, competing directory retained=true, and
fresh publication=true. That probe predates this Rust implementation.
Linux now uses libc::syscall(SYS_renameat2, AT_FDCWD, stage, AT_FDCWD, journal, RENAME_NOREPLACE). The existing locked libc 0.2.189 package becomes a direct
Linux-only core dependency; no new package/version is introduced. Its supplied
architecture constants avoid handwritten syscall numbers. Calling syscall
also avoids adding a glibc 2.28 renameat2 wrapper symbol requirement.
The Linux manual documents no-replacement refusal, kernel and filesystem support requirements, and EINVAL for unsupported flags. Kernel/ext4 support begins with Linux 3.15; this is a requirement on this save operation, not a claim that the whole application supports that old platform. Sources: rename(2), syscall(2).
Paths use original Unix OsStr bytes in live NUL-terminated buffers; embedded
NUL is rejected before the syscall. Failure preserves the OS error and journal
path. There is no fallback, retry, or substitute ordinary rename on Linux:
unavailable/blocked syscalls and unsupported filesystems stop the save before
the write-start marker or model writes. Windows retains MoveFileExW without
replacement and its existing bounded scanner retries. Other operating systems
explicitly retain their former standard-rename behavior; the stronger publication
guarantee is scoped to Windows and Linux.
Verification boundary
Four Linux-only tests in source_files.rs, filter linux_journal_publication:
- A competing empty directory, nonempty directory, file, or dangling symlink appearing after precheck keeps its inode and contents; our staged metadata remains intact and publication returns EEXIST at the journal path.
- Fresh publication preserves non-UTF8 Unix path bytes. Either path containing NUL fails before moving the stage.
- Two synchronized staged publishers have exactly one winner. The other keeps its complete stage and receives EEXIST.
- A competitor injected immediately before full-save publication prevents ownership transfer, the write-start marker, and every model-file replacement; captured original bytes and dependency expectations remain unchanged.
An additional explicitly ignored Linux test requires syscall fault injection
and OA_TEST_RENAMEAT2_ERRNO. It rejects unconfigured execution and accepts only
ENOSYS, EINVAL, or EOPNOTSUPP. It exercises a full save and requires the original
errno/journal path, no ownership transfer or write marker, no pending journal,
and unchanged source bytes. Run the compiled core test binary directly under
strace (not Cargo) with --ignored --exact source_files::transaction_tests::linux_journal_publication_injected_unsupported_error_is_fail_closed.
For example, set OA_TEST_RENAMEAT2_ERRNO=38 and use
strace -f -e inject=renameat2:error=ENOSYS TEST_BINARY ...; repeat EINVAL/22 and
EOPNOTSUPP/95 on the x86_64 guest. Normal test runs report this test as ignored.
Source formatted and git diff --check passed. The exact three-file delta was
copied into a separately hash-verified guest snapshot, preserving the original.
Actual Rust 1.90 execution on Ubuntu24.04.4 x86_64/glibc2.39/ext4 passed all four
normal focused tests (one fault oracle ignored), then all 42 normal library
tests using the same built test binary. The ignored full-save oracle was
subsequently explicitly executed and passed three times under strace6.8:
ENOSYS38, EINVAL22 and EOPNOTSUPP95. Each raw trace confirms the actual
renameat2(..., RENAME_NOREPLACE) injected errno; all original-byte, no-write,
no-journal and exact-error assertions passed. Run:
cargo test --locked -p oa-core --lib linux_journal_publication
Frozen implementation input SHA-256:
| File | SHA-256 |
|---|---|
crates/oa-core/src/source_files.rs |
669491DBDDB8E8E6556161426DFFA1A49AC1C826D31020BFF0956D81A4D84683 |
crates/oa-core/Cargo.toml |
64824EC447BE777841A972BF06C894EF2624A37E4725583BCFE0389DEB9F00A3 |
Cargo.lock |
A6E070ED59B0BA19F18D60E8104494B777B470BD0268AE348CBE2B229790449F |
Logs: .local/linux-qa/runtime/linux-journal-focused.log,
linux-journal-all-and-injection.log, and
journal-injection-{ENOSYS,EINVAL,EOPNOTSUPP}.strace in that directory.
The delta archive SHA-256 is
3e57dad3be4c3c23b7952500924c41c5b671bcadb0e02a1d49d86f5bfa8f9300;
all 226 original and patched snapshot file hashes were verified. Independent
read-only review found the current x86_64 syscall ABI and path-buffer lifetime
sound. The full library rerun includes existing interrupted-save and v1/v2
recovery cases. Linux environment and timing report.
Remaining boundaries: complete current Linux integration/application/package verification; other architectures/libcs/filesystems; network filesystems and hostile ancestor-path mutation. These results do not establish power-loss durability or model-file CAS. Journal retirement/recovery behavior was not redesigned in this change.