What changed, and why it matters
This commit adds a brand-new Rust helper crate called bitbox-bytequeue. It is a small wrapper around Rust's standard growable queue type (VecDeque) that exposes a C-compatible API matching an older fixed-size C ringbuffer. The code is purely additive and does not change any existing security-critical logic. It includes overflow protection by deliberately crashing (panicking) if too many bytes are queued, which is intended to prevent memory exhaustion bugs.
No immediate action required. Treat as normal code-review item: verify that future commits replacing the old C ringbuffer with this crate preserve capacity limits and call sites respect the documented safety invariants (valid pointer, no aliasing, no double-free).
Security signals we found
New C API with raw pointer handling and documented safety preconditions
Panic-on-overflow defensive design to limit memory growth
Null-pointer handling returns early/false rather than dereferencing
No use-after-free or double-free mitigation beyond documentation; C caller must obey safety contract
Evidence from the diff
The patch introduces a new workspace crate bitbox-bytequeue providing ByteQueue (a VecDeque<u8> wrapper) and opaque C bindings (rust_bytequeue_init/free/put/get/num/flush). It panics on put when len() >= initial_capacity, preserving a fixed-size semantic while using a heap-backed collection. The crate is wired into the workspace, cbindgen config, and bitbox02-rust-c. No existing code is modified to use it yet, and no production ringbuffer is replaced in this commit.
Changed components
src/rust/bitbox-bytequeuesrc/rust/bitbox02-rust-csrc/rust/bitbox02-cbindgen.tomlInspect captured patch +357 / −2
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index d96d381..7e99681 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -103,6 +103,10 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "bitbox-bytequeue"
+version = "0.1.0"
+
[[package]]
name = "bitbox-executor"
version = "0.1.0"
@@ -208,6 +212,7 @@ version = "0.1.0"
dependencies = [
"bip39",
"bitbox-aes",
+ "bitbox-bytequeue",
"bitbox-framed-serial-link",
"bitbox-hal",
"bitbox02",
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
index 456210e..43c51a3 100644
--- a/src/rust/Cargo.toml
+++ b/src/rust/Cargo.toml
@@ -5,6 +5,7 @@
members = [
"bitbox02-rust-c",
"bitbox02-rust",
+ "bitbox-bytequeue",
"bitbox-hal",
"bitbox-framed-serial-link",
"util",
diff --git a/src/rust/bitbox-bytequeue/Cargo.toml b/src/rust/bitbox-bytequeue/Cargo.toml
new file mode 100644
index 0000000..3423f10
--- /dev/null
+++ b/src/rust/bitbox-bytequeue/Cargo.toml
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: Apache-2.0
+
+[package]
+name = "bitbox-bytequeue"
+version = "0.1.0"
+authors = ["Shift Crypto AG <support@bitbox.swiss>"]
+edition = "2024"
+license = "Apache-2.0"
diff --git a/src/rust/bitbox-bytequeue/src/lib.rs b/src/rust/bitbox-bytequeue/src/lib.rs
new file mode 100644
index 0000000..9e2c253
--- /dev/null
+++ b/src/rust/bitbox-bytequeue/src/lib.rs
@@ -0,0 +1,327 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#![no_std]
+
+extern crate alloc;
+
+use alloc::boxed::Box;
+use alloc::collections::VecDeque;
+
+pub struct ByteQueue {
+ queue: VecDeque<u8>,
+ initial_capacity: usize,
+}
+
+/// Opaque C handle for a [`ByteQueue`] allocated and owned by Rust.
+#[repr(C)]
+pub struct RustByteQueue {
+ _private: [u8; 0],
+}
+
+impl ByteQueue {
+ /// Creates a bytequeue with a fixed maximum number of elements.
+ ///
+ /// The buffer panics on [`put`](Self::put) once it reaches `capacity`.
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self {
+ queue: VecDeque::with_capacity(capacity),
+ initial_capacity: capacity,
+ }
+ }
+
+ /// Pushes one byte to the back of the queue.
+ ///
+ /// Panics if inserting would exceed the initial capacity.
+ pub fn put(&mut self, data: u8) {
+ if self.queue.len() >= self.initial_capacity {
+ panic!("bytequeue overflow");
+ }
+ self.queue.push_back(data);
+ }
+
+ /// Pops one byte from the front of the queue.
+ ///
+ /// Returns `None` if the queue is empty.
+ pub fn get(&mut self) -> Option<u8> {
+ self.queue.pop_front()
+ }
+
+ /// Returns the number of queued bytes.
+ ///
+ /// Panics if the internal length does not fit into `u32`.
+ pub fn num(&self) -> u32 {
+ u32::try_from(self.queue.len()).unwrap()
+ }
+
+ /// Removes all queued bytes.
+ pub fn flush(&mut self) {
+ self.queue.clear();
+ }
+}
+
+unsafe fn bytequeue_mut<'a>(rb: *mut RustByteQueue) -> Option<&'a mut ByteQueue> {
+ if rb.is_null() {
+ return None;
+ }
+ Some(unsafe { &mut *(rb.cast::<ByteQueue>()) })
+}
+
+unsafe fn bytequeue_ref<'a>(rb: *const RustByteQueue) -> Option<&'a ByteQueue> {
+ if rb.is_null() {
+ return None;
+ }
+ Some(unsafe { &*(rb.cast::<ByteQueue>()) })
+}
+
+/// Allocates a new bytequeue and returns an opaque handle.
+///
+/// The returned pointer must be freed with [`rust_bytequeue_free`].
+#[unsafe(no_mangle)]
+pub extern "C" fn rust_bytequeue_init(capacity: usize) -> *mut RustByteQueue {
+ Box::into_raw(Box::new(ByteQueue::with_capacity(capacity))).cast::<RustByteQueue>()
+}
+
+/// Frees a bytequeue previously created by [`rust_bytequeue_init`].
+///
+/// # Safety
+/// If `rb` is non-null, it must be a pointer returned by
+/// [`rust_bytequeue_init`] that has not already been freed. Passing any other
+/// pointer, or freeing the same pointer more than once, is undefined behavior.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_bytequeue_free(rb: *mut RustByteQueue) -> bool {
+ if rb.is_null() {
+ return false;
+ }
+
+ unsafe { drop(Box::from_raw(rb.cast::<ByteQueue>())) };
+ true
+}
+
+/// Pops one byte from the front of the queue into `data_out`.
+///
+/// # Safety
+/// `rb` must be either null or a valid pointer to a [`ByteQueue`] for the
+/// duration of this call. If non-null, the pointed-to bytequeue must not be
+/// aliased for mutable access elsewhere.
+///
+/// `data_out` must be non-null and valid for writing one byte.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_bytequeue_get(rb: *mut RustByteQueue, data_out: *mut u8) -> bool {
+ if data_out.is_null() {
+ return false;
+ }
+ let Some(rb) = (unsafe { bytequeue_mut(rb) }) else {
+ return false;
+ };
+ let Some(data) = rb.get() else {
+ return false;
+ };
+ unsafe { *data_out = data };
+ true
+}
+
+/// Pushes one byte to the back of the queue.
+///
+/// # Safety
+/// `rb` must be either null or a valid pointer to a [`ByteQueue`] for the
+/// duration of this call. If non-null, the pointed-to bytequeue must not be
+/// aliased for mutable access elsewhere.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_bytequeue_put(rb: *mut RustByteQueue, data: u8) {
+ if let Some(rb) = unsafe { bytequeue_mut(rb) } {
+ rb.put(data);
+ }
+}
+
+/// Returns the current number of queued bytes.
+///
+/// # Safety
+/// `rb` must be either null or a valid pointer to a [`ByteQueue`] for the
+/// duration of this call.
+///
+/// If non-null, the pointed-to bytequeue must not be aliased for mutable
+/// access elsewhere.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_bytequeue_num(rb: *const RustByteQueue) -> u32 {
+ let Some(rb) = (unsafe { bytequeue_ref(rb) }) else {
+ return 0;
+ };
+ rb.num()
+}
+
+/// Removes all queued bytes.
+///
+/// # Safety
+/// `rb` must be either null or a valid pointer to a [`ByteQueue`] for the
+/// duration of this call. If non-null, the pointed-to bytequeue must not be
+/// aliased for mutable access elsewhere.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_bytequeue_flush(rb: *mut RustByteQueue) {
+ if let Some(rb) = unsafe { bytequeue_mut(rb) } {
+ rb.flush();
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use core::ptr;
+
+ struct TestByteQueue {
+ ptr: *mut RustByteQueue,
+ }
+
+ impl TestByteQueue {
+ fn new() -> Self {
+ Self::with_capacity(16)
+ }
+
+ fn with_capacity(capacity: usize) -> Self {
+ let ptr = rust_bytequeue_init(capacity);
+ assert!(!ptr.is_null());
+ Self { ptr }
+ }
+ }
+
+ impl Drop for TestByteQueue {
+ fn drop(&mut self) {
+ unsafe {
+ rust_bytequeue_free(self.ptr);
+ }
+ }
+ }
+
+ #[test]
+ fn test_bytequeue() {
+ let mut rb = ByteQueue::with_capacity(2);
+ assert_eq!(rb.num(), 0);
+
+ rb.put(1);
+ rb.put(2);
+ assert_eq!(rb.num(), 2);
+ assert_eq!(rb.get(), Some(1));
+ assert_eq!(rb.get(), Some(2));
+ assert_eq!(rb.get(), None);
+ assert_eq!(rb.num(), 0);
+ }
+
+ #[test]
+ fn test_bytequeue_flush() {
+ let mut rb = ByteQueue::with_capacity(2);
+ rb.put(1);
+ rb.put(2);
+ rb.flush();
+ assert_eq!(rb.num(), 0);
+ assert_eq!(rb.get(), None);
+ }
+
+ #[test]
+ #[should_panic]
+ fn test_bytequeue_put_overflow_panics() {
+ let mut rb = ByteQueue::with_capacity(1);
+ rb.put(1);
+ rb.put(2);
+ }
+
+ #[test]
+ fn test_rust_bytequeue_init_free() {
+ let rb = rust_bytequeue_init(0);
+ assert!(!rb.is_null());
+ assert!(unsafe { rust_bytequeue_free(rb) });
+ }
+
+ #[test]
+ fn test_rust_bytequeue_put_get_fifo() {
+ let rb = TestByteQueue::new();
+ unsafe {
+ rust_bytequeue_put(rb.ptr, 1);
+ rust_bytequeue_put(rb.ptr, 2);
+ rust_bytequeue_put(rb.ptr, 3);
+
+ let mut out = 0;
+ assert!(rust_bytequeue_get(rb.ptr, &mut out));
+ assert_eq!(out, 1);
+
+ assert!(rust_bytequeue_get(rb.ptr, &mut out));
+ assert_eq!(out, 2);
+
+ assert!(rust_bytequeue_get(rb.ptr, &mut out));
+ assert_eq!(out, 3);
+ }
+ }
+
+ #[test]
+ fn test_rust_bytequeue_get_empty() {
+ let rb = TestByteQueue::new();
+ let mut out = 0;
+ assert!(!unsafe { rust_bytequeue_get(rb.ptr, &mut out) });
+ }
+
+ #[test]
+ fn test_rust_bytequeue_num() {
+ let rb = TestByteQueue::new();
+ unsafe {
+ assert_eq!(rust_bytequeue_num(rb.ptr), 0);
+ rust_bytequeue_put(rb.ptr, 0x11);
+ rust_bytequeue_put(rb.ptr, 0x22);
+ assert_eq!(rust_bytequeue_num(rb.ptr), 2);
+
+ let mut out = 0;
+ assert!(rust_bytequeue_get(rb.ptr, &mut out));
+ assert_eq!(out, 0x11);
+ assert_eq!(rust_bytequeue_num(rb.ptr), 1);
+ }
+ }
+
+ #[test]
+ fn test_rust_bytequeue_flush() {
+ let rb = TestByteQueue::new();
+ unsafe {
+ rust_bytequeue_put(rb.ptr, 1);
+ rust_bytequeue_put(rb.ptr, 2);
+ assert_eq!(rust_bytequeue_num(rb.ptr), 2);
+
+ rust_bytequeue_flush(rb.ptr);
+ assert_eq!(rust_bytequeue_num(rb.ptr), 0);
+
+ let mut out = 0;
+ assert!(!rust_bytequeue_get(rb.ptr, &mut out));
+ }
+ }
+
+ #[test]
+ fn test_rust_bytequeue_at_capacity() {
+ let rb = TestByteQueue::with_capacity(4096);
+ unsafe {
+ for i in 0..4096usize {
+ rust_bytequeue_put(rb.ptr, (i % 251) as u8);
+ }
+ assert_eq!(rust_bytequeue_num(rb.ptr), 4096);
+
+ for i in 0..4096usize {
+ let mut out = 0;
+ assert!(rust_bytequeue_get(rb.ptr, &mut out));
+ assert_eq!(out, (i % 251) as u8);
+ }
+ assert_eq!(rust_bytequeue_num(rb.ptr), 0);
+ }
+ }
+
+ #[test]
+ fn test_rust_bytequeue_null_pointer_handling() {
+ unsafe {
+ rust_bytequeue_put(ptr::null_mut(), 1);
+ rust_bytequeue_flush(ptr::null_mut());
+ assert_eq!(rust_bytequeue_num(ptr::null()), 0);
+
+ let mut out = 0;
+ assert!(!rust_bytequeue_get(ptr::null_mut(), &mut out));
+ assert!(!rust_bytequeue_get(ptr::null_mut(), ptr::null_mut()));
+ assert!(!rust_bytequeue_free(ptr::null_mut()));
+ }
+
+ let rb = TestByteQueue::new();
+ assert!(!unsafe { rust_bytequeue_get(rb.ptr, ptr::null_mut()) });
+ }
+}
diff --git a/src/rust/bitbox02-cbindgen.toml b/src/rust/bitbox02-cbindgen.toml
index 13e91ef..2224776 100644
--- a/src/rust/bitbox02-cbindgen.toml
+++ b/src/rust/bitbox02-cbindgen.toml
@@ -22,10 +22,10 @@ header = '''
parse_deps = true
# ... but only parse these crates.
-include = ["bitbox02-rust", "util", "bitbox-aes", "bitbox-framed-serial-link"]
+include = ["bitbox02-rust", "util", "bitbox-aes", "bitbox-framed-serial-link", "bitbox-bytequeue"]
# also generate bindings from these crates.
-extra_bindings = ["bitbox02-rust", "util", "bitbox-aes", "bitbox-framed-serial-link"]
+extra_bindings = ["bitbox02-rust", "util", "bitbox-aes", "bitbox-framed-serial-link", "bitbox-bytequeue"]
[export]
# malloc, free declared in bitbox02-rust-c/src/alloc.rs, but does not need to be exported, as it
diff --git a/src/rust/bitbox02-rust-c/Cargo.toml b/src/rust/bitbox02-rust-c/Cargo.toml
index fee9b07..e1aedad 100644
--- a/src/rust/bitbox02-rust-c/Cargo.toml
+++ b/src/rust/bitbox02-rust-c/Cargo.toml
@@ -9,6 +9,7 @@ license = "Apache-2.0"
[dependencies]
bitbox02-rust = { path = "../bitbox02-rust", optional = true }
+bitbox-bytequeue = { path = "../bitbox-bytequeue" }
bitbox-aes = { path = "../bitbox-aes", optional = true }
bitbox02 = { path = "../bitbox02" }
bitbox-hal = { path = "../bitbox-hal" }
diff --git a/src/rust/bitbox02-rust-c/src/lib.rs b/src/rust/bitbox02-rust-c/src/lib.rs
index 0824dde..e940822 100644
--- a/src/rust/bitbox02-rust-c/src/lib.rs
+++ b/src/rust/bitbox02-rust-c/src/lib.rs
@@ -48,6 +48,9 @@ extern crate bitbox02_rust;
// Expose C interface defined in bitbox-framed-serial-link
extern crate bitbox_framed_serial_link;
+// Expose C interface defined in bitbox-bytequeue
+extern crate bitbox_bytequeue;
+
// Expose C interface defined in util
extern crate util;
diff --git a/test/simulator-graphical-bb03/Cargo.lock b/test/simulator-graphical-bb03/Cargo.lock
index ff25580..cb02a7b 100644
--- a/test/simulator-graphical-bb03/Cargo.lock
+++ b/test/simulator-graphical-bb03/Cargo.lock
@@ -329,6 +329,10 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "bitbox-bytequeue"
+version = "0.1.0"
+
[[package]]
name = "bitbox-executor"
version = "0.1.0"
@@ -426,6 +430,7 @@ version = "0.1.0"
dependencies = [
"bip39",
"bitbox-aes",
+ "bitbox-bytequeue",
"bitbox-framed-serial-link",
"bitbox-hal",
"bitbox02",
diff --git a/test/simulator-graphical/Cargo.lock b/test/simulator-graphical/Cargo.lock
index 91084ca..51ed854 100644
--- a/test/simulator-graphical/Cargo.lock
+++ b/test/simulator-graphical/Cargo.lock
@@ -291,6 +291,10 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "bitbox-bytequeue"
+version = "0.1.0"
+
[[package]]
name = "bitbox-executor"
version = "0.1.0"
@@ -388,6 +392,7 @@ version = "0.1.0"
dependencies = [
"bip39",
"bitbox-aes",
+ "bitbox-bytequeue",
"bitbox-framed-serial-link",
"bitbox-hal",
"bitbox02",
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.