Skip to content

fix(Vss): correct wrong-field NULL checks and integer overflow in string-array serialization#137

Open
SoundMatt wants to merge 3 commits into
COVESA:mainfrom
SoundMatt:fix/vss-wrong-field-and-length-overflow
Open

fix(Vss): correct wrong-field NULL checks and integer overflow in string-array serialization#137
SoundMatt wants to merge 3 commits into
COVESA:mainfrom
SoundMatt:fix/vss-wrong-field-and-length-overflow

Conversation

@SoundMatt
Copy link
Copy Markdown
Contributor

Summary

Three bugs in Avtp_Vss_GetVssData() and Avtp_Vss_SerializeStringArray() in src/avtp/acf/custom/Vss.c:

1. VSS_UINT64_ARRAY — NULL guard checks the wrong field (line 352)

// Before
if (val->data_int64_array->data != NULL) {   // wrong field
    // writes into data_uint64_array->data

The guard reads data_int64_array->data instead of data_uint64_array->data. If data_uint64_array->data is NULL but data_int64_array->data is not, the loop writes through a NULL pointer. Conversely, if data_uint64_array->data is valid but data_int64_array->data is NULL, the copy is silently skipped.

2. VSS_STRING_ARRAY — NULL guard checks the wrong field (line 401)

// Before
if (val->data_double_array->data != NULL) {  // wrong field
    memcpy(val->data_string_array->data, ...);

The guard reads data_double_array->data while the memcpy destination is data_string_array->data. A crafted VSS packet that sets VSS_STRING_ARRAY type while data_string_array->data is NULL and data_double_array->data is non-NULL would trigger a NULL-pointer memcpy write.

3. SerializeStringArrayuint16_t accumulator overflows for large arrays (line 623)

// Before
uint16_t total_length = 0;
total_length += strings[i]->data_length + 2;   // wraps at 65535

total_length is uint16_t. Accumulating many strings can silently wrap to a value much smaller than the actual payload, causing the stored data_length to be wrong. Downstream deserialization then uses this truncated length as the memcpy size, enabling a buffer over-read.

Changes

  • Fix the NULL guards in VSS_UINT64_ARRAY and VSS_STRING_ARRAY to check the correct field.
  • Accumulate total_length in uint32_t; cap at UINT16_MAX before assigning to the uint16_t struct field.

Testing

  • VSS_UINT64_ARRAY deserialization with valid data_uint64_array->data now correctly copies data.
  • VSS_STRING_ARRAY deserialization with NULL data_string_array->data no longer writes through NULL.
  • SerializeStringArray with strings summing to >65535 bytes now saturates at UINT16_MAX rather than wrapping.

SoundMatt added 3 commits May 5, 2026 10:58
Avtp_Vss_DeserializeStringArray and Avtp_Vss_GetVSSDataStringArrayLength
in src/avtp/acf/custom/Vss.c walk an on-wire string-array layout of
[length-prefix : 2 bytes][string : length bytes] pairs.  Both had
serious bounds-check defects allowing crafted network input to drive
out-of-bounds reads:

1. Avtp_Vss_DeserializeStringArray declared `uint16_t idx = 0;` and
   only checked `if (idx >= array_length) break;` — but `idx` was
   never incremented, so the break was dead code (it only fired when
   array_length == 0).  With attacker-controlled str_length values
   from the wire (uint16_t, up to 65535), the memcpy() inside the
   loop could read up to ~64 KiB past the end of
   vss_data_string_array->data on every iteration after the first
   malicious entry.  Heap-disclosure / crash class.

2. Avtp_Vss_GetVSSDataStringArrayLength advanced
   `ptr_idx += 2 + str_length` in uint16_t arithmetic.  A str_length
   value near UINT16_MAX wraps ptr_idx around to a small number,
   making the loop iterate from a low offset again.  Pseudo-infinite
   loop / DoS class.  In addition, the loop condition
   `ptr_idx < total_length` was off by one — it permitted reading the
   2-byte length prefix when only 1 byte remained.

3. Avtp_Vss_GetVSSDataStringArrayLength also returned uint8_t for a
   count that could plausibly exceed 255 (since total_length is
   uint16_t and a string can be 0 bytes, the protocol allows up to
   ~32k strings in a single array).  Same shape as
   Avtp_Can_GetCanPayloadLength fixed in the previous PR.

Fixes:

- Add a `consumed` counter in DeserializeStringArray that is actually
  incremented per iteration and used to drive the break.
- Add an explicit "remaining bytes >= str_length" check before
  memcpy() so a single malformed string-length value cannot trigger
  the OOB read.
- In GetVSSDataStringArrayLength, change the loop guard to
  `ptr_idx + 2 <= total_length` (uint32_t arithmetic), and add a
  matching post-length-read check that the string body fits.
- Widen the GetVSSDataStringArrayLength return type and the function
  declaration in include/avtp/acf/custom/Vss.h from uint8_t to
  uint16_t.
- Add a NULL-check for strings[i] before dereferencing it (the
  previous code would NULL-deref strings[i]->data_length on the
  first malformed entry of an under-allocated output array).

All bound-check additions are computed in uint32_t so the additions
themselves cannot overflow when str_length approaches UINT16_MAX.

No behaviour change for well-formed input.  Existing unit tests in
unit/test-vss.c continue to pass (they exercise the 3-string happy
path).

Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
Per @SebastianSchildt's review on this PR. Constructs a 23-byte
on-wire fixture identical to the well-formed vss_data_string_array
test, except the third length prefix is patched from 0x0007 to
0x00AA — so the third entry claims 170 bytes of body but only 7
bytes of buffer remain after its length prefix.

Asserts:
- Avtp_Vss_GetVSSDataStringArrayLength returns 2 (not 3, and not 0
  which would be the result of bailing on the first read).
- Avtp_Vss_DeserializeStringArray fills the first two output structs
  with 'Hello' and 'World' and leaves the third struct at its initial
  state (data_length == 0). The third entry is rejected by the new
  remaining-bytes-vs-str_length check before memcpy.

Without the bounds-check fix in this PR the same input would have
driven memcpy() to read up to ~64 KiB past the end of the buffer in
DeserializeStringArray, and the count loop in
GetVSSDataStringArrayLength would have wrapped uint16_t arithmetic
on the (uint16_t + 0xAA) addition, producing a pseudo-infinite loop.

Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
…ing-array serialization

Three bugs in Avtp_Vss_GetVssData / Avtp_Vss_SerializeStringArray:

1. VSS_UINT64_ARRAY (line 352): NULL guard read data_int64_array->data
   instead of data_uint64_array->data.  If data_int64_array happens to be
   non-NULL while data_uint64_array is NULL the memcpy writes through a
   NULL pointer; conversely a valid buffer could be skipped.

2. VSS_STRING_ARRAY (line 401): NULL guard read data_double_array->data
   instead of data_string_array->data.  Same consequence: the memcpy
   destination (data_string_array->data) could be NULL even when the
   guard passes.

3. SerializeStringArray: total_length was uint16_t.  Accumulating
   num_strings entries each up to 65535+2 bytes silently wraps, causing
   the caller to store a truncated length in the wire packet which
   downstream deserialization then uses as a buffer size, enabling an
   over-read.  Fix: accumulate in uint32_t and cap at UINT16_MAX before
   writing back to the uint16_t field.

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