Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion src/compiler/expression/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,19 @@ impl Expression for Op {
}

// "bar" * 1
//
// Deliberately stays infallible (OBE-10736). `try_mul` enforces a
// MAX_REPEAT_BYTES cap and returns an error above it, but marking this
// op fallible would be a breaking language change: every existing
// program doing `"x" * n` would stop compiling with E100. Integer
// `Add`/`Sub`/`Mul` make the same trade-off by wrapping rather than
// erroring. The over-limit error still surfaces as a graceful runtime
// error rather than an OOM, which is what the ticket required.
Mul if lhs_def.is_bytes() && rhs_def.is_integer() => {
lhs_def.union(rhs_def).with_kind(K::bytes())
}

// 1 * "bar"
// 1 * "bar" — see note above.
Mul if lhs_def.is_integer() && rhs_def.is_bytes() => {
lhs_def.union(rhs_def).with_kind(K::bytes())
}
Expand Down
73 changes: 70 additions & 3 deletions src/compiler/value/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ use bytes::{BufMut, Bytes, BytesMut};

use super::ValueError;

/// Maximum byte length of a string produced by the `*` (repeat) operator.
/// Prevents OOM when an attacker supplies a large integer multiplier (OBE-10736).
const MAX_REPEAT_BYTES: usize = 64 * 1024 * 1024; // 64 MiB

