Skip to content

Commit a8acd1a

Browse files
authored
Merge pull request #49 from NodeDB-Lab/fix/issues-36-44-security-and-robustness
fix: resolve issues #36#44 — security, robustness, and correctness
2 parents edafc6b + 09bf7de commit a8acd1a

33 files changed

Lines changed: 1989 additions & 437 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodedb-columnar/src/mutation.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ impl MutationEngine {
8585
}
8686

8787
// Generate WAL record BEFORE applying the mutation.
88-
let row_data = encode_row_for_wal(values);
88+
let row_data = encode_row_for_wal(values)?;
8989
let wal = ColumnarWalRecord::InsertRow {
9090
collection: self.collection.clone(),
9191
row_data,
@@ -310,6 +310,13 @@ impl MutationEngine {
310310
self.memtable.get_row(row_idx)
311311
}
312312

313+
/// The segment ID that will be assigned to the next flushed segment.
314+
///
315+
/// Use this to obtain the ID to pass to `on_memtable_flushed`.
316+
pub fn next_segment_id(&self) -> u32 {
317+
self.next_segment_id
318+
}
319+
313320
/// Whether a segment should be compacted based on its delete ratio.
314321
pub fn should_compact(&self, segment_id: u32, total_rows: u64) -> bool {
315322
self.delete_bitmaps

nodedb-columnar/src/wal_record.rs

Lines changed: 162 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ impl ColumnarWalRecord {
9191
/// Each value is written as: [type_tag: u8][value_bytes].
9292
/// This is more compact than MessagePack for typed columns and enables
9393
/// direct replay into the memtable without schema interpretation overhead.
94-
pub fn encode_row_for_wal(values: &[nodedb_types::value::Value]) -> Vec<u8> {
94+
pub fn encode_row_for_wal(
95+
values: &[nodedb_types::value::Value],
96+
) -> Result<Vec<u8>, crate::error::ColumnarError> {
9597
use nodedb_types::value::Value;
9698

9799
let mut buf = Vec::with_capacity(values.len() * 10); // Rough estimate.
@@ -152,14 +154,63 @@ pub fn encode_row_for_wal(values: &[nodedb_types::value::Value]) -> Vec<u8> {
152154
_ => {
153155
// Geometry and other complex types: serialize as JSON bytes.
154156
buf.push(10);
155-
let json = sonic_rs::to_vec(value).unwrap_or_default();
157+
let json = sonic_rs::to_vec(value).map_err(|e| {
158+
crate::error::ColumnarError::Serialization(format!(
159+
"failed to serialize value as JSON: {e}"
160+
))
161+
})?;
156162
buf.extend_from_slice(&(json.len() as u32).to_le_bytes());
157163
buf.extend_from_slice(&json);
158164
}
159165
}
160166
}
161167

162-
buf
168+
Ok(buf)
169+
}
170+
171+
/// Maximum length for a variable-length field in a WAL record (256 MiB).
172+
/// Prevents OOM from crafted/corrupt records with bogus length prefixes.
173+
const MAX_FIELD_LEN: usize = 256 * 1024 * 1024;
174+
175+
/// Read exactly `n` bytes from `data` at `cursor`, advancing cursor.
176+
/// Returns `Err` if not enough bytes remain.
177+
fn read_slice<'a>(
178+
data: &'a [u8],
179+
cursor: &mut usize,
180+
n: usize,
181+
context: &str,
182+
) -> Result<&'a [u8], crate::error::ColumnarError> {
183+
let end = cursor.checked_add(n).ok_or_else(|| {
184+
crate::error::ColumnarError::Serialization(format!("overflow in {context}"))
185+
})?;
186+
if end > data.len() {
187+
return Err(crate::error::ColumnarError::Serialization(format!(
188+
"truncated {context}: need {n} bytes at offset {cursor}, have {}",
189+
data.len().saturating_sub(*cursor)
190+
)));
191+
}
192+
let slice = &data[*cursor..end];
193+
*cursor = end;
194+
Ok(slice)
195+
}
196+
197+
/// Read a u32 length prefix, validate it against MAX_FIELD_LEN, then read
198+
/// that many bytes. Returns the payload slice.
199+
fn read_length_prefixed<'a>(
200+
data: &'a [u8],
201+
cursor: &mut usize,
202+
context: &str,
203+
) -> Result<&'a [u8], crate::error::ColumnarError> {
204+
let len_bytes = read_slice(data, cursor, 4, context)?;
205+
let len = u32::from_le_bytes(len_bytes.try_into().map_err(|_| {
206+
crate::error::ColumnarError::Serialization(format!("truncated {context} len"))
207+
})?) as usize;
208+
if len > MAX_FIELD_LEN {
209+
return Err(crate::error::ColumnarError::Serialization(format!(
210+
"{context} length {len} exceeds maximum {MAX_FIELD_LEN}"
211+
)));
212+
}
213+
read_slice(data, cursor, len, context)
163214
}
164215

