# Rust audit logging: fence failed writers and recover into fresh files

## Reproduction

The baseline audit source is unchanged from `445548cd8`; the HP baseline checkout
is `e559f3695`, retained in `baseline-pin`. The 14 prospective public-API tests run against
that actual baseline implementation: 1 passes and 13 fail. The successful case
is ordinary short writes with EINTR retries. Hard-error cases observe appended
later records after the failed batch; three additional lifecycle tests prove
that the original logger admits existing paths after failure, permits both
recovery owners to open one target, and still admits the old file after owner
transfer. The multiple-batch case observes writes beyond the injected failed
prefix. Test faults are restricted to this executable and one canonical file
pathname at a time.

The prior accounting repair accurately counts incomplete records in a partial
batch, but continues appending later records after a hard I/O fault. A prefix
without its record terminator then merges with a later record. Reopening on the
same owner also appends to that file. Inferring complete records from a terminal
newline is insufficient: payloads can contain newlines. The writer itself has
the actual formatted record boundaries.

## Contract and repair

A hard or zero-byte write failure permanently fences the active writer. It
preserves complete prefix accounting, counts incomplete/unwritten records in
that batch once, and publishes a cold lifecycle fault flag. Later accepted
records are drained into `write_failures` without reading their payloads or
performing more writes or syncs. Full-ring offers still count as drops. The
producer implementation is unchanged: no new allocation, lock, syscall or fault
flag load is added to its capture path.

`disable` joins the drained writer and carries the fault flag into a monotonic
rotation requirement. Every subsequent `enable` of that same `FileLog` uses
`OpenOptions::create_new`, including after healthy rotated cycles. Existing
files, hard links and symbolic aliases are refused without mutation; a fresh
pathname is admitted. Two recovering owners racing for one fresh path admit
exactly one. This uses the standard library's primary
[atomic create-new contract](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new),
not a separate pathname-exists check. No metadata identity registry, extra read
permission or truncate/repair operation is introduced.

Healthy loggers retain append mode and healthy disable/re-enable behavior.
Public API signatures and counter shapes remain unchanged. Truncation counts
accepted frames clipped before queueing, including writes which later fail.
The logger remains enabled while its fenced writer is alive, so subsequent
enabled offers remain accounted; disabled record calls retain their no-op
behavior. Recovery requires disabling first and choosing an unused pathname.

## Regression coverage

All ten previous syscall tests now require exact stopped output, failed
record accounting including the later accepted frame, zero post-fault syncs,
non-mutating same-file refusal, fresh-file recovery, and persistent rotation
history after a healthy cycle. They retain every complete-record boundary in a
64-record batch, empty and oversized frames, embedded newlines, SOH bytes, EIO,
ENOSPC, zero-byte writes and interrupted/short successful writes.

Four additional cases cover 256 queued records across multiple drain batches;
existing paths and hard/symbolic aliases, including an unrelated healthy file;
a faulted producer moved to another owner and returned after healthy rotation;
and two recovering producers racing on one fresh pathname. Expected timestamps
and lines are assembled independently from literal epoch text and original
payloads. The scoped fdatasync observer verifies that a faulted writer avoids
final sync, while a healthy writer still syncs. Fixture directories delete on
normal teardown; the fault guard releases parked writers before log teardown.

## Verification

The verified candidate is `4d32cc502fa19865276e0e996d36b33ef790cf8e`.
All 344 tracked Rust file hashes match between the publishing worktree and HP.
Cargo/Rust 1.92.0 runs offline at one job, with one test thread and absolute
session build/target paths. Test and development profiles omit debug symbols;
optimization and debug assertions retain their normal test settings. Strict
all-target Clippy uses the isolated 1.93.1 toolchain, also offline at one job.

| Gate | Result |
|---|---|
| Prospective public-API tests on the actual baseline | 1 passed, 13 failed, 0 ignored |
| `rust-engine-tests`, full default Rust Cargo suite | 1,146 passed, 0 failed, 1 existing ignored builders doctest |
| `rust-open-edition-tests`, full open Rust Cargo suite | 980 passed, 0 failed, the same 1 ignored doctest |
| External audit write/fence tests, included above | 14 passed in each edition |
| Strict Clippy 1.93.1, all targets with `-D warnings` | Passed in both editions |
| Rustfmt on the final fault integration test | Passed |
| Public-export scan on both owned audit files | 2 files, 0 findings |

Each selected conformance stage reports 1 ran, 0 failed and 0 skipped. Each report
is PARTIAL/exit 2 because just one of the 359 configured stages was selected.
The counts above describe the complete Rust Cargo suites invoked by those stages;
the rest of the monorepo conformance suite was not selected. A scratch wrapper
retains the real runner's successful Cargo stdout/stderr and command/exit metadata
without changing stage arguments, results or process-group cleanup.

Integration onto newer main produces `2de03e7e1`, with all 344 Rust inputs still
byte-identical to the tested candidate. The conformance runner's only intervening
change updates a .NET provider-parity stage; the selected Rust commands are
unchanged. Source hash checks retain this evidence without repeating unchanged
suites. `publication.json` records the final code and documentation revisions.

Owned heavy jobs run serially on HP after checking active compiler/suite processes
and at least 8 GiB available memory. The guard yields to foreign heavy jobs and
stops only its own process group if one appears. No heavy job was added on
BEELINK2. Fixture directories delete on normal test teardown.

Exact baseline and candidate revisions, patches, source hashes, negative/positive
logs, conformance reports, commands and a Git candidate bundle are retained in
`~/archive/libhft/rust-audit-fault-fence-20261004`. `SHA256SUMS` verifies the kept
evidence; HP retains its original logs in the matching archive directory.

## Limits and remaining work

This fence belongs to the `FileLog` lifetime. Another logger or process has no
persisted fault marker and can open an existing file under the original append
contract. Keep the faulted owner and rotate its path; replacing that owner does
not transfer this history. The damaged prefix remains intact as evidence; this
repair does not validate or repair old files and adds no on-disk format or global
writer exclusion. A faulted owner conservatively requires a new file even when
the failure occurred at a complete-record boundary, and for every later cycle.
A failed thread spawn after fresh-file creation may leave an empty created file;
retry still needs an unused pathname, as no pathname is deleted automatically.

Sync errors on otherwise healthy writers remain uncounted by this record-failure
API. A separate, coordinated durability-error contract is still needed rather
than counting sync syscalls as failed records. Writer fencing cannot bound a
kernel call that has not returned; the tested drain avoids additional file I/O
after an observed write error. Actual production endpoint and venue/FIXP
certification remains pending; inventory parity scores do not change.
