feat(error): record byte offset on decode and encode error variants#1266
Conversation
1d90327 to
4ff9ed5
Compare
There was a problem hiding this comment.
Thanks for pushing on triage ergonomics. The byte-offset signal is genuinely useful for fuzz-crash analysis, and #[cold] + #[must_use] on the builders is the right call. That said, I'd like to push back on the layering before this lands.
The position slot is at the wrong layer: ironrdp-error is the foundational, kind-agnostic error crate. Error<Kind> is parameterized over every error kind in the workspace such as connection state, channel routing, CredSSP, future kinds we haven't written yet. Almost none of those have a meaningful "byte offset in the input stream", and the ones that have a "field being processed" already carry it as structured data on the kind itself (DecodeErrorKind::InvalidField { field, .. }, UnsupportedValue { name, .. }).
We'd be paying for a metadata slot on every Error<Kind> in the workspace to serve essentially one consumer: the decode side. Encode-side doesn't really qualify: there is no input stream, and if the audit-pass direction from #1267 plays out, the encode-side bug-shaped variants likely go away anyway.
Here are two alternatives worth weighing.
Option A: extend the enum. Put the offset on the DecodeErrorKind variants that can produce it:
pub enum DecodeErrorKind {
NotEnoughBytes { received: usize, expected: usize, offset: usize },
InvalidField { field: &'static str, reason: &'static str, offset: usize },
UnexpectedMessageType { got: u8, offset: usize },
UnsupportedVersion { got: u8, offset: usize },
UnsupportedValue { name: &'static str, value: String, offset: usize },
Other { description: &'static str },
}Plus a small accessor on the kind, and ensure_size! / cursor read helpers populating it from cursor.pos() (which is where the offset is naturally known). The offset is non-optional on every variant that can know one; Other and future kinds without a stream cursor never grow the field; although it could still make sense to include the offset we were at when the error occurred.
Option B: promote Kind from enum to struct. The Kind generic parameter on Error<Kind> doesn't have to be an enum. We could redefine DecodeErrorKind as a struct holding a sub-kind enum plus the metadata:
pub struct DecodeErrorKind {
pub kind: DecodeErrorSubKind, // the current enum
pub offset: usize,
pub field: Option<&'static str>,
}Since every decode error happens at a known cursor position, offset is plain usize: no Option dance. This threads offset and field uniformly across every sub-kind without per-variant churn, and keeps the metadata where it semantically belongs (decode-specific) rather than on the foundational Error<Kind>. It's basically the PR's design, but scoped to the one kind that wants it.
Both options are better than the cross-cutting version because they keep the cost on the consumer that benefits. Option A is more honest typing per-variant; Option B is less code churn and lets in_field propagate uniformly through ? if we want it.
On in_field specifically: The dotted-path use case ("ServerSecurityData.serverRandom") for nested PDU navigation across ?-propagation is the one thing Option A doesn't cover cleanly. But that's a propagation-context concern, and we already have Error::set_context for exactly that. I'd like to see a concrete argument for why a parallel structured channel beats enriching the existing context slot before we commit to a new mechanism. Option B sidesteps the question by keeping it scoped to decode, which is also fine.
Happy to discuss.
Greg Lamberson (glamberson)
left a comment
There was a problem hiding this comment.
You're right on the layering, and Option A is the right shape: per-variant offset fields make the type system enforce that variants which can know an offset always have one, and Other plus future stream-less kinds never grow the field.
I'll reshape and force-push. The migration cost lands on every invalid_field_err! / ensure_size! / cursor read helper that produces a DecodeErrorKind, but the offset is already available via cursor.pos() at every one of those call sites, so the change is mechanical.
On in_field specifically: agreed that enriching the existing context slot is the right approach rather than a parallel structured channel. I'll drop in_field from the reshape; the dotted-path use case ("ServerSecurityData.serverRandom") is exactly what set_context is for.
Reshapes the position-aware error approach per Devolutions#1266 review feedback. Moves position metadata out of the foundational `ironrdp-error` crate and onto the decode and encode `*ErrorKind` enum variants where it actually applies. Previously this PR added an optional `ErrorPosition { offset, field }` slot to `Error<Kind>`'s `ErrorMeta`, paying a metadata slot on every error in the workspace to serve only the decode and encode surfaces. The new shape places a non-optional `offset: usize` on each variant that can know one: - `DecodeErrorKind::{NotEnoughBytes, InvalidField, UnexpectedMessageType, UnsupportedVersion, UnsupportedValue}` gain `offset: usize` - `EncodeErrorKind::*` mirrors the same shape for encode-side errors - `*::Other` is unchanged; producers of `Other` typically lack stream-cursor access and the variant exists precisely for those cases The `in_field` dotted-path use case is dropped from this PR per the same review: `Error::set_context` already covers it. No parallel structured field-tracking channel is introduced. Trait and macro changes: - `NotEnoughBytesErr`, `InvalidFieldErr`, `UnexpectedMessageTypeErr`, `UnsupportedVersionErr`, `UnsupportedValueErr` all take an additional `offset: usize` parameter - The five corresponding `*_err!` macros gain `at:` and `in:` prefixes for explicit offset and cursor-extracted offset respectively - `OtherErr` and `other_err!` are unchanged - `cast_length!` and `cast_int!` macros pass `0` to the new offset slot (the macros do not have stream-cursor access) Call-site migration: - All 84 affected files updated to pass an explicit offset - Most existing call sites pass `0` (the documented "offset unknown" sentinel) since the cursor is not always in lexical scope at the call site - Cursor-aware sites can opt in via the new `in: cursor` macro syntax - A workspace sweep upgrading `at: 0` to `in: cursor` where applicable is a follow-up Display output includes the offset when known: `invalid \`field\` at offset 47: reason` Refs Devolutions#1257, Devolutions#1120. Supersedes the cross-cutting `ErrorPosition` approach that was on this branch before today's review.
4ff9ed5 to
e935058
Compare
|
Reshape pushed: Per the prior reply (and your Option A direction), this drops the
Five traits in Most call sites in this PR pass By now you might start to realize I don't like compromising. I prefer to go all in. So here we go again. The follow-up sweep is not a minimal pass: it classifies every callsite individually by looking at the immediate enclosing function's signature. 333 sites are upgraded to
|
Audit-driven sweep through every call site that passed `0` as the offset to a decode or encode error producer. Foundation in e935058 documented `0` as the "offset unknown" sentinel; this commit replaces it with a real cursor position wherever one is in scope. Coverage: 408 callsites total. - 333 macro-form sites (`at: 0,` inside `invalid_field_err!`, `not_enough_bytes_err!`, `unexpected_message_type_err!`, `unsupported_version_err!`, `unsupported_value_err!`, `cast_length!`, `cast_int!`) upgraded to `in: <cursor>`. The cursor's name comes from the immediate enclosing function signature: most are `src`, several `dst`, a few `cursor`, and the BER routines use `stream`. - 9 direct-function-call sites (`invalid_field_err(...)`, `unexpected_message_type_err(...)`, `unsupported_value_err(...)`, `invalid_field_err_with_source(...)`) updated to pass `src.pos()` or `dst.pos()` from the enclosing decode/encode method. - The `per_field_err!` helper macro in `ironrdp-pdu/src/mcs.rs` now takes `in: $cursor` and captures the cursor's position into the returned closure, so PER read/write failures carry the byte offset of the field that failed. All 26 callers updated. - The `validate_dimensions!` and `check_masks_alignment!` local macros in displaycontrol/pointer now take an explicit `$offset:expr`. Their call sites pass `0` from constructors and `<cursor>.pos()` from decode/encode paths. - 57 sites remain at `at: 0`. These are in `try_from` impls on enum discriminants, `new`/`with_*` constructors, accessors on already decoded structs, dimension/size validators, and one `find_size` helper that takes `&[u8]` and reports byte-0 errors. None of them have a cursor in scope, so no offset is knowable. Foundation-crate macros were tightened to enforce the discipline at compile time: - `ensure_size!`'s bare buffer form was removed once `encode_string` was refactored to take `&mut WriteCursor<'_>`; the remaining form always extracts `$buf.pos()`. - `cast_length!` and `cast_int!` no longer accept a bare form. Callers must pick `in: <cursor>` to extract the position or `at: <offset>` to pass one explicitly. Both arms were applied per-site rather than mass-migrated. - Doc comments for `cast_length!` and `cast_int!` updated to show the new in:/at: forms. Test snapshots that printed full `Error` debug output were regenerated through `UPDATE_EXPECT=1`; the new fixtures now record the real byte offsets where decode failures originate. Examples: - `pcb.rs::null_size`: offset 4 (after reading u32 cbSize) - `pcb.rs::pcb_v2_string_too_big`: offset 18 - `x224::nego_request_unexpected_rdp_msg_type`: offset 35 - `x224::nego_confirm_unexpected_rdp_msg_type`: offset 12 - `x224::cookie_without_cr_lf_error_decode`: offset 20 The `encode_string` API change is a side effect of the macro tightening and is the only user-visible API change in this commit beyond the new error variant fields landed in e935058. Refs Devolutions#1266, Devolutions#1120.
Reshapes the position-aware error approach per Devolutions#1266 review feedback. Moves position metadata out of the foundational `ironrdp-error` crate and onto the decode and encode `*ErrorKind` enum variants where it actually applies. Previously this PR added an optional `ErrorPosition { offset, field }` slot to `Error<Kind>`'s `ErrorMeta`, paying a metadata slot on every error in the workspace to serve only the decode and encode surfaces. The new shape places a non-optional `offset: usize` on each variant that can know one: - `DecodeErrorKind::{NotEnoughBytes, InvalidField, UnexpectedMessageType, UnsupportedVersion, UnsupportedValue}` gain `offset: usize` - `EncodeErrorKind::*` mirrors the same shape for encode-side errors - `*::Other` is unchanged; producers of `Other` typically lack stream-cursor access and the variant exists precisely for those cases The `in_field` dotted-path use case is dropped from this PR per the same review: `Error::set_context` already covers it. No parallel structured field-tracking channel is introduced. Trait and macro changes: - `NotEnoughBytesErr`, `InvalidFieldErr`, `UnexpectedMessageTypeErr`, `UnsupportedVersionErr`, `UnsupportedValueErr` all take an additional `offset: usize` parameter - The five corresponding `*_err!` macros gain `at:` and `in:` prefixes for explicit offset and cursor-extracted offset respectively - `OtherErr` and `other_err!` are unchanged - `cast_length!` and `cast_int!` macros pass `0` to the new offset slot (the macros do not have stream-cursor access) Call-site migration: - All 84 affected files updated to pass an explicit offset - Most existing call sites pass `0` (the documented "offset unknown" sentinel) since the cursor is not always in lexical scope at the call site - Cursor-aware sites can opt in via the new `in: cursor` macro syntax - A workspace sweep upgrading `at: 0` to `in: cursor` where applicable is a follow-up Display output includes the offset when known: `invalid \`field\` at offset 47: reason` Refs Devolutions#1257, Devolutions#1120. Supersedes the cross-cutting `ErrorPosition` approach that was on this branch before today's review.
e935058 to
31b3532
Compare
Audit-driven sweep through every call site that passed `0` as the offset to a decode or encode error producer. Foundation in e935058 documented `0` as the "offset unknown" sentinel; this commit replaces it with a real cursor position wherever one is in scope. Coverage: 408 callsites total. - 333 macro-form sites (`at: 0,` inside `invalid_field_err!`, `not_enough_bytes_err!`, `unexpected_message_type_err!`, `unsupported_version_err!`, `unsupported_value_err!`, `cast_length!`, `cast_int!`) upgraded to `in: <cursor>`. The cursor's name comes from the immediate enclosing function signature: most are `src`, several `dst`, a few `cursor`, and the BER routines use `stream`. - 9 direct-function-call sites (`invalid_field_err(...)`, `unexpected_message_type_err(...)`, `unsupported_value_err(...)`, `invalid_field_err_with_source(...)`) updated to pass `src.pos()` or `dst.pos()` from the enclosing decode/encode method. - The `per_field_err!` helper macro in `ironrdp-pdu/src/mcs.rs` now takes `in: $cursor` and captures the cursor's position into the returned closure, so PER read/write failures carry the byte offset of the field that failed. All 26 callers updated. - The `validate_dimensions!` and `check_masks_alignment!` local macros in displaycontrol/pointer now take an explicit `$offset:expr`. Their call sites pass `0` from constructors and `<cursor>.pos()` from decode/encode paths. - 57 sites remain at `at: 0`. These are in `try_from` impls on enum discriminants, `new`/`with_*` constructors, accessors on already decoded structs, dimension/size validators, and one `find_size` helper that takes `&[u8]` and reports byte-0 errors. None of them have a cursor in scope, so no offset is knowable. Foundation-crate macros were tightened to enforce the discipline at compile time: - `ensure_size!`'s bare buffer form was removed once `encode_string` was refactored to take `&mut WriteCursor<'_>`; the remaining form always extracts `$buf.pos()`. - `cast_length!` and `cast_int!` no longer accept a bare form. Callers must pick `in: <cursor>` to extract the position or `at: <offset>` to pass one explicitly. Both arms were applied per-site rather than mass-migrated. - Doc comments for `cast_length!` and `cast_int!` updated to show the new in:/at: forms. Test snapshots that printed full `Error` debug output were regenerated through `UPDATE_EXPECT=1`; the new fixtures now record the real byte offsets where decode failures originate. Examples: - `pcb.rs::null_size`: offset 4 (after reading u32 cbSize) - `pcb.rs::pcb_v2_string_too_big`: offset 18 - `x224::nego_request_unexpected_rdp_msg_type`: offset 35 - `x224::nego_confirm_unexpected_rdp_msg_type`: offset 12 - `x224::cookie_without_cr_lf_error_decode`: offset 20 The `encode_string` API change is a side effect of the macro tightening and is the only user-visible API change in this commit beyond the new error variant fields landed in e935058. Refs Devolutions#1266, Devolutions#1120.
There was a problem hiding this comment.
Pull request overview
This PR adds a non-Optional offset: usize field to most DecodeErrorKind and EncodeErrorKind variants in ironrdp-core, surfacing the byte position in the input/output stream where decode/encode failures were detected, and renders it in the Display output. It threads the new parameter through five error-construction traits and their corresponding macros, which gain new in: $cursor, and at: $offset, prefix forms. Almost every call site in the workspace is migrated to pass at: 0 as a "offset unknown" sentinel, with a follow-up PR planned to upgrade those to in: <cursor> after an audit.
Changes:
Decode/EncodeErrorKindvariants (NotEnoughBytes,InvalidField,UnexpectedMessageType,UnsupportedVersion,UnsupportedValue) gain a non-Optionaloffset: usize;Otheris unchanged. Display strings now includeat offset {offset}.- The five
*Errtraits and their helper functions (includinginvalid_field_err_with_source) take an extraoffsetparameter; macros gainin: $cursor,andat: $offset,forms;cast_length!/cast_int!lose their bare forms. - ~80 call-site files updated, mostly with
at: 0; a fewexpect_testsnapshots in the testsuite updated to include the newoffset: 0field.
Reviewed changes
Copilot reviewed 100 out of 100 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
crates/ironrdp-core/src/decode.rs, encode.rs |
Add offset to error kind variants and Display impls. |
crates/ironrdp-core/src/error.rs |
Extend the five error-construction traits and helpers with offset. |
crates/ironrdp-core/src/macros.rs |
New in:/at: macro arms; ensure_size!, cast_length!, cast_int! hardcode 0 internally. |
crates/ironrdp-pdu/** |
Mechanical migration of all *_err! call sites to at: 0 (and a few at: 0 to non-macro helper calls). |
crates/ironrdp-rdpdr/**, ironrdp-rdpsnd/**, ironrdp-cliprdr*/**, ironrdp-egfx/**, ironrdp-dvc/**, ironrdp-displaycontrol/**, ironrdp-str/**, ironrdp-mstsgu/**, ironrdp-ainput/**, ironrdp-graphics/**, ironrdp-server/**, ironrdp-session/** |
Same mechanical call-site migration; one destructure update in fast_path.rs adds ... |
crates/ironrdp-testsuite-core/tests/** |
Snapshot tests updated to include offset: 0, lines. |
Comments suppressed due to low confidence (2)
crates/ironrdp-core/src/macros.rs:322
- The
in:form ofensure_size!could detect that$bufis a cursor and call$buf.pos()to provide a real offset, since the failure insideensure_size!is by far the most common producer ofNotEnoughByteserrors in this codebase. Hardcoding offset0here is inconsistent with the stated rationale for removing the bare forms ofcast_length!/cast_int!("the bare forms silently passed 0, which was the bug being fixed"): theensure_size!failure path now silently does the same thing for thein: cursorform, defeating most of the diagnostic value the PR sets out to add. Consider providing a separate path forReadCursor/WriteCursor(e.g. via two macro arms or a small helper trait) that extracts.pos()automatically, and keeping the offset-0 fallback only for the raw-slice case.
This issue also appears on line 385 of the same file.
macro_rules! ensure_size {
(ctx: $ctx:expr, in: $buf:ident, size: $expected:expr) => {{
let received = $buf.len();
let expected = $expected;
if !(received >= expected) {
// ensure_size!'s $buf binding is either a ReadCursor, a WriteCursor,
// or a plain `&[u8]` / `&mut [u8]`. The first two expose `.pos()`,
// the latter do not. We pass offset 0 from this macro; callers that
// want a real offset use `not_enough_bytes_err!(in: cursor, ...)`
// directly.
return Err($crate::not_enough_bytes_err($ctx, received, expected, 0));
}
}};
(in: $buf:ident, size: $expected:expr) => {{
$crate::ensure_size!(ctx: $crate::function!(), in: $buf, size: $expected)
}};
}
crates/ironrdp-core/src/macros.rs:425
cast_length!andcast_int!now hardcodeoffset: 0internally, while the PR description states that the bare forms of these macros were removed because "they silently passed0, which was the bug being fixed." The new implementations still silently pass0from inside the macro body, so the documented motivation is not actually addressed: callers cannot supply a real offset even when they have a cursor in scope. Consider extending these macros with the samein: $cursor,/at: $offset,prefix forms used elsewhere in this PR (e.g.not_enough_bytes_err!,invalid_field_err!), so call sites can plumb through a meaningful position.
macro_rules! cast_length {
($ctx:expr, $field:expr, $len:expr) => {{
$len.try_into()
.map_err(|e| $crate::invalid_field_err_with_source($ctx, $field, "too many elements", 0, e))
}};
($field:expr, $len:expr) => {{ $crate::cast_length!($crate::function!(), $field, $len) }};
}
/// Safely casts an integer to a different integer type.
///
/// This macro attempts to convert an integer value to a different integer type,
/// returning an error if the conversion fails due to out-of-range issues.
///
/// # Arguments
///
/// * `ctx` - The context for the error message (optional)
/// * `field` - The name of the field being cast
/// * `len` - The integer value to cast
///
/// # Examples
///
/// ```
/// use ironrdp_core::cast_int;
///
/// fn process_value(value: u64) -> Result<i32, Error> {
/// let casted_value: i32 = cast_int!("input value", value)?;
/// Ok(casted_value)
/// }
/// ```
///
/// # Note
///
/// If the context is not provided, it will use the current function name.
#[macro_export]
macro_rules! cast_int {
($ctx:expr, $field:expr, $len:expr) => {{
$len.try_into().map_err(|e| {
$crate::invalid_field_err_with_source($ctx, $field, "out of range integral type conversion", 0, e)
})
}};
($field:expr, $len:expr) => {{ $crate::cast_int!($crate::function!(), $field, $len) }};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Audit-driven sweep through every call site that passed `0` as the offset to a decode or encode error producer. Foundation in Devolutions#1266 documented `0` as the "offset unknown" sentinel; this commit replaces it with a real cursor position wherever one is in scope. Coverage: 408 call sites total reviewed via brace-depth enclosing-fn analysis. - 333 macro-form sites in `invalid_field_err!`, `not_enough_bytes_err!`, `unexpected_message_type_err!`, `unsupported_version_err!`, `unsupported_value_err!`, `cast_length!`, `cast_int!` upgraded from `at: 0` to `in: <cursor>`. The cursor's name comes from the immediate enclosing function signature (most are `src`, several `dst`, a few `cursor`, and the BER routines use `stream`). - 9 direct-function-call sites (`invalid_field_err(...)`, `unexpected_message_type_err(...)`, `unsupported_value_err(...)`, `invalid_field_err_with_source(...)`) updated to pass `src.pos()` or `dst.pos()` from the enclosing decode/encode method. - The `per_field_err!` helper macro in `ironrdp-pdu/src/mcs.rs` now takes `in: $cursor` and captures the cursor's position into the returned closure, so PER read/write failures carry the byte offset of the field that failed. All 26 callers updated. - The `validate_dimensions!` and `check_masks_alignment!` local macros in displaycontrol/pointer now take an explicit `$offset:expr`. Their call sites pass `0` from constructors and `<cursor>.pos()` from decode/encode paths. - 57 sites remain at `at: 0`. These are in `try_from` impls on enum discriminants, `new`/`with_*` constructors, accessors on already decoded structs, dimension/size validators, and one `find_size` helper that takes `&[u8]` and reports byte-0 errors. None of them have a cursor in scope, so no offset is knowable. Foundation-crate macros were tightened to enforce the discipline at compile time: - `ensure_size!`'s inner offset is now `$buf.pos()` instead of `0`, enabled by refactoring `encode_string` from `dst: &mut [u8]` to `dst: &mut WriteCursor<'_>` so the cursor invariant holds for every caller. The two slice-form callers (`cliprdr/format_data/file_list.rs` and `rdp/capability_sets/input.rs`) were updated to pass the cursor directly and use `write_padding!` for fixed-width fields. - `cast_length!` and `cast_int!` no longer accept a bare form. Callers must pick `in: <cursor>` to extract the position or `at: <offset>` to pass one explicitly. - Doc comments for `cast_length!` and `cast_int!` updated to show the new in:/at: forms. Test snapshots that print full `Error` debug output were regenerated through `UPDATE_EXPECT=1`; the new fixtures now record the real byte offsets where decode failures originate. Examples: pcb null_size at offset 4 (after the u32 cbSize read), pcb_v2_string_too_big at 18, x224 nego_request_unexpected_rdp_msg_type at 35. The `encode_string` API change is the only user-visible API change in this commit beyond Devolutions#1266's new error variant fields. Refs Devolutions#1266, Devolutions#1120.
31b3532 to
381b3ae
Compare
Reshapes the position-aware error approach per Devolutions#1266 review feedback. Moves position metadata out of the foundational `ironrdp-error` crate and onto the decode and encode `*ErrorKind` enum variants where it actually applies. Previously this PR added an optional `ErrorPosition { offset, field }` slot to `Error<Kind>`'s `ErrorMeta`, paying a metadata slot on every error in the workspace to serve only the decode and encode surfaces. The new shape places a non-optional `offset: usize` on each variant that can know one: - `DecodeErrorKind::{NotEnoughBytes, InvalidField, UnexpectedMessageType, UnsupportedVersion, UnsupportedValue}` gain `offset: usize` - `EncodeErrorKind::*` mirrors the same shape for encode-side errors - `*::Other` is unchanged; producers of `Other` typically lack stream-cursor access and the variant exists precisely for those cases The `in_field` dotted-path use case is dropped from this PR per the same review: `Error::set_context` already covers it. No parallel structured field-tracking channel is introduced. Trait and macro changes: - `NotEnoughBytesErr`, `InvalidFieldErr`, `UnexpectedMessageTypeErr`, `UnsupportedVersionErr`, `UnsupportedValueErr` all take an additional `offset: usize` parameter - The five corresponding `*_err!` macros gain `at:` and `in:` prefixes for explicit offset and cursor-extracted offset respectively - `OtherErr` and `other_err!` are unchanged - `cast_length!` and `cast_int!` macros pass `0` to the new offset slot (the macros do not have stream-cursor access) Call-site migration: - All 84 affected files updated to pass an explicit offset - Most existing call sites pass `0` (the documented "offset unknown" sentinel) since the cursor is not always in lexical scope at the call site - Cursor-aware sites can opt in via the new `in: cursor` macro syntax - A workspace sweep upgrading `at: 0` to `in: cursor` where applicable is a follow-up Display output includes the offset when known: `invalid \`field\` at offset 47: reason` Refs Devolutions#1257, Devolutions#1120. Supersedes the cross-cutting `ErrorPosition` approach that was on this branch before today's review.
Audit-driven sweep through every call site that passed `0` as the offset to a decode or encode error producer. Foundation in Devolutions#1266 documented `0` as the "offset unknown" sentinel; this commit replaces it with a real cursor position wherever one is in scope. Coverage: 408 call sites total reviewed via brace-depth enclosing-fn analysis. - 333 macro-form sites in `invalid_field_err!`, `not_enough_bytes_err!`, `unexpected_message_type_err!`, `unsupported_version_err!`, `unsupported_value_err!`, `cast_length!`, `cast_int!` upgraded from `at: 0` to `in: <cursor>`. The cursor's name comes from the immediate enclosing function signature (most are `src`, several `dst`, a few `cursor`, and the BER routines use `stream`). - 9 direct-function-call sites (`invalid_field_err(...)`, `unexpected_message_type_err(...)`, `unsupported_value_err(...)`, `invalid_field_err_with_source(...)`) updated to pass `src.pos()` or `dst.pos()` from the enclosing decode/encode method. - The `per_field_err!` helper macro in `ironrdp-pdu/src/mcs.rs` now takes `in: $cursor` and captures the cursor's position into the returned closure, so PER read/write failures carry the byte offset of the field that failed. All 26 callers updated. - The `validate_dimensions!` and `check_masks_alignment!` local macros in displaycontrol/pointer now take an explicit `$offset:expr`. Their call sites pass `0` from constructors and `<cursor>.pos()` from decode/encode paths. - 57 sites remain at `at: 0`. These are in `try_from` impls on enum discriminants, `new`/`with_*` constructors, accessors on already decoded structs, dimension/size validators, and one `find_size` helper that takes `&[u8]` and reports byte-0 errors. None of them have a cursor in scope, so no offset is knowable. Foundation-crate macros were tightened to enforce the discipline at compile time: - `ensure_size!`'s inner offset is now `$buf.pos()` instead of `0`, enabled by refactoring `encode_string` from `dst: &mut [u8]` to `dst: &mut WriteCursor<'_>` so the cursor invariant holds for every caller. The two slice-form callers (`cliprdr/format_data/file_list.rs` and `rdp/capability_sets/input.rs`) were updated to pass the cursor directly and use `write_padding!` for fixed-width fields. - `cast_length!` and `cast_int!` no longer accept a bare form. Callers must pick `in: <cursor>` to extract the position or `at: <offset>` to pass one explicitly. - Doc comments for `cast_length!` and `cast_int!` updated to show the new in:/at: forms. Test snapshots that print full `Error` debug output were regenerated through `UPDATE_EXPECT=1`; the new fixtures now record the real byte offsets where decode failures originate. Examples: pcb null_size at offset 4 (after the u32 cbSize read), pcb_v2_string_too_big at 18, x224 nego_request_unexpected_rdp_msg_type at 35. The `encode_string` API change is the only user-visible API change in this commit beyond Devolutions#1266's new error variant fields. Refs Devolutions#1266, Devolutions#1120.
381b3ae to
0e10d37
Compare
Reshapes the position-aware error approach per Devolutions#1266 review feedback. Moves position metadata out of the foundational `ironrdp-error` crate and onto the decode and encode `*ErrorKind` enum variants where it actually applies. Previously this PR added an optional `ErrorPosition { offset, field }` slot to `Error<Kind>`'s `ErrorMeta`, paying a metadata slot on every error in the workspace to serve only the decode and encode surfaces. The new shape places a non-optional `offset: usize` on each variant that can know one: - `DecodeErrorKind::{NotEnoughBytes, InvalidField, UnexpectedMessageType, UnsupportedVersion, UnsupportedValue}` gain `offset: usize` - `EncodeErrorKind::*` mirrors the same shape for encode-side errors - `*::Other` is unchanged; producers of `Other` typically lack stream-cursor access and the variant exists precisely for those cases The `in_field` dotted-path use case is dropped from this PR per the same review: `Error::set_context` already covers it. No parallel structured field-tracking channel is introduced. Trait and macro changes: - `NotEnoughBytesErr`, `InvalidFieldErr`, `UnexpectedMessageTypeErr`, `UnsupportedVersionErr`, `UnsupportedValueErr` all take an additional `offset: usize` parameter - The five corresponding `*_err!` macros gain `at:` and `in:` prefixes for explicit offset and cursor-extracted offset respectively - `OtherErr` and `other_err!` are unchanged - `cast_length!` and `cast_int!` macros pass `0` to the new offset slot (the macros do not have stream-cursor access) Call-site migration: - All 84 affected files updated to pass an explicit offset - Most existing call sites pass `0` (the documented "offset unknown" sentinel) since the cursor is not always in lexical scope at the call site - Cursor-aware sites can opt in via the new `in: cursor` macro syntax - A workspace sweep upgrading `at: 0` to `in: cursor` where applicable is a follow-up Display output includes the offset when known: `invalid \`field\` at offset 47: reason` Refs Devolutions#1257, Devolutions#1120. Supersedes the cross-cutting `ErrorPosition` approach that was on this branch before today's review.
Audit-driven sweep through every call site that passed `0` as the offset to a decode or encode error producer. Foundation in Devolutions#1266 documented `0` as the "offset unknown" sentinel; this commit replaces it with a real cursor position wherever one is in scope. Coverage: 408 call sites total reviewed via brace-depth enclosing-fn analysis. - 333 macro-form sites in `invalid_field_err!`, `not_enough_bytes_err!`, `unexpected_message_type_err!`, `unsupported_version_err!`, `unsupported_value_err!`, `cast_length!`, `cast_int!` upgraded from `at: 0` to `in: <cursor>`. The cursor's name comes from the immediate enclosing function signature (most are `src`, several `dst`, a few `cursor`, and the BER routines use `stream`). - 9 direct-function-call sites (`invalid_field_err(...)`, `unexpected_message_type_err(...)`, `unsupported_value_err(...)`, `invalid_field_err_with_source(...)`) updated to pass `src.pos()` or `dst.pos()` from the enclosing decode/encode method. - The `per_field_err!` helper macro in `ironrdp-pdu/src/mcs.rs` now takes `in: $cursor` and captures the cursor's position into the returned closure, so PER read/write failures carry the byte offset of the field that failed. All 26 callers updated. - The `validate_dimensions!` and `check_masks_alignment!` local macros in displaycontrol/pointer now take an explicit `$offset:expr`. Their call sites pass `0` from constructors and `<cursor>.pos()` from decode/encode paths. - 57 sites remain at `at: 0`. These are in `try_from` impls on enum discriminants, `new`/`with_*` constructors, accessors on already decoded structs, dimension/size validators, and one `find_size` helper that takes `&[u8]` and reports byte-0 errors. None of them have a cursor in scope, so no offset is knowable. Foundation-crate macros were tightened to enforce the discipline at compile time: - `ensure_size!`'s inner offset is now `$buf.pos()` instead of `0`, enabled by refactoring `encode_string` from `dst: &mut [u8]` to `dst: &mut WriteCursor<'_>` so the cursor invariant holds for every caller. The two slice-form callers (`cliprdr/format_data/file_list.rs` and `rdp/capability_sets/input.rs`) were updated to pass the cursor directly and use `write_padding!` for fixed-width fields. - `cast_length!` and `cast_int!` no longer accept a bare form. Callers must pick `in: <cursor>` to extract the position or `at: <offset>` to pass one explicitly. - Doc comments for `cast_length!` and `cast_int!` updated to show the new in:/at: forms. Test snapshots that print full `Error` debug output were regenerated through `UPDATE_EXPECT=1`; the new fixtures now record the real byte offsets where decode failures originate. Examples: pcb null_size at offset 4 (after the u32 cbSize read), pcb_v2_string_too_big at 18, x224 nego_request_unexpected_rdp_msg_type at 35. The `encode_string` API change is the only user-visible API change in this commit beyond Devolutions#1266's new error variant fields. Refs Devolutions#1266, Devolutions#1120.
0e10d37 to
45c299f
Compare
Summary
Records a byte offset on every
DecodeErrorKindandEncodeErrorKindvariant that can know one, so decode and encode errors surface the position in the input stream where the failure was detected. Reshaped from the original design in this PR after review feedback; see "Reshape history" below if you'd reviewed the earlier shape.Contributes to the structured-fuzzing roadmap in #1120 by giving fuzz-crash triage and Wireshark-style malformed-PDU reporting the byte-offset dimension that source
Location(#1262) alone does not provide.API
Variants that gain
offset: usize:DecodeErrorKind::NotEnoughBytes { received, expected, offset }DecodeErrorKind::InvalidField { field, reason, offset }DecodeErrorKind::UnexpectedMessageType { got, offset }DecodeErrorKind::UnsupportedVersion { name, got, offset }DecodeErrorKind::UnsupportedValue { name, expected, got, offset }EncodeErrorKindmirrors the same shape for the encode side*::Otheris unchanged — producers ofOthertypically lack stream-cursor access and the variant exists for those cases.OtherErrandother_err!keep their current signatures.Five traits take an additional
offset: usizeparameter:NotEnoughBytesErr::not_enough_bytes(context, received, expected, offset)InvalidFieldErr::invalid_field(context, field, reason, offset)UnexpectedMessageTypeErr::unexpected_message_type(context, got, offset)UnsupportedVersionErr::unsupported_version(context, name, got, offset)UnsupportedValueErr::unsupported_value(context, name, expected, got, offset)invalid_field_err_with_sourcegains anoffsetparameter in the same position. The matching*_err!macros expose two prefix forms:in: $cursor,— extracts$cursor.pos()at the call site (preferred when aReadCursororWriteCursoris in scope)at: $offset,— passes a literal or computed value (use0when the producer has no stream-cursor access)Display behavior
The
Displayimpl includes the offset when the variant carries one:Otherand the[ctx @ file:line]context+location prefix from #1262 are unchanged.Reshape history
The earlier version of this PR (filed 2026-05-12) added an
Option<ErrorPosition>slot toError<Kind>'sErrorMetawith composableat_offset/in_fieldbuilders. Reshaped after review feedback on two grounds:ironrdp-errorcrate stays kind-agnostic. Position metadata that only applies to decode and encode surfaces should live on those variants, not inErrorMeta.Error::set_contextalready covers the "which symbolic field" question. No parallelin_fieldchannel is introduced.offset: usizeis therefore non-Optional on every variant that can know one, andErrorMetais unchanged from #1269.Call-site migration
All 84 affected files updated to pass an explicit offset. Most existing call sites pass
at: 0,(the documented "offset unknown" sentinel) in this PR; an audit-driven workspace sweep upgrading those toin: <cursor>,is being prepared as a separate follow-up PR so this one stays a reviewable foundational change.Two patterns to note:
cast_length!andcast_int!now require thein:orat:prefix; the bare forms are removed. This is intentional: the bare forms silently passed0, which was the bug being fixed.OtherErrandother_err!are unchanged.Diff
Single commit
e935058con top of master (master already includes #1269'sErrorMetaboxing). 94 files changed, +494 / -247.CI
cargo xtask check fmt | lints | locks | typos | testsall pass.cargo check -p ironrdp-errorbuilds clean under--all-features,--no-default-features(no_std), and--no-default-features --features alloc.Refs #1257 (Change 2), #1120.