Skip to content

security(codecs): bound chunked_gelf decoder memory - #26137

Open
pront wants to merge 1 commit into
masterfrom
pront-chunked-gelf-pending-message-dos
Open

security(codecs): bound chunked_gelf decoder memory#26137
pront wants to merge 1 commit into
masterfrom
pront-chunked-gelf-pending-message-dos

Conversation

@pront

@pront pront commented Aug 18, 2026

Copy link
Copy Markdown
Member

Summary

The chunked_gelf decoder keeps a table of half-finished messages keyed by the message ID in each chunk header. Nothing bounded that table, so an unauthenticated sender could exhaust memory by naming message IDs it never completes, each holding buffer state until it times out. Most exposed on the socket source in UDP mode, where datagrams need no handshake.

Why the existing options can't express the bound

pending_messages_limit caps how many messages may be pending and max_length caps how large one may be, but memory is their product. Any pair permissive enough for real traffic multiplies out past what a host has.

Memory is actually a sum, so bound each term separately:

memory ~= pending count x per-message overhead + buffered payload

MAX_PENDING_MESSAGES = 4096      messages awaiting completion
MAX_BUFFERED_PAYLOAD = 128 MiB   chunk payload across all of them

Capping the count covers every per-message cost at once (state struct, map entry, timeout task, table slack) without having to price any of them individually. Capping payload in aggregate rather than per message is what keeps the two terms from multiplying.

Both existing options survive as overrides, clamped so they can only tighten the caps.

Headroom

Only chunked messages are buffered at all, so this affects senders emitting messages too large for one datagram. Pending count is roughly rate x loss x 5s timeout, so reaching 4096 needs ~82k chunked messages/sec at 1% packet loss. The payload cap allows ~1,340 pending 100 KB messages, so the count is the binding limit for anything under ~32 KB.

Rejection behaviour

  • A full budget refuses the chunk, not the message. The condition isn't that message's fault, and discarding would let a sender parking the budget pick off everything in flight.
  • A chunk that finishes its message is weighed against that message alone rather than the aggregate, since completing is what returns budget and refusing it would wedge the decoder. It is still held to the bound.
  • A lone chunk (total_chunks == 1) completes in the same call, so it is exempt from occupancy but not from size.
  • Exceeding max_length does discard, because that message can never become valid.
  • Everything a new message must satisfy is settled before it gets a state and a timer, so no rejection path leaves either behind.

Also fixed

  • A one-byte message panicked the source when trace logging was enabled, by formatting two bytes of a frame only known to be non-empty.
  • Chunks pinned the framer's read buffer, since BytesDecoder slices it without copying. add_chunk now copies.
  • The pending limit was checked before the table lookup, so a full table rejected chunks of already-pending messages, which could then never complete.
  • Dropping a message for exceeding max_length leaked its timeout task.
  • MessageState is boxed to keep ~4 KiB out of every HashMap bucket.

The limits apply to chunked messages, the only ones buffered. An unchunked frame passes straight through, bounded by what the source accepts as a frame.

References

N/A

Vector configuration

sources:
  graylog:
    type: socket
    mode: udp
    address: 0.0.0.0:12201
    framing:
      method: chunked_gelf
    decoding:
      codec: gelf

sinks:
  out:
    type: blackhole
    inputs:
      - graylog

How did you test this PR?

Unit tests in lib/codecs/src/decoding/framing/chunked_gelf.rs. The bound had no coverage before this, so the new tests cover the accounting invariant across every exit path, both caps, each rejection asymmetry above, and that a stored chunk does not alias the decoder's input buffer.

Each new test was checked against a deliberately broken build to confirm it fails, rather than assumed to work.

make fmt, cargo clippy -p codecs --lib --tests, make check-generated-docs and the full codecs lib suite are clean.

Is this a breaking change?

  • Yes
  • No

No configuration becomes invalid and nothing fails to start. Reaching either cap requires tens of thousands of multi-chunk messages per second with packet loss, since only chunked messages are buffered. Anyone hitting them was already holding hundreds of MB of half-finished messages, where the alternative to a dropped message was an OOM. Refusals surface as decoding errors with the usual component error metrics.

Does this PR include user facing changes?

  • Yes. Please add a changelog fragment based on our guidelines.
  • No. A maintainer will apply the no-changelog label to this PR.

Measured

Debug build on a laptop, so absolute rates run well below release; the shapes are the point.

Well-formed 4-chunk messages:

rate datagrams/s delivered kernel UDP drops RSS
1k 4k 100% 0% 86 MB
5k 20k 100% 0% 86 MB
10k 40k 100% 0% 88 MB
15k 60k 24.8% 11.6% 115 MB

Stall flood, first chunk only with unique IDs, 88k datagrams/s for 20s (1.76M abandoned messages):

RSS 83 MB -> peak 135 MB -> 129 MB after the timeout drained

Unbounded that is roughly 1.76M x 4 KiB = 7 GB, so the bound holds under the case it exists for.

Two honest caveats. Once datagram loss makes messages permanently incomplete, they hold slots for the full timeout_secs, so the table can saturate and start refusing healthy messages too: at 11.6% loss above, delivery fell to 24.8% where independent loss alone predicts ~61%. And during a stall flood, legitimate traffic gets roughly its proportional share of the table (500 msg/s alongside an 88k/s flood landed 1.2%), which is inherent to an unauthenticated shared buffer rather than specific to these limits.

