What changed, and why it matters
This commit fixes a class of low-level memory-safety bugs where Rust code was given buffers containing uninitialized bytes. Rust's rules require every byte of a slice to be initialized, even if the function will overwrite them. Passing uninitialized memory could let the compiler make unsafe assumptions, but the commit message says no actual exploit or data leak was observed. The fix initializes buffers before handing them to Rust and switches some helpers to use raw pointers so they can legally write into uninitialized memory.
Treat as a hardening/security-improvement commit. No immediate CVE or advisory is warranted based on the commit content alone, but downstream consumers should ensure all C callers of Rust FFI functions initialize buffers before constructing `BytesMut`. Review remaining call sites of `rust_util_bytes_mut` for similar patterns.
Security signals we found
Undefined behavior at C/Rust FFI due to uninitialized buffers being treated as Rust slices
Potential optimizer-dependent behavior from violating Rust slice initialization rules
Hardening of cryptographic output paths (SHA-256, HMAC-SHA256, HMAC-SHA512)
Hardening of secret-handling paths (Optiga shared secret, U2F seed, memory reset random bytes)
Safety documentation updated to require initialized/readable/writable buffers
Evidence from the diff
The patch addresses undefined behavior (UB) at the C/Rust FFI boundary. Several C callers passed uninitialized output buffers into rust_util_bytes_mut, which constructs a BytesMut (effectively a Rust &mut [u8]). Rust requires slice backing storage to be initialized; reading or even constructing a slice from uninitialized memory is formal UB. The commit adds memset calls before FFI calls in bootloader formatting, DA14531 protocol framing, U2F app string conversion, BIP39 word lookup, Optiga datastore reads, and hex encoding. It also initializes local arrays in factorysetup RTT receive, memory reset, and U2F keyhandle generation. In Rust, rust_util_zero is changed from taking a BytesMut to taking a raw pointer/length pair so it can legally zero memory without requiring initialization. SHA/HMAC helpers now write results via raw-pointer copy rather than constructing mutable slices from potentially uninitialized C buffers. Safety comments are updated to document the ‘initialized’ invariant.
Changed components
src/bootloader/bootloader_format.csrc/da14531/da14531_protocol.csrc/factorysetup.csrc/memory/memory.csrc/optiga/pal/pal_os_datastore.csrc/rust/util/src/bytes.rssrc/rust/util/src/lib.rssrc/rust/util/src/log.rssrc/rust/util/src/sha2.rssrc/u2f.csrc/u2f/u2f_app.csrc/ui/components/trinary_input_string.csrc/util.cInspect captured patch +52 / −27
diff --git a/src/bootloader/bootloader_format.c b/src/bootloader/bootloader_format.c
index e78f8a4..c546902 100644
--- a/src/bootloader/bootloader_format.c
+++ b/src/bootloader/bootloader_format.c
@@ -10,12 +10,14 @@
void bootloader_format_pairing_code(char* out, size_t out_len, uint32_t pairing_code)
{
ASSERT(out_len >= sizeof("000000"));
+ memset(out, 0, out_len);
rust_format_uint(rust_util_bytes_mut((uint8_t*)out, out_len), pairing_code, 6, '0');
}
void bootloader_format_progress(char* out, size_t out_len, float progress)
{
ASSERT(out_len >= sizeof("100%"));
+ memset(out, 0, out_len);
size_t out_pos = rust_format_uint(
rust_util_bytes_mut((uint8_t*)out, out_len - 1), (uint32_t)(100 * progress), 2, ' ');
out[out_pos++] = '%';
@@ -35,6 +37,7 @@ void bootloader_format_hash_multiline(char* out, size_t out_len, const char* has
void bootloader_format_timer(char* out, size_t out_len, uint8_t seconds)
{
ASSERT(out_len >= sizeof("99s"));
+ memset(out, 0, out_len);
size_t out_pos =
rust_format_uint(rust_util_bytes_mut((uint8_t*)out, out_len - 1), seconds, 1, '0');
out[out_pos++] = 's';
@@ -48,6 +51,7 @@ void bootloader_format_ble_firmware_version(
const uint8_t* hash)
{
ASSERT(out_len >= sizeof("ble: 65535 (00112233)"));
+ memset(out, 0, out_len);
util_strlcpy(out, "ble: ", out_len);
size_t out_pos = strlen(out);
@@ -66,6 +70,7 @@ void bootloader_format_unknown_command(char* out, size_t out_len, uint8_t comman
const char suffix[] = " unknown";
ASSERT(out_len >= sizeof("Command: 255 unknown"));
+ memset(out, 0, out_len);
size_t out_pos = sizeof(prefix) - 1;
memcpy(out, prefix, out_pos);
out_pos += rust_format_uint(
diff --git a/src/da14531/da14531_protocol.c b/src/da14531/da14531_protocol.c
index e6d991f..5e0ae12 100644
--- a/src/da14531/da14531_protocol.c
+++ b/src/da14531/da14531_protocol.c
@@ -329,6 +329,7 @@ uint16_t da14531_protocol_format(
const uint8_t* payload,
uint16_t payload_len)
{
+ memset(buf, 0, buf_len);
return rust_da14531_protocol_format(
rust_util_bytes_mut(buf, buf_len),
(ProtocolPacketType)type,
diff --git a/src/factorysetup.c b/src/factorysetup.c
index be591eb..e7b99d9 100644
--- a/src/factorysetup.c
+++ b/src/factorysetup.c
@@ -860,7 +860,7 @@ static void _rtt_send(const uint8_t* msg, size_t len)
static bool _rtt_receive(uint8_t* msg_out, size_t* len_out)
{
- uint8_t buffer[BUFFER_SIZE_DOWN]; // Adjust size as needed
+ uint8_t buffer[BUFFER_SIZE_DOWN] = {0}; // Adjust size as needed
while (1) {
int read = rust_rtt_ch0_read(buffer, sizeof(buffer));
if (read == 0) {
diff --git a/src/memory/memory.c b/src/memory/memory.c
index b73fef2..c544866 100644
--- a/src/memory/memory.c
+++ b/src/memory/memory.c
@@ -445,7 +445,7 @@ bool memory_reset_hww(void)
// Reset bond-db and reinitialize IRK and identity address
if (memory_get_platform() == MEMORY_PLATFORM_BITBOX02_PLUS) {
- uint8_t random_bytes[32];
+ uint8_t random_bytes[32] = {0};
_interface_functions->random_32_bytes(&random_bytes[0]);
chunk_shared_t chunk_shared = {0};
memory_read_shared_bootdata(&chunk_shared);
diff --git a/src/optiga/pal/pal_os_datastore.c b/src/optiga/pal/pal_os_datastore.c
index 6b394b2..c245810 100644
--- a/src/optiga/pal/pal_os_datastore.c
+++ b/src/optiga/pal/pal_os_datastore.c
@@ -37,6 +37,7 @@
#include "pal_os_datastore.h"
#include <rust/rust.h>
+#include <string.h>
#include <util.h>
/// @cond hidden
@@ -64,6 +65,7 @@ pal_status_t pal_os_datastore_read(
switch (datastore_id) {
case OPTIGA_PLATFORM_BINDING_SHARED_SECRET_ID: {
+ memset(p_buffer, 0, 32);
rust_memory_get_io_protection_key(rust_util_bytes_mut(p_buffer, 32));
*p_buffer_length = 32;
return_status = PAL_STATUS_SUCCESS;
diff --git a/src/rust/util/src/bytes.rs b/src/rust/util/src/bytes.rs
index e9b3cc5..b5dec4f 100644
--- a/src/rust/util/src/bytes.rs
+++ b/src/rust/util/src/bytes.rs
@@ -153,8 +153,9 @@ pub unsafe extern "C" fn rust_util_bytes(buf: *const c_uchar, len: usize) -> Byt
///
/// # Safety
///
-/// `buf` must point to a valid memory area of size `len`, unless `len == 0`, in which case `buf`
-/// may be NULL.
+/// `buf` must point to an initialized, valid memory area of size `len`, unless `len == 0`, in
+/// which case `buf` may be NULL. The memory must remain writable and exclusively borrowed for the
+/// lifetime of the returned value.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_util_bytes_mut(buf: *mut c_uchar, len: usize) -> BytesMut {
BytesMut { buf, len }
diff --git a/src/rust/util/src/lib.rs b/src/rust/util/src/lib.rs
index b343be2..28caccc 100644
--- a/src/rust/util/src/lib.rs
+++ b/src/rust/util/src/lib.rs
@@ -38,12 +38,18 @@ pub fn zero(dst: &mut [u8]) {
/// Zero a buffer using volatile writes. Accepts null-ptr and 0-length buffers and does nothing.
///
/// * `dst` - Buffer to zero
+///
+/// # Safety
+///
+/// `dst` must point to a writable memory area of size `len`, unless it is null or `len == 0`.
#[unsafe(no_mangle)]
-pub extern "C" fn rust_util_zero(mut dst: bytes::BytesMut) {
- if dst.buf.is_null() || dst.len == 0 {
+pub unsafe extern "C" fn rust_util_zero(dst: *mut u8, len: usize) {
+ if dst.is_null() || len == 0 {
return;
}
- zero(dst.as_mut())
+ for i in 0..len {
+ unsafe { core::ptr::write_volatile(dst.add(i), 0) };
+ }
}
// # Tests
@@ -78,18 +84,18 @@ mod tests {
#[test]
fn zeroing_ciface() {
let mut buf = [1u8, 2, 3, 4];
- rust_util_zero(unsafe { bytes::rust_util_bytes_mut(buf.as_mut_ptr(), buf.len() - 1) });
+ unsafe { rust_util_zero(buf.as_mut_ptr(), buf.len() - 1) };
assert_eq!(&buf[..], &[0, 0, 0, 4]);
}
#[test]
fn zeroing_ciface_empty() {
let mut buf = [];
- rust_util_zero(unsafe { bytes::rust_util_bytes_mut(buf.as_mut_ptr(), 0) });
+ unsafe { rust_util_zero(buf.as_mut_ptr(), 0) };
}
#[test]
fn zeroing_ciface_null() {
- rust_util_zero(unsafe { bytes::rust_util_bytes_mut(core::ptr::null_mut(), 0) });
+ unsafe { rust_util_zero(core::ptr::null_mut(), 0) };
}
}
diff --git a/src/rust/util/src/log.rs b/src/rust/util/src/log.rs
index 6f497c4..f96330b 100644
--- a/src/rust/util/src/log.rs
+++ b/src/rust/util/src/log.rs
@@ -95,7 +95,7 @@ pub unsafe extern "C" fn rust_log(ptr: *const core::ffi::c_char) {
/// # Safety
///
-/// The pointer `data` must point to a buffer of length `len`.
+/// The pointer `data` must point to an initialized, readable buffer of length `len`.
#[unsafe(no_mangle)]
#[allow(static_mut_refs)]
#[cfg_attr(not(all(feature = "rtt", target_os = "none")), allow(unused))]
@@ -113,7 +113,7 @@ pub unsafe extern "C" fn rust_rtt_ch1_write(data: *const u8, len: usize) {
/// # Safety
///
-/// The pointer `data` must point to a buffer of length `len`.
+/// The pointer `data` must point to an initialized, writable buffer of length `len`.
#[unsafe(no_mangle)]
#[allow(static_mut_refs)]
#[cfg_attr(not(all(feature = "rtt", target_os = "none")), allow(unused))]
diff --git a/src/rust/util/src/sha2.rs b/src/rust/util/src/sha2.rs
index 00b812c..a9db4c7 100644
--- a/src/rust/util/src/sha2.rs
+++ b/src/rust/util/src/sha2.rs
@@ -20,6 +20,11 @@ fn hmac_sha256_result(key: &[u8], data: &[u8]) -> [u8; 32] {
hmac_result.to_byte_array()
}
+unsafe fn write_output(out: *mut c_uchar, value: &[u8]) {
+ assert!(!out.is_null());
+ unsafe { core::ptr::copy(value.as_ptr(), out, value.len()) };
+}
+
pub fn sha256(data: &[u8], out: &mut [u8; 32]) {
out.copy_from_slice(&sha256_result(data));
}
@@ -60,11 +65,10 @@ pub unsafe extern "C" fn rust_sha256_update(ctx: *mut c_void, data: *const c_voi
/// used anymore.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_sha256_finish(ctx: *mut *mut c_void, out: *mut c_uchar) {
- let out = unsafe { core::slice::from_raw_parts_mut(out, 32) };
#[allow(clippy::cast_ptr_alignment)] // ctx is properly aligned, see `Box::into_raw`.
let hasher = unsafe { Box::from_raw(*ctx as *mut Sha256) }; // dropped at the end
let hash = hasher.finalize();
- out.copy_from_slice(&hash[..]);
+ unsafe { write_output(out, &hash) };
unsafe { *ctx = core::ptr::null_mut() };
}
@@ -77,8 +81,7 @@ pub unsafe extern "C" fn rust_sha256(data: *const c_void, len: usize, out: *mut
let data = unsafe { core::slice::from_raw_parts(data as *const u8, len) };
sha256_result(data)
};
- let out = unsafe { core::slice::from_raw_parts_mut(out, 32) };
- out.copy_from_slice(&result);
+ unsafe { write_output(out, &result) };
}
/// # Safety
@@ -101,8 +104,7 @@ pub unsafe extern "C" fn rust_hmac_sha256(
let data = unsafe { core::slice::from_raw_parts(data as *const u8, data_len) };
hmac_sha256_result(key, data)
};
- let out = unsafe { core::slice::from_raw_parts_mut(out, 32) };
- out.copy_from_slice(&result);
+ unsafe { write_output(out, &result) };
}
/// # Safety
@@ -132,8 +134,7 @@ pub unsafe extern "C" fn rust_hmac_sha512(
hmac_result.to_byte_array()
};
- let out = unsafe { core::slice::from_raw_parts_mut(out, 64) };
- out.copy_from_slice(&result);
+ unsafe { write_output(out, &result) };
}
#[cfg(test)]
@@ -169,10 +170,15 @@ mod tests {
#[test]
fn test_sha256() {
let data = b"foo abc def xyz bar";
- let mut result = [0u8; 32];
- unsafe {
- rust_sha256(data.as_ptr() as *const _, data.len(), result.as_mut_ptr());
- }
+ let mut result = core::mem::MaybeUninit::<[u8; 32]>::uninit();
+ let result = unsafe {
+ rust_sha256(
+ data.as_ptr() as *const _,
+ data.len(),
+ result.as_mut_ptr().cast(),
+ );
+ result.assume_init()
+ };
assert_eq!(result, &Sha256::digest(b"foo abc def xyz bar")[..]);
}
diff --git a/src/u2f.c b/src/u2f.c
index 1af6a76..3b75f03 100644
--- a/src/u2f.c
+++ b/src/u2f.c
@@ -243,7 +243,7 @@ USE_RESULT static bool _keyhandle_gen(
uint8_t* mac)
{
uint8_t hmac_in[U2F_APPID_SIZE + U2F_NONCE_LENGTH];
- uint8_t seed[32];
+ uint8_t seed[32] = {0};
UTIL_CLEANUP_32(seed);
if (!rust_keystore_get_u2f_seed(rust_util_bytes_mut(seed, sizeof(seed)))) {
return false;
diff --git a/src/u2f/u2f_app.c b/src/u2f/u2f_app.c
index 2fed797..8b9fbb3 100644
--- a/src/u2f/u2f_app.c
+++ b/src/u2f/u2f_app.c
@@ -9,6 +9,7 @@
#include <stddef.h>
#include <stdio.h>
+#include <string.h>
#define APPID_BOGUS_CHROMIUM "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
#define APPID_BOGUS_FIREFOX "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
@@ -24,6 +25,7 @@ static struct {
// out: string,
static void _app_string(const uint8_t* app_id, char* out, size_t out_len)
{
+ memset(out, 0, out_len);
rust_u2f_app_string(rust_util_bytes(app_id, 32), rust_util_bytes_mut((uint8_t*)out, out_len));
}
diff --git a/src/ui/components/trinary_input_string.c b/src/ui/components/trinary_input_string.c
index 43514f5..5fb172b 100644
--- a/src/ui/components/trinary_input_string.c
+++ b/src/ui/components/trinary_input_string.c
@@ -51,6 +51,7 @@ static const UG_FONT* _font = &font_password_11X12;
static void _get_bip39_word_stack(uint16_t idx, char* word_out, size_t word_out_size)
{
+ memset(word_out, 0, word_out_size);
if (!rust_get_bip39_word(idx, rust_util_bytes_mut((uint8_t*)word_out, word_out_size))) {
Abort("_get_bip39_word_stack");
}
diff --git a/src/util.c b/src/util.c
index 9273f01..a4215e7 100644
--- a/src/util.c
+++ b/src/util.c
@@ -18,12 +18,12 @@ void util_zero(volatile void* dst, size_t len)
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers"
- rust_util_zero(rust_util_bytes_mut(dst, len));
+ rust_util_zero(dst, len);
#pragma clang diagnostic pop
#else
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdiscarded-qualifiers"
- rust_util_zero(rust_util_bytes_mut(dst, len));
+ rust_util_zero(dst, len);
#pragma GCC diagnostic pop
#endif
}
@@ -41,6 +41,7 @@ void util_strlcpy(char* dst, const char* src, size_t dst_len)
void util_uint8_to_hex(const uint8_t* in_bin, const size_t in_len, char* out)
{
+ memset(out, 0, in_len * 2 + 1);
rust_util_uint8_to_hex(
rust_util_bytes(in_bin, in_len), rust_util_bytes_mut((uint8_t*)out, in_len * 2 + 1));
}
Why this scored 39/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.