--- id: core-fix-display-log-forging name: Control-char-safe Display for the consumer-string error variants (review 004 item 14) status: completed depends_on: [] scope: single risk: low impact: isolated level: implementation tags: [review-004-fixes, core, errors] --- ## Description The core error `Display` log-forging risk, per `docs/reviews/004-pre-release-general-review.md` §"Findings recorded, not fixed" item 14: `InvalidName`/`ReservedName`/`InvalidSpec` interpolate raw consumer strings into `Display`; control characters can forge log lines in naive consumers. Typical-idiom risk, low — but the fix is small and real. Fix: escape/sanitize the interpolated payload in the three variants' `Display` arms (e.g. `char::escape_debug` or a control-char filter) so the rendered message cannot carry raw newlines/control bytes from consumer input. The error taxonomy itself is untouched — variants, kinds, and match sites stay exactly as pinned (ADR-008 §5); only the rendered text's hygiene changes. While there: grep the `Error` enum's other `Display` arms for the same consumer-string interpolation class; if any other variant carries it, include it in the same fix (and say so in Notes). ## Acceptance Criteria - [x] The three variants' `Display` renders consumer strings control-char-safe — pinned by test (a name/spec carrying `\n` and control bytes renders without any raw control byte) - [x] The error taxonomy unchanged (variants, kinds, match sites); existing error/validation tests green unchanged - [x] Any additional same-class `Display` arms included (or the absence recorded in Notes) - [x] `cargo test -p alkstore`, clippy `-D warnings`, fmt clean; doc gate green ## References - docs/reviews/004-pre-release-general-review.md §"Findings recorded, not fixed" item 14 (the finding) - docs/architecture/decisions/008-*.md §5 (the error taxonomy — the part that must not change) - alkstore/src/errors (the `Display` arms) ## Notes - Additional same-class arms found and fixed: one — `Error::Codec(String)`. Its `Display` interpolated the stored `String`, and the engine decode paths mint it with `format!("...: {e}")` embedding driver/serde error text that is not under core's control, so the same raw-control-byte-forge class was structurally reachable (in practice serde's `Unexpected::Str` already escapes via `Debug`, but the invariant was un-pinned and un-fuzzable). Escaping at the `Display` arm makes "no raw control byte in any rendered error message" a single-point invariant. Side effect (accepted): serde messages that already use `Debug` quoting render double-escaped (`\"`, `\\n`) — slightly uglier, never unsafe; no existing test asserted literal quote/backslash rendering. - Implementation form: thiserror `#[error("... `{}`", display_safe(.name))]` field-shorthand args (thiserror 2) — the variant/field/match-site taxonomy is byte-identical (ADR-008 §5 untouched); only the three (+1) format args changed. The escaping helper (`char::escape_debug` per char, collected) is private to `error.rs`. - Deliberately left alone: `Database(#[source] ...)` renders only the static `"database error"` line (the source chain is a field of the opaque variant, reached by the consumer's own `Error::source` call, not by our `Display`); `LeadershipLost`/`Closed`/`PayloadTooLarge` interpolate no consumer strings. ## Summary - `alkstore/src/error.rs`: `display_safe` helper (per-char `char::escape_debug`) applied in the `Display` arms of `ReservedName`, `InvalidName`, `InvalidSpec`, and `Codec` — rendered messages can no longer carry raw control bytes from consumer input. Taxonomy untouched (variants, kinds, fields, match sites identical). - `alkstore/src/tests.rs`: new `error_display_escapes_consumer_control_chars` — a name/spec/message carrying `\u{1}`/`\n`/`\u{7}`/`\t` renders with zero control chars (exact-text pin: ``invalid name `x\u{1} \ny` ``), and plain names still render verbatim. - Verified: `cargo test -p alkstore` (28 green, including all pre-existing error/validation tests unchanged), workspace `cargo build`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`, `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p alkstore`, and `cargo test --manifest-path fuzz/shared/Cargo.toml` (corpus replay gate) — all clean.