Skip to content

kafka/protocol: bound parse_tags by remaining message bytes - #30191

Merged
rockwotj merged 1 commit into
redpanda-data:devfrom
rockwotj:kafka-server-limits
Apr 16, 2026
Merged

kafka/protocol: bound parse_tags by remaining message bytes#30191
rockwotj merged 1 commit into
redpanda-data:devfrom
rockwotj:kafka-server-limits

Conversation

@rockwotj

Copy link
Copy Markdown
Contributor

parse_tags now takes a max_bytes parameter and checks two limits
before allocating each tagged field:

  1. A cumulative budget check: the field length cannot exceed the
    bytes remaining in the message. Both callers already have a
    validated total message size (connection_context has sz from
    parse_size; transport has bytes_remaining), so this bound is
    derived from actual data rather than a hardcoded constant.

  2. A per-field cap of 128 KiB, retained as a sanity limit on any
    single field.

All exceptions are now net::parsing_exception subclasses so the
server classifies them as parse-error disconnects rather than
unexpected errors.

Backports Required

  • none - not a bug fix
  • none - this is a backport
  • none - issue does not exist in previous branches
  • none - papercut/not impactful enough to backport
  • v26.1.x
  • v25.3.x
  • v25.2.x

Release Notes

Improvements

  • Better guard against malformed requests when parsing kafka messages

Copilot AI review requested due to automatic review settings April 16, 2026 14:21
@rockwotj
rockwotj requested review from a team, WillemKauf and nguyen-andrew and removed request for a team April 16, 2026 14:22
@rockwotj
rockwotj force-pushed the kafka-server-limits branch from d3753ab to ec227f5 Compare April 16, 2026 14:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens Kafka flexible tagged-field parsing by adding an explicit byte budget (max_bytes) and ensuring parse failures are surfaced as net::parsing_exception-derived exceptions, so the server classifies them as parse errors.

Changes:

  • Change parse_tags to accept max_bytes and enforce both a remaining-message budget and a per-field 128 KiB cap.
  • Thread request-size / remaining-bytes information into tag parsing from server (parse_header) and client (transport) call sites.
  • Replace std::invalid_argument / std::logic_error throws in tag parsing with net::parsing_exception subclasses.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/v/kafka/server/protocol_utils.h Updates parse_header signature to accept request size.
src/v/kafka/server/protocol_utils.cc Passes size into tag parsing when handling flexible headers.
src/v/kafka/server/connection_context.cc Supplies request size from parse_size into parse_header.
src/v/kafka/protocol/flex_versions.h Extends parse_tags API to include max_bytes.
src/v/kafka/protocol/flex_versions.cc Implements byte-budget enforcement and parsing_exception-based errors.
src/v/kafka/protocol/BUILD Adds the Bazel dependency needed for 128_KiB units.
src/v/kafka/client/transport.cc Bounds response tag parsing by bytes_remaining.

Comment thread src/v/kafka/protocol/flex_versions.cc Outdated
Comment thread src/v/kafka/protocol/flex_versions.cc
Comment thread src/v/kafka/protocol/flex_versions.cc
Comment on lines 109 to 113
} else if (
flex_versions::is_flexible_request(header->key, header->version)) {
auto [tags, bytes_read] = co_await parse_tags(src);
auto [tags, bytes_read] = co_await parse_tags(src, request_size);
header->tags = std::move(tags);
header->tags_size_bytes = bytes_read;

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_tags is given request_size (the whole message size), but parse_tags only tracks bytes consumed within the tags section. This means tags can consume up to request_size bytes, exceeding the bytes remaining after parse_v1_header and causing remaining = size - request_header_size - client_id - tags_size_bytes to underflow later (potentially leading to huge reads). Pass the bytes remaining after header+client_id (and validate request_size >= header_bytes) instead of the full request size.

Copilot uses AI. Check for mistakes.
Comment thread src/v/kafka/protocol/flex_versions.cc Outdated
Comment on lines +101 to +111
if (next_len > max_bytes - total_bytes_read) {
throw protocol_parse_exception(
fmt::format(
"tagged field {} length {} exceeds remaining message budget {}",
id,
next_len,
max_bytes - total_bytes_read));
}
if (next_len > 128_KiB) {
throw std::invalid_argument(
fmt::format("Too large of a tagged field: {}", next_len));
throw protocol_parse_exception(
fmt::format(

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new max_bytes enforcement in parse_tags is security-sensitive (bounds checks + exception type changes), but there are no unit tests covering: remaining-budget violations, the 128KiB per-field cap, duplicate tag ids, and short reads. Consider adding focused tests under src/v/kafka/protocol/tests/ to ensure these cases throw net::parsing_exception and report correct bytes_read.

Copilot uses AI. Check for mistakes.
@rockwotj
rockwotj force-pushed the kafka-server-limits branch from ec227f5 to e0f0bb3 Compare April 16, 2026 14:35
parse_tags now takes a max_bytes parameter and checks two limits
before allocating each tagged field:

1. A cumulative budget check: the field length cannot exceed the
   bytes remaining in the message. Both callers already have a
   validated total message size (connection_context has sz from
   parse_size; transport has bytes_remaining), so this bound is
   derived from actual data rather than a hardcoded constant.

2. A per-field cap of 128 KiB, retained as a sanity limit on any
   single field.

All exceptions are now net::parsing_exception subclasses so the
server classifies them as parse-error disconnects rather than
unexpected errors.
@rockwotj
rockwotj force-pushed the kafka-server-limits branch from e0f0bb3 to 17b5168 Compare April 16, 2026 14:49

namespace {
// TODO(C++26): replace with std::sub_sat
size_t sub_sat(size_t a, size_t b) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder how many other places in our parsing logic could use some serious combing through with saturating arithmetic ☹️

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

none its perfect now

@WillemKauf WillemKauf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine and sufficiently hardened to me

@rockwotj
rockwotj merged commit 28ce3fc into redpanda-data:dev Apr 16, 2026
19 checks passed
@vbotbuildovich

Copy link
Copy Markdown
Collaborator

/backport v26.1.x

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

/backport v25.3.x

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

/backport v25.2.x

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

Failed to create a backport PR to v25.3.x branch. I tried:

git remote add upstream https://github.com/redpanda-data/redpanda.git
git fetch --all
git checkout -b backport-pr-30191-v25.3.x-140 remotes/upstream/v25.3.x
git cherry-pick -x 17b5168c69

Workflow run logs.

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

Failed to create a backport PR to v25.2.x branch. I tried:

git remote add upstream https://github.com/redpanda-data/redpanda.git
git fetch --all
git checkout -b backport-pr-30191-v25.2.x-832 remotes/upstream/v25.2.x
git cherry-pick -x 17b5168c69

Workflow run logs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants