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.
98 lines
4.3 KiB
Markdown
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. |