[CORE-15247] kafka: add size validation to string & arrays read off the wire#29208
Conversation
kafka: add size validation to string & arrays read off the wirekafka: add size validation to string & arrays read off the wire
There was a problem hiding this comment.
Pull request overview
This PR enhances input validation for Kafka protocol deserialization by adding bounds checks to prevent reading beyond available buffer space when parsing strings and arrays from client requests.
Changes:
- Added validation to ensure string and array lengths don't exceed remaining bytes in the parser buffer
- Enhanced error logging when disconnecting clients to include the exception details
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/v/kafka/protocol/wire.h | Added bounds validation for string and array deserialization to prevent buffer overruns |
| src/v/kafka/server/connection_context.cc | Enhanced disconnect logging to include exception details for better debugging |
| if (static_cast<size_t>(len) > _parser.bytes_left()) { | ||
| throw std::out_of_range( | ||
| fmt::format( | ||
| "Array length {} exceeds remaining bytes {}", | ||
| len, | ||
| _parser.bytes_left())); | ||
| } | ||
|
|
There was a problem hiding this comment.
The validation compares the array length against remaining bytes, but this check is insufficient. The array length represents the number of elements, not the byte size. Each element will consume a variable number of bytes depending on type T. This validation will fail to catch cases where the total size of all elements exceeds remaining bytes. Consider validating during element-by-element parsing instead, or calculating the minimum required bytes based on element type.
| if (static_cast<size_t>(len) > _parser.bytes_left()) { | |
| throw std::out_of_range( | |
| fmt::format( | |
| "Array length {} exceeds remaining bytes {}", | |
| len, | |
| _parser.bytes_left())); | |
| } |
There was a problem hiding this comment.
Yeah, this is fine though
dotnwat
left a comment
There was a problem hiding this comment.
is there a jira ticket or a description of how you encountered this?
| _parser.bytes_left())); | ||
| } | ||
|
|
||
| return _parser.read_string(n); |
There was a problem hiding this comment.
maybe the above validations should be inside the parser read_string method?
There was a problem hiding this comment.
It could be, though iobuf_parser is used a lot internally and performing validation here at the kafka layer seems reasonable as well. WDYT?
There was a problem hiding this comment.
iobuf_parser is used a lot internally and performing validation here at the kafka layer seems reasonable as well. WDYT?
maybe i'm missing something, but isn't this an argument for putting the validation in the parser itself, so that the validation happens for all users not just this one spot?
There was a problem hiding this comment.
Not if two extra if statements is a concern for performance 👍
WDYT about putting these checks into iobuf_parser::read_string_safe() and calling that from wire.h instead?
There was a problem hiding this comment.
oh sorry, you're right. i was thinking that the iobuf_parser interfaces were reading the size, but infact we are providing it as parsed out of the kafka protocol. so yeh, doing the check above the iobuf_parser makes sense!
|
3b7ad38 to
a968756
Compare
michael-redpanda
left a comment
There was a problem hiding this comment.
do we need to perform a remaining bytes check in read_tags and consume_unknown_tag as well?
| throw std::out_of_range("Asked to read a 0 byte flex string"); | ||
| } | ||
|
|
||
| if (static_cast<size_t>(n - 1) > _parser.bytes_left()) { |
There was a problem hiding this comment.
for performance resons should these be marked [[unlikely]]?
I haven't been able to hit any crashes within |
a968756 to
943e418
Compare
For string, array, and number of tags.
943e418 to
61bfa92
Compare
michael-redpanda
left a comment
There was a problem hiding this comment.
Would it be possible to get a unit test written that triggers this behavior?
Added one courtesy of Claude. |
Add wire_validation_test.cc with tests that verify bounds checking and error handling in the Kafka protocol decoder. Tests cover: - Array length validation (exceeds buffer, negative, max int32) - Flex array validation (exceeds buffer, zero length) - String/flex string validation (exceeds buffer, negative/zero length) - Bytes/flex bytes validation - Tagged fields validation (count exceeds buffer, duplicate/non-ascending IDs, size exceeds buffer) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
8137870 to
a78c238
Compare
|
Alright done with force pushing (just wanted to make what Claude wrote for the unit test a tiny bit nicer). Approve away |
| if (unlikely(static_cast<size_t>(num_tags) > _parser.bytes_left())) { | ||
| throw std::out_of_range( | ||
| fmt::format( | ||
| "Number of tags {} exceeds remaining bytes {}", | ||
| num_tags, | ||
| _parser.bytes_left())); | ||
| } | ||
| int64_t prev_tag_id = -1; | ||
| while (num_tags-- > 0) { | ||
| auto id = read_unsigned_varint(); // consume tag id |
There was a problem hiding this comment.
@michael-redpanda what was the concern for this case? it looks like if num_tags is too large then the loop will hit end-of-buffer exception. the other cases were large allocations because the bogus value was plugged directly into malloc.
There was a problem hiding this comment.
Sorry my concern isn't here, but below in the iobuf_to_bytes(_parser.share(size)) call. Maybe that one is ok as well
There was a problem hiding this comment.
Pretty sure _parser.share() won't malloc() either, it iterates over fragments.
|
Any outstanding concerns here? |
|
@michael-redpanda I'll wait on your approval. |
|
/backport v25.3.x |
|
/backport v25.2.x |
|
/backport v25.1.x |
Backports Required
Release Notes
Bug Fixes
kafkaclients.