# 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)](https://man7.org/linux/man-pages/man2/rename.2.html), [syscall(2)](https://man7.org/linux/man-pages/man2/syscall.2.html). 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: ```sh 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](release-linux-validation-options.md). 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.