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

98 lines
4.3 KiB
Markdown

---
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.