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
29 changes: 29 additions & 0 deletions rust/flexbuffers/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "flexbuffers-fuzz"
version = "0.0.0"
publish = false
edition = "2018"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"

[dependencies.flexbuffers]
path = ".."

# Prevent this from interfering with workspace
[workspace]

[[bin]]
name = "fuzz_reader"
path = "fuzz_targets/fuzz_reader.rs"
test = false
doc = false

[[bin]]
name = "fuzz_roundtrip"
path = "fuzz_targets/fuzz_roundtrip.rs"
test = false
doc = false
53 changes: 53 additions & 0 deletions rust/flexbuffers/fuzz/fuzz_targets/fuzz_reader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Fuzz target: exhaustively exercise the FlexBuffers Reader API with arbitrary input.
//
// Exercises every public accessor on Reader so that any panic from malformed
// data is caught by the fuzzer. Before the fix for read_usize, this harness
// would quickly find the crash with a 4-byte payload.
//
// Run with:
// cargo +nightly fuzz run fuzz_reader

#![no_main]

use libfuzzer_sys::fuzz_target;
use flexbuffers::Reader;

fuzz_target!(|data: &[u8]| {
let Ok(reader) = Reader::get_root(data) else { return };

// Exercise every accessor — any panic here is a bug.
let _ = reader.as_bool();
let _ = reader.as_u8();
let _ = reader.as_u16();
let _ = reader.as_u32();
let _ = reader.as_u64();
let _ = reader.as_i8();
let _ = reader.as_i16();
let _ = reader.as_i32();
let _ = reader.as_i64();
let _ = reader.as_f32();
let _ = reader.as_f64();
let _ = reader.as_str();
let _ = reader.as_bytes();
let _ = reader.length();
let _ = reader.flexbuffer_type();
let _ = reader.bitwidth();

// Exercise map reader if applicable.
if let Ok(map) = reader.as_map().ok().map(|_| reader.as_map()) {
if let Ok(m) = map {
for i in 0..m.len().min(16) {
let _ = m.idx(i);
}
}
}

// Exercise vector reader if applicable.
if let Ok(vec) = reader.as_vector().ok().map(|_| reader.as_vector()) {
if let Ok(v) = vec {
for i in 0..v.len().min(16) {
let _ = v.idx(i);
}
}
}
});
27 changes: 27 additions & 0 deletions rust/flexbuffers/fuzz/fuzz_targets/fuzz_roundtrip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Fuzz target: roundtrip encode→decode must never panic on any Reader accessor.
//
// Run with:
// cargo +nightly fuzz run fuzz_roundtrip

#![no_main]

use libfuzzer_sys::{arbitrary, fuzz_target, Corpus};
use flexbuffers::Reader;

fuzz_target!(|data: &[u8]| -> Corpus {
// Only test parseable inputs.
if Reader::get_root(data).is_err() {
return Corpus::Reject;
}
let reader = Reader::get_root(data).unwrap();

// Any of these panicking is a bug in the Reader.
let _ = reader.as_bool();
let _ = reader.as_u64();
let _ = reader.as_i64();
let _ = reader.as_f64();
let _ = reader.as_str();
let _ = reader.length();

Corpus::Keep
});
72 changes: 68 additions & 4 deletions rust/flexbuffers/src/reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,15 @@ impl<B: Buffer> Reader<B> {
/// Otherwise Returns error.
pub fn get_bool(&self) -> Result<bool, Error> {
self.expect_type(FlexBufferType::Bool)?;
Ok(self.buffer[self.address..self.address + self.width.n_bytes()].iter().any(|&b| b != 0))
let end = self
.address
.checked_add(self.width.n_bytes())
.ok_or(Error::FlexbufferOutOfBounds)?;
let slice = self
.buffer
.get(self.address..end)
.ok_or(Error::FlexbufferOutOfBounds)?;
Ok(slice.iter().any(|&b| b != 0))
}

/// Gets the length of the key if this type is a key.
Expand All @@ -332,7 +340,11 @@ impl<B: Buffer> Reader<B> {
#[inline]
fn get_key_len(&self) -> Result<usize, Error> {
self.expect_type(FlexBufferType::Key)?;
let (length, _) = self.buffer[self.address..]
let tail = self
.buffer
.get(self.address..)
.ok_or(Error::FlexbufferOutOfBounds)?;
let (length, _) = tail
.iter()
.enumerate()
.find(|(_, &b)| b == b'\0')
Expand Down Expand Up @@ -601,9 +613,9 @@ fn f64_from_le_bytes(bytes: [u8; 8]) -> f64 {
}

fn read_usize(buffer: &[u8], address: usize, width: BitWidth) -> usize {
let cursor = &buffer[address..];
let cursor = buffer.get(address..).unwrap_or_default();
match width {
BitWidth::W8 => cursor[0] as usize,
BitWidth::W8 => cursor.first().copied().unwrap_or_default() as usize,
BitWidth::W16 => cursor
.get(0..2)
.and_then(|s| s.try_into().ok())
Expand All @@ -627,3 +639,55 @@ fn unpack_type(ty: u8) -> Result<(FlexBufferType, BitWidth), Error> {
let t = FlexBufferType::try_from(ty >> 2).map_err(|_| Error::InvalidPackedType)?;
Ok((t, w))
}

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

// Regression tests for panics triggered by crafted FlexBuffer input.
// Before the fix, read_usize used direct indexing (&buffer[address..] and cursor[0])
// which panicked when address was at or past the end of the buffer.
// See: https://github.com/google/flatbuffers/issues/8923

/// Crafted payload whose declared W8 length slot falls exactly at buffer end.
/// read_usize was called as: &buffer[address..] where address == buffer.len(),
/// yielding an empty cursor, then cursor[0] panicked.
#[test]
fn read_usize_w8_address_at_end_does_not_panic() {
// Byte layout (little-endian FlexBuffer):
// [value_byte, type_byte, root_width_byte]
// Crafting a Bool with a width that pushes the length slot OOB.
let data: &[u8] = &[0x5d, 0x79, 0x6b, 0x02];
let reader = Reader::get_root(data).unwrap_or_default();
// Must not panic — should return false or an error gracefully.
let _ = reader.as_bool();
}

/// Crafted payload where the computed address for the length slot exceeds buffer length.
/// Before the fix, &buffer[address..] panicked immediately (address > len).
#[test]
fn read_usize_address_past_end_does_not_panic() {
// A minimal FlexBuffer whose internal offset arithmetic resolves to an address
// beyond the buffer bounds for a W8-width length field.
let data: &[u8] = &[0x00, 0x25, 0x01];
let reader = Reader::get_root(data).unwrap_or_default();
let _ = reader.length();
}

/// read_usize with W8 and address exactly at buffer end returns 0, not panic.
#[test]
fn read_usize_w8_returns_zero_on_oob() {
let buffer: &[u8] = &[0x01, 0x02];
// Address at len — empty cursor.
let result = read_usize(buffer, buffer.len(), BitWidth::W8);
assert_eq!(result, 0, "Expected 0 (safe default) for OOB W8 read");
}

/// read_usize with an address past the end returns 0, not panic.
#[test]
fn read_usize_past_end_returns_zero() {
let buffer: &[u8] = &[0xAB];
let result = read_usize(buffer, 999, BitWidth::W8);
assert_eq!(result, 0, "Expected 0 (safe default) for address past buffer end");
}
}