Repository navigation
CLI seam has no catch-all: an uncaught exception exits 1 with a traceback, so a crafted tampered audit log makes verify-audit report 1 ('nothing compared') instead of 6 (CR-A05-002) #376
Description
Wave 1 · theme D1-2: Audit chain, integrity manifest and verifier robustness · highest severity High · 1 finding(s) · effort small
Summary
An adversary with write access to audit_log.jsonl replaces or appends one line made of 200000 '[' followed by 200000 ']'. 'forgelm verify-audit audit_log.jsonl' exits 1 with a RecursionError traceback, and stdout is empty in --output-format json mode. CI alarms wired to exit 6 ('wire alarms to 6', CLAUDE.md principle 4) do not fire. The event is classified as an operator typo instead of tampering.
Primary finding
CR-A05-002 — CLI seam has no catch-all: an uncaught exception exits 1 with a traceback, so a crafted tampered audit log makes verify-audit report 1 ('nothing compared') instead of 6
severity High · reachability default · intent contract-violation · confidence confirmed · independently confirmed · reproduced end to end · already tracked: KNOWN-OPEN C33-CLI-ERROR-ENVELOPE — docs/roadmap/phase-16-trust-surface-hardening.md:497-508 (S7 open); also docs/roadmap/risks-and-decisions.md:263
What happens. main() (forgelm/cli/_dispatch.py:166-169) and _dispatch_subcommand (forgelm/cli/_dispatch.py:113-122) catch only KeyboardInterrupt. Any other exception escaping a dispatcher falls through to Python's default handler. That handler prints a traceback, exits with code 1 and writes no JSON envelope. Code 1 is a public code with a specific meaning: config error, and for verify-* 'the verifier never got to compare'. _clamp_exit_code (forgelm/cli/_exit_codes.py:54-62) cannot see these exits, because it only runs on returned values. Concrete instance: the verify-audit chain walk (forgelm/compliance.py:2543-2551) catches only json.JSONDecodeError around json.loads(line). A log line holding a deeply nested JSON array raises RecursionError, which escapes every layer. The verifier has read and is parsing the artefact, so by the documented 1-vs-6 rule this must be 6. A normal undecodable line ('garbage{') and a shallow non-object line both exit 6 as designed. The defence in depth that would prevent the misroute is a top-level exception mapping in main(), to 2 with an envelope, plus a per-verifier catch. Neither exists.
Where.
forgelm/cli/_dispatch.py:113-122forgelm/cli/_dispatch.py:166-169forgelm/compliance.py:2541-2551forgelm/cli/subcommands/_verify_audit.py:244forgelm/cli/_exit_codes.py:54-62docs/reference/verify_audit.md:33-35forgelm/compliance.py:2814-2819forgelm/compliance.py:2909
Evidence.
- forgelm/compliance.py:2543-2545 'try: entry = json.loads(line) except json.JSONDecodeError as exc:'; forgelm/cli/_dispatch.py:120 'except KeyboardInterrupt:' is the only handler.
Recommended fix. In the chain walk, catch (json.JSONDecodeError, RecursionError, ValueError) and route it to the undecodable-line verdict (6). Also add a top-level 'except Exception' in main()/_dispatch_subcommand that logs, emits the 2-key envelope in JSON mode and exits EXIT_TRAINING_ERROR, so no uncaught exception can surface as 1.
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