Hi!
We are a team of researchers studying memory safety in Rust. As part of our ongoing research, we tested quick-protobuf (version: 0.8.1) and found that the following code snippet is reported as undefined behavior by Miri:
Minimal Problematic Snippet
#![feature(allocator_api)]
extern crate alloc;
use quick_protobuf::*;
fn main() {
let v24 = [121u8, 103u8, 246u8];
let v25 = Box::new(v24);
let v26 = &v25[..];
let mut v27 = reader::BytesReader::from_bytes(v26);
let v38: &'_ mut reader::BytesReader = &mut v27;
let v39 = [206u8, 109u8, 153u8];
let v40 = Box::new(v39);
let v41 = &v40[..];
let _ = reader::BytesReader::read_string(v38, v41);
let v48: &'_ mut reader::BytesReader = &mut v27;
let v49 = [109u8, 180u8, 69u8];
let v50 = Box::new(v49);
let v51 = &v50[..];
let _ = reader::BytesReader::read_packed_fixed::<u8>(v48, v51);
}
Miri Error Excerpt
error: Undefined Behavior: `assume` called with `false`
--> /home/rose/projects/lifesonar-tests/quick-protobuf-0.8.1/src/reader.rs:456:17
|
456 | bytes.get_unchecked(self.start) as *const u8 as *const M,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
= note: inside `quick_protobuf::BytesReader::read_packed_fixed::<u8>` at /home/rose/projects/lifesonar-tests/quick-protobuf-0.8.1/src/reader.rs:456:17: 456:48
note: inside `main`
--> src/main.rs:18:15
Root-Cause Hypothesis
After analyzing the Miri report and the source code, we assume the UB is rooted in:
- Suspect location:
src/reader.rs:394-402 and src/reader.rs:448-456
- Invariant being broken:
BytesReader cursor/bounds invariants are not restored when read_len returns early on error.
- Causality chain: crafted length-delimited input ->
read_len sets self.end = self.start + len -> closure errors and ? returns before rollback -> later read_packed_fixed trusts stale logical bounds -> get_unchecked/from_raw_parts executes with invalid assumptions -> UB.
Human-Readable Reproduction
use quick_protobuf::BytesReader;
fn main() {
let original_bytes = [121u8, 103, 246];
let mut reader = BytesReader::from_bytes(&original_bytes);
let _ = reader.read_string(&[206u8, 109, 153]);
let _ = reader.read_packed_fixed::<u8>(&[109u8, 180, 69]);
}
Miri Output of Human-Readable Reproduction
Command used:
MIRIFLAGS="-Zmiri-ignore-leaks -Zmiri-tree-borrows" RUSTFLAGS=-Awarnings RUST_BACKTRACE=1 cargo miri run
Result:
error: Undefined Behavior: `assume` called with `false`
--> /home/rose/projects/lifesonar-tests/quick-protobuf-0.8.1/src/reader.rs:456:17
|
456 | bytes.get_unchecked(self.start) as *const u8 as *const M,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
= note: inside `quick_protobuf::BytesReader::read_packed_fixed::<u8>` at /home/rose/projects/lifesonar-tests/quick-protobuf-0.8.1/src/reader.rs:456:17: 456:48
note: inside `main`
--> src/main.rs:8:13
We don't know whether this is a truly undefined behaviour or just because of our misuse, or this is a API design flaw which results in the creation of this test case.
Possible Fix
- Make
read_len restore start/end on both success and error paths (for example with a guard that always runs on scope exit).
- Before
from_raw_parts, prove self.start <= bytes.len() and self.start + len <= bytes.len() using checked arithmetic.
- Keep unsafe operations behind a single checked helper to prevent state leakage from earlier fallible paths.
We would appreciate it if you could take a look and confirm whether this behavior indicates a real issue, or if it is a false positive / expected limitation of Miri.
Thank you very much for your time and for maintaining this great project!
Hi!
We are a team of researchers studying memory safety in Rust. As part of our ongoing research, we tested quick-protobuf (version: 0.8.1) and found that the following code snippet is reported as undefined behavior by Miri:
Minimal Problematic Snippet
Miri Error Excerpt
Root-Cause Hypothesis
After analyzing the Miri report and the source code, we assume the UB is rooted in:
src/reader.rs:394-402andsrc/reader.rs:448-456BytesReadercursor/bounds invariants are not restored whenread_lenreturns early on error.read_lensetsself.end = self.start + len-> closure errors and?returns before rollback -> laterread_packed_fixedtrusts stale logical bounds ->get_unchecked/from_raw_partsexecutes with invalid assumptions -> UB.Human-Readable Reproduction
Miri Output of Human-Readable Reproduction
Command used:
MIRIFLAGS="-Zmiri-ignore-leaks -Zmiri-tree-borrows" RUSTFLAGS=-Awarnings RUST_BACKTRACE=1 cargo miri runResult:
We don't know whether this is a truly undefined behaviour or just because of our misuse, or this is a API design flaw which results in the creation of this test case.
Possible Fix
read_lenrestorestart/endon both success and error paths (for example with a guard that always runs on scope exit).from_raw_parts, proveself.start <= bytes.len()andself.start + len <= bytes.len()using checked arithmetic.We would appreciate it if you could take a look and confirm whether this behavior indicates a real issue, or if it is a false positive / expected limitation of Miri.
Thank you very much for your time and for maintaining this great project!