Skip to content

sd/configuration_option: enforce per-option byte budget on deserialize#1042

Open
SoundMatt wants to merge 1 commit into
COVESA:masterfrom
SoundMatt:fix/sd-configuration-option-bounds
Open

sd/configuration_option: enforce per-option byte budget on deserialize#1042
SoundMatt wants to merge 1 commit into
COVESA:masterfrom
SoundMatt:fix/sd-configuration-option-bounds

Conversation

@SoundMatt
Copy link
Copy Markdown

Problem

configuration_option_impl::deserialize (implementation/service_discovery/src/configuration_option_impl.cpp:90-125) parses a key=value list with no per-option byte budget:

do {
    ...
    is_successful = is_successful && _from->deserialize(l_itemLength);
    if (l_itemLength > 0) {
        is_successful = is_successful && _from->deserialize(l_item, l_itemLength);
        ...
    } else { break; }
} while (is_successful && _from->get_remaining() > 0);

The loop only stops on a 0-length item or when the outer deserializer's remaining_ runs out. Neither corresponds to this option's own declared length_ field — and the parent dispatcher (message_impl::deserialize_option) does not scope the deserializer to a single option before calling it.

So if a multicast SD peer sends a CONFIGURATION option that:

  • omits its trailing \0 terminator, or
  • declares an item-length larger than its own declared payload,

the loop greedily consumes bytes from the next option(s) in the SD message until it finds a 0 byte or runs out of remaining_. Subsequent legitimate options (typically the IPv4/IPv6 endpoint options that drive subscription routing) are then either dropped or misinterpreted as a different option type, depending on which byte boundary the desync lands on.

This is not a memory-safety bug — deserializer::deserialize(string, len) enforces len <= remaining_, so every read still lands inside the SD message buffer. But the parser-state corruption is reachable from any peer on the SD multicast group, with no authentication, so it's worth fixing.

Fix

Track bytes_remaining = length_ - 1 (the bytes that belong to this option after option_impl::deserialize consumed length+type+reserved) and reject malformed input at three points:

  1. length_ < 1 — option header is smaller than the reserved byte that option_impl::deserialize already consumed.
  2. An item-length that exceeds bytes_remaining — lying length, would otherwise consume bytes from the next option.
  3. The loop running out of bytes without seeing the \0 terminator — missing terminator.

Behaviour is unchanged for well-formed inputs: a CONFIGURATION option whose payload is exactly reserved + items + '\0' still decodes identically. The early-terminator-with-trailing-padding case (not described by SWS_SD_00292 but tolerated by the previous code) continues to return success.

Diff: +66 / −22 in a single file. Loop restructured from do/while with implicit is_successful chaining into a while with explicit early-return paths, which makes the budget-enforcement points easier to reason about.

Notes

  • Compiles cleanly against the existing headers (g++ -fsyntax-only).
  • No existing tests cover configuration_option_impl deserialization (grep -r configuration_option_impl test/ returns nothing). I'd be happy to follow up with a malformed-input test case once I have the full vsomeip build working locally — pointers to where SD-option deserialize tests would naturally live in test/ would be appreciated.
  • Companion to protocol/routing_info_entry: bounds-check the address/port memcpys #1040 (routing_info_entry bounds checks) and message_impl: reject SOME/IP messages whose length < 8 #1041 (message_impl length underflow). Both came from the same audit pass over the protocol/SD/endpoint subsystems. One latent off-by-one in deserializer::look_ahead remains; happy to file as a separate small PR if useful.

configuration_option_impl::deserialize had no per-option byte budget
on its key=value-pair loop. The loop only stopped when an
item-length of 0 was read or when the outer deserializer's
remaining_ ran out, neither of which corresponds to the option's
own declared length_ field. A multicast SD peer sending a malformed
CONFIGURATION option (no trailing '\\0' terminator, or an item
declaring a length larger than the option's payload) could make
the loop greedily consume bytes from the next options in the SD
message — silently desynchronising the option stream so that
subsequent legitimate options (typically the IPv4/IPv6 endpoint
options that drive subscription routing) were either dropped or
misinterpreted as a different option type.

Not a memory-safety bug — the deserializer's own remaining_ check
keeps every read in-bounds of the SD message. But the parser-state
corruption is reachable from any peer on the SD multicast group,
without authentication, so it's worth fixing.

Track bytes_remaining = length_ - 1 (the bytes that belong to this
option after option_impl::deserialize consumed length+type+reserved)
and reject:
- length_ < 1 (option header smaller than the already-consumed
  reserved byte);
- an item-length that exceeds bytes_remaining (lying length);
- the loop running out of bytes without seeing the '\\0' terminator
  (missing terminator).

Behaviour unchanged for well-formed inputs: a CONFIGURATION option
whose payload is exactly reserved + items + '\\0' still decodes
identically. The early-terminator-with-trailing-padding case (which
isn't described by the SWS but does occur if a sender adds padding)
continues to return success, matching the previous behaviour.

Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant