acf/vss: fix unbounded reads in string-array (de)serialisation#129
Conversation
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>
There was a problem hiding this comment.
Pull request overview
This PR hardens the VSS ACF string-array parsing helpers against crafted on-wire inputs that could previously trigger out-of-bounds reads during string-array length counting and deserialization.
Changes:
- Adds explicit bounds checks while walking
[u16 length][bytes...]string-array elements to prevent OOB reads and overflow-driven looping. - Fixes a dead bounds check in
Avtp_Vss_DeserializeStringArrayby tracking consumed bytes and validating each element beforememcpy. - Widens
Avtp_Vss_GetVSSDataStringArrayLength’s return type (and public declaration) fromuint8_ttouint16_t.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/avtp/acf/custom/Vss.c | Adds overflow-safe bounds checks for string-array counting and deserialization to prevent unbounded reads. |
| include/avtp/acf/custom/Vss.h | Updates the public API to return uint16_t for string-array length counting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Hello,
thank you for pointing out the issues. The ACF VSS code is experimental and not very high quality currently, so we surely appreciate any improvements.
For the current PR I see two issues
- There are still lot of AI artifacts in the comments, i.e. it describes the "history" of this code and summaries of steps taken. This is not what is expected in code. Comments should be helpful explaining the code at hand, not explaining its genesis
- As this fixes a non-trivial/not super trivial part of the code it would be good to have some simple unit tests, especially since the code claims more robustness, maybe also with malformed input data. Or at least maybe explain in the issue (as there are no good tests for this currently) how this has been tested/verified working
|
Hi @SebastianSchildt, thanks for the thoughtful review. On the comments: I'd like to push back gently on the framing. The Where I think you have a fair point is the few spots where the On unit tests: agreed, the malformed-input cases deserve coverage, If you'd rather close this PR than work through revisions, that's |
|
Please adjust the comments as disussed: Leave the ones explaining what the current code does, and scrub those that refer "previous code" If you can add a unit test that is appreciated, as I would need a couple of days to look into this. If that is so easily achieved, I will look into this and maybe try a seperate PR for this, but as I said, likely not this week. My hints (untested) ideas how to go about the test would be Currently the existing tests already compares results from constructing the VSS packet with fixed data such as seen here Line 982 in 438f851 uint8_t arr_in_mem[] = {0x00, 0x05, 'H', 'e', 'l', 'l', 'o',
0x00, 0x05, 'W', 'o', 'r', 'l', 'd',
0x00, 0x07, 'T', 's', 'c', 'h', 'u', 's', 's'};I would add a seperate "Malformed string array" test (to not make the existing test longer), where you could try creating a a truncated version with a wrong length field i.e. uint8_t arr_in_mem[] = {0x00, 0x05, 'H', 'e', 'l', 'l', 'o',
0x00, 0x05, 'W', 'o', 'r', 'l', 'd',
0x00, 0xAA, 'T', 's', 'c', 'h', 'u', 's', 's'};Open point: Is there an "easy" way to put it into an ACF Frame? I guess it would always work constructing the packet "correctly" with the helper functions (which we hopefully trust due to the tests already there), and then just patching the last length field in the array Then we can parse this malformed packet/get the length and my expectation (if I understand the hardened parser correctly, otherwise please do the correct assumptions in line with the new code) would be, that it behaves like the array is finished already after the second element. Wrt to Mac OS I do remember having issues with cmocka as well, but I then reverted to using the Dev container, so I am not entirely sure this can run on Mac natively |
|
Thanks for the test guidance — your malformed-fixture sketch (patching the third length field to On the comments: walking back my previous offer to scrub the "previous code" references. Looking at them again with fresh eyes, each one is forward-looking documentation for the next maintainer rather than narrative history:
In code parsing untrusted network input, those rationales are exactly what stops the bug class from being reintroduced. The That said — "Allow edits by maintainers" is checked on this branch. If you feel strongly enough that specific lines should be scrubbed, please push the edits directly and I'll defer to the call; I just don't want to be the one to pull them out. The substantive offer (the malformed-input test commit) is the part I'll work on next. |
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>
|
Test commit pushed as 89d74a3. The new vss_data_string_array_malformed test uses your fixture sketch — a 23-byte buffer where the third length is 0x00AA against only 7 bytes of remaining body. Asserts GetVSSDataStringArrayLength returns 2 and DeserializeStringArray fills only the first two output structs ("Hello" and "World"), leaving the third at its initial state. CI will run once the workflow gets approval. If anything surfaces I'll address it. |
Signed-off-by: Sebastian Schildt <sebastian.schildt@hs-heilbronn.de>
|
Tests green, merging now, thank your for your contribution |
Problem
Avtp_Vss_DeserializeStringArrayandAvtp_Vss_GetVSSDataStringArrayLengthin
src/avtp/acf/custom/Vss.cwalk an on-wire string-array layout of[length-prefix : 2 bytes][string : length bytes]pairs. Both hadserious bounds-check defects letting crafted network input drive
out-of-bounds reads.
1.
DeserializeStringArray— dead bounds check + attacker-controlledmemcpylengthThe break is dead code —
idxis initialised to 0 and neverincremented, so the check only fires when
array_length == 0. Withattacker-controlled
str_lengthvalues up to UINT16_MAX from thewire,
memcpyreads up to ~64 KiB past the end ofvss_data_string_array->dataon every iteration after the firstmalicious entry. Heap disclosure / crash.
2.
GetVSSDataStringArrayLength— uint16_t overflow + off-by-one + truncated return typeThree issues:
ptr_idx += 2 + str_lengthperforms uint16_t arithmetic. Astr_lengthnear UINT16_MAX wrapsptr_idxaround to a smallvalue, making the loop iterate from a low offset again —
pseudo-infinite loop / DoS.
while (ptr_idx < total_length)permits reading the 2-bytelength prefix when only 1 byte remains. Off-by-one.
uint8_t, buttotal_lengthisuint16_tanda string can be 0 bytes — the protocol allows up to ~32 k strings
in a single array. Counts > 255 are silently truncated mod 256.
Same shape as the
Avtp_Can_GetCanPayloadLengthfix inacf/can: widen Can_GetCanPayloadLength return type to uint16_t #128.
Fix
consumedcounter inDeserializeStringArraythat'sactually incremented per iteration and used to drive the break.
memcpyso a single malformed length value can't trigger the OOBread.
GetVSSDataStringArrayLength, change the loop guard toptr_idx + 2 <= total_lengthand add a post-length-read checkthat the string body fits.
GetVSSDataStringArrayLengthreturn type (and itsdeclaration in
include/avtp/acf/custom/Vss.h) fromuint8_ttouint16_t.strings[i]before dereferencing it.uint32_tso theadditions themselves cannot overflow when
str_lengthapproachesUINT16_MAX.
No behaviour change for well-formed input. Existing unit tests in
unit/test-vss.c(the 3-string happy path) continue to pass.Notes
fix). Same class of "uint8_t holding a quantity that can exceed
255" bug, in a different protocol handler.
PR to keep scope focused, including unbounded
memcpycalls inthe various
Avtp_*_SetPayloadsetters andmemcpys of fixedarray types in
Avtp_Vss_GetVssData. Happy to follow up withseparate PRs (or, for the API-shape question of whether setters
should grow a
bufferSizeparameter to mirror the existingIsValid(pdu, bufferSize)helpers, a META-Issue first to getmaintainer guidance).