165216
/// Decode a row from the columnar wire format back into Values.
@@ -172,37 +223,40 @@ pub fn decode_row_from_wal(
172223
let mut cursor = 0;
173224

174225
while cursor < data.len() {
175-
let tag = data[cursor];
176-
cursor += 1;
226+
let tag_slice = read_slice(data, &mut cursor, 1, "tag")?;
227+
let tag = tag_slice[0];
177228

178229
let value = match tag {
179230
0 => Value::Null,
180231
1 => {
181-
let v = i64::from_le_bytes(data[cursor..cursor + 8].try_into().map_err(|_| {
232+
let bytes = read_slice(data, &mut cursor, 8, "i64")?;
233+
let v = i64::from_le_bytes(bytes.try_into().map_err(|_| {
182234
crate::error::ColumnarError::Serialization("truncated i64".into())
183235
})?);
184-
cursor += 8;
185236
Value::Integer(v)
186237
}
187238
2 => {
188-
let v = f64::from_le_bytes(data[cursor..cursor + 8].try_into().map_err(|_| {
239+
let bytes = read_slice(data, &mut cursor, 8, "f64")?;
240+
let v = f64::from_le_bytes(bytes.try_into().map_err(|_| {
189241
crate::error::ColumnarError::Serialization("truncated f64".into())
190242
})?);
191-
cursor += 8;
192243
Value::Float(v)
193244
}
194245
3 => {
195-
let v = data[cursor] != 0;
196-
cursor += 1;
197-
Value::Bool(v)
246+
let bytes = read_slice(data, &mut cursor, 1, "bool")?;
247+
Value::Bool(bytes[0] != 0)
198248
}
199249
4 | 5 | 8 => {
200-
let len = u32::from_le_bytes(data[cursor..cursor + 4].try_into().map_err(|_| {
201-
crate::error::ColumnarError::Serialization("truncated len".into())
202-
})?) as usize;
203-
cursor += 4;
204-
let bytes = &data[cursor..cursor + len];
205-
cursor += len;
250+
let bytes = read_length_prefixed(
251+
data,
252+
&mut cursor,
253+
match tag {
254+
4 => "string",
255+
5 => "bytes",
256+
8 => "uuid",
257+
_ => unreachable!(),
258+
},
259+
)?;
206260
match tag {
207261
4 => Value::String(String::from_utf8_lossy(bytes).into_owned()),
208262
5 => Value::Bytes(bytes.to_vec()),
@@ -211,43 +265,41 @@ pub fn decode_row_from_wal(
211265
}
212266
}
213267
6 => {
214-
let micros =
215-
i64::from_le_bytes(data[cursor..cursor + 8].try_into().map_err(|_| {
216-
crate::error::ColumnarError::Serialization("truncated timestamp".into())
217-
})?);
218-
cursor += 8;
268+
let bytes = read_slice(data, &mut cursor, 8, "timestamp")?;
269+
let micros = i64::from_le_bytes(bytes.try_into().map_err(|_| {
270+
crate::error::ColumnarError::Serialization("truncated timestamp".into())
271+
})?);
219272
Value::DateTime(nodedb_types::datetime::NdbDateTime::from_micros(micros))
220273
}
221274
7 => {
222-
let mut bytes = [0u8; 16];
223-
bytes.copy_from_slice(&data[cursor..cursor + 16]);
224-
cursor += 16;
225-
Value::Decimal(rust_decimal::Decimal::deserialize(bytes))
275+
let bytes = read_slice(data, &mut cursor, 16, "decimal")?;
276+
let mut arr = [0u8; 16];
277+
arr.copy_from_slice(bytes);
278+
Value::Decimal(rust_decimal::Decimal::deserialize(arr))
226279
}
227280
9 => {
228-
let count =
229-
u32::from_le_bytes(data[cursor..cursor + 4].try_into().map_err(|_| {
230-
crate::error::ColumnarError::Serialization("truncated vector count".into())
231-
})?) as usize;
232-
cursor += 4;
281+
let count_bytes = read_slice(data, &mut cursor, 4, "vector count")?;
282+
let count = u32::from_le_bytes(count_bytes.try_into().map_err(|_| {
283+
crate::error::ColumnarError::Serialization("truncated vector count".into())
284+
})?) as usize;
285+
if count > MAX_FIELD_LEN / 4 {
286+
return Err(crate::error::ColumnarError::Serialization(format!(
287+
"vector count {count} exceeds maximum {}",
288+
MAX_FIELD_LEN / 4
289+
)));
290+
}
233291
let mut arr = Vec::with_capacity(count);
234292
for _ in 0..count {
235-
let f =
236-
f32::from_le_bytes(data[cursor..cursor + 4].try_into().map_err(|_| {
237-
crate::error::ColumnarError::Serialization("truncated f32".into())
238-
})?);
239-
cursor += 4;
293+
let fb = read_slice(data, &mut cursor, 4, "vector f32")?;
294+
let f = f32::from_le_bytes(fb.try_into().map_err(|_| {
295+
crate::error::ColumnarError::Serialization("truncated f32".into())
296+
})?);
240297
arr.push(Value::Float(f as f64));
241298
}
242299
Value::Array(arr)
243300
}
244301
10 => {
245-
let len = u32::from_le_bytes(data[cursor..cursor + 4].try_into().map_err(|_| {
246-
crate::error::ColumnarError::Serialization("truncated json len".into())
247-
})?) as usize;
248-
cursor += 4;
249-
let json_bytes = &data[cursor..cursor + len];
250-
cursor += len;
302+
let json_bytes = read_length_prefixed(data, &mut cursor, "json")?;
251303
sonic_rs::from_slice(json_bytes).unwrap_or(Value::Null)
252304
}
253305
_ => {
@@ -316,7 +368,7 @@ mod tests {
316368
Value::Array(vec![Value::Float(1.0), Value::Float(2.0)]),
317369
];
318370

319-
let encoded = encode_row_for_wal(&values);
371+
let encoded = encode_row_for_wal(&values).expect("encode");
320372
let decoded = decode_row_from_wal(&encoded).expect("decode");
321373

322374
assert_eq!(decoded.len(), values.len());
@@ -335,4 +387,70 @@ mod tests {
335387
);
336388
assert_eq!(decoded[8], Value::Null);
337389
}
390+
391+
#[test]
392+
fn decode_truncated_i64_returns_error() {
393+
// Tag 1 (i64) requires 8 payload bytes; supply none.
394+
// Today the slice index `data[cursor..cursor+8]` panics with an index
395+
// out-of-bounds. After the fix, `try_into()` returns the
396+
// Serialization error instead.
397+
let result = decode_row_from_wal(&[1]);
398+
assert!(
399+
result.is_err(),
400+
"truncated i64 payload must return Err, not panic"
401+
);
402+
}
403+
404+
#[test]
405+
fn decode_truncated_string_returns_error() {
406+
// Tag 4 (string): length prefix says 255 bytes but the slice ends
407+
// immediately after the 4-byte length field. The read of
408+
// `data[cursor..cursor+255]` panics today; after the fix it errors.
409+
let input = {
410+
let mut v = vec![4u8]; // tag = string
411+
v.extend_from_slice(&255u32.to_le_bytes()); // len = 255
412+
// no payload bytes follow
413+
v
414+
};
415+
let result = decode_row_from_wal(&input);
416+
assert!(
417+
result.is_err(),
418+
"truncated string payload must return Err, not panic"
419+
);
420+
}
421+
422+
#[test]
423+
fn decode_huge_vector_count_returns_error() {
424+
// Tag 9 (vector array): count = 0x7FFFFFFF. After reading the count,
425+
// the very first iteration tries to read 4 bytes of f32 from an empty
426+
// slice, which panics today. After the fix the loop errors out cleanly
427+
// before any allocation proportional to count is attempted.
428+
let input = {
429+
let mut v = vec![9u8]; // tag = vector array
430+
v.extend_from_slice(&0x7FFF_FFFFu32.to_le_bytes()); // count
431+
// no f32 bytes follow
432+
v
433+
};
434+
let result = decode_row_from_wal(&input);
435+
assert!(
436+
result.is_err(),
437+
"huge vector count with no payload must return Err, not panic or OOM"
438+
);
439+
}
440+
441+
#[test]
442+
fn decode_truncated_decimal_returns_error() {
443+
// Tag 7 (Decimal) requires 16 bytes; supply only 4.
444+
// `data[cursor..cursor+16]` panics today; after the fix it errors.
445+
let input = {
446+
let mut v = vec![7u8]; // tag = decimal
447+
v.extend_from_slice(&[0u8; 4]); // only 4 bytes, need 16
448+
v
449+
};
450+
let result = decode_row_from_wal(&input);
451+
assert!(
452+
result.is_err(),
453+
"truncated decimal payload must return Err, not panic"
454+
);
455+
}
338456
}

0 commit comments

Comments
 (0)