Files
alkstore/tasks/core-fix-display-log-forging.md
T
glm-5.3-flash 7700079158 core: control-char-safe Display for consumer-string error variants
Escapes the interpolated strings in the ReservedName/InvalidName/
InvalidSpec (and, for the same consumer-string interpolation class,
Codec) Display arms with char::escape_debug so rendered error
messages cannot carry raw control bytes from consumer input —
review 004 item 14's log-forging finding. The error taxonomy
(variants, kinds, match sites, ADR-008 §5) is unchanged; only the
rendered text's hygiene moved. Pinned by
error_display_escapes_consumer_control_chars.
2026-10-11 14:38:27 +00:00

4.3 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
core-fix-display-log-forging Control-char-safe Display for the consumer-string error variants (review 004 item 14) completed
single low isolated implementation
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

  • 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)
  • The error taxonomy unchanged (variants, kinds, match sites); existing error/validation tests green unchanged
  • Any additional same-class Display arms included (or the absence recorded in Notes)
  • 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.