Repository navigation
Description
Wave 2 · theme D2-1: Security and integrity: medium/large effort · highest severity High · 1 finding(s) · effort large
Summary
An output-dir writer holds a failed or reverted run A and a passing sibling run B that grew from a copy of the same dir under the same FORGELM_AUDIT_SECRET. They overwrite A's audit_log.jsonl with prefix + B's suffix and leave the genesis manifest untouched. forgelm verify-audit --require-hmac prints OK with hmac_verified=true and exits 0. A's directory now carries an authenticated record whose last safety event reads passed=True, beside A's artefacts.
Primary finding
CR-A07-016 — Fork splice: a signed suffix from a sibling continuation of the same log prefix verifies with exit 0 under --require-hmac, which forges the run's gate verdict
severity High · reachability default · intent contract-violation · confidence confirmed · independently confirmed · reproduced end to end
What happens. Each audit line is authenticated with a key that depends only on the secret and the line's own run_id (writer compliance.py:258, verifier compliance.py:2429). The genesis manifest pins only line 1's hash and run_id (compliance.py:443-461, checked at 2449-2520). The chain walk (2523-2578) only checks that each line's prev_hash matches its predecessor. When two runs append to copies of the same output dir (a copied checkpoint dir, or --resume-from in two copies of a pipeline dir), both continuations hang validly off the same last common line and share a byte-identical genesis manifest. Replacing run A's suffix (gate failed / model.reverted) with run B's suffix (gate passed) keeps every prev_hash, every HMAC and the genesis pin valid. This is not the same as M-A07-002 (tail truncation): truncation can only delete, while a splice appends authenticated content that contradicts what run A did. A fix for 002 does not close it either: a tail anchor kept beside the log, keyed or not, is forked and swapped together with the suffix. With the same pipeline_run_id on both forks (b2), the spliced log is indistinguishable from a genuine one even by run_id. Nothing a verifier receives says which continuation belongs to this directory, run attempt or model artefact.
Where.
forgelm/compliance.py:254forgelm/compliance.py:2449-2520forgelm/compliance.py:443-461forgelm/compliance.py:2449forgelm/compliance.py:2523-2578forgelm/compliance.py:2882forgelm/cli/subcommands/_verify_audit.py:228forgelm/cli/_pipeline.py:1417forgelm/trainer.py:354docs/reference/audit_event_catalog.md:140
Evidence.
- repro.log, case (b1): copied output dir, fresh run_ids per process (ForgeTrainer pattern, trainer.py:354). The genesis manifests of forkA and forkB are byte-identical (True). After the splice, verify-audit --require-hmac --output-format json returns {success:true, valid:true, entries_count:6, hmac_verified:true}, exit 0, and the last event is safety.evaluation_completed passed=True. Case (b2): pipeline_run_id 'pl_20261001_fork' on both forks. Splicing B's dpo-passed suffix into A returns exit 0…
Recommended fix. Add a verifier option that pins an externally recorded expectation (run_id or attempt nonce, final head hash). Record the head hash in artefacts that leave the dir (approval event, CI output). Disclose in verify_audit.md and RTP R-04 that the chain proves internal consistency, not which continuation of a forked prefix belongs to a run. Rate this separately from M-A07-002.
Acceptance criteria
- A regression test reproduces the failure scenario and asserts the corrected behaviour.
- Documentation listed above is corrected in English and in its Turkish mirror in the same PR.
- Every finding in this issue is resolved or explicitly closed with a reason.
Found by the October 2026 ForgeLM code and documentation review against commit cab2947. Code links point at that commit; line numbers may have moved since. Finding IDs (CR-…, DOC-…, TECH-…) are stable references for this backlog.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity