Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cd930a5
docs(specs): add security OOM allocation bounds spec
JuanMantica45 Aug 7, 2026
c86fbcf
docs(plans): add OOM/unbounded allocation bounds implementation plan
JuanMantica45 Aug 7, 2026
b034a7f
fix(logstash): [OBE-10712] cap decompressed frame size, reject nested…
JuanMantica45 Aug 7, 2026
e990543
fix(tcp): [OBE-11555] release RequestLimiterPermit before ack write_all
JuanMantica45 Aug 7, 2026
15c3bc3
fix(codecs): [OBE-11232] default NewlineDelimitedDecoder to 100 KiB m…
JuanMantica45 Aug 7, 2026
0153d5a
fix(codecs): [OBE-11235] set finite defaults for GELF pending_message…
JuanMantica45 Aug 7, 2026
b170eb5
trivial: update progress checklist — Tasks 2, 4, 5, 10 integrated
JuanMantica45 Aug 7, 2026
637e03e
test(logstash): [OBE-10712] add unit tests for decompression bomb and…
JuanMantica45 Aug 7, 2026
2b175f6
chore: bump lib/observo/private to security-oom-bounds (Tasks 1, 7, 3)
JuanMantica45 Aug 7, 2026
683cf21
trivial: update progress checklist — Tasks 1, 3, 7 integrated
JuanMantica45 Aug 7, 2026
bd3735b
fix(codecs): [OBE-11235] replace O(N) per-message tokio::spawn with D…
JuanMantica45 Aug 7, 2026
a43b463
fix(codecs): [OBE-11235] drop unused timeout field from ChunkedGelfDe…
JuanMantica45 Aug 7, 2026
17debea
chore: update private submodule pointer (OBE-11234, OBE-11556)
JuanMantica45 Aug 7, 2026
e76fee3
trivial: mark Tasks 6, 8, 9 complete in plan
JuanMantica45 Aug 7, 2026
9a97ddf
chore(docs): resolve planning artifacts for security-oom-allocation-b…
JuanMantica45 Aug 7, 2026
2b5ba9b
chore(docs): remove ADR from vector repo
JuanMantica45 Aug 7, 2026
294d6c3
fix(security): [OBE-11232,OBE-10712,OBE-11235,OBE-11555] address revi…
JuanMantica45 Aug 10, 2026
035c552
fix(security): widen OOM bounds to eliminate customer impact
JuanMantica45 Aug 10, 2026
5bc486e
fix(codecs): apply newline max_length as a serde default, not in build()
JuanMantica45 Aug 10, 2026
70a999e
fix(security): drop the newline max_length default; it bought no prot…
JuanMantica45 Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/security_oom_allocation_bounds.enhancement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Added default upper bounds to previously-unbounded allocation paths in several sources, so a
malicious or malformed peer can no longer exhaust the heap. Every default is set above documented
producer maxima, so legitimate traffic is unaffected; each is overridable.

- `logstash`: new `max_decompressed_bytes` (256 MiB) caps compressed-frame inflation; nested
compressed frames are rejected.
- `gcp_gcs`: new `max_decompressed_bytes` (4 GiB); truncation is logged and counted by
`gcs_object_truncated_total`.
- `stcp`: new `max_frame_bytes` (4x `max_event_size`, 64 MiB) and `max_lines_per_event` (1e6).
- `wef`: `max_content_length` is now enforced on the inbound HTTP body, defaulting to 4x the
advertised `max_envelope_size` and never dropping below it.
- GELF chunked framing: `pending_messages_limit` 10000, `max_length` 8 MiB — both above the
protocol's own ceiling of 128 chunks per message.

The `tcp` source now releases its `RequestLimiter` permit before writing the acknowledgement and
bounds that write with a 30-second timeout, so a peer that stops reading cannot starve others.
68 changes: 60 additions & 8 deletions lib/codecs/src/decoding/framing/chunked_gelf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,28 @@ use vector_config::configurable_component;
const GELF_MAGIC: &[u8] = &[0x1e, 0x0f];
const GELF_MAX_TOTAL_CHUNKS: u8 = 128;
const DEFAULT_TIMEOUT_SECS: f64 = 5.0;
/// Cap on concurrent incomplete messages, bounding the reassembly map.
/// Graylog Server itself has no such cap, so this is sized well above what a
/// legitimate sender holds in flight within the 5s reassembly window.
pub const DEFAULT_PENDING_MESSAGES_LIMIT: usize = 10_000;
/// Cap on one reassembled message. The protocol ceiling is 128 chunks
/// (`GELF_MAX_TOTAL_CHUNKS`) times the 65507-byte max UDP payload, so 8 MiB is
/// above anything the wire format can produce. Matches Graylog's own
/// `decompress_size_limit` default.
pub const DEFAULT_MAX_MESSAGE_LENGTH: usize = 8 * 1024 * 1024;

const fn default_timeout_secs() -> f64 {
DEFAULT_TIMEOUT_SECS
}

const fn default_pending_messages_limit() -> Option<usize> {
Some(DEFAULT_PENDING_MESSAGES_LIMIT)
}

const fn default_max_message_length() -> Option<usize> {
Some(DEFAULT_MAX_MESSAGE_LENGTH)
}

/// Config used to build a `ChunkedGelfDecoder`.
#[configurable_component]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -58,21 +75,22 @@ pub struct ChunkedGelfDecoderOptions {

/// The maximum number of pending incomplete messages. If this limit is reached, the decoder starts
/// dropping chunks of new messages, ensuring the memory usage of the decoder's state is bounded.
/// If this option is not set, the decoder does not limit the number of pending messages and the memory usage
/// of its messages buffer can grow unbounded. This matches Graylog Server's behavior.
#[serde(default, skip_serializing_if = "vector_core::serde::is_default")]
/// Defaults to 10000. Set explicitly to raise or lower it.
#[serde(default = "default_pending_messages_limit")]
#[derivative(Default(value = "default_pending_messages_limit()"))]
pub pending_messages_limit: Option<usize>,

/// The maximum length of a single GELF message, in bytes. Messages longer than this length will
/// be dropped. If this option is not set, the decoder does not limit the length of messages and
/// the per-message memory is unbounded.
/// be dropped. Defaults to 8 MiB, which is above the protocol's own ceiling of 128 chunks per
/// message.
///
/// Note that a message can be composed of multiple chunks and this limit is applied to the whole
/// message, not to individual chunks.
///
/// This limit takes only into account the message's payload and the GELF header bytes are excluded from the calculation.
/// The message's payload is the concatenation of all the chunks' payloads.
#[serde(default, skip_serializing_if = "vector_core::serde::is_default")]
#[serde(default = "default_max_message_length")]
#[derivative(Default(value = "default_max_message_length()"))]
pub max_length: Option<usize>,

/// Decompression configuration for GELF messages.
Expand Down Expand Up @@ -486,8 +504,8 @@ impl Default for ChunkedGelfDecoder {
fn default() -> Self {
Self::new(
DEFAULT_TIMEOUT_SECS,
None,
None,
default_pending_messages_limit(),
default_max_message_length(),
ChunkedGelfDecompressionConfig::Auto,
)
}
Expand Down Expand Up @@ -1278,4 +1296,38 @@ mod tests {

assert_eq!(detected_compression, ChunkedGelfDecompression::None);
}

#[tokio::test]
async fn defaults_are_finite_and_above_the_protocol_ceiling() {
let options = ChunkedGelfDecoderOptions::default();
assert_eq!(
options.pending_messages_limit,
Some(DEFAULT_PENDING_MESSAGES_LIMIT)
);
assert_eq!(options.max_length, Some(DEFAULT_MAX_MESSAGE_LENGTH));

// 128 chunks x the 65507-byte max UDP payload is the most the wire format can carry,
// so the default can never reject a well-formed message.
let protocol_ceiling = GELF_MAX_TOTAL_CHUNKS as usize * 65_507;
assert!(DEFAULT_MAX_MESSAGE_LENGTH >= protocol_ceiling);
}

#[tokio::test]
async fn limits_are_per_message_and_do_not_kill_the_stream() {
// Both are per-message conditions; tearing down the connection would let one bad sender
// drop every other message multiplexed over it.
assert!(ChunkedGelfDecoderError::MaxLengthExceed {
message_id: 1,
sequence_number: 0,
length: 10,
max_length: 5,
}
.can_continue());
assert!(ChunkedGelfDecoderError::PendingMessagesLimitReached {
message_id: 1,
sequence_number: 0,
pending_messages_limit: 1,
}
.can_continue());
}
}
2 changes: 1 addition & 1 deletion lib/observo/private
Submodule private updated from b90e4c to c377df
Loading