Viewing files at commit ef78687eef1aCommit detailsView latest
Robustness suite (program pillar P4)
Crash points, network fault injection, concurrency, and environment faults for
Oak and Git, judged by independent readers. Rows are the robustness lane
(scripts/oakbench/rows.py, scenarios/robustness.yaml). Nothing here is a
timing measurement: elapsed_ms is the faulted operation's wall time, recorded
for context only.
# the independent Oak tree reader needs blake3 + zstandard in a pinned Python
python3 -m venv /tmp/readerenv && /tmp/readerenv/bin/pip install blake3 zstandard
export OAK_BENCH_OAK_READER_PYTHON=/tmp/readerenv/bin/python
python3 suites/robustness/run_robustness.py --quick # smoke, ~10 min
python3 suites/robustness/run_robustness.py # full matrix
python3 suites/robustness/run_robustness.py --subjects oak --families proxy --ops push
Output: results/robustness/<bench_id>.robustness.jsonl (+ .summary.md, a
.partial.jsonl flushed per case, and <bench_id>.robustness-evidence/ with
the trial directories of violated / harness_error cases). Results are never
committed (ADR-0003). Cases are not timing measurements, so the runner does not
hold the shared benchmark measurement lock by default (a long run would block
every other lane); the fsync storm and N-client runs do perturb timing, so pass
--measurement-lock to serialize behind a running campaign, and never run the
full suite beside one.
Safety: every server is local — a loopback oak serve (fixed per-fixture port,
restarted from a copied data dir per trial), a local bare Git repository, or the
stdlib smart-HTTP Git server. The fault proxy refuses non-loopback upstreams and
the hosted spec refuses non-loopback URLs; nothing here ever targets oak.space.
Classification
| class | row | meaning |
|---|---|---|
held | returncode 0 | every measured invariant held |
violated | returncode 1 | at least one invariant is false (a subject bug; see violations) |
unsupported | returncode 77, skipped: true, skip_reason | the case cannot run for this subject/host (a work item, ADR-0002) |
harness_error | returncode 2, harness_error | the harness failed (incl. an independent reader raising); never counted as a subject result |
fault_not_fired | returncode 77, skipped: true, skip_reason, fault_fired: false | the fault never took effect (the op exited before the kill; the disk was not short of the op's need; no proxy rule matched). A clean run is never held |
Invariants are true / false / null (unmeasured, e.g. the tree reader is
unavailable). null never becomes a pass or a fail. The lane validator
(oakbench.rows.validate_rows(..., "robustness")) rejects unknown classes,
a classification whose returncode does not match, and held/violated rows
with fault_fired: false.
Fired evidence: a time or syscall kill fired only if the client's exit status
is SIGKILL (rc == -9); a proxy fault only if its rule matched a request; disk
full only if the measured free space was below the op's need, or the op failed
/ reported ENOSPC. A time kill that lands after the op exited is re-aimed at
the same fraction of the duration the op actually took (up to 2 retries, fresh
fixture copy each; observations.time_attempts), then recorded
fault_not_fired.
Known bugs: violated rows whose violations match robustness.KNOWN_BUGS carry
known_bugs (e.g. fb-535 torn switch/pull worktree, fb-534 lost
acknowledged push); unmatched violations are new findings.
Independent readers
- Oak committed graph: stdlib
sqlite3on a private copy of.oak/oak.db(local) or<owner>/<repo>.oakdb(serve data dir). The copy's WAL is folded in by SQLite itself; the subject's files are never opened for write. Refs =branch_heads+metadata.head+tags; reachability walkscommits.parent_hash/merge_parent_hash.PRAGMA integrity_checkis the "store opens" oracle. - Oak committed trees:
scripts/oakbench/oak_sqlite_reader.py(P1's reader, blake3/zstd hash-verified) run on the same private copy; for a non-HEAD commit the copy's metadata is pointed at it. - Git: the real Git plumbing (
for-each-ref --contains,merge-base --is-ancestor,fsck,archive) — Git is the fixture/oracle subject.
Families and invariants
crash_time_v1 / crash_syscall_v1 (robustness/crash.py). Ops: commit,
push, pull_ff, pull_diverged (re-parent / merge), clone, switch,
merge_local (Git), merge_hosted and mount_finish (unsupported rows, with the
reason). Each (subject, op) fixture is built once and copied per trial. A clean
calibration run records the post-state, the duration, and the number of
durability boundaries. crash_time kills at 11 fractions of the duration;
crash_syscall kills immediately before the N-th fsync / F_FULLFSYNC /
rename of the subject process (ALICE/CrashMonkey method, process-crash model:
the page cache survives; power loss needs LazyFS on Linux). The interposer is a
DYLD library loaded into an ad-hoc re-signed copy of the same binary (hardened
runtime ignores DYLD_INSERT_LIBRARIES); both sha256s are recorded.
After each kill:
op_no_hang,repo_opens(integrity_check / fsck),acked_local_reachable: every commit a local ref named before the op is still reachable from a ref (checked again after the next op:acked_after_next),server_intact: remote store opens, pre-op heads reachable, branch head is the pre-op head or the op's intended head (no torn / regressed ref),worktree_consistent: worktree equals the pre/post state or a committed tree, or status reports dirty / refuses actionably — never "clean" while it differs,worktree_not_torn: the worktree is compared file by file against the op's pre side (pristine client) and target side (calibrated post-op worktree), and against where the ref is (HEAD unchanged = pre side, moved = post side). If it holds the op's output out of step with the ref (target-side files while HEAD is old, pre-side files while HEAD moved, or the op's temp files) and status lists those as ordinary uncommitted changes with nothing in progress, it fails: committing would record the op's partial output as the user's work. Passes when the state is flagged (MERGE_HEAD,progress_state.in_progress, a stale git lock that makes the nextgit commitrefuse with "remove the file manually":index.lock,HEAD.lockor the current branch'srefs/heads/<branch>.lock;config.lock, other refs' locks,packed-refs.lockdo not block a commit and do not count), status refuses, or the failing op itself names a recovery command,partial_clone_detected(clone): a torn clone directory must be recognised by the subject's status with an actionable error,next_op_ok_or_actionable: rerunning the op succeeds or fails with an actionable message: a concrete recovery command (a realoak/gitsubcommand or--continue/--abort), an explicit manual step ("remove the file manually"), a documented retryable condition,recommended_next_commands, or a specific (non-generic) JSON error code. Mentioning "oak"/"git" is not enough. Corruption / internal / opaque-store wording (malformed, corrupt, bad or invalid object, unable to read sha1 file/tree, early EOF, segfault, empty object file, HTTP 500, "unable to open database file", "not a git repository") is checked first and makes the failure non-actionable even when it also names a recovery command. Git's normal outcomes "Automatic merge failed; fix conflicts and then commit the result." and "Not possible to fast-forward, aborting." are actionable only for the git subject and only when every output line is one of git's ordinary merge/pull lines (Auto-merging, CONFLICT, hint:, From, FETCH_HEAD, Updating): any other line (fatal: bad object HEAD, a segfault, "unable to open database file") makes the hint not count; corruption / internal / panic text and generic codes (error,internal, ...) are never actionable; hang, panic or signal is a violation,no_false_success: after a successful next op the state is exactly the calibrated outcome (trees compared by the independent readers).
fault_proxy_v1 (robustness/faultproxy.py, proxycases.py). A clean record
run enumerates the op's requests; each trial injects one fault on the first /
last occurrence of each request key: fail_status (503), drop_after_send
(server applied it, reply lost: unknown outcome), truncate_reply,
kill_before_forward / kill_after_upstream (network-phase crash points), plus
slow_reply and seeded jitter (reordering of concurrent replies). Per-request
trials pin OAK_UPLOAD_CONCURRENCY=OAK_DOWNLOAD_CONCURRENCY=1 for a
deterministic request sequence. Extra invariants: op_success_is_true (exit 0
implies the calibrated outcome — an acknowledged push is on the server) and
unknown_not_reported_success (a lost reply to a mutating request is never
followed by exit 0 without further server contact). Git runs the same plan over
git http-backend. applied_but_reported_failure is recorded as an observation.
conc_same_branch_v1 / conc_distinct_branches_v1 / env_fsync_load_v1
(concurrency.py, lincheck.py). N checkouts (copies of one seed, the jj
"two machines" pattern) commit and push, pulling and retrying on rejection.
Each push is an operation with invoke/return times and outcome ok / rejected /
unknown. lincheck.py is a stdlib Wing–Gong/Lowe search (the Porcupine
approach, re-implemented; no Porcupine code) over a fast-forward register
model, with ancestry from the union of independently-read commit graphs.
Invariants: linearizable, no_lost_acknowledged_write, acked_content_on_server
(every acknowledged file is in the final server tree), server_opens,
rejections_actionable, no_hang. env_fsync_load_v1 repeats same-branch under
4 background F_FULLFSYNC writers.
lock_shared_commit_v1 / lock_shared_push_v1 / lock_stale_owner_v1: 8 processes
commit or push concurrently in one checkout (workdir / publication locks);
lock_stale_owner_v1 first SIGKILLs a commit at its first durability boundary
(owner dead while holding the lock) — contenders must reap it (stale_owner_reaped).
lock_credentials_v1 is unsupported (no non-interactive credential writer).
Oak pushes run with --json; each acknowledged push's reported pushed_head is
cross-checked against the local head read before the push
(acked_head_matches_reported).
env_disk_full_v1 / env_readonly_v1 (envfaults.py). The trial lives on a
96 MiB RAM disk (hdiutil attach -nomount ram://, newfs_hfs, mounted inside
the work dir; no sudo) filled until writes fail at 1-byte granularity (1 MiB,
64 KiB, 4 KiB, 512 B, 1 B rounds, fsync between), then 0 / 256 KiB / 4 MiB are
released. The free space is measured (statvfs) right before the op and
recorded as free_bytes_measured, beside op_need_bytes (net allocation of a
clean run on an ordinary disk); or .oak/.git
(store) or one worktree subdir is made read-only. The op must fail actionably
(op_failure_actionable) or succeed exactly (op_success_is_true); after the
fault is removed the crash invariants and next-op convergence apply.
fault_fired (reason in observations.fault_fired_evidence): the disk was
really filled, so an op that fails, errors or hangs is always judged: an opaque
error (fb-548 "unable to open database file", a panic) violates
op_failure_actionable, and a hang violates op_no_hang. Only a successful op is
decided by evidence: fired if it reported ENOSPC / "No space left" / SQLITE_FULL
or the measured free space was below op_need_bytes, else fault_not_fired
(it had room). A failed calibration run leaves the need unmeasured and the row
is harness_error, never need=0. Read-only works the same way: a failing op is
judged; a successful op counts as fired only if the path was really unwritable
(not as root) and the op wrote there: it printed a permission / read-only
error, or a clean probe run of the same op (observations.readonly_exercise)
changes something under the locked path (store: any file; subdir: the
directory's own entries). Otherwise it is fault_not_fired ("not exercised").
If the probe itself failed, the row is harness_error.
hosted_merge_invariants_v1 (hosted.py): the merge owner's invariants
(acknowledged merge is on main; main never loses a landed change; push during
merge not dropped; concurrent merges serialize; ambiguous merge never reported
as success). Spec only: loopback oak serve returns 404 for merge publication,
so these are unsupported rows until a local oakspace driver exists
(OAK_BENCH_ROBUSTNESS_OAKSPACE_URL, loopback only).
Provenance and attribution
Ideas, re-implemented here (no code copied): ALICE (OSDI'14) and CrashMonkey/ACE
(OSDI'18) crash-state enumeration; Porcupine (MIT) linearizability checking;
jj's test_bad_locking (Apache-2.0) copied-workspace pattern; Jepsen nemeses.
Seed harnesses: the 2026-09-26 L9 pull-loss kill-9 matrix, the L8 lock/fsync
harnesses, and the W3a QA fault proxy.