feat(core/rust): move nrf and smp out of trezor_lib
What changed, and why it matters
This commit is a straightforward internal code reorganization. It moves several low-level hardware communication modules (NRF radio, SMP firmware-update protocol, IRQ utilities, and C-string helpers) into different Rust crates so the project structure is cleaner. No security vulnerability is introduced or fixed; it is purely a refactoring change.
No security action required. Treat as normal refactoring; review for build/feature-flag correctness during regular CI.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change relocates smp and trezorhal::nrf into the io crate, irq into sys::irq, and from_c_str/from_c_array into rtl::util. Corresponding Cargo feature flags are updated so projects depend on io/nrf, io/smp, etc., instead of trezor_lib/nrf or trezor_lib/smp. Bindings generation is moved to the owning crates (io/nrf/build.rs, sys/irq/build.rs). The actual logic of the moved functions remains identical.
Changed components
core/embed/iocore/embed/sys/irqcore/embed/rtl/utilcore/embed/rust/trezorhalcore/embed/projects/* Cargo.toml feature flagsInspect captured patch +123 / −86
### core/embed/Cargo.lock
@@ -448,8 +448,12 @@ dependencies = [
name = "io"
version = "0.0.0"
dependencies = [
+ "bindgen",
"color-eyre",
+ "cty",
+ "minicbor",
"models",
+ "rtl",
"sec",
"sys",
"xbuild",
### core/embed/io/Cargo.toml
@@ -5,11 +5,16 @@ edition = "2024"
links = "io"
[build-dependencies]
+bindgen.workspace = true
color-eyre.workspace = true
xbuild.workspace = true
[dependencies]
+cty.workspace = true
+minicbor = { workspace = true, optional = true }
+
models.workspace = true
+rtl.workspace = true
sec.workspace = true
sys.workspace = true
@@ -60,7 +65,7 @@ raspi_emulator = []
rgb_led = []
sbu = []
sd_card = []
-smp = []
+smp = ["minicbor", "nrf"]
suspend = ["sec/suspend"]
touch = []
touch_wakeup = []
### core/embed/io/nrf/build.rs
@@ -2,6 +2,7 @@ use xbuild::{CLibrary, Result, bail_unsupported};
pub fn def_module(lib: &mut CLibrary) -> Result<()> {
lib.add_include("nrf/inc");
+ lib.add_rust_bindings(add_rust_bindings)?;
lib.add_define("USE_NRF", Some("1"));
@@ -34,3 +35,11 @@ pub fn def_module(lib: &mut CLibrary) -> Result<()> {
Ok(())
}
+
+fn add_rust_bindings(builder: bindgen::Builder) -> Result<bindgen::Builder> {
+ let builder = builder
+ .header("nrf/inc/io/nrf.h")
+ .allowlist_function("nrf_send_uart_data");
+
+ Ok(builder)
+}
### core/embed/io/src/ffi.rs
@@ -0,0 +1,5 @@
+#![allow(non_camel_case_types)]
+#![allow(non_upper_case_globals)]
+#![allow(dead_code)]
+
+include!(concat!(env!("OUT_DIR"), "/io.rs"));
### core/embed/io/src/lib.rs
@@ -3,6 +3,13 @@
#![feature(custom_test_frameworks)]
#![reexport_test_harness_main = "test_main"]
+mod ffi;
+
+#[cfg(feature = "nrf")]
+pub mod nrf;
+#[cfg(feature = "smp")]
+pub mod smp;
+
#[cfg(test)]
#[unsafe(no_mangle)]
pub fn main() -> i32 {
### core/embed/io/src/nrf.rs
[binary or diff unavailable]
### core/embed/io/src/smp/api.rs
@@ -1,14 +1,16 @@
+use rtl::error::unwrap;
+use rtl::util::from_c_array;
+
use super::{echo, image_info, process_rx_byte, reset, upload};
-use crate::util::from_c_array;
-#[no_mangle]
+#[unsafe(no_mangle)]
extern "C" fn smp_echo(text: *const cty::c_char, text_len: u8) -> bool {
let text = unwrap!(unsafe { from_c_array(text, text_len as usize) });
echo::send(text)
}
-#[no_mangle]
+#[unsafe(no_mangle)]
extern "C" fn smp_reset() {
reset::send();
}
@@ -32,7 +34,7 @@ pub struct NrfAppVersion {
///
/// # Returns
/// `true` if version was successfully retrieved and parsed, `false` otherwise.
-#[no_mangle]
+#[unsafe(no_mangle)]
extern "C" fn smp_image_version_get(out: *mut NrfAppVersion) -> bool {
if out.is_null() {
return false;
@@ -51,7 +53,7 @@ extern "C" fn smp_image_version_get(out: *mut NrfAppVersion) -> bool {
}
}
-#[no_mangle]
+#[unsafe(no_mangle)]
extern "C" fn smp_upload_app_image(
data: *const cty::uint8_t,
len: cty::size_t,
@@ -64,7 +66,7 @@ extern "C" fn smp_upload_app_image(
upload::upload_image(data_slice, hash_slice)
}
-#[no_mangle]
+#[unsafe(no_mangle)]
extern "C" fn smp_process_rx_byte(byte: u8) {
process_rx_byte(byte)
}
### core/embed/io/src/smp/base64.rs
[binary or diff unavailable]
### core/embed/io/src/smp/crc16.rs
[binary or diff unavailable]
### core/embed/io/src/smp/echo.rs
@@ -1,10 +1,11 @@
use minicbor::data::Type;
-use minicbor::{decode, Decoder, Encoder};
+use minicbor::{Decoder, Encoder, decode};
use sys::time::Duration;
+use rtl::error::unwrap;
use super::{
- receiver_acquire, receiver_release, send_request, wait_for_response, MsgType, SmpBuffer,
- SmpHeader, SMP_CMD_ID_ECHO, SMP_GROUP_OS, SMP_HEADER_SIZE, SMP_OP_READ,
+ MsgType, SMP_CMD_ID_ECHO, SMP_GROUP_OS, SMP_HEADER_SIZE, SMP_OP_READ, SmpBuffer, SmpHeader,
+ receiver_acquire, receiver_release, send_request, wait_for_response,
};
pub fn send(text: &str) -> bool {
### core/embed/io/src/smp/image_info.rs
@@ -1,10 +1,11 @@
use minicbor::data::Type;
-use minicbor::{decode, Decoder, Encoder};
+use minicbor::{Decoder, Encoder, decode};
+use rtl::error::unwrap;
use sys::time::Duration;
use super::{
- receiver_acquire, receiver_release, send_request, wait_for_response, MsgType, SmpBuffer,
- SmpHeader, SMP_CMD_ID_IMAGE_STATE, SMP_GROUP_IMAGE, SMP_HEADER_SIZE, SMP_OP_READ,
+ MsgType, SMP_CMD_ID_IMAGE_STATE, SMP_GROUP_IMAGE, SMP_HEADER_SIZE, SMP_OP_READ, SmpBuffer,
+ SmpHeader, receiver_acquire, receiver_release, send_request, wait_for_response,
};
/// MCUboot-compatible version structure matching image header format
### core/embed/io/src/smp/mod.rs
@@ -12,10 +12,11 @@ use core::convert::Infallible;
use base64::{base64_decode, base64_encode};
use crc16::crc16_itu_t;
use minicbor::encode::write::Write;
+use rtl::error::{fatal_error, unwrap};
+use sys::irq::{irq_lock, irq_unlock};
use sys::time::{Duration, Instant};
-use crate::trezorhal::irq::{irq_lock, irq_unlock};
-use crate::trezorhal::nrf::send_data;
+use crate::nrf::send_data;
pub const SMP_HEADER_SIZE: usize = 8;
### core/embed/io/src/smp/reset.rs
@@ -1,8 +1,9 @@
use minicbor::Encoder;
+use rtl::error::unwrap;
use super::{
- send_request, SmpBuffer, SmpHeader, SMP_CMD_ID_RESET, SMP_GROUP_OS, SMP_HEADER_SIZE,
- SMP_OP_READ,
+ SMP_CMD_ID_RESET, SMP_GROUP_OS, SMP_HEADER_SIZE, SMP_OP_READ, SmpBuffer, SmpHeader,
+ send_request,
};
pub fn send() {
### core/embed/io/src/smp/upload.rs
@@ -1,9 +1,10 @@
use minicbor::Encoder;
+use rtl::error::unwrap;
use sys::time::Duration;
use super::{
- receiver_acquire, receiver_release, send_request, wait_for_response, MsgType, SmpBuffer,
- SmpHeader, SMP_CMD_ID_IMAGE_UPLOAD, SMP_GROUP_IMAGE, SMP_HEADER_SIZE, SMP_OP_WRITE,
+ MsgType, SMP_CMD_ID_IMAGE_UPLOAD, SMP_GROUP_IMAGE, SMP_HEADER_SIZE, SMP_OP_WRITE, SmpBuffer,
+ SmpHeader, receiver_acquire, receiver_release, send_request, wait_for_response,
};
const CHUNK_SIZE: usize = 256;
### core/embed/projects/bootloader/Cargo.toml
@@ -79,7 +79,7 @@ hash_processor = ["sec/hash_processor"]
iwdg = ["sec/iwdg"]
lockable_bootloader = ["io/lockable_bootloader"]
mcu_attestation = ["sec/mcu_attestation"]
-nrf = ["io/nrf", "trezor_lib/nrf"]
+nrf = ["io/nrf"]
nrf_auth = ["io/nrf_auth"]
power_manager = ["io/power_manager", "trezor_lib/power_manager", "trezor_lib/pmic"]
pvd = ["sys/pvd"]
### core/embed/projects/firmware/Cargo.toml
@@ -119,7 +119,7 @@ hash_processor = ["sec/hash_processor"]
iwdg = ["sec/iwdg"]
lockable_bootloader = ["io/lockable_bootloader"]
mcu_attestation = ["sec/mcu_attestation", "upymod/mcu_attestation"]
-nrf = ["io/nrf", "trezor_lib/nrf"]
+nrf = ["io/nrf"]
nrf_auth = ["io/nrf_auth"]
optiga = ["sec/optiga", "upymod/optiga", "trezor_lib/optiga"]
power_manager = [
### core/embed/projects/kernel/Cargo.toml
@@ -81,7 +81,7 @@ secmon_layout = ["models/secmon_layout"]
secret = ["sec/secret"]
secure_aes = ["sec/secure_aes"]
secure_mode = ["io/secure_mode"]
-smp = ["io/smp", "trezor_lib/smp", "trezor_lib/nrf"]
+smp = ["io/smp"]
suspend = ["io/suspend"]
tamper = ["sec/tamper"]
telemetry = ["sec/telemetry"]
### core/embed/projects/prodtest/Cargo.toml
@@ -79,7 +79,7 @@ iwdg = ["sec/iwdg"]
lockable_bootloader = ["io/lockable_bootloader"]
mcu_attestation = ["sec/mcu_attestation"]
nfc = ["io/nfc"]
-nrf = ["io/nrf", "trezor_lib/nrf"]
+nrf = ["io/nrf"]
nrf_auth = ["io/nrf_auth"]
optiga = [
"sec/optiga",
@@ -100,7 +100,7 @@ sdram = ["sys/sdram"]
secmon_header = []
secret = ["sec/secret"]
secure_aes = ["sec/secure_aes"]
-smp = ["io/smp", "trezor_lib/smp"]
+smp = ["io/smp"]
suspend = ["io/suspend"]
tamper = ["sec/tamper"]
telemetry = ["sec/telemetry", "trezor_lib/telemetry"]
### core/embed/projects/unix/Cargo.toml
@@ -117,7 +117,7 @@ hash_processor = ["sec/hash_processor"]
iwdg = ["sec/iwdg"]
lockable_bootloader = ["io/lockable_bootloader"]
mcu_attestation = ["sec/mcu_attestation", "upymod/mcu_attestation"]
-nrf = ["io/nrf", "trezor_lib/nrf"]
+nrf = ["io/nrf"]
nrf_auth = ["io/nrf_auth"]
optiga = ["sec/optiga", "upymod/optiga", "trezor_lib/optiga"]
power_manager = ["io/power_manager", "upymod/power_manager", "trezor_lib/power_manager", "trezor_lib/pmic"]
### core/embed/rtl/src/util.rs
@@ -85,6 +85,50 @@ impl From<&str> for FatPtr<cty::c_char> {
}
}
+/// Constructs a string from a C string.
+///
+/// # Safety
+///
+/// The caller is responsible that the pointer is valid, which means that:
+/// (a) it points to a memory containing a valid C string (zero-terminated
+/// sequence of characters), and
+/// (b) that the pointer has appropriate lifetime.
+pub unsafe fn from_c_str<'a>(c_str: *const cty::c_char) -> Option<&'a str> {
+ if c_str.is_null() {
+ return None;
+ }
+ unsafe {
+ let bytes = core::ffi::CStr::from_ptr(c_str as _).to_bytes();
+ if bytes.is_ascii() {
+ Some(core::str::from_utf8_unchecked(bytes))
+ } else {
+ None
+ }
+ }
+}
+
+/// Construct str from a C array.
+///
+/// # Safety
+///
+/// The caller is responsible that the pointer is valid, which means that:
+/// (a) it points to a memory containing array of characters, with length `len`,
+/// and
+/// (b) that the pointer has appropriate lifetime.
+pub unsafe fn from_c_array<'a>(c_str: *const cty::c_char, len: usize) -> Option<&'a str> {
+ if c_str.is_null() {
+ return None;
+ }
+ unsafe {
+ let slice = core::slice::from_raw_parts(c_str as *const u8, len);
+ if slice.is_ascii() {
+ Some(core::str::from_utf8_unchecked(slice))
+ } else {
+ None
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
### core/embed/rust/Cargo.toml
@@ -88,7 +88,6 @@ sbu = []
sd_card = []
secmon_layout = []
serial_number = []
-smp = []
storage = []
telemetry = []
thp = ["crypto/thp", "dep:trezor-thp", "dep:zeroize"]
@@ -146,7 +145,6 @@ test = [
"nrf",
"optiga",
"protobuf",
- "smp",
"storage",
"thp",
"touch",
### core/embed/rust/build.rs
@@ -325,9 +325,6 @@ fn generate_trezorhal_bindings(lib: &mut CLibrary) -> Result<()> {
.allowlist_function("pm_hibernate")
.allowlist_function("pm_charging_enable")
.allowlist_function("pm_charging_disable")
- // irq
- .allowlist_function("irq_lock_fn")
- .allowlist_function("irq_unlock_fn")
// nrf
.allowlist_function("nrf_send_uart_data")
// c_layout
### core/embed/rust/src/lib.rs
@@ -50,9 +50,6 @@ mod trezorhal;
#[cfg(feature = "ui")]
pub mod ui;
-#[cfg(feature = "smp")]
-pub mod smp;
-
pub mod util;
#[cfg(feature = "bootloader")]
### core/embed/rust/src/trezorhal/mod.rs
@@ -43,9 +43,3 @@ pub mod bootloader;
#[cfg(any(feature = "bootloader", feature = "prodtest"))]
pub mod layout_buf;
-
-#[cfg(feature = "nrf")]
-pub mod irq;
-
-#[cfg(feature = "nrf")]
-pub mod nrf;
### core/embed/rust/src/ui/api/bootloader_c.rs
@@ -1,7 +1,8 @@
+use rtl::util::{from_c_array, from_c_str};
+
use crate::strutil::hexlify;
use crate::ui::ui_bootloader::BootloaderUI;
use crate::ui::ModelUI;
-use crate::util::{from_c_array, from_c_str};
#[no_mangle]
extern "C" fn screen_welcome(ui_action_result: *mut u32) -> u32 {
### core/embed/rust/src/ui/api/common_c.rs
@@ -1,10 +1,11 @@
//! Reexporting the `screens` module according to the
//! current feature (Trezor model)
+use rtl::util::from_c_str;
+
#[cfg(feature = "ui_debug")]
use crate::ui::util::set_animation_disabled;
use crate::ui::{shape, CommonUI, ModelUI};
-use crate::util::from_c_str;
#[no_mangle]
extern "C" fn display_rsod_rust(
### core/embed/rust/src/ui/api/prodtest_c.rs
@@ -2,6 +2,7 @@
use cty::int16_t;
#[cfg(feature = "touch")]
use heapless::Vec;
+use rtl::util::from_c_array;
use crate::trezorhal::layout_buf::{c_layout_t, LayoutBuffer};
use crate::trezorhal::sysevent::{parse_event, sysevents_t};
@@ -11,7 +12,6 @@ use crate::ui::ui_prodtest::{ProdtestLayoutType, ProdtestUI};
use crate::ui::ModelUI;
#[cfg(feature = "touch")]
use crate::ui::{event::TouchEvent, layout::simplified::touch_unpack};
-use crate::util::from_c_array;
#[no_mangle]
extern "C" fn screen_prodtest_event(layout: *mut c_layout_t, signalled: &sysevents_t) -> u32 {
### core/embed/rust/src/util/mod.rs
@@ -2,47 +2,3 @@
pub mod interpolate;
#[cfg(feature = "dbg_console")]
pub mod logger;
-
-/// Constructs a string from a C string.
-///
-/// # Safety
-///
-/// The caller is responsible that the pointer is valid, which means that:
-/// (a) it points to a memory containing a valid C string (zero-terminated
-/// sequence of characters), and
-/// (b) that the pointer has appropriate lifetime.
-pub unsafe fn from_c_str<'a>(c_str: *const cty::c_char) -> Option<&'a str> {
- if c_str.is_null() {
- return None;
- }
- unsafe {
- let bytes = core::ffi::CStr::from_ptr(c_str as _).to_bytes();
- if bytes.is_ascii() {
- Some(core::str::from_utf8_unchecked(bytes))
- } else {
- None
- }
- }
-}
-
-/// Construct str from a C array.
-///
-/// # Safety
-///
-/// The caller is responsible that the pointer is valid, which means that:
-/// (a) it points to a memory containing array of characters, with length `len`,
-/// and
-/// (b) that the pointer has appropriate lifetime.
-pub unsafe fn from_c_array<'a>(c_str: *const cty::c_char, len: usize) -> Option<&'a str> {
- if c_str.is_null() {
- return None;
- }
- unsafe {
- let slice = core::slice::from_raw_parts(c_str as *const u8, len);
- if slice.is_ascii() {
- Some(core::str::from_utf8_unchecked(slice))
- } else {
- None
- }
- }
-}
### core/embed/sys/irq/build.rs
@@ -2,6 +2,7 @@ use xbuild::{CLibrary, Result, bail_unsupported};
pub fn def_module(lib: &mut CLibrary) -> Result<()> {
lib.add_include("irq/inc");
+ lib.add_rust_bindings(add_rust_bindings)?;
if cfg!(feature = "emulator") {
// No implementation
@@ -13,3 +14,13 @@ pub fn def_module(lib: &mut CLibrary) -> Result<()> {
Ok(())
}
+
+fn add_rust_bindings(builder: bindgen::Builder) -> Result<bindgen::Builder> {
+ let builder = builder
+ .header("irq/inc/sys/irq.h")
+ .allowlist_function("irq_lock_fn")
+ .allowlist_function("irq_unlock_fn")
+ .allowlist_type("irq_key_t");
+
+ Ok(builder)
+}
### core/embed/sys/src/irq.rs
[binary or diff unavailable]
### core/embed/sys/src/lib.rs
@@ -2,6 +2,7 @@
mod ffi;
+pub mod irq;
#[cfg(feature = "dbg_console")]
pub mod syslog;
Why this scored 15/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.