Skip to content

Commit a348c9b

Browse files
serde_bser/mincode: validate wire-supplied lengths before indexing
BSER and mincode deserializers take lengths directly from the encoded data and use them to index into buffers without validation, panicking on malformed input instead of returning an error. serde_bser: - Reject negative bytestring/utf8string lengths in read_bytes instead of casting them to a huge usize. - Grow the IoRead scratch buffer in bounded chunks instead of pre-allocating the wire length, avoiding a capacity-overflow panic when the declared length is far larger than the stream. mincode: - Reject string/bytes lengths that exceed the remaining input before calling split_at. - Reject empty or truncated chars instead of indexing out of bounds. Add regression tests for each panic.
1 parent 3dceb5b commit a348c9b

6 files changed

Lines changed: 127 additions & 12 deletions

File tree

eden/scm/lib/mincode/src/de.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ impl<'de> Deserializer<'de> {
3434
#[inline]
3535
fn read_slice(&mut self) -> Result<&'de [u8]> {
3636
let len = Deserialize::deserialize(&mut *self)?;
37+
if len > self.bytes.len() {
38+
return Err(Error::new(format!(
39+
"length {} exceeds remaining input of {} bytes",
40+
len,
41+
self.bytes.len()
42+
)));
43+
}
3744
let (slice, rest) = self.bytes.split_at(len);
3845
self.bytes = rest;
3946
Ok(slice)
@@ -103,13 +110,23 @@ impl<'de, 'a> serde::Deserializer<'de> for &'a mut Deserializer<'de> {
103110
where
104111
V: Visitor<'de>,
105112
{
113+
if self.bytes.is_empty() {
114+
return Err(Error::new("EOF while deserializing char"));
115+
}
106116
let width = utf8_char_width(self.bytes[0]);
107117
if width == 1 {
108118
return visitor.visit_char(self.bytes[0] as char);
109119
}
110120
if width == 0 {
111121
return Err(Error::new("invalid char"));
112122
}
123+
if width > self.bytes.len() {
124+
return Err(Error::new(format!(
125+
"truncated char: expected {} bytes, only {} remain",
126+
width,
127+
self.bytes.len()
128+
)));
129+
}
113130
let res = match str::from_utf8(&self.bytes[..width]) {
114131
Ok(s) => s.chars().next().unwrap(),
115132
Err(err) => {

eden/scm/lib/mincode/src/tests.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,39 @@ quickcheck! {
3737
foo == foo_deserialized
3838
}
3939
}
40+
41+
#[test]
42+
fn string_length_longer_than_input_is_rejected() {
43+
// VLQ length 5 followed by a single byte: the declared string is longer
44+
// than the remaining input.
45+
let bytes = [0x05, b'a'];
46+
let r: Result<String, _> = crate::deserialize(&bytes);
47+
assert!(
48+
r.is_err(),
49+
"expected an error for an oversized string length, got {:?}",
50+
r
51+
);
52+
}
53+
54+
#[test]
55+
fn empty_input_for_char_is_rejected() {
56+
let r: Result<char, _> = crate::deserialize(&[]);
57+
assert!(
58+
r.is_err(),
59+
"expected an error for an empty char, got {:?}",
60+
r
61+
);
62+
}
63+
64+
#[test]
65+
fn truncated_multibyte_char_is_rejected() {
66+
// 0xC3 is the lead byte of a two-byte UTF-8 sequence; the continuation
67+
// byte is missing.
68+
let bytes = [0xC3];
69+
let r: Result<char, _> = crate::deserialize(&bytes);
70+
assert!(
71+
r.is_err(),
72+
"expected an error for a truncated char, got {:?}",
73+
r
74+
);
75+
}

watchman/rust/serde_bser/src/de/bunser.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ where
9090
/// Return a borrowed or copied version of the next n bytes.
9191
#[inline]
9292
pub fn read_bytes<'s>(&'s mut self, len: i64) -> Result<Reference<'de, 's, [u8]>> {
93+
if len < 0 {
94+
// A negative wire-supplied length would cast to a huge `usize`
95+
// below and panic in the underlying reader.
96+
return Err(Error::DeNegativeLength { len });
97+
}
9398
let len = len as usize;
9499
self.read
95100
.next_bytes(len, &mut self.scratch)

watchman/rust/serde_bser/src/de/read.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use std::fmt;
1010
use std::io;
1111
use std::result;
1212

13-
use anyhow::Context as _;
1413
use anyhow::bail;
14+
use anyhow::Context as _;
1515
use byteorder::ByteOrder;
1616
use byteorder::NativeEndian;
1717

@@ -210,18 +210,24 @@ where
210210
len: usize,
211211
scratch: &'s mut Vec<u8>,
212212
) -> anyhow::Result<Reference<'de, 's, [u8]>> {
213-
scratch.resize(len, 0);
214-
let mut idx = 0;
215-
if self.peeked.is_some() {
216-
idx += 1;
217-
}
218-
if idx < len {
219-
self.reader.read_exact(&mut scratch[idx..len])?;
220-
debug_bytes!("{:x}", ByteBuf(&scratch[idx..len]));
221-
self.read_count += len - idx;
222-
}
213+
// Grow the scratch buffer in bounded chunks instead of pre-allocating
214+
// `len` bytes, so a wire-supplied length that is far larger than the
215+
// stream can ever provide yields an EOF error rather than a capacity
216+
// overflow panic.
217+
scratch.clear();
223218
if let Some(peeked) = self.peeked.take() {
224-
scratch[0] = peeked;
219+
scratch.push(peeked);
220+
}
221+
let mut buf = [0u8; 4096];
222+
while scratch.len() < len {
223+
let want = (len - scratch.len()).min(buf.len());
224+
let n = self.reader.read(&mut buf[..want])?;
225+
if n == 0 {
226+
bail!("eof while reading bytes/string");
227+
}
228+
debug_bytes!("{:x}", ByteBuf(&buf[..n]));
229+
scratch.extend_from_slice(&buf[..n]);
230+
self.read_count += n;
225231
}
226232
Ok(Reference::Copied(&scratch[0..len]))
227233
}

watchman/rust/serde_bser/src/errors.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub enum Error {
2929
#[error("Expected {} bytes read, but only read {} bytes", .expected, .read)]
3030
DeEof { expected: usize, read: usize },
3131

32+
#[error("while deserializing BSER: negative length {}", .len)]
33+
DeNegativeLength { len: i64 },
34+
3235
#[error("Invalid magic header: {:?}", .magic)]
3336
DeInvalidMagic { magic: Vec<u8> },
3437

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
//! Regression tests for deserializing BSER payloads with a negative
9+
//! bytestring/utf8string length. The wire length is a signed integer and used
10+
//! to index into the buffer, so a negative value must be rejected instead of
11+
//! being reinterpreted as a huge `usize`.
12+
13+
use std::io::Cursor;
14+
15+
use serde_bser::de::from_reader;
16+
use serde_bser::de::from_slice;
17+
18+
fn negative_len_bytestring() -> Vec<u8> {
19+
// BSER magic + capabilities + PDU length (INT8 = 3) + a bytestring whose
20+
// length is INT8 = -1 (0xFF).
21+
let mut msg = vec![0x00, 0x02];
22+
msg.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
23+
msg.extend_from_slice(&[0x03, 0x03]);
24+
msg.extend_from_slice(&[0x02, 0x03, 0xFF]);
25+
msg
26+
}
27+
28+
#[test]
29+
fn negative_bytestring_length_is_rejected() {
30+
let msg = negative_len_bytestring();
31+
let r: Result<String, _> = from_slice(&msg);
32+
assert!(
33+
r.is_err(),
34+
"expected a deserialization error for a negative bytestring length, got {:?}",
35+
r
36+
);
37+
}
38+
39+
#[test]
40+
fn negative_bytestring_length_is_rejected_from_reader() {
41+
let msg = negative_len_bytestring();
42+
let r: Result<String, _> = from_reader(Cursor::new(msg));
43+
assert!(
44+
r.is_err(),
45+
"expected a deserialization error for a negative bytestring length, got {:?}",
46+
r
47+
);
48+
}

0 commit comments

Comments
 (0)