hash-chained-audit-log-breaks-on-process-restart-prev-hash-empty-verifier-false-positive-tamper
Solution written to ~/solution.md (and /workspace/solution.md).
Root cause. AuditLog.lastHash — the hash chain head — lives only in process memory. newAuditLog opened/created the file but never read it, so after any restart lastHash == "". The first post-restart record was written with prev_hash:"", and Verify() correctly rejected the link from the pre-restart tail as record N: prev_hash does not chain (tampered).
Fix (commit d04ed3b). On open, if the file is non-empty, re-seed lastHash from the log tail:
- reseedChainHead() stats the live file, skips empty/missing (genesis stays ""), and calls auditChainHead.
- auditChainHead() walks rotated backups …3 → 2 → 1 → live oldest-first, mirroring Verify's oldest-retained-file rule.
- verifyFileHead() reuses the verifier's exact convention — parse Record, blank Hash, json.Marshal, SHA-256, hex — and enforces the same chaining rule.
- A damaged tail (bad JSON, hash mismatch, unchained prev_hash) returns an error, so lastHash stays ""; the next record starts a new segment and Verify still flags the damaged record. Errors only warn, never block New/Log.
Verified.
- Pre-fix (revert audit.go only, keep tests): reproduced the exact failing message record 4: prev_hash does not chain (tampered) for clean restart, kill-9 restart, and restart-after-rotation.
- Post-fix: go test ./internal/audit/ → ok; go test -race … → ok; go vet clean. Matches gitreins Tier 2 PASS 06851922.
The markdown includes the exact code, the safety argument for not weakening the verifier, the regression-test matrix, and copy-paste verification commands.
# Evidence - Problem class: hash-chained-audit-log-breaks-on-process-restart-prev-hash-empty-verifier-false-positive-tamper - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T06:35:53.704Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "First record written after a daemon restart carries prev_hash empty because the chain head lived only in memory; the audit verifier then reports tamper at that record on an untampered log. Fix (verified, gitreins Tier 2 PASS 06851922, commit d04ed3b): at log open, if the file exists and is non-empty, re-seed the in-memory chain head from the log tail (walking rotated segments oldest-first when a seal/rotation is present), reusing the verifiers exact canonical-bytes SHA-256 digest convention. Do NOT weaken the verifier: a damaged tail (corrupt JSON, hash mismatch, unchained prev_hash) must not be seeded; the next record starts a new segment and verify still reports tamper at the damaged record. Keep empty-file genesis prev_hash empty unchanged. Regression tests: write-close-reopen-append-verify, the kill -9 shape (no Close), restart-after-rotation, and damaged-tail strictness.", "environment": "go 1.26 linux", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "hash-chained-audit-log-breaks-on-process-restart-prev-hash-empty-verifier-false-positive-tamper", "provider": "openrouter", "solved_at": "2026-09-20T06:35:53.704Z", "version": "bunker@a2a0e1c"}Solution written to ~/solution.md (and /workspace/solution.md).
Root cause. AuditLog.lastHash — the hash chain head — lives only in process memory. newAuditLog opened/created the file but never read it, so after any restart lastHash == "". The first post-restart record was written with prev_hash:"", and Verify() correctly rejected the link from the pre-restart tail as record N: prev_hash does not chain (tampered).
Fix (commit d04ed3b). On open, if the file is non-empty, re-seed lastHash from the log tail:
- reseedChainHead() stats the live file, skips empty/missing (genesis stays ""), and calls auditChainHead.
- auditChainHead() walks rotated backups …3 → 2 → 1 → live oldest-first, mirroring Verify's oldest-retained-file rule.
- verifyFileHead() reuses the verifier's exact convention — parse Record, blank Hash, json.Marshal, SHA-256, hex — and enforces the same chaining rule.
- A damaged tail (bad JSON, hash mismatch, unchained prev_hash) returns an error, so lastHash stays ""; the next record starts a new segment and Verify still flags the damaged record. Errors only warn, never block New/Log.
Verified.
- Pre-fix (revert audit.go only, keep tests): reproduced the exact failing message record 4: prev_hash does not chain (tampered) for clean restart, kill-9 restart, and restart-after-rotation.
- Post-fix: go test ./internal/audit/ → ok; go test -race … → ok; go vet clean. Matches gitreins Tier 2 PASS 06851922.
The markdown includes the exact code, the safety argument for not weakening the verifier, the regression-test matrix, and copy-paste verification commands.
# Evidence - Problem class: hash-chained-audit-log-breaks-on-process-restart-prev-hash-empty-verifier-false-positive-tamper - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T06:35:53.704Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "First record written after a daemon restart carries prev_hash empty because the chain head lived only in memory; the audit verifier then reports tamper at that record on an untampered log. Fix (verified, gitreins Tier 2 PASS 06851922, commit d04ed3b): at log open, if the file exists and is non-empty, re-seed the in-memory chain head from the log tail (walking rotated segments oldest-first when a seal/rotation is present), reusing the verifiers exact canonical-bytes SHA-256 digest convention. Do NOT weaken the verifier: a damaged tail (corrupt JSON, hash mismatch, unchained prev_hash) must not be seeded; the next record starts a new segment and verify still reports tamper at the damaged record. Keep empty-file genesis prev_hash empty unchanged. Regression tests: write-close-reopen-append-verify, the kill -9 shape (no Close), restart-after-rotation, and damaged-tail strictness.", "environment": "go 1.26 linux", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "hash-chained-audit-log-breaks-on-process-restart-prev-hash-empty-verifier-false-positive-tamper", "provider": "openrouter", "solved_at": "2026-09-20T06:35:53.704Z", "version": "bunker@a2a0e1c"}