pub trait VrlValueArithmetic: Sized {
/// Similar to [`std::ops::Mul`], but fallible (e.g. `TryMul`).
fn try_mul(self, rhs: Self) -> Result<Self, ValueError>;
Expand Down Expand Up @@ -75,11 +79,23 @@ impl VrlValueArithmetic for Value {

// When multiplying a string by an integer, if the number is negative we set it to zero to
// return an empty string.
let as_usize = |num| if num < 0 { 0 } else { num as usize };
let as_usize = |num: i64| if num < 0 { 0 } else { num as usize };

let value = match self {
Value::Integer(lhv) if rhs.is_bytes() => {
Bytes::from(rhs.try_bytes()?.repeat(as_usize(lhv))).into()
// `try_bytes` consumes `rhs`, so the `err` closure above (which borrows
// it) cannot be used past this point. Both operand kinds are known
// exactly in this arm, so build the error from them directly rather than
// deriving `Kind` from the values — `Kind::from(&Value)` deep-walks
// containers, and doing that eagerly would cost every multiplication.
let repeat_err = || ValueError::Mul(Kind::integer(), Kind::bytes());
let bytes = rhs.try_bytes()?;
let n = as_usize(lhv);
let out_len = bytes.len().checked_mul(n).ok_or_else(repeat_err)?;
if out_len > MAX_REPEAT_BYTES {
return Err(repeat_err());
}
Bytes::from(bytes.repeat(n)).into()
}
Value::Integer(lhv) if rhs.is_float() => {
Value::from_f64_or_zero(lhv as f64 * rhs.try_float()?)
Expand All @@ -93,7 +109,14 @@ impl VrlValueArithmetic for Value {
lhv.mul(rhs).into()
}
Value::Bytes(lhv) if rhs.is_integer() => {
Bytes::from(lhv.repeat(as_usize(rhs.try_integer()?))).into()
// See the note in the `Integer * Bytes` arm above.
let repeat_err = || ValueError::Mul(Kind::bytes(), Kind::integer());
let n = as_usize(rhs.try_integer()?);
let out_len = lhv.len().checked_mul(n).ok_or_else(repeat_err)?;
if out_len > MAX_REPEAT_BYTES {
return Err(repeat_err());
}
Bytes::from(lhv.repeat(n)).into()
}
_ => return Err(err()),
};
Expand Down Expand Up @@ -342,3 +365,47 @@ impl VrlValueArithmetic for Value {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

// OBE-10736: string * int must not OOM or capacity-overflow abort.

#[test]
fn try_mul_bytes_large_count_returns_error() {
let s = Value::Bytes(bytes::Bytes::from("a"));
let n = Value::Integer(64 * 1024 * 1024 + 1);
assert!(s.try_mul(n).is_err(), "expected error for repeat count exceeding MAX_REPEAT_BYTES");
}

#[test]
fn try_mul_bytes_overflow_count_returns_error() {
let s = Value::Bytes(bytes::Bytes::from_static(b"aaaa")); // len = 4
let n = Value::Integer(i64::MAX); // 4 * i64::MAX overflows usize on 64-bit
assert!(s.try_mul(n).is_err(), "expected error on checked_mul overflow");
}

#[test]
fn try_mul_int_bytes_large_count_returns_error() {
let n = Value::Integer(64 * 1024 * 1024 + 1);
let s = Value::Bytes(bytes::Bytes::from("b"));
assert!(n.try_mul(s).is_err(), "expected error for int * bytes exceeding MAX_REPEAT_BYTES");
}

#[test]
fn try_mul_bytes_small_count_succeeds() {
let s = Value::Bytes(bytes::Bytes::from("ab"));
let n = Value::Integer(3);
let result = s.try_mul(n).expect("expected success for small repeat");
assert_eq!(result, Value::Bytes(bytes::Bytes::from("ababab")));
}

#[test]
fn try_mul_bytes_zero_count_returns_empty() {
let s = Value::Bytes(bytes::Bytes::from("hello"));
let n = Value::Integer(0);
let result = s.try_mul(n).expect("expected success for zero repeat");
assert_eq!(result, Value::Bytes(bytes::Bytes::new()));
}
}
7 changes: 5 additions & 2 deletions src/datadog/search/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,12 @@ impl QueryVisitor {
) => match (lc, rc) {
(Comparison::Gte, Comparison::Lte) => (true, lv, rv, true),
(Comparison::Gt, Comparison::Lt) => (false, lv, rv, false),
_ => panic!("invalid range comparison"),
// Mixed-bracket ranges: [lo TO hi} or {lo TO hi]
(Comparison::Gte, Comparison::Lt) => (true, lv, rv, false),
(Comparison::Gt, Comparison::Lte) => (false, lv, rv, true),
_ => unreachable!("grammar only produces Gt/Gte left and Lt/Lte right"),
},
_ => panic!("invalid range value"),
_ => unreachable!("grammar always emits bracket, value, value, bracket"),
};

return QueryNode::AttributeRange {
Expand Down
33 changes: 33 additions & 0 deletions src/datadog/search/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,39 @@ mod tests {
}
}

// OBE-10726: mixed-bracket ranges ([lo TO hi} and {lo TO hi]) must not panic.
#[test]
fn parses_mixed_bracket_range_inclusive_lower() {
let res = parse("foo:[10 TO 20}");
assert!(
matches!(res,
QueryNode::AttributeRange {
ref attr,
lower_inclusive: true,
upper_inclusive: false,
..
} if attr == "foo"),
"expected inclusive lower, exclusive upper; got {:?}",
res
);
}

#[test]
fn parses_mixed_bracket_range_exclusive_lower() {
let res = parse("foo:{10 TO 20]");
assert!(
matches!(res,
QueryNode::AttributeRange {
ref attr,
lower_inclusive: false,
upper_inclusive: true,
..
} if attr == "foo"),
"expected exclusive lower, inclusive upper; got {:?}",
res
);
}

#[test]
fn parses_match_no_docs_query() {
let cases = [
Expand Down
28 changes: 17 additions & 11 deletions src/parsing/xml.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::compiler::prelude::*;
use once_cell::sync::Lazy;

// OBE-10742: bound recursion depth to prevent stack overflow on deeply-nested XML.
const MAX_XML_DEPTH: u32 = 128;
use regex::{Regex, RegexBuilder};
use roxmltree::{Document, Node, NodeType};
use rust_decimal::prelude::Zero;
Expand Down Expand Up @@ -91,14 +94,17 @@ pub(crate) fn parse_xml(value: Value, options: ParseOptions) -> Resolved {
// Trim whitespace around XML elements, if applicable.
let parse = if trim { trim_xml(&string) } else { string };
let doc = Document::parse(&parse).map_err(|e| format!("unable to parse xml: {e}"))?;
let value = process_node(doc.root(), &config);
Ok(value)
process_node(doc.root(), &config, 0)
}

/// Process an XML node, and return a VRL `Value`.
fn process_node(node: Node, config: &ParseXmlConfig) -> Value {
fn process_node(node: Node, config: &ParseXmlConfig, depth: u32) -> Resolved {
if depth > MAX_XML_DEPTH {
return Err(format!("xml nesting limit ({MAX_XML_DEPTH}) exceeded").into());
}

// Helper to recurse over a `Node`s children, and build an object.
let recurse = |node: Node| -> ObjectMap {
let recurse = |node: Node| -> Result<ObjectMap, ExpressionError> {
let mut map = BTreeMap::new();

// Expand attributes, if required.
Expand All @@ -119,7 +125,7 @@ fn process_node(node: Node, config: &ParseXmlConfig) -> Value {
};

// Transform the node into a VRL `Value`.
let value = process_node(n, config);
let value = process_node(n, config, depth + 1)?;

// If the key already exists, add it. Otherwise, insert.
match map.entry(name) {
Expand All @@ -143,21 +149,21 @@ fn process_node(node: Node, config: &ParseXmlConfig) -> Value {
}
}

map
Ok(map)
};

match node.node_type() {
NodeType::Root => Value::Object(recurse(node)),
NodeType::Root => Ok(Value::Object(recurse(node)?)),

NodeType::Element => {
match (
config.always_use_text_key,
node.attributes().len().is_zero(),
) {
// If the node has attributes, *always* recurse to expand default keys.
(_, false) if config.include_attr => Value::Object(recurse(node)),
(_, false) if config.include_attr => Ok(Value::Object(recurse(node)?)),
// If a text key should be used, always recurse.
(true, true) => Value::Object(recurse(node)),
(true, true) => Ok(Value::Object(recurse(node)?)),
// Otherwise, check the node count to determine what to do.
_ => match node.children().count() {
// For a single node, 'flatten' the object if necessary.
Expand All @@ -171,7 +177,7 @@ fn process_node(node: Node, config: &ParseXmlConfig) -> Value {

map.insert(
node.tag_name().name().to_string().into(),
process_node(node, config),
process_node(node, config, depth + 1)?,
);

Value::Object(map)
Expand All @@ -186,7 +192,7 @@ fn process_node(node: Node, config: &ParseXmlConfig) -> Value {
}
}
// For 2+ nodes, expand.
_ => Value::Object(recurse(node)),
_ => Ok(Value::Object(recurse(node)?)),
},
}
}
Expand Down
31 changes: 26 additions & 5 deletions src/stdlib/decode_gzip.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
use crate::compiler::prelude::*;
use crate::stdlib::util::{DECOMPRESS_LIMIT_ERROR, DEFAULT_DECOMPRESS_LIMIT};
use flate2::read::MultiGzDecoder;
use std::io::Read;


fn decode_gzip(value: Value) -> Resolved {
let value = value.try_bytes()?;
let mut buf = Vec::new();
let result = MultiGzDecoder::new(std::io::Cursor::new(value)).read_to_end(&mut buf);

match result {
Ok(_) => Ok(Value::Bytes(buf.into())),
Err(_) => Err("unable to decode value with Gzip decoder".into()),
MultiGzDecoder::new(std::io::Cursor::new(value))
.take(DEFAULT_DECOMPRESS_LIMIT + 1)
.read_to_end(&mut buf)
.map_err(|_| "unable to decode value with Gzip decoder")?;
if buf.len() as u64 > DEFAULT_DECOMPRESS_LIMIT {
return Err(DECOMPRESS_LIMIT_ERROR.into());
}
Ok(Value::Bytes(buf.into()))
}

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -97,4 +101,21 @@ mod tests {
tdef: TypeDef::bytes().fallible(),
}
];

// OBE-10737: a gzip bomb that decompresses to >64 MiB must return an error.
#[test]
fn gzip_bomb_exceeds_limit() {
// Compress 65 MiB of zeros — zeros compress to <1 KiB with gzip.
let zeros = vec![0u8; 65 * 1024 * 1024];
let mut compressed = Vec::new();
let mut enc = GzEncoder::new(zeros.as_slice(), flate2::Compression::best());
enc.read_to_end(&mut compressed).unwrap();

let result = decode_gzip(Value::Bytes(compressed.into()));
assert!(result.is_err(), "expected error for gzip bomb exceeding size limit");
assert!(
result.unwrap_err().to_string().contains("exceeds size limit"),
"error should mention size limit"
);
}
}
60 changes: 56 additions & 4 deletions src/stdlib/decode_snappy.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
use crate::compiler::prelude::*;
use snap::raw::Decoder;
use crate::stdlib::util::{DECOMPRESS_LIMIT_ERROR, DEFAULT_DECOMPRESS_LIMIT};
use snap::raw::{decompress_len, Decoder};

fn decode_snappy(value: Value) -> Resolved {
let value = value.try_bytes()?;
let mut decoder = Decoder::new();
let result = decoder.decompress_vec(&value);

match result {
// A snappy frame declares its uncompressed length up front, and
// `decompress_vec` sizes its output buffer from that value before doing any
// work. A few bytes of input can therefore claim gigabytes, so reject an
// over-limit claim before allocating anything (OBE-10737).
let claimed_len =
decompress_len(&value).map_err(|_| "unable to decode value with Snappy decoder")?;
if claimed_len as u64 > DEFAULT_DECOMPRESS_LIMIT {
return Err(DECOMPRESS_LIMIT_ERROR.into());
}

let mut decoder = Decoder::new();
match decoder.decompress_vec(&value) {
Ok(buf) => Ok(Value::Bytes(buf.into())),
Err(_) => Err("unable to decode value with Snappy decoder".into()),
}
Expand Down Expand Up @@ -97,4 +107,46 @@ mod tests {
tdef: TypeDef::bytes().fallible(),
}
];

// OBE-10737: a snappy frame that *claims* a huge uncompressed length must be
// rejected before the output buffer is allocated. Unfixed, `decompress_vec`
// eagerly allocates the claimed size from this handful of input bytes.
//
// The claim is 1 GiB: comfortably above DEFAULT_DECOMPRESS_LIMIT but below
// snap's own `u32::MAX` ceiling, so snap itself will not reject it — our
// check is the only thing standing between this input and a 1 GiB alloc.
#[test]
fn snappy_oversized_claimed_length_rejected() {
// A snappy stream begins with the uncompressed length as a varint.
let mut payload = Vec::new();
let mut claim: u64 = 1024 * 1024 * 1024;
while claim >= 0x80 {
payload.push((claim as u8) | 0x80);
claim >>= 7;
}
payload.push(claim as u8);
// Body is deliberately truncated — we must fail on the size claim, not
// by decoding to completion.
payload.extend_from_slice(&[0x00, 0x00, 0x00]);

let result = decode_snappy(Value::Bytes(payload.into()));
assert!(result.is_err(), "oversized claimed length must be rejected");
assert_eq!(
result.unwrap_err().to_string(),
DECOMPRESS_LIMIT_ERROR,
"must be rejected for exceeding the size limit, not as a decode error"
);
}

// A real snappy payload well under the limit must still round-trip.
#[test]
fn snappy_under_limit_still_decodes() {
let original = vec![b'a'; 1024 * 1024];
let compressed = snap::raw::Encoder::new()
.compress_vec(&original)
.expect("snappy encode failed");

let result = decode_snappy(Value::Bytes(compressed.into())).expect("must decode");
assert_eq!(result, Value::Bytes(original.into()));
}
}
Loading