acf/can: widen Can_GetCanPayloadLength return type to uint16_t#128
acf/can: widen Can_GetCanPayloadLength return type to uint16_t#128SoundMatt wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes an integer-width truncation bug in the ACF CAN / CAN Brief helpers where payload lengths could wrap modulo 256 for frames larger than 256 bytes, by widening the computed byte-length and return types to uint16_t.
Changes:
- Widen
Avtp_Can_GetCanPayloadLengthreturn type and intermediateacf_msg_lengthtouint16_t. - Widen
Avtp_CanBrief_GetCanPayloadLengthreturn type and intermediateacf_msg_lengthtouint16_t. - Update the corresponding public header prototypes to match.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/avtp/acf/Can.c |
Widen CAN payload-length computation/return type to avoid modulo-256 truncation. |
src/avtp/acf/CanBrief.c |
Widen CAN Brief payload-length computation/return type to avoid modulo-256 truncation. |
include/avtp/acf/Can.h |
Update exported CAN API prototype to return uint16_t. |
include/avtp/acf/CanBrief.h |
Update exported CAN Brief API prototype to return uint16_t. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| uint16_t acf_msg_length = Avtp_Can_GetAcfMsgLength(pdu) * 4; | ||
| uint8_t acf_pad_length = Avtp_Can_GetPad(pdu); | ||
| return acf_msg_length - AVTP_CAN_HEADER_LEN - acf_pad_length; |
| uint16_t acf_msg_length = Avtp_CanBrief_GetAcfMsgLength(pdu) * 4; | ||
| uint8_t acf_pad_length = Avtp_CanBrief_GetPad(pdu); | ||
| return acf_msg_length - AVTP_CAN_BRIEF_HEADER_LEN - acf_pad_length; |
| * @return Length of CAN payload in bytes | ||
| */ | ||
| uint8_t Avtp_Can_GetCanPayloadLength(const Avtp_Can_t* const pdu); | ||
| uint16_t Avtp_Can_GetCanPayloadLength(const Avtp_Can_t* const pdu); |
| * @return Length of CAN payload in bytes | ||
| */ | ||
| uint8_t Avtp_CanBrief_GetCanPayloadLength(const Avtp_CanBrief_t* const pdu); | ||
| uint16_t Avtp_CanBrief_GetCanPayloadLength(const Avtp_CanBrief_t* const pdu); |
| uint16_t Avtp_Can_GetCanPayloadLength(const Avtp_Can_t* const pdu) | ||
| { | ||
| uint8_t acf_msg_length = Avtp_Can_GetAcfMsgLength(pdu) * 4; | ||
| /* AcfMsgLength is a 9-bit field counted in quadlets (4-byte units), so its | ||
| * value can range up to 511 quadlets = 2044 bytes. The previous | ||
| * implementation stored both the byte-count and the return value in | ||
| * uint8_t, which truncated mod 256 for any frame larger than ~64 quadlets, | ||
| * yielding a wrong (and possibly underflowed) payload length. Widen the | ||
| * intermediate and return type to uint16_t so the full range is preserved. */ | ||
| uint16_t acf_msg_length = Avtp_Can_GetAcfMsgLength(pdu) * 4; |
|
Hi @SoundMatt, thanks for pointing this issue and with the other PRs. Firstly, the lengths are indeed inconsistently used in the parts (Can.c, acf-can-common.c) of the CAN code in Open1722. We need to use a consistent length throughout -- either uint8_t or uint16_t. So my idea would be:
I would like to hear your ideas on this, especially if you have another opinion. We are also looking into the PR for VSS. The VSS part is anyways a non-standard extension for IEEE 1722 and is more an experiment. Nonetheless, we need to fix those. |
|
Hi @nayakned, thanks for the considered review and the broader You're right that classic CAN tops out at 8 bytes and CAN-FD at 64 That said, I think the bug we found isn't really the return type A few directions that address both concerns:
How would you like to proceed with this PR?
I lean (b) for keeping scope contained and addressing the actual I went ahead and tried what felt like a reasonable middle ground
This preserves the The broader For the VSS-over-AVTP PR (#129), good to know it's an experimental |
c6dca51 to
73fd019
Compare
|
Hi @nayakned I think using uint16 for all ACF message lenghts makes sense. Even though CAN-FD frames are relatively small (<64Byte), it's possible to encode a bigger length in an ACF-CAN message and the application should be able to read this value. It might even be dangerous to truncate the length value when iterating through a list of ACF messages, because someone could calculate the wrong start position of the next message. For people to not shoot themself in the foot I think it's good to have headerLength + payloadLength + padding = totalLength and leave bounds check to the application. Why not leave the 64byte boundary check to the isValid function? |
|
Hi @adriaan-niess, Agreed about using uint16_t as far as ACF message length is concerned. In fact we have to use uint16_t since ACF message lengths are 9-bit. The question is about the API to get CAN payload length and is specific to only the CAN v1 format. This API is mainly used at the receiver/listener side to extract payload data from an ACF message and remove the padding bytes so as to regenerate the original CAN message. A value of greater than 64 for the CAN payload length renders the CAN frame is invalid. To detect the start of the next ACF message, ACF message lengths will be used which remain 16-bit. |
This is the option 2 that @SoundMatt is suggesting. |
@SoundMatt The GetCanPayloadLength() can remain as it is. I think returning 0 as length is also equivalent to a "silent" truncation. If the ACF-CAN message is malformed, better discard the encapsulated CAN frame rather than use it with a truncated payload (either at 0, 8, 64 or 255)... Lets see if others have contradicting opinion. |
| uint8_t pad_length = Avtp_CanBrief_GetPad(pdu); | ||
| uint16_t header_and_pad = (uint16_t)AVTP_CAN_BRIEF_HEADER_LEN + pad_length; | ||
|
|
||
| if (msg_length_bytes < header_and_pad) { |
There was a problem hiding this comment.
I would vote against bounds checking here, this will seriously affect performance on applications that are doing can bridging as it will be done all the time, even though „correct“ packages do not need it.
I think a silent truncation is much better here in case of a malformed packet, because even in case of maliciously crafted packets this does not pose a security risk.
There was a problem hiding this comment.
Agree, better move bounds check etc. in isValid() function and keep the other getters/setters very efficient.
As @nayakned suggested, an application might just skip processing an ACF-CAN message if isValid() returns false.
| uint16_t payload_length = msg_length_bytes - header_and_pad; | ||
|
|
||
| /* Overflow guard: a valid CAN/CAN-FD payload can't exceed 64 bytes. */ | ||
| if (payload_length > MAX_CAN_FD_PAYLOAD) { |
There was a problem hiding this comment.
Same argument as for Can Brief: We should not waste cycles on the bounds check here.
|
So let me summarize. We use this pull request and do the following changes.
Further changes including the compiler warning flags, we do in subsequent PRs. |
nayakned
left a comment
There was a problem hiding this comment.
Changes as requested in the conversational thread.
Per the discussion in COVESA#128 with @nayakned, @adriaan-niess, and @SebastianSchildt, restructure the fix to: 1. Keep Avtp_Can_GetCanPayloadLength / Avtp_CanBrief_GetCanPayloadLength lean. Compute in uint16_t internally to avoid the original silent-truncation hazard, narrow back to uint8_t for the return. Document the precondition that callers must invoke IsValid first. No bounds-checking in this hot path. 2. Extend Avtp_Can_IsValid and Avtp_CanBrief_IsValid with the CAN payload-length invariant — payload <= 8 bytes for classic CAN (FDF == 0), <= 64 bytes for CAN-FD (FDF == 1). Also reject frames whose encoded message length is shorter than header + declared padding (would underflow GetCanPayloadLength). 4. Update examples/acf-can/acf-can-common.c to call Avtp_Can_IsValid before consuming the payload length, so example consumers don't see garbage values from malformed input. Item (3) — extending unit/test-can.c with coverage for the new IsValid invariants — will follow in a separate commit; I haven't got the cmocka unit/ suite building cleanly on macOS yet and want to verify the harness behaviour locally before submitting tests. Compiler-warnings cleanup is tracked separately in COVESA#131. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
Avtp_Can_GetCanPayloadLength and Avtp_CanBrief_GetCanPayloadLength silently produced wrong values when a PDU encoded an AcfMsgLength field larger than ~63 quadlets: uint8_t acf_msg_length = Avtp_Can_GetAcfMsgLength(pdu) * 4; return acf_msg_length - AVTP_CAN_HEADER_LEN - pad; The (uint8_t) variable holding the byte-count truncated mod 256, so a frame encoding e.g. 100 quadlets (400 bytes) produced byte-count = 144, then subtracted header and pad to yield a plausible-looking but wrong payload length. Per @nayakned's review observation that classic CAN tops out at 8 bytes and CAN-FD at 64, any encoded length outside that range cannot represent a valid CAN frame anyway. Add bounds checks that return 0 in two cases: 1. Underflow: encoded message length is smaller than header + pad. 2. Overflow: computed payload length exceeds CAN-FD's 64-byte max. Calculate in uint16_t internally to avoid the previous truncation hazard; narrow back to uint8_t for the return so the API type still matches the protocol's actual valid range. Callers cannot accidentally accept invalid sizes. Replaces the previous proposal in this PR to widen the return type to uint16_t. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
Per the discussion in COVESA#128 with @nayakned, @adriaan-niess, and @SebastianSchildt, restructure the fix to: 1. Keep Avtp_Can_GetCanPayloadLength / Avtp_CanBrief_GetCanPayloadLength lean. Compute in uint16_t internally to avoid the original silent-truncation hazard, narrow back to uint8_t for the return. Document the precondition that callers must invoke IsValid first. No bounds-checking in this hot path. 2. Extend Avtp_Can_IsValid and Avtp_CanBrief_IsValid with the CAN payload-length invariant — payload <= 8 bytes for classic CAN (FDF == 0), <= 64 bytes for CAN-FD (FDF == 1). Also reject frames whose encoded message length is shorter than header + declared padding (would underflow GetCanPayloadLength). 4. Update examples/acf-can/acf-can-common.c to call Avtp_Can_IsValid before consuming the payload length, so example consumers don't see garbage values from malformed input. Item (3) — extending unit/test-can.c with coverage for the new IsValid invariants — will follow in a separate commit; I haven't got the cmocka unit/ suite building cleanly on macOS yet and want to verify the harness behaviour locally before submitting tests. Compiler-warnings cleanup is tracked separately in COVESA#131. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
30a339f to
825bcd8
Compare
|
Hi @nayakned, @adriaan-niess, @SebastianSchildt — thanks all of you Just to make sure I have the plan right before I start coding:
One practical ask: I haven't yet got the existing cmocka I've pushed (1), (2), and the bonus (4) as a follow-up commit on |
We provide a devcontainer with the libcmocka library installed in it. |
|
The CI failure is a single assertion in // Valid IEEE 1722 CAN Frame
Avtp_Can_Init((Avtp_Can_t*)pdu);
assert_int_equal(Avtp_Can_IsValid((Avtp_Can_t*)pdu, MAX_PDU_SIZE), 1);After The other three assertions in Two reasonable paths from here, and I'd like your call on which Option A — revert the Option B — update the test fixture to match the agreed-on I lean B, because:
But this is your codebase and your test suite, so if you'd rather |
|
I would agree that B - change the test and replace it with one (or more) building an actually correct/useful frame is defintely the better option. In any case some form of B. (And not reverting the code to make this bogus test pass) |
|
Pushed Option B as a third commit on this branch:
CI should now pass. Ping me if you want any of the new fixtures |
The IsValid extension agreed in the review thread (classic CAN <= 8 bytes, CAN-FD <= 64 bytes, plus the underflow guard msg_length_bytes >= header + pad) caused the existing first assertion in can_is_valid to fail: a freshly Avtp_Can_Init'd PDU has AcfMsgLength == 0, which describes a frame shorter than its own header and so is now correctly rejected as malformed. Replace that assertion with a properly-formed classic CAN frame constructed via Avtp_Can_CreateAcfMessage, and keep the empty Init frame as a negative case asserting IsValid returns FALSE (per Sebastian's bonus suggestion). Add tests for the new payload-bound invariants: - classic CAN with a 12-byte payload is rejected (> 8-byte max) - CAN-FD at 64 bytes is accepted (FD max) - CAN-FD with 68 bytes is rejected (> 64-byte max) Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
Problem
Avtp_Can_GetCanPayloadLength(src/avtp/acf/Can.c) andAvtp_CanBrief_GetCanPayloadLength(src/avtp/acf/CanBrief.c)truncated mod 256 for any frame larger than ~64 quadlets:
AcfMsgLengthis a 9-bit field counted in quadlets (4-byte units),so the byte-count can be up to 2044. Storing both the intermediate
acf_msg_lengthand the return value inuint8_twraps mod 256once the frame exceeds 256 bytes. Downstream callers then read the
wrong number of payload bytes — sometimes underflowing because the
truncated value is smaller than
AVTP_CAN_HEADER_LEN + pad_length,sometimes mid-frame, depending on the actual size.
Evidence the receiving side already uses
uint16_tThe IsValid helpers in this file already use
uint16_tfor thesame quadlet-derived computation. And in
examples/acf-can/acf-can-common.c:The example code already assigns the result to a
uint16_tlocal,suggesting someone noticed the issue but only fixed the receiving
side. This PR fixes the source.
Fix
Widen both functions' return types and the local intermediate
acf_msg_lengthtouint16_t. No behaviour change for frames≤ 256 bytes; correct behaviour for larger frames.
Notes
include/avtp/acf/Can.h,include/avtp/acf/CanBrief.h), 2 sources (src/avtp/acf/Can.c,src/avtp/acf/CanBrief.c). The 4 changes are mechanicallysymmetric.
(unbounded
memcpyin setters, VSS string-array iteration withoutbuffer-size tracking) — happy to follow up with separate PRs once
this lands and after maintainer guidance on the API-shape
questions (e.g., whether setters should grow a
bufferSizeparameter to mirror the existing
IsValid(pdu, bufferSize)helpers).