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
2 changes: 2 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ jobs:
run: cargo test --verbose --no-default-features
- name: Run tests default
run: cargo test --verbose
- name: Run compile-fail tests
run: python3 compile_fail_tests/verify_soundness_failures.py
19 changes: 14 additions & 5 deletions benches/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fn parse_bench() {
runner.set_name(name);

let access = get_access_for_input_name(name);
runner.register("serde_json", move |_data| {
runner.register("serde_json parse only", move |_data| {
let mut val = None;
for line in input_gen() {
let json: serde_json::Value = serde_json::from_str(&line).unwrap();
Expand All @@ -60,15 +60,15 @@ fn parse_bench() {
black_box(val);
});

runner.register("serde_json + access by key", move |_data| {
runner.register("serde_json access by key", move |_data| {
let mut total_size = 0;
for line in input_gen() {
let json: serde_json::Value = serde_json::from_str(&line).unwrap();
total_size += access_json(&json, access);
}
black_box(total_size);
});
runner.register("serde_json_borrow::OwnedValue", move |_data| {
runner.register("serde_json_borrow::OwnedValue parse only", move |_data| {
let mut val = None;
for line in input_gen() {
let json: OwnedValue = OwnedValue::parse_from(line).unwrap();
Expand All @@ -78,7 +78,7 @@ fn parse_bench() {
});

runner.register(
"serde_json_borrow::OwnedValue + access by key",
"serde_json_borrow::OwnedValue access by key",
move |_data| {
let mut total_size = 0;
for line in input_gen() {
Expand All @@ -89,13 +89,22 @@ fn parse_bench() {
},
);

runner.register("SIMD_json_borrow", move |_data| {
runner.register("serde_json_borrow::ReusableMap parse only", move |_data| {
let mut map = serde_json_borrow::ReusableMap::new();
for line in input_gen() {
let parsed = map.deserialize(&line).unwrap();
black_box(parsed);
}
});

runner.register("SIMD_json_borrow parse only", move |_data| {
for line in input_gen() {
let mut data: Vec<u8> = line.into();
let v: simd_json::BorrowedValue = simd_json::to_borrowed_value(&mut data).unwrap();
black_box(v);
}
});

runner.run();
}
}
Expand Down
2 changes: 2 additions & 0 deletions compile_fail_tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
/Cargo.lock
33 changes: 33 additions & 0 deletions compile_fail_tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "compile_fail_tests"
version = "0.0.0"
edition = "2024"

[workspace]

[dependencies]
serde_json_borrow = { version = "0.8.0", path = ".." }

[[bin]]
name = "deserialize_borrow_conflict"
path = "deserialize_borrow_conflict.rs"

[[bin]]
name = "guard_thread_safety"
path = "guard_thread_safety.rs"

[[bin]]
name = "map_lifetime_soundness"
path = "map_lifetime_soundness.rs"

[[bin]]
name = "guard_outlives_string_scope"
path = "guard_outlives_string_scope.rs"

[[bin]]
name = "guard_return_from_function"
path = "guard_return_from_function.rs"

[[bin]]
name = "leak_references_from_guard"
path = "leak_references_from_guard.rs"
65 changes: 65 additions & 0 deletions compile_fail_tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Compile-Fail Tests

This directory contains tests that are designed to fail at compile time. These tests verify that our lifetime safety guarantees in the `deser` module are properly enforced by the Rust compiler.

## Purpose

These tests ensure that:

1. A guard cannot outlive the JSON string it references (`guard_outlives_string_scope`)
2. A guard cannot be returned from a function where the JSON string is local (`guard_return_from_function`)
3. References cannot be leaked from a guard to outlive their source (`leak_references_from_guard`)
4. A deserializer cannot be used while a guard exists (`safety_tests/deserializer_borrow_conflict`)
5. A guard's internal map cannot be borrowed mutably multiple times (`safety_tests/guard_multiple_borrows`)
6. Guards cannot be sent between threads if they contain references to thread-local data (`safety_tests/guard_thread_safety`)
7. The lifetime of the map is properly tied to the JSON string's lifetime (`safety_tests/map_lifetime_soundness`)

## How to Run

These tests are meant to fail at compile time, so you can verify them by trying to build each test individually:

```bash
# Test that a guard cannot outlive its string reference
cd guard_outlives_string_scope
cargo build
# Should fail with: error[E0597]: `json_string` does not live long enough

# Test that a guard cannot be returned from a function with local string
cd ../guard_return_from_function
cargo build
# Should fail with lifetime errors

# Test that references cannot be leaked from a guard
cd ../leak_references_from_guard
cargo build
# Should fail with lifetime errors

# Additional safety tests
cd ../safety_tests
cargo check --bin deserializer_borrow_conflict # Should fail with "cannot borrow as mutable"
cargo check --bin guard_multiple_borrows # Should fail with "cannot borrow as mutable more than once"
cargo check --bin guard_thread_safety # Should fail with "cannot be sent between threads safely"
cargo check --bin map_lifetime_soundness # Should fail with "does not live long enough"
```

## Expected Errors

Each test demonstrates a different aspect of lifetime safety:

- `guard_outlives_string_scope`: Should fail with `error[E0597]: 'json_string' does not live long enough`
- `guard_return_from_function`: Should fail with errors about lifetimes not matching
- `leak_references_from_guard`: Should fail with `error[E0597]: 'json_string' does not live long enough`
- `deserializer_borrow_conflict`: Should fail with "cannot borrow as mutable" error
- `guard_multiple_borrows`: Should fail with "cannot borrow as mutable more than once" error
- `guard_thread_safety`: Should fail with "cannot be sent between threads safely" error
- `map_lifetime_soundness`: Should fail with "does not live long enough" error

## Why This Matters

These tests are crucial for verifying that our guard pattern correctly prevents use-after-free bugs by enforcing compile-time lifetime guarantees. If any of these tests were to compile successfully, it would indicate a flaw in our safety guarantees.

The safety of our deserialization approach depends on these lifetime constraints being properly enforced by the compiler. These tests give us confidence that the guard pattern is working as intended to prevent memory safety issues.

## Additional Runtime Tests

In addition to these compile-fail tests, there is a runtime test `test_map_cleared_on_drop` in the main test suite that verifies that the map is properly cleared when the guard is dropped, ensuring no dangling references remain.
24 changes: 24 additions & 0 deletions compile_fail_tests/deserialize_borrow_conflict.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! This test demonstrates that the deserializer cannot be used while a guard exists.
//! This file should fail to compile because the deserializer is already borrowed by the first guard.
//! EXPECT: error[E0499]: cannot borrow `deserializer` as mutable more than once at a time

use serde_json_borrow::ReusableMap;

fn main() {
let mut deserializer = ReusableMap::new();

// First JSON string
let json_str1 = r#"{"first":"value"}"#;

// Get a guard from the deserializer
let guard1 = deserializer.deserialize(json_str1).unwrap();

// Try to use the deserializer again while guard1 exists
// This should fail to compile because the deserializer is already mutably borrowed
let json_str2 = r#"{"second":"value"}"#;
let guard2 = deserializer.deserialize(json_str2).unwrap(); // Should fail with borrow error

// Use both guards to ensure the compiler doesn't optimize away
println!("First guard length: {}", guard1.len());
println!("Second guard length: {}", guard2.len());
}
24 changes: 24 additions & 0 deletions compile_fail_tests/guard_outlives_string_scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! This test demonstrates that a guard cannot outlive the JSON string it references.
//! This file should fail to compile with an error like:
//! EXPECT: error[E0597]: `json_string` does not live long enough

use serde_json_borrow::ReusableMap;

fn main() {
let mut deserializer = ReusableMap::new();
let guard;

{
// Create a JSON string with a limited scope
let json_string = r#"{"temporary":"value"}"#.to_string();

// This should fail to compile because the guard would outlive json_string
guard = deserializer.deserialize(&json_string).unwrap();

// The guard borrows from json_string, but json_string will be dropped
// at the end of this block, while guard would live longer
}

// This would be a use-after-free if the compiler allowed it
assert_eq!(guard.len(), 1);
}
26 changes: 26 additions & 0 deletions compile_fail_tests/guard_return_from_function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! This test demonstrates that a guard cannot be returned from a function
//! where the JSON string is local to that function.
//! This file should fail to compile with an error about lifetimes not matching.
//! EXPECT: error[E0515]: cannot return value referencing local variable `json_string`

use serde_json_borrow::{BorrowedMap, ReusableMap};

// This function tries to return a guard that references a local JSON string
fn create_guard<'d>(deserializer: &'d mut ReusableMap) -> BorrowedMap<'static, 'd> {
// Local JSON string that will be dropped when the function returns
let json_string = r#"{"escape":"attempt"}"#.to_string();

// This should fail to compile - cannot convert BorrowedMap<'_, 'd> to BorrowedMap<'static, 'd>
// because that would allow the guard to outlive the json_string
deserializer.deserialize(&json_string).unwrap()
}

fn main() {
let mut deserializer = ReusableMap::new();

// Try to get a guard with an invalid 'static lifetime for the JSON string
let guard = create_guard(&mut deserializer);

// This would be a use-after-free if the compiler allowed it
assert_eq!(guard.len(), 1);
}
31 changes: 31 additions & 0 deletions compile_fail_tests/guard_thread_safety.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! This test demonstrates that a BorrowedMap cannot be sent across thread boundaries
//! when it contains references to stack data.
//!
//! This file should fail to compile because we're trying to move a guard with
//! references to stack variables into a new thread.
//! EXPECT: error[E0597]: `json_string` does not live long enough
//! EXPECT: argument requires that `json_string` is borrowed for `'static`

use std::thread;

use serde_json_borrow::ReusableMap;

fn main() {
// Create a JSON string on the stack
let json_string = r#"{"key":"value"}"#.to_string();

// Create a deserializer and guard
let mut deserializer = ReusableMap::new();
let guard = deserializer.deserialize(&json_string).unwrap();

// Attempt to move the guard to a new thread
// This should fail to compile because the guard contains references
// to stack data (json_string and deserializer) that won't be valid
// in the new thread.
let handle = thread::spawn(move || {
// Try to use the guard in the new thread
println!("Guard in thread: {} elements", guard.len());
});

handle.join().unwrap();
}
29 changes: 29 additions & 0 deletions compile_fail_tests/leak_references_from_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! This test demonstrates that references cannot be leaked from a guard.
//! This file should fail to compile with an error about lifetimes not matching.
//! EXPECT: error[E0597]: `json_string` does not live long enough
//! EXPECT: error[E0597]: `guard` does not live long enough

use serde_json_borrow::ReusableMap;
use serde_json_borrow::Value;

fn main() {
let mut deserializer = ReusableMap::new();
let leaked_ref: &str;

{
let json_string = r#"{"name":"test"}"#.to_string();
let guard = deserializer.deserialize(&json_string).unwrap();

// Try to extract and leak a reference from the guard
if let Some(Value::Str(name)) = guard.get("name") {
// This should fail to compile - cannot assign a reference with lifetime
// tied to json_string to a variable that outlives json_string
leaked_ref = name;
} else {
unreachable!();
}
}

// This would be a use-after-free if the compiler allowed it
println!("Leaked reference: {}", leaked_ref);
}
31 changes: 31 additions & 0 deletions compile_fail_tests/map_lifetime_soundness.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! This test demonstrates that the lifetime of the map is properly tied to the JSON string.
//! This file should fail to compile because we're trying to store a reference from the map
//! that would outlive the JSON string it references.
//! EXPECT: error[E0597]: `json_string` does not live long enough
//! EXPECT: error[E0597]: `guard` does not live long enough

use serde_json_borrow::{ReusableMap, Value};

fn main() {
let mut deserializer = ReusableMap::new();
let stored_ref: &str;

{
// Create a JSON string with a limited scope
let json_string = r#"{"key":"value"}"#.to_string();
let guard = deserializer.deserialize(&json_string).unwrap();

// Try to store a reference from the map that would outlive the JSON string
if let Some(Value::Str(val)) = guard.get("key") {
// This should fail to compile - cannot assign a reference with lifetime
// tied to json_string to a variable that outlives json_string
stored_ref = val;
} else {
unreachable!();
}
}

// Try to use the stored reference after the JSON string is dropped
// This would be a use-after-free if the compiler allowed it
println!("Stored ref: {}", stored_ref);
}
Loading