What changed, and why it matters
This commit hardens how the BitBox02/BitBox03 firmware handles sensitive text, such as recovery seed words and confirmation dialog bodies. It replaces ordinary Rust strings with versions that automatically wipe their memory when no longer needed, and it pre-allocates buffers to avoid creating leftover copies during growth. The change reduces the risk that secret words or passphrases remain in device memory after use, but it is a defensive hardening patch rather than a fix for a known active attack.
Treat as a security-hardening improvement and include in release notes; verify that all secret-displaying UI paths now use zeroizing helpers and that no non-zeroizing copies remain in the call graph. No urgent patch deployment is required absent evidence of active exploitation.
Security signals we found
Use of zeroize::Zeroizing for secret-bearing strings
Pre-allocation of buffers to avoid reallocations that could leave unwiped copies
Centralized constant for maximum mnemonic byte size
Replacement of CString with zeroizing C-string conversion in UI widgets
Defensive memory-clearing/hardening change
Evidence from the diff
The merge replaces CString/String allocations with zeroize::Zeroizing-backed buffers in UI label/textarea helpers and in confirmation/recovery-word rendering. A new MAX_MNEMONIC_BYTES constant is centralized in bitbox-hal, and callers pre-size buffers to avoid reallocations that could leave unwiped copies. The str_to_cstr_vec_zeroizing helper is documented as producing a wiped C-string buffer. No vulnerability class (e.g., buffer overflow, use-after-free) is directly fixed; the change mitigates information-disclosure risk from residual secret data in heap/stack memory.
Changed components
src/rust/bitbox-hal/src/ui.rssrc/rust/bitbox-lvgl/src/widgets/label.rssrc/rust/bitbox-lvgl/src/widgets/textarea.rssrc/rust/bitbox02-rust/src/bip39.rssrc/rust/bitbox03/src/ui/confirm.rssrc/rust/bitbox03/src/ui/recovery_words.rssrc/rust/util/src/strings.rsInspect captured patch +42 / −17
### src/rust/bitbox-hal/src/ui.rs
@@ -8,6 +8,10 @@ use core::time::Duration;
/// Keep this in sync with `src/ui/components/label.h:MAX_LABEL_SIZE`.
pub const MAX_CONFIRM_BODY_SIZE: usize = 640;
+/// English BIP39 words contain at most 8 ASCII bytes. Reserve space for 24 words and 23 separators
+/// before writing any recovery words, avoiding reallocations that could leave unwiped copies.
+pub const MAX_MNEMONIC_BYTES: usize = 24 * 8 + 23;
+
pub struct UserAbort;
/// How the user left a recovery-word entry screen without entering a word.
### src/rust/bitbox-lvgl/src/widgets/label.rs
@@ -6,14 +6,14 @@ use core::ffi::CStr;
use core::ptr::NonNull;
use crate::{LvHandle, LvLabelLongMode, LvObj, LvPoint, ObjExt, class, ffi};
-use util::strings::optional_cstr_from_ptr;
+use util::strings::{optional_cstr_from_ptr, str_to_cstr_vec_zeroizing};
pub type LvLabelTextError = super::LvTextError;
pub type LvLabel = LvHandle<class::LabelTag>;
pub trait LabelExt: ObjExt {
fn set_text(&self, txt: &str) -> Result<(), LvLabelTextError> {
- let txt = CString::new(txt).map_err(|_| LvLabelTextError::ContainsNul)?;
+ let txt = str_to_cstr_vec_zeroizing(txt).map_err(|_| LvLabelTextError::ContainsNul)?;
unsafe { ffi::lv_label_set_text(self.as_ptr(), txt.as_ptr()) }
Ok(())
}
### src/rust/bitbox-lvgl/src/widgets/textarea.rs
@@ -7,7 +7,7 @@ use core::ptr::NonNull;
use super::label::LvLabel;
use crate::{LvHandle, LvObj, LvTextAlign, ObjExt, class, ffi};
-use util::strings::optional_cstr_from_ptr;
+use util::strings::{optional_cstr_from_ptr, str_to_cstr_vec_zeroizing};
pub type LvTextareaTextError = super::LvTextError;
pub type LvTextarea = LvHandle<class::TextareaTag>;
@@ -18,7 +18,7 @@ pub trait TextareaExt: ObjExt {
}
fn add_text(&self, txt: &str) -> Result<(), LvTextareaTextError> {
- let txt = CString::new(txt).map_err(|_| LvTextareaTextError::ContainsNul)?;
+ let txt = str_to_cstr_vec_zeroizing(txt).map_err(|_| LvTextareaTextError::ContainsNul)?;
unsafe { ffi::lv_textarea_add_text(self.as_ptr(), txt.as_ptr()) }
Ok(())
}
@@ -32,7 +32,7 @@ pub trait TextareaExt: ObjExt {
}
fn set_text(&self, txt: &str) -> Result<(), LvTextareaTextError> {
- let txt = CString::new(txt).map_err(|_| LvTextareaTextError::ContainsNul)?;
+ let txt = str_to_cstr_vec_zeroizing(txt).map_err(|_| LvTextareaTextError::ContainsNul)?;
unsafe { ffi::lv_textarea_set_text(self.as_ptr(), txt.as_ptr()) }
Ok(())
}
### src/rust/bitbox02-rust/src/bip39.rs
@@ -4,9 +4,7 @@ use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt::Write;
-// English BIP39 words contain at most 8 ASCII bytes. Reserve space for 24 words and 23 separators
-// before writing any recovery words, avoiding reallocations that could leave unwiped copies.
-pub(crate) const MAX_MNEMONIC_BYTES: usize = 24 * 8 + 23;
+pub(crate) use crate::hal::ui::MAX_MNEMONIC_BYTES;
/// `idx` must be smaller than BIP39_WORDLIST_LEN.
pub fn get_word(idx: u16) -> Result<zeroize::Zeroizing<String>, ()> {
### src/rust/bitbox03/src/ui/confirm.rs
@@ -6,20 +6,25 @@ use bitbox_lvgl::{
self as lvgl, LabelExt, LvLabel, LvLabelLongMode, LvObj, LvObjFlag, LvOpacityLevel, ObjExt,
};
use util::futures::completion::Responder;
+use zeroize::Zeroizing;
use super::nav_button::{NavIcon, build_close_button, build_nav_button};
use super::slide_to_confirm::build_slide_to_confirm;
-fn truncate_body(body: &str) -> String {
+// Confirmation bodies can contain secrets. Preallocate to avoid unwiped copies from growth.
+fn truncate_body(body: &str) -> Zeroizing<String> {
if body.len() <= MAX_CONFIRM_BODY_SIZE {
- return String::from(body);
+ let mut text = Zeroizing::new(String::with_capacity(body.len()));
+ text.push_str(body);
+ return text;
}
let mut end = MAX_CONFIRM_BODY_SIZE;
while !body.is_char_boundary(end) {
end -= 1;
}
- let mut truncated = String::from(&body[..end]);
+ let mut truncated = Zeroizing::new(String::with_capacity(end + 3));
+ truncated.push_str(&body[..end]);
truncated.push_str("...");
truncated
}
@@ -123,18 +128,28 @@ mod tests {
#[test]
fn test_truncate_body() {
+ let short = "abandon";
+ let truncated = truncate_body(short);
+ assert_eq!(truncated.as_str(), short);
+ assert_eq!(truncated.capacity(), short.len());
+
let exact = "a".repeat(MAX_CONFIRM_BODY_SIZE);
- assert_eq!(truncate_body(&exact), exact);
+ let truncated = truncate_body(&exact);
+ assert_eq!(truncated.as_str(), exact);
+ assert_eq!(truncated.capacity(), exact.len());
let overlong = "a".repeat(MAX_CONFIRM_BODY_SIZE + 1);
let mut expected = String::from(&overlong[..MAX_CONFIRM_BODY_SIZE]);
expected.push_str("...");
- assert_eq!(truncate_body(&overlong), expected);
+ let truncated = truncate_body(&overlong);
+ assert_eq!(truncated.as_str(), expected);
+ assert_eq!(truncated.capacity(), expected.len());
let mut utf8 = "a".repeat(MAX_CONFIRM_BODY_SIZE - 1);
utf8.push('€');
let truncated = truncate_body(&utf8);
assert_eq!(truncated.len(), MAX_CONFIRM_BODY_SIZE + 2);
assert!(truncated.ends_with("..."));
+ assert_eq!(truncated.capacity(), truncated.len());
}
}
### src/rust/bitbox03/src/ui/recovery_words.rs
@@ -12,7 +12,7 @@
use alloc::string::String;
use core::fmt::Write as _;
-use bitbox_hal::ui::UserAbort;
+use bitbox_hal::ui::{MAX_MNEMONIC_BYTES, UserAbort};
use bitbox_lvgl::{self as lvgl, LabelExt, LvFont, LvLabel, LvObj, LvOpacityLevel, ObjExt, fonts};
use util::futures::completion::Responder;
@@ -78,7 +78,7 @@ fn build_column(parent: &LvObj, words: &[&str], first_number: usize) {
column.set_style_bg_opa(LvOpacityLevel::LV_OPA_TRANSP as u8, 0);
let mut numbers_text = String::new();
- let mut words_text = String::new();
+ let mut words_text = zeroize::Zeroizing::new(String::with_capacity(MAX_MNEMONIC_BYTES));
for (i, word) in words.iter().enumerate() {
if i > 0 {
numbers_text.push('\n');
@@ -88,6 +88,13 @@ fn build_column(parent: &LvObj, words: &[&str], first_number: usize) {
words_text.push_str(word);
}
+ #[cfg(test)]
+ assert_eq!(
+ words_text.capacity(),
+ MAX_MNEMONIC_BYTES,
+ "recovery-word buffer must not reallocate"
+ );
+
let numbers = build_column_label(&column, &numbers_text, NUMBER_FONT);
numbers.set_width(NUMBER_WIDTH);
numbers.set_style_text_align(lvgl::LvTextAlign::LV_TEXT_ALIGN_RIGHT, 0);
### src/rust/util/src/strings.rs
@@ -58,8 +58,9 @@ pub fn str_to_cstr_vec(input: &str) -> Result<Vec<core::ffi::c_char>, ()> {
Ok(cstr.into_iter().map(|c| c as _).collect())
}
-/// Converts a Rust string to a null terminated C string by appending a null
-/// terminator. Returns `Err(())` if the input already contains a null byte.
+/// Converts a Rust string to a null terminated C string buffer that is wiped when dropped.
+/// Allocates the full buffer before copying the text. Returns `Err(())` if the input contains
+/// a null byte.
pub fn str_to_cstr_vec_zeroizing(
input: &str,
) -> Result<zeroize::Zeroizing<Vec<core::ffi::c_char>>, ()> {Why this scored 49/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.