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
14 changes: 14 additions & 0 deletions lib/tests/tests/issues/obe_10735_array_index_cap.vrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# issue: OBE-10735
# Assigning to a large array index pads the array with `Value::Null` up to that index. The padding
# is observable semantics (`length` sees it), so the write genuinely commits `index + 1` elements —
# an event-controlled index was enough to exhaust memory. Indices beyond +/-1048576 (2^20) are now
# dropped. 2^20 bounds one indexed write to ~42 MB at today's 40-byte `Value`.
# result: [0, 1048577]

capped = []
capped[2000000] = 1

allowed = []
allowed[1048576] = 1

[length(capped), length(allowed)]
45 changes: 36 additions & 9 deletions src/value/value/crud/insert.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::ValueCollection;
use super::{ValueCollection, MAX_ARRAY_INDEX};
use crate::path::BorrowedSegment;
use crate::value::Value;
use std::borrow::Borrow;
Expand Down Expand Up @@ -26,11 +26,14 @@ pub fn insert<'a, T: ValueCollection>(
if let Some(Value::Array(array)) = value.get_mut_value(key.borrow()) {
insert(array, index, path_iter, insert_value)
} else {
const MAX_ARRAY_CAPACITY: usize = 32_769;
// Bounded by the same cap `insert_value` enforces, so an out-of-range index
// cannot reserve memory here before being rejected there.
let max_capacity = MAX_ARRAY_INDEX + 1;
let capacity = if index >= 0 {
((index as usize) + 1).min(MAX_ARRAY_CAPACITY)
((index as usize) + 1).min(max_capacity)
} else {
((-index) as usize).min(MAX_ARRAY_CAPACITY)
// `unsigned_abs` rather than `-index`, which overflows on `isize::MIN`.
index.unsigned_abs().min(max_capacity)
};
let mut array = Vec::with_capacity(capacity);
let prev_value = insert(&mut array, index, path_iter, insert_value);
Expand Down Expand Up @@ -84,24 +87,48 @@ mod test {
#[test]
fn test_insert_beyond_max_array_index_is_rejected() {
let mut value = Value::Null;
assert_eq!(value.insert("[40000]", 1), None);
assert_eq!(value.insert("[1048577]", 1), None);
assert_eq!(value, Value::from(json!([])));
}

#[test]
fn test_insert_beyond_max_negative_array_index_is_rejected() {
let mut value = Value::Null;
assert_eq!(value.insert("[-40000]", 1), None);
assert_eq!(value.insert("[-1048577]", 1), None);
assert_eq!(value, Value::from(json!([])));
}

#[test]
fn test_insert_at_max_array_index_is_allowed() {
let mut value = Value::Null;
assert_eq!(value.insert("[32768]", 1), None);
assert_eq!(value.insert("[1048576]", 1), None);
let array = value.as_array().expect("expected an array");
assert_eq!(array.len(), 32769);
assert_eq!(array[32768], Value::Integer(1));
assert_eq!(array.len(), 1_048_577);
assert_eq!(array[1_048_576], Value::Integer(1));
}

// OBE-10735: the capacity calculation negated the index with `(-index) as usize`, which
// overflows on `isize::MIN` (there is no positive `isize` counterpart). `unsigned_abs` is
// the total operation.
#[test]
fn test_insert_at_isize_min_does_not_panic() {
let mut value = Value::Null;
let path = vec![BorrowedSegment::Index(isize::MIN)].into_iter();
assert_eq!(insert(&mut value, (), path, Value::Integer(1)), None);
assert_eq!(value, Value::from(json!([])));
}

// Drift detector, not a correctness assertion: the cap is justified in terms of the memory a
// single indexed write may commit (`MAX_ARRAY_INDEX + 1` elements of this size, ~42 MB today).
// If `Value` grows a variant, that budget changes and the cap deserves a fresh look.
#[test]
fn test_value_size_is_pinned() {
assert_eq!(
std::mem::size_of::<Value>(),
40,
"size_of::<Value>() changed; re-check the MAX_ARRAY_INDEX memory budget \
(cap x size = worst-case allocation for one indexed write)"
);
}

#[test]
Expand Down
8 changes: 6 additions & 2 deletions src/value/value/crud/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use crate::value::{KeyString, ObjectMap, Value};
use std::borrow::Borrow;

/// Largest array index `insert_value` will grow an array to, in either direction.
/// Largest array index an indexed write will grow an array to, in either direction.
/// Prevents an event-controlled index (e.g. `.foo[40000000] = 1`) from exhausting memory.
const MAX_ARRAY_INDEX: usize = 32_768;
///
/// Assigning to index `N` materialises `N + 1` elements — the null padding is observable VRL
/// semantics, not just a preallocation — so this cap is what bounds the memory a single write may
/// commit: ~42 MB at today's 40-byte `Value` (see `test_value_size_is_pinned`).
pub(super) const MAX_ARRAY_INDEX: usize = 1_048_576;

mod get;
mod get_mut;
Expand Down