@github-actions github-actions Bot added docs review on hold The documentation team reviews PRs only after a PR is approved by the COSE team. domain: external docs Anything related to Vector's external, public documentation labels Aug 18, 2026
@pront
pront marked this pull request as ready for review August 18, 2026 13:19
@pront
pront requested review from a team as code owners August 18, 2026 13:19
@pront pront changed the title security(codecs): bound chunked_gelf buffered state by default security(codecs): set default chunked_gelf limits Aug 18, 2026
@pront
pront enabled auto-merge August 18, 2026 16:37
chatgpt-codex-connector[bot]

This comment was marked as outdated.

chatgpt-codex-connector[bot]

This comment was marked as outdated.

chatgpt-codex-connector[bot]

This comment was marked as outdated.

chatgpt-codex-connector[bot]

This comment was marked as outdated.

@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 597e488 to 2db2e2b Compare August 19, 2026 19:03
@pront pront changed the title security(codecs): set default chunked_gelf limits security(codecs): bound chunked_gelf decoder memory Aug 19, 2026
chatgpt-codex-connector[bot]

This comment was marked as outdated.

chatgpt-codex-connector[bot]

This comment was marked as outdated.

@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch 2 times, most recently from c4244b3 to 76018e3 Compare August 19, 2026 22:25
chatgpt-codex-connector[bot]

This comment was marked as outdated.

@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 76018e3 to 556f752 Compare August 19, 2026 22:35
chatgpt-codex-connector[bot]

This comment was marked as outdated.

@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 556f752 to 2960a0a Compare August 20, 2026 00:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2960a0a922

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/codecs/src/decoding/framing/chunked_gelf.rs Outdated
@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch 2 times, most recently from 0abddf6 to 7e26e03 Compare August 20, 2026 00:31

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e26e0346f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/codecs/src/decoding/framing/chunked_gelf.rs Outdated
Comment thread lib/codecs/src/decoding/framing/chunked_gelf.rs Outdated
@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 7e26e03 to e5723c1 Compare August 20, 2026 00:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5723c11dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/codecs/src/decoding/framing/chunked_gelf.rs Outdated
@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from e5723c1 to 2ac4a65 Compare August 20, 2026 00:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ac4a6562e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/codecs/src/decoding/framing/chunked_gelf.rs Outdated
@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 2ac4a65 to 8a27780 Compare August 20, 2026 00:54
@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 8a27780 to 1f093ad Compare August 20, 2026 00:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f093ad744

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/codecs/src/decoding/framing/chunked_gelf.rs
@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 1f093ad to 71fbd8a Compare August 20, 2026 01:04
@pront

pront commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 71fbd8adc2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 71fbd8a to b208ebf Compare August 20, 2026 13:26
@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from b208ebf to 045c360 Compare August 20, 2026 14:17
@pront
pront changed the base branch from master to pront-chunked-gelf-preexisting-fixes August 20, 2026 14:18
auto-merge was automatically disabled August 20, 2026 14:18

Merge commits are not allowed on this repository

Base automatically changed from pront-chunked-gelf-preexisting-fixes to master August 24, 2026 14:35
The decoder keeps a table of half-finished messages keyed by the message ID in
each chunk header. Nothing bounded that table, so an unauthenticated sender
could exhaust memory by naming message IDs it never completes, each holding
buffer state until it times out. Most exposed on the `socket` source in UDP
mode, where datagrams need no handshake.

Memory here is `count x per-message overhead + payload`, a sum, so bound each
term rather than trying to measure allocations:

    MAX_PENDING_MESSAGES  = 4096      messages awaiting completion
    MAX_BUFFERED_PAYLOAD  = 128 MiB   chunk payload across all of them

Capping the count covers every per-message cost at once (state, map entry,
timeout task, table slack) without pricing any of them. Capping payload in
aggregate rather than per message keeps the two terms from multiplying, which
is what `pending_messages_limit` x `max_length` cannot avoid and why those
options cannot express this bound themselves. Both survive as overrides,
clamped so they can only tighten the caps.

`buffered_payload` counts chunk payload and nothing else, so its one increment
and one decrement are symmetric by inspection. No single peak figure is
advertised: reassembly adds a transient copy, allocator size classes round each
chunk up, and decompression is bounded separately by `CappedDecoder`, so any
such figure would need re-deriving whenever one of those changed.

Rejection is asymmetric:

- A full budget refuses the chunk but keeps the message, since the condition is
  not that message's fault and discarding would let a sender parking the budget
  pick off everything in flight.
- A chunk that finishes its message is weighed against that message alone,
  because completing is what returns budget and refusing it would wedge the
  decoder. It is never exempt from the bound itself, since one chunk can be any
  size on a message-based source.
- A lone chunk (`total_chunks == 1`) completes in the same call, so it is
  exempt from occupancy but not from size.
- Exceeding `max_length` does discard: that message can never become valid.

Everything a new message must satisfy is settled before it gets a state and a
timer, so no rejection path leaves either behind.

The limits apply to chunked messages, the only ones buffered. An unchunked
frame passes straight through, bounded by what the source accepts as a frame.

Builds on #26162, which carries the pre-existing fixes this work uncovered.
@pront
pront force-pushed the pront-chunked-gelf-pending-message-dos branch from 045c360 to 567083a Compare August 24, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs review on hold The documentation team reviews PRs only after a PR is approved by the COSE team. domain: external docs Anything related to Vector's external, public documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant