Skip to content
Merged
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
8 changes: 8 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"
cargo-fuzz = true

[dependencies]
arbitrary = { version = "1", features = ["derive"] }
libfuzzer-sys = "0.4"
serde_json = "1"

Expand All @@ -20,3 +21,10 @@ path = "fuzz_targets/fuzz_parse_eager.rs"
test = false
doc = false
bench = false

[[bin]]
name = "fuzz_ffi_ops"
path = "fuzz_targets/fuzz_ffi_ops.rs"
test = false
doc = false
bench = false
252 changes: 252 additions & 0 deletions fuzz/fuzz_targets/fuzz_ffi_ops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#![no_main]

use std::hint::black_box;
use std::os::raw::{c_char, c_int};
use std::ptr;

use arbitrary::{Arbitrary, Unstructured};
use libfuzzer_sys::fuzz_target;
use qjson::ffi::{
qjson_cursor, qjson_cursor_field, qjson_cursor_get_bool, qjson_cursor_get_f64,
qjson_cursor_get_i64, qjson_cursor_get_str, qjson_cursor_index,
qjson_cursor_object_entry_at, qjson_doc, qjson_free, qjson_get_bool, qjson_get_f64,
qjson_get_i64, qjson_get_str, qjson_open, qjson_parse,
};

const MAX_OPS: usize = 256;
const MAX_CURSORS: usize = 16;

#[derive(Arbitrary, Debug)]
struct Case<'a> {
json: &'a [u8],
ops: Vec<Op<'a>>,
}

#[derive(Arbitrary, Debug)]
enum Op<'a> {
Parse { json: &'a [u8] },
GetStr { cursor_slot: u8, path: &'a [u8] },
GetI64 { cursor_slot: u8, path: &'a [u8] },
GetF64 { cursor_slot: u8, path: &'a [u8] },
GetBool { cursor_slot: u8, path: &'a [u8] },
CursorField { cursor_slot: u8, key: &'a [u8] },
CursorIndex { cursor_slot: u8, index: u32 },
ObjectEntryAt { cursor_slot: u8, index: u32 },
Free,
}

fuzz_target!(|data: &[u8]| {
let Ok(case) = Case::arbitrary(&mut Unstructured::new(data)) else {
return;
};

let mut state = State::default();
unsafe {
state.parse(case.json);
for op in case.ops.iter().take(MAX_OPS) {
state.apply(op);
}
}
});

#[derive(Default)]
struct State {
doc: *mut qjson_doc,
cursors: Vec<qjson_cursor>,
}

impl State {
unsafe fn parse(&mut self, json: &[u8]) {
self.free();

let mut err: c_int = -1;
self.doc = qjson_parse(json.as_ptr(), json.len(), &mut err);
black_box(err);

if self.doc.is_null() {
return;
}

let mut root = zero_cursor();
let rc = qjson_open(self.doc, ptr::null(), 0, &mut root);
black_box(rc);
if rc == 0 {
self.store_cursor(root, 0);
}
}

unsafe fn apply(&mut self, op: &Op<'_>) {
match *op {
Op::Parse { json } => self.parse(json),
Op::GetStr { cursor_slot, path } => self.get_str(cursor_slot, path),
Op::GetI64 { cursor_slot, path } => self.get_i64(cursor_slot, path),
Op::GetF64 { cursor_slot, path } => self.get_f64(cursor_slot, path),
Op::GetBool { cursor_slot, path } => self.get_bool(cursor_slot, path),
Op::CursorField { cursor_slot, key } => self.cursor_field(cursor_slot, key),
Op::CursorIndex { cursor_slot, index } => self.cursor_index(cursor_slot, index),
Op::ObjectEntryAt { cursor_slot, index } => {
self.object_entry_at(cursor_slot, index);
}
Op::Free => self.free(),
}
}

unsafe fn get_str(&self, cursor_slot: u8, path: &[u8]) {
let (path_ptr, path_len) = ffi_bytes(path);

let mut ptr_out: *const u8 = ptr::null();
let mut len_out = 0usize;
let rc = qjson_get_str(self.doc, path_ptr, path_len, &mut ptr_out, &mut len_out);
black_box(rc);
if rc == 0 {
consume_ffi_bytes(ptr_out, len_out);
}

ptr_out = ptr::null();
len_out = 0;
let rc = qjson_cursor_get_str(
self.cursor_ptr(cursor_slot),
path_ptr,
path_len,
&mut ptr_out,
&mut len_out,
);
black_box(rc);
if rc == 0 {
consume_ffi_bytes(ptr_out, len_out);
}
}

unsafe fn get_i64(&self, cursor_slot: u8, path: &[u8]) {
let (path_ptr, path_len) = ffi_bytes(path);

let mut out = 0i64;
let rc = qjson_get_i64(self.doc, path_ptr, path_len, &mut out);
black_box((rc, out));

out = 0;
let rc = qjson_cursor_get_i64(self.cursor_ptr(cursor_slot), path_ptr, path_len, &mut out);
black_box((rc, out));
}

unsafe fn get_f64(&self, cursor_slot: u8, path: &[u8]) {
let (path_ptr, path_len) = ffi_bytes(path);

let mut out = 0.0f64;
let rc = qjson_get_f64(self.doc, path_ptr, path_len, &mut out);
black_box((rc, out));

out = 0.0;
let rc = qjson_cursor_get_f64(self.cursor_ptr(cursor_slot), path_ptr, path_len, &mut out);
black_box((rc, out));
}

unsafe fn get_bool(&self, cursor_slot: u8, path: &[u8]) {
let (path_ptr, path_len) = ffi_bytes(path);

let mut out = 0;
let rc = qjson_get_bool(self.doc, path_ptr, path_len, &mut out);
black_box((rc, out));

out = 0;
let rc = qjson_cursor_get_bool(self.cursor_ptr(cursor_slot), path_ptr, path_len, &mut out);
black_box((rc, out));
}

unsafe fn cursor_field(&mut self, cursor_slot: u8, key: &[u8]) {
let (key_ptr, key_len) = ffi_bytes(key);
let mut out = zero_cursor();
let rc = qjson_cursor_field(self.cursor_ptr(cursor_slot), key_ptr, key_len, &mut out);
black_box(rc);
if rc == 0 {
self.store_cursor(out, cursor_slot);
}
}

unsafe fn cursor_index(&mut self, cursor_slot: u8, index: u32) {
let mut out = zero_cursor();
let rc = qjson_cursor_index(self.cursor_ptr(cursor_slot), index as usize, &mut out);
black_box(rc);
if rc == 0 {
self.store_cursor(out, cursor_slot);
}
}

unsafe fn object_entry_at(&mut self, cursor_slot: u8, index: u32) {
let mut key_ptr: *const u8 = ptr::null();
let mut key_len = 0usize;
let mut value = zero_cursor();
let rc = qjson_cursor_object_entry_at(
self.cursor_ptr(cursor_slot),
index as usize,
&mut key_ptr,
&mut key_len,
&mut value,
);
black_box(rc);
if rc == 0 {
consume_ffi_bytes(key_ptr, key_len);
self.store_cursor(value, cursor_slot);
}
}

unsafe fn free(&mut self) {
qjson_free(self.doc);
self.doc = ptr::null_mut();
self.cursors.clear();
}

fn cursor_ptr(&self, slot: u8) -> *const qjson_cursor {
if self.cursors.is_empty() {
return ptr::null();
}
&self.cursors[slot as usize % self.cursors.len()]
}

fn store_cursor(&mut self, cursor: qjson_cursor, slot: u8) {
if self.cursors.len() < MAX_CURSORS {
self.cursors.push(cursor);
} else {
self.cursors[slot as usize % MAX_CURSORS] = cursor;
}
}
}

impl Drop for State {
fn drop(&mut self) {
unsafe {
self.free();
}
}
}

fn ffi_bytes(bytes: &[u8]) -> (*const c_char, usize) {
if bytes.is_empty() {
(ptr::null(), 0)
} else {
(bytes.as_ptr() as *const c_char, bytes.len())
}
}

fn zero_cursor() -> qjson_cursor {
qjson_cursor {
doc: ptr::null(),
idx_start: 0,
idx_end: 0,
_reserved0: 0,
_reserved1: 0,
}
}

unsafe fn consume_ffi_bytes(ptr: *const u8, len: usize) {
if len == 0 {
black_box(0u8);
return;
}

assert!(!ptr.is_null(), "qjson returned NULL pointer with non-zero length");

let bytes = std::slice::from_raw_parts(ptr, len);
let acc = bytes[0] ^ bytes[len / 2] ^ bytes[len - 1];
black_box(acc);
}
5 changes: 4 additions & 1 deletion src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ impl Cursor {
// Find the closing index of the outermost container.
// indices has a u32::MAX sentinel at the end.
let n = doc.indices.len() as u32;
debug_assert!(n >= 2);
debug_assert!(n >= 1);
if n == 1 {
return Cursor { idx_start: 0, idx_end: 1 };
}
Cursor { idx_start: 0, idx_end: n - 2 }
}

Expand Down
42 changes: 35 additions & 7 deletions src/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,24 @@ use crate::cursor::{Cursor, find_value_span};
use crate::error::qjson_type;

impl<'a> Document<'a> {
pub(crate) fn is_root_scalar_cursor(&self, cur: Cursor) -> bool {
cur.idx_start == 0 && self.indices.len() == 1 && self.indices[0] == u32::MAX
}

pub(crate) fn root_scalar_start(&self) -> usize {
let mut p = 0;
while p < self.buf.len() && matches!(self.buf[p], b' '|b'\t'|b'\n'|b'\r') {
p += 1;
}
p
}

/// Inspect a cursor and return its JSON value type.
pub(crate) fn type_of(&self, cur: Cursor) -> Result<qjson_type, qjson_err> {
if self.is_root_scalar_cursor(cur) {
return self.scalar_type_at(self.root_scalar_start());
}

let pos = *self.indices.get(cur.idx_start as usize)
.ok_or(qjson_err::QJSON_PARSE_ERROR)? as usize;
let lead = self.buf.get(pos).copied().ok_or(qjson_err::QJSON_PARSE_ERROR)?;
Expand All @@ -66,16 +82,20 @@ impl<'a> Document<'a> {
// structural char AFTER the scalar; the scalar's first byte
// lives between the previous structural char and this one.
let scalar_start = self.find_scalar_start(cur.idx_start)?;
match self.buf.get(scalar_start).copied() {
Some(b't') | Some(b'f') => Ok(qjson_type::QJSON_T_BOOL),
Some(b'n') => Ok(qjson_type::QJSON_T_NULL),
Some(b'-') | Some(b'0'..=b'9') => Ok(qjson_type::QJSON_T_NUM),
_ => Err(qjson_err::QJSON_PARSE_ERROR),
}
self.scalar_type_at(scalar_start)
}
}
}

fn scalar_type_at(&self, scalar_start: usize) -> Result<qjson_type, qjson_err> {
match self.buf.get(scalar_start).copied() {
Some(b't') | Some(b'f') => Ok(qjson_type::QJSON_T_BOOL),
Some(b'n') => Ok(qjson_type::QJSON_T_NULL),
Some(b'-') | Some(b'0'..=b'9') => Ok(qjson_type::QJSON_T_NUM),
_ => Err(qjson_err::QJSON_PARSE_ERROR),
}
}

/// Find the byte position of the first non-whitespace byte after the
/// structural character at `indices[idx - 1]`. Used to locate the first
/// byte of a scalar value.
Expand All @@ -96,6 +116,10 @@ impl<'a> Document<'a> {
/// Returns `QJSON_TYPE_MISMATCH` for non-object cursors, `QJSON_NOT_FOUND`
/// when `i` is past the end.
pub(crate) fn nth_object_entry(&self, cur: Cursor, n: usize) -> Result<(u32, Cursor), qjson_err> {
if self.is_root_scalar_cursor(cur) {
return Err(qjson_err::QJSON_TYPE_MISMATCH);
}

let pos = self.indices[cur.idx_start as usize] as usize;
let b = *self.buf.get(pos).ok_or(qjson_err::QJSON_PARSE_ERROR)?;
if b != b'{' {
Expand Down Expand Up @@ -139,6 +163,10 @@ impl<'a> Document<'a> {
/// Count direct children of the container at `cur`.
/// Returns QJSON_TYPE_MISMATCH for non-container cursors.
pub(crate) fn cursor_len(&self, cur: Cursor) -> Result<usize, qjson_err> {
if self.is_root_scalar_cursor(cur) {
return Err(qjson_err::QJSON_TYPE_MISMATCH);
}

let pos = self.indices[cur.idx_start as usize] as usize;
let b = *self.buf.get(pos).ok_or(qjson_err::QJSON_PARSE_ERROR)?;
if b != b'{' && b != b'[' {
Expand Down Expand Up @@ -207,4 +235,4 @@ mod tests {
let opts = crate::options::Options { mode: crate::options::QJSON_MODE_LAZY, max_depth: 0 };
assert!(Document::parse_with_options(b"{}garbage", &opts).is_ok());
}
}
}
21 changes: 19 additions & 2 deletions src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,16 @@ pub unsafe extern "C" fn qjson_get_bool(
/// scalar's bytes sit between `find_scalar_start(cur.idx_start)` and that
/// structural char, with trailing whitespace stripped.
unsafe fn scalar_byte_range(d: &Document<'_>, cur: Cursor) -> Result<(usize, usize), qjson_err> {
let start = d.find_scalar_start(cur.idx_start)?;
let end = d.indices[cur.idx_start as usize] as usize;
let start = if d.is_root_scalar_cursor(cur) {
d.root_scalar_start()
} else {
d.find_scalar_start(cur.idx_start)?
};
let end = if d.is_root_scalar_cursor(cur) {
d.buf.len()
} else {
d.indices[cur.idx_start as usize] as usize
};
if end < start { return Err(qjson_err::QJSON_PARSE_ERROR); }
let mut e = end;
while e > start && matches!(d.buf[e - 1], b' '|b'\t'|b'\n'|b'\r') { e -= 1; }
Expand Down Expand Up @@ -730,6 +738,15 @@ pub unsafe extern "C" fn qjson_cursor_bytes(
let (d, cur) = match cursor_to_internal(c) {
Ok(x) => x, Err(e) => return e as c_int,
};
if d.is_root_scalar_cursor(cur) {
let (s, e) = match scalar_byte_range(d, cur) {
Ok(x) => x, Err(e) => return e as c_int,
};
*byte_start = s;
*byte_end = e;
return qjson_err::QJSON_OK as c_int;
}

let pos = d.indices[cur.idx_start as usize] as usize;
let lead = match d.buf.get(pos) {
Some(b) => *b,
Expand Down
Loading
Loading