SOW-0156: systemd v260.1 header rules and 4 GiB ceiling in both SDKs
KT
1d ago+4073+4073+0123456789012345678901234567890123456789-326-326-012345678901234567890123456789
Stacked on #17. Merge order: #13, #14, #15, #16, #17, then this. SOW-0156 (.agents/sow/done/SOW-0156-20261010-fixed-header-consistency-audit.md) is completed.
Decisions for review
- Agent-chosen, pending your review:
- Error names: Go
ErrTimestampOutOfRange/ErrJournalFileFull, RustJournalError::TimestampOutOfRange/JournalError::JournalFileFull(both Rust variants are unreleased and listed indocs/Rust-API.md). - A1, A4, A5 (Rust header gates at Go parity), approved 2026-11-03.
- The effective file ceiling is
2^32 - 8 MiB, not 4 GiB: the arena grows in 8 MiB steps, so the last object must end there for the file to stay at or below2^32 - 1.
- Error names: Go
- Decided by you (2026-11-03): D1 = A, D2 = B, D3 = A (old large files stay supported), D4 = A, D5 = A, D6 = A, N1 = A (continuation is SOW-0171), N2 = A.
- New, needs a decision later (not blocking): SOW-0172. The Rust
Logaccepts a relative directory, then fails on the first write; Go works. Options and a recommendation are in the SOW.
What changes
- Append-open in both SDKs refuses, before mutation, what systemd v260.1 will not write: header size other than 272, unknown compatible flags, a missing tail-entry-boot-ID flag, archived or sealed files, invalid tail metadata, and files over 4 GiB.
Logsets such an active file aside; root-retentionLogs archive it. - Timestamps: writers refuse realtime outside
1..2^55-1and monotonic at or above2^55. - 4 GiB ceiling (chunk 2): both writers bound each entry's growth from payload sizes and return the file-full error when the entry could pass the ceiling. Nothing is written and the writer stays usable. Entry arrays stop doubling near the ceiling.
Logrotates once and retries. - Readers: refuse unknown header states and short sealed headers. Rust accepts single-bucket hash tables. Both ignore the truncated 32-bit tail array hint above 4 GiB, so files earlier releases wrote stay readable.
Validation
go test ./...andcargo test --workspacepass, on this branch and on the top of the stack (#22).- Byte identity, compression, compact, live, verify and root-retention parity matrices pass. The journalctl query matrix passes against systemd v262.
- Docs examples: 35/35 pass.
- Fixtures written by the v0.10.0 writer (regular, compact, compressed, sealed, out-of-range timestamps, archived-at-active-name, 9.9 GB) are read by both SDKs and continued by
Log. - Real use: a
Logwith no limits, fed 1 MiB entries.- Go: 22 files and 87,457 rows. Every full file is exactly
2^32 - 8 MiB. - Rust: rotates at the same point.
- Seqnums are continuous in both runs. Go and Rust read each other's files, and both verifiers pass on all 24 files.
- Go: 22 files and 87,457 rows. Every full file is exactly
- Benchmarks: interleaved pairs on a loaded host. Wall-clock results are inconclusive; paired user-CPU medians are within noise (Go -1.0% to +4.3%, Rust -8.1% to +3.9%). File size, mmap remap and map counts are identical in every pair.
Summary by cubic
Applies systemd v260.1's writable-open rules and timestamp bounds to both SDKs, bringing Rust header gates to Go parity, and records the SOW-0156 decisions that unblocked the work.
- Both SDKs' append-open refuses header sizes other than 272, unknown compatible flags, a missing tail-entry-boot-ID flag, archived files, sealed files, invalid tail metadata, and regular files over 4 GiB, before any mutation.
Logsets such an active file aside and continues the sequence; root-retention logs archive it under its archive name instead of failing to start. Continuing sealed files with the sealing key is tracked in SOW-0171. - Both SDKs' writers refuse realtime outside
1..2^55-1and monotonic at or above2^55before mutation via newErrTimestampOutOfRange/TimestampOutOfRangeerrors;Logno longer inherits out-of-range tails as clock floors. - Both SDKs' writers now refuse an entry that could grow the file past 4 GiB via new
ErrJournalFileFull/JournalError::JournalFileFullerrors, rotating once and retrying in directory writers, and returning the entry when it does not fit an empty file. - Both SDKs' opens reject unknown header states and short sealed headers before mutation;
Lognow sets such active files aside at startup and keeps the sequence instead of stopping, with Go's chain scan reading those headers for sequence identity. Rust accepts single-bucket hash tables, and both ignore the truncated 32-bit tail array hint above 4 GiB, keeping files from earlier releases readable. - Rejected opens and refused creates leave files byte-for-byte unchanged, proven by paired Go/Rust tests: both writers validate a new file's layout and sealing options before opening. Fixtures come from the v0.10.0 writer. The user confirmed the error names and parked the Rust relative-directory finding as SOW-0172.