Files
ARCADIA-Studio/harness/release-linux-journal-publication.md
T

102 lines
5.7 KiB
Markdown

# 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.