From 06f08b4a843fbcd75334c869fedfb85df2907b40 Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Fri, 26 Sep 2025 14:35:47 +0200 Subject: [PATCH 1/7] cmse: lint when a (partially) uninitialized value crosses the secure boundary When a union passes from secure to non-secure (so, passed as an argument to an nonsecure call, or returned by a nonsecure entry), warn that there may be secure information lingering in the unused or uninitialized parts of a union value. https://godbolt.org/z/vq9xnrnEs --- Cargo.lock | 1 + compiler/rustc_lint/Cargo.toml | 1 + .../rustc_lint/src/cmse_uninitialized_leak.rs | 181 ++++++++++++++++++ compiler/rustc_lint/src/lib.rs | 3 + compiler/rustc_lint/src/lints.rs | 10 + .../params-uninitialized.rs | 76 ++++++++ .../params-uninitialized.stderr | 59 ++++++ .../cmse-nonsecure-call/return-via-stack.rs | 6 +- .../return-via-stack.stderr | 2 +- .../return-uninitialized.rs | 76 ++++++++ .../return-uninitialized.stderr | 55 ++++++ .../cmse-nonsecure-entry/return-via-stack.rs | 23 +-- .../return-via-stack.stderr | 34 +--- 13 files changed, 477 insertions(+), 50 deletions(-) create mode 100644 compiler/rustc_lint/src/cmse_uninitialized_leak.rs create mode 100644 tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs create mode 100644 tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr create mode 100644 tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs create mode 100644 tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr diff --git a/Cargo.lock b/Cargo.lock index 6c14a9f26e175..e4848c0b4b342 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4293,6 +4293,7 @@ dependencies = [ "rustc_symbol_mangling", "rustc_target", "rustc_trait_selection", + "rustc_transmute", "smallvec", "tracing", "unicode-security", diff --git a/compiler/rustc_lint/Cargo.toml b/compiler/rustc_lint/Cargo.toml index 176890bab85f2..0d327820e23f7 100644 --- a/compiler/rustc_lint/Cargo.toml +++ b/compiler/rustc_lint/Cargo.toml @@ -25,6 +25,7 @@ rustc_span = { path = "../rustc_span" } rustc_symbol_mangling = { path = "../rustc_symbol_mangling" } rustc_target = { path = "../rustc_target" } rustc_trait_selection = { path = "../rustc_trait_selection" } +rustc_transmute = { path = "../rustc_transmute" } smallvec = { version = "1.8.1", features = ["union", "may_dangle"] } tracing = "0.1" unicode-security = "0.1.0" diff --git a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs new file mode 100644 index 0000000000000..43a9397a9a9b1 --- /dev/null +++ b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs @@ -0,0 +1,181 @@ +use rustc_abi::ExternAbi; +use rustc_hir::{self as hir, Expr, ExprKind}; +use rustc_middle::ty::layout::TyAndLayout; +use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; +use rustc_session::{declare_lint, declare_lint_pass}; + +use crate::{LateContext, LateLintPass, LintContext, lints}; + +declare_lint! { + /// The `cmse_uninitialized_leak` lint detects values that may be (partially) uninitialized that + /// cross the secure boundary. + /// + /// ### Example + /// + /// ```rust,ignore (ABI is only supported on thumbv8) + /// extern "cmse-nonsecure-entry" fn foo() -> MaybeUninit { + /// MaybeUninit::uninit() + /// } + /// ``` + /// + /// This will produce: + /// + /// ```text + /// warning: passing a union across the security boundary may leak information + /// --> lint_example.rs:2:5 + /// | + /// 2 | MaybeUninit::uninit() + /// | ^^^^^^^^^^^^^^^^^^^^^ + /// | + /// = note: the bits not used by the current variant may contain stale secure data + /// = note: `#[warn(cmse_uninitialized_leak)]` on by default + /// ``` + /// + /// ### Explanation + /// + /// The cmse calling conventions normally take care of clearing registers to make sure that + /// stale secure information is not observable from non-secure code. Uninitialized memory may + /// still contain secret information, so the programmer must be careful when (partially) + /// uninitialized values cross the secure boundary. This lint fires when a partially + /// uninitialized value (e.g. a `union` value or a type with a niche) crosses the secure + /// boundary, i.e.: + /// + /// - when returned from a `cmse-nonsecure-entry` function + /// - when passed as an argument to a `cmse-nonsecure-call` function + /// + /// This lint is a best effort: not all cases of (partially) uninitialized data crossing the + /// secure boundary are caught. + pub CMSE_UNINITIALIZED_LEAK, + Warn, + "(partially) uninitialized value may leak secure information" +} + +declare_lint_pass!(CmseUninitializedLeak => [CMSE_UNINITIALIZED_LEAK]); + +impl<'tcx> LateLintPass<'tcx> for CmseUninitializedLeak { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + check_cmse_entry_return(cx, expr); + check_cmse_call_call(cx, expr); + } +} + +fn check_cmse_call_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + let ExprKind::Call(callee, arguments) = expr.kind else { + return; + }; + + // Determine the callee ABI. + let callee_ty = cx.typeck_results().expr_ty(callee); + let sig = match callee_ty.kind() { + ty::FnPtr(poly_sig, header) if header.abi() == ExternAbi::CmseNonSecureCall => { + poly_sig.skip_binder() + } + _ => return, + }; + + let fn_sig = cx.tcx.erase_and_anonymize_regions(sig); + let typing_env = ty::TypingEnv::fully_monomorphized(); + + for (arg, ty) in arguments.iter().zip(fn_sig.inputs()) { + // `impl Trait` is not allowed in the argument types. + if ty.has_opaque_types() { + continue; + } + + let Ok(layout) = cx.tcx.layout_of(typing_env.as_query_input(*ty)) else { + continue; + }; + + if !is_transmutable_to_array_u8(cx.tcx, ty.clone(), layout) { + // Some part of the source type may be uninitialized. + cx.emit_span_lint( + CMSE_UNINITIALIZED_LEAK, + arg.span, + lints::CmseUninitializedMayLeakInformation, + ); + } + } +} + +fn check_cmse_entry_return<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + let owner = cx.tcx.hir_enclosing_body_owner(expr.hir_id); + + match cx.tcx.def_kind(owner) { + hir::def::DefKind::Fn | hir::def::DefKind::AssocFn => {} + _ => return, + } + + // Only continue if the current expr is an (implicit) return. + let body = cx.tcx.hir_body_owned_by(owner); + let is_implicit_return = expr.hir_id == body.value.hir_id; + if !(matches!(expr.kind, ExprKind::Ret(_)) || is_implicit_return) { + return; + } + + let sig = cx.tcx.fn_sig(owner).skip_binder(); + if sig.abi() != ExternAbi::CmseNonSecureEntry { + return; + } + + let fn_sig = cx.tcx.instantiate_bound_regions_with_erased(sig); + let fn_sig = cx.tcx.erase_and_anonymize_regions(fn_sig); + let return_type = fn_sig.output(); + + // `impl Trait` is not allowed in the return type. + if return_type.has_opaque_types() { + return; + } + + let typing_env = ty::TypingEnv::fully_monomorphized(); + + let Ok(ret_layout) = cx.tcx.layout_of(typing_env.as_query_input(return_type)) else { + return; + }; + + if !is_transmutable_to_array_u8(cx.tcx, return_type.clone(), ret_layout) { + let return_expr_span = if is_implicit_return { + match expr.kind { + ExprKind::Block(block, _) => match block.expr { + Some(tail) => tail.span, + None => expr.span, + }, + _ => expr.span, + } + } else { + expr.span + }; + + // Some part of the source type may be uninitialized. + cx.emit_span_lint( + CMSE_UNINITIALIZED_LEAK, + return_expr_span, + lints::CmseUninitializedMayLeakInformation, + ); + } +} + +/// Check whether the source type `T` can be safely transmuted to `[u8; size_of::()]`. +/// +/// If the transmute is valid, `T` must be fully initialized. Otherwise, parts of it may be +/// uninitialized, which should trigger the lint defined by this module. +fn is_transmutable_to_array_u8<'tcx>( + tcx: TyCtxt<'tcx>, + ty: Ty<'tcx>, + layout: TyAndLayout<'tcx>, +) -> bool { + use rustc_transmute::{Answer, Assume, TransmuteTypeEnv}; + + let mut transmute_env = TransmuteTypeEnv::new(tcx); + let assume = Assume { alignment: true, lifetimes: true, safety: false, validity: false }; + + let array_u8_ty = Ty::new_array_with_const_len( + tcx, + tcx.types.u8, + ty::Const::from_target_usize(tcx, layout.size.bytes()), + ); + + match transmute_env.is_transmutable(ty, array_u8_ty, assume) { + Answer::Yes => true, + Answer::No(_) | Answer::If(_) => false, + } +} diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs index 0e96b9f118790..eae7707e8aa64 100644 --- a/compiler/rustc_lint/src/lib.rs +++ b/compiler/rustc_lint/src/lib.rs @@ -33,6 +33,7 @@ mod async_fn_in_trait; mod autorefs; pub mod builtin; mod c_void_returns; +mod cmse_uninitialized_leak; mod context; mod dangling; mod default_could_be_derived; @@ -88,6 +89,7 @@ use async_fn_in_trait::AsyncFnInTrait; use autorefs::*; use builtin::*; use c_void_returns::*; +use cmse_uninitialized_leak::*; use dangling::*; use default_could_be_derived::DefaultCouldBeDerived; use deref_into_dyn_supertrait::*; @@ -270,6 +272,7 @@ late_lint_methods!( CheckTransmutes: CheckTransmutes, LifetimeSyntax: LifetimeSyntax, InternalEqTraitMethodImpls: InternalEqTraitMethodImpls, + CmseUninitializedLeak: CmseUninitializedLeak, ImplicitProvenanceCasts: ImplicitProvenanceCasts, CVoidReturns: CVoidReturns, ] diff --git a/compiler/rustc_lint/src/lints.rs b/compiler/rustc_lint/src/lints.rs index eb82afab13186..5e3abb65e965c 100644 --- a/compiler/rustc_lint/src/lints.rs +++ b/compiler/rustc_lint/src/lints.rs @@ -1826,6 +1826,16 @@ pub(crate) struct NonLocalDefinitionsCargoUpdateNote { pub crate_name: Symbol, } +// cmse_uninitialized_leak.rs +#[derive(Diagnostic)] +#[diag( + "passing a (partially) uninitialized value across the security boundary may leak information" +)] +#[note( + "padding or fields not used by the current variant of a union may contain stale secure data" +)] +pub(crate) struct CmseUninitializedMayLeakInformation; + // precedence.rs #[derive(Diagnostic)] #[diag("`-` has lower precedence than method calls, which might be unexpected")] diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs new file mode 100644 index 0000000000000..b07078c1c5875 --- /dev/null +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs @@ -0,0 +1,76 @@ +//@ add-minicore +//@ compile-flags: --target thumbv8m.main-none-eabi --crate-type lib +//@ needs-llvm-components: arm +//@ check-pass +#![feature(abi_cmse_nonsecure_call, no_core, lang_items)] +#![no_core] +#![allow(improper_ctypes_definitions)] + +extern crate minicore; +use minicore::*; + +#[repr(Rust)] +union ReprRustUnionU64 { + _unused: u64, +} + +#[repr(Rust)] +union ReprRustUnionPartiallyUninit { + _unused1: u32, + _unused2: u16, +} + +#[repr(C)] +union ReprCUnionU64 { + _unused: u64, + _unused1: u32, +} + +#[repr(C)] +struct ReprCAggregate { + a: usize, + b: ReprCUnionU64, +} + +// This is an aggregate that cannot be unwrapped, and has 1 (uninitialized) padding byte. +#[repr(C, align(4))] +struct PaddedStruct { + a: u8, + b: u16, +} + +#[no_mangle] +fn test_uninitialized( + f1: extern "cmse-nonsecure-call" fn(ReprRustUnionU64), + f2: extern "cmse-nonsecure-call" fn(ReprCUnionU64), + f3: extern "cmse-nonsecure-call" fn(MaybeUninit), + f4: extern "cmse-nonsecure-call" fn(MaybeUninit), + f5: extern "cmse-nonsecure-call" fn((usize, MaybeUninit)), + f6: extern "cmse-nonsecure-call" fn(ReprCAggregate), + f7: extern "cmse-nonsecure-call" fn(ReprRustUnionPartiallyUninit), + f8: extern "cmse-nonsecure-call" fn(PaddedStruct), +) { + // With `repr(Rust)` this union is always initialized. + f1(ReprRustUnionU64 { _unused: 1 }); + + f2(ReprCUnionU64 { _unused: 1 }); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + + f3(MaybeUninit::uninit()); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + + f4(MaybeUninit::uninit()); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + + f5((0, MaybeUninit::uninit())); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + + f6(ReprCAggregate { a: 0, b: ReprCUnionU64 { _unused: 1 } }); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + + f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + + f8(PaddedStruct { a: 0, b: 0 }); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information +} diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr new file mode 100644 index 0000000000000..3b922b2cb352e --- /dev/null +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr @@ -0,0 +1,59 @@ +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/params-uninitialized.rs:56:8 + | +LL | f2(ReprCUnionU64 { _unused: 1 }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: `#[warn(cmse_uninitialized_leak)]` on by default + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/params-uninitialized.rs:59:8 + | +LL | f3(MaybeUninit::uninit()); + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/params-uninitialized.rs:62:8 + | +LL | f4(MaybeUninit::uninit()); + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/params-uninitialized.rs:65:8 + | +LL | f5((0, MaybeUninit::uninit())); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/params-uninitialized.rs:68:8 + | +LL | f6(ReprCAggregate { a: 0, b: ReprCUnionU64 { _unused: 1 } }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/params-uninitialized.rs:71:8 + | +LL | f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/params-uninitialized.rs:74:8 + | +LL | f8(PaddedStruct { a: 0, b: 0 }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: 7 warnings emitted + diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs index a8c69216e2048..f31625a0d004f 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs @@ -52,11 +52,15 @@ pub union ReprCUnionU64 { _unused: u64, } +#[repr(C)] +pub union ReprCUnionU64 { + _unused: u64, +} + #[no_mangle] pub fn test_union( f1: extern "cmse-nonsecure-call" fn() -> ReprRustUnionU64, //~ ERROR [E0798] f2: extern "cmse-nonsecure-call" fn() -> ReprCUnionU64, //~ ERROR [E0798] - // MaybeUninit is a transparent union, and hence MaybeUninit is abi-compatible with u64, // and thus allowed as a return type. f3: extern "cmse-nonsecure-call" fn() -> MaybeUninit, diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.stderr index 6b7446abc8eb8..65bc9e748333c 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.stderr @@ -80,7 +80,7 @@ LL | f1: extern "cmse-nonsecure-call" fn() -> ReprRustUnionU64, = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size error[E0798]: return value of `"cmse-nonsecure-call"` function too large to pass via registers - --> $DIR/return-via-stack.rs:58:46 + --> $DIR/return-via-stack.rs:59:46 | LL | f2: extern "cmse-nonsecure-call" fn() -> ReprCUnionU64, | ^^^^^^^^^^^^^ this type doesn't fit in the available registers diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs new file mode 100644 index 0000000000000..3a4822980bb86 --- /dev/null +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs @@ -0,0 +1,76 @@ +//@ add-minicore +//@ compile-flags: --target thumbv8m.main-none-eabi --crate-type lib +//@ needs-llvm-components: arm +//@ check-pass + +#![feature(cmse_nonsecure_entry, no_core, lang_items)] +#![no_core] + +extern crate minicore; +use minicore::*; + +#[repr(Rust)] +union ReprRustUnionU32 { + _unused: u32, +} + +#[no_mangle] +#[allow(improper_ctypes_definitions)] +extern "cmse-nonsecure-entry" fn union_rust() -> ReprRustUnionU32 { + // With `repr(Rust)` value is always fully initialized. + ReprRustUnionU32 { _unused: 1 } +} + +#[repr(Rust)] +union ReprRustUnionPartiallyUninit { + _unused1: u32, + _unused2: u16, +} + +#[no_mangle] +#[allow(improper_ctypes_definitions)] +extern "cmse-nonsecure-entry" fn union_rust_partially_uninit() -> ReprRustUnionPartiallyUninit { + ReprRustUnionPartiallyUninit { _unused1: 1 } + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn maybe_uninit_32bit() -> MaybeUninit { + MaybeUninit::uninit() + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn maybe_uninit_64bit() -> MaybeUninit { + if true { + return MaybeUninit::new(6.28); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + } + MaybeUninit::new(3.14) + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information +} + +#[repr(transparent)] +struct Wrapper(MaybeUninit); + +#[no_mangle] +extern "cmse-nonsecure-entry" fn repr_transparent_union() -> Wrapper { + match 0 { + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + 0 => Wrapper(MaybeUninit::new(0)), + _ => Wrapper(MaybeUninit::new(1)), + } +} + +// This is an aggregate that cannot be unwrapped, and has 1 (uninitialized) padding byte. +#[repr(C, align(4))] +struct PaddedStruct { + a: u8, + b: u16, +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn padded_struct() -> PaddedStruct { + PaddedStruct { a: 0, b: 1 } + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information +} diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr new file mode 100644 index 0000000000000..5bc6a8292a23f --- /dev/null +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr @@ -0,0 +1,55 @@ +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/return-uninitialized.rs:33:5 + | +LL | ReprRustUnionPartiallyUninit { _unused1: 1 } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: `#[warn(cmse_uninitialized_leak)]` on by default + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/return-uninitialized.rs:39:5 + | +LL | MaybeUninit::uninit() + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/return-uninitialized.rs:49:5 + | +LL | MaybeUninit::new(3.14) + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/return-uninitialized.rs:46:9 + | +LL | return MaybeUninit::new(6.28); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/return-uninitialized.rs:58:5 + | +LL | / match 0 { +LL | | +LL | | 0 => Wrapper(MaybeUninit::new(0)), +LL | | _ => Wrapper(MaybeUninit::new(1)), +LL | | } + | |_____^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/return-uninitialized.rs:74:5 + | +LL | PaddedStruct { a: 0, b: 1 } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: 6 warnings emitted + diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-via-stack.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-via-stack.rs index bfc8ad4583599..f7309f1b16c56 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-via-stack.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-via-stack.rs @@ -5,6 +5,7 @@ #![feature(cmse_nonsecure_entry, no_core, lang_items)] #![no_core] +#![warn(cmse_uninitialized_leak)] extern crate minicore; use minicore::*; @@ -60,25 +61,3 @@ pub extern "cmse-nonsecure-entry" fn i128() -> i128 { //~^ ERROR [E0798] 456 } - -#[repr(Rust)] -pub union ReprRustUnionU64 { - _unused: u64, -} - -#[repr(C)] -pub union ReprCUnionU64 { - _unused: u64, -} - -#[no_mangle] -#[allow(improper_ctypes_definitions)] -pub extern "cmse-nonsecure-entry" fn union_rust() -> ReprRustUnionU64 { - //~^ ERROR [E0798] - ReprRustUnionU64 { _unused: 1 } -} -#[no_mangle] -pub extern "cmse-nonsecure-entry" fn union_c() -> ReprCUnionU64 { - //~^ ERROR [E0798] - ReprCUnionU64 { _unused: 2 } -} diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-via-stack.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-via-stack.stderr index c5effed92ae92..41d9b56dab63b 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-via-stack.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-via-stack.stderr @@ -1,5 +1,5 @@ error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:25:46 + --> $DIR/return-via-stack.rs:26:46 | LL | pub extern "cmse-nonsecure-entry" fn f1() -> ReprCU64 { | ^^^^^^^^ this type doesn't fit in the available registers @@ -8,7 +8,7 @@ LL | pub extern "cmse-nonsecure-entry" fn f1() -> ReprCU64 { = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:30:46 + --> $DIR/return-via-stack.rs:31:46 | LL | pub extern "cmse-nonsecure-entry" fn f2() -> ReprCBytes { | ^^^^^^^^^^ this type doesn't fit in the available registers @@ -17,7 +17,7 @@ LL | pub extern "cmse-nonsecure-entry" fn f2() -> ReprCBytes { = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:35:46 + --> $DIR/return-via-stack.rs:36:46 | LL | pub extern "cmse-nonsecure-entry" fn f3() -> U64Compound { | ^^^^^^^^^^^ this type doesn't fit in the available registers @@ -26,7 +26,7 @@ LL | pub extern "cmse-nonsecure-entry" fn f3() -> U64Compound { = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:40:46 + --> $DIR/return-via-stack.rs:41:46 | LL | pub extern "cmse-nonsecure-entry" fn f4() -> ReprCAlign16 { | ^^^^^^^^^^^^ this type doesn't fit in the available registers @@ -35,7 +35,7 @@ LL | pub extern "cmse-nonsecure-entry" fn f4() -> ReprCAlign16 { = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:47:46 + --> $DIR/return-via-stack.rs:48:46 | LL | pub extern "cmse-nonsecure-entry" fn f5() -> [u8; 5] { | ^^^^^^^ this type doesn't fit in the available registers @@ -44,7 +44,7 @@ LL | pub extern "cmse-nonsecure-entry" fn f5() -> [u8; 5] { = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:53:48 + --> $DIR/return-via-stack.rs:54:48 | LL | pub extern "cmse-nonsecure-entry" fn u128() -> u128 { | ^^^^ this type doesn't fit in the available registers @@ -53,7 +53,7 @@ LL | pub extern "cmse-nonsecure-entry" fn u128() -> u128 { = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:59:48 + --> $DIR/return-via-stack.rs:60:48 | LL | pub extern "cmse-nonsecure-entry" fn i128() -> i128 { | ^^^^ this type doesn't fit in the available registers @@ -61,24 +61,6 @@ LL | pub extern "cmse-nonsecure-entry" fn i128() -> i128 { = note: functions with the `"cmse-nonsecure-entry"` ABI must pass their result via the available return registers = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size -error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:76:54 - | -LL | pub extern "cmse-nonsecure-entry" fn union_rust() -> ReprRustUnionU64 { - | ^^^^^^^^^^^^^^^^ this type doesn't fit in the available registers - | - = note: functions with the `"cmse-nonsecure-entry"` ABI must pass their result via the available return registers - = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size - -error[E0798]: return value of `"cmse-nonsecure-entry"` function too large to pass via registers - --> $DIR/return-via-stack.rs:81:51 - | -LL | pub extern "cmse-nonsecure-entry" fn union_c() -> ReprCUnionU64 { - | ^^^^^^^^^^^^^ this type doesn't fit in the available registers - | - = note: functions with the `"cmse-nonsecure-entry"` ABI must pass their result via the available return registers - = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size - -error: aborting due to 9 previous errors +error: aborting due to 7 previous errors For more information about this error, try `rustc --explain E0798`. From 29a5d8c2a73583e8df04268f189bd8d7a93fa648 Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Thu, 25 Jun 2026 15:11:03 +0200 Subject: [PATCH 2/7] only lint on value-dependent padding Any guaranteed padding (bytes that are padding regardless of the value of the type) are now zeroed, so we only need to lint on enums and unions where some offsets are padding for some variants, but not for others. --- Cargo.lock | 1 - compiler/rustc_abi/src/layout/ty.rs | 100 +++++++++++++++++- compiler/rustc_codegen_ssa/src/mir/block.rs | 4 +- .../rustc_data_structures/src/range_set.rs | 69 ++++++++++++ compiler/rustc_lint/Cargo.toml | 1 - .../rustc_lint/src/cmse_uninitialized_leak.rs | 33 +----- .../params-uninitialized.rs | 33 ++++++ .../params-uninitialized.stderr | 28 +++-- .../cmse-nonsecure-call/return-via-stack.rs | 5 - .../return-via-stack.stderr | 2 +- .../return-uninitialized.rs | 39 +++++++ .../return-uninitialized.stderr | 14 ++- 12 files changed, 275 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4848c0b4b342..6c14a9f26e175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4293,7 +4293,6 @@ dependencies = [ "rustc_symbol_mangling", "rustc_target", "rustc_trait_selection", - "rustc_transmute", "smallvec", "tracing", "unicode-security", diff --git a/compiler/rustc_abi/src/layout/ty.rs b/compiler/rustc_abi/src/layout/ty.rs index 9ca4aa2254547..566e635c1129e 100644 --- a/compiler/rustc_abi/src/layout/ty.rs +++ b/compiler/rustc_abi/src/layout/ty.rs @@ -291,7 +291,7 @@ impl<'a, Ty> TyAndLayout<'a, Ty> { /// This is the "guaranteed" padding. There may be more bytes that are padding for some /// but not all variants of this type; those are not included. /// (E.g. `Option` has no guaranteed padding so the empty range set is returned, but its `None` value still has padding). - pub fn padding_ranges(&self, cx: &C) -> Vec> + pub fn guaranteed_padding_ranges(&self, cx: &C) -> Vec> where Ty: TyAbiInterface<'a, C> + Copy, { @@ -316,6 +316,31 @@ impl<'a, Ty> TyAndLayout<'a, Ty> { uninit_ranges } + /// The ranges of bytes that are padding for *some, but not all*, valid values of this type. + /// + /// These bytes contain data for at least one valid value, but are padding for at least one + /// other valid value. In other words, whether such a byte is initialized depends on the + /// concrete value, hence "value-dependent". This is exactly the padding that is *not* + /// already returned by [`Self::guaranteed_padding_ranges`]. + /// + /// For example, `Option` has no guaranteed padding, but the byte holding the payload is + /// value-dependent padding: it is data for `Some(_)` and padding for `None`. + pub fn value_dependent_padding_ranges(&self, cx: &C) -> Vec> + where + Ty: TyAbiInterface<'a, C> + Copy, + { + // Bytes that carry data for *at least one* valid value. + let mut maybe_data = RangeSet::new(); + self.add_data_ranges(cx, Size::ZERO, &mut maybe_data); + + // Bytes that carry data for *every* valid value. + let always_data = self.always_data_ranges(cx, Size::ZERO); + + // A byte is value-dependent padding iff it is data for some value but padding for some + // other value. + maybe_data.difference(&always_data).0.into_iter().map(|(a, b)| a..b).collect() + } + /// Extend `out` with all ranges of bytes that *may* carry relevant data for values of this type. /// For enums and unions there are offsets that are initialized for some /// variants but not for others; those offset *will* get added to `out`. @@ -368,4 +393,77 @@ impl<'a, Ty> TyAndLayout<'a, Ty> { } } } + + /// The ranges of bytes that contain data for *every* valid value of this type. + /// + /// This is the dual of [`Self::add_data_ranges`], which returns the bytes that contain data + /// for *some* valid value. + /// + /// The difference is in how enums and unions are handled: there a byte may be data for one + /// variant/field, but padding for another. + fn always_data_ranges(self, cx: &C, base_offset: Size) -> RangeSet + where + Ty: TyAbiInterface<'a, C> + Copy, + { + let mut out = RangeSet::new(); + + if self.is_zst() { + return out; + } + + match &self.variants { + Variants::Empty => { /* done */ } + Variants::Single { index: _ } => match &self.fields { + FieldsShape::Primitive => { + out.add_range(base_offset, self.size); + } + &FieldsShape::Union(field_count) => { + // A byte is data for every value only when it is data for every field. + out = (0..field_count.get()) + .map(|field| self.field(cx, field).always_data_ranges(cx, base_offset)) + .reduce(|acc, f| acc.intersection(&f)) + .unwrap_or(out); + } + &FieldsShape::Array { stride, count } => { + let elem = self.field(cx, 0); + + // For scalars we know there is no padding between the elements, + // so the entire array is a single big data range. + if elem.backend_repr.is_scalar() { + out.add_range(base_offset, elem.size * count); + } else { + // FIXME: this is really inefficient for large arrays. + for idx in 0..count { + let elem = elem.always_data_ranges(cx, base_offset + idx * stride); + for &(offset, size) in elem.0.iter() { + out.add_range(offset, size); + } + } + } + } + FieldsShape::Arbitrary { offsets, in_memory_order: _ } => { + // A byte that is always data for any field is always data for the struct. + for (field, &offset) in offsets.iter_enumerated() { + let field = self.field(cx, field.as_usize()); + let field = field.always_data_ranges(cx, base_offset + offset); + for &(offset, size) in field.0.iter() { + out.add_range(offset, size); + } + } + } + }, + Variants::Multiple { variants, .. } => { + // A byte is data for every value only when it is data for every value of every + // variant. + out = variants + .indices() + .map(|variant| self.for_variant(cx, variant)) + .map(|variant| variant.always_data_ranges(cx, base_offset)) + .reduce(|acc, f| acc.intersection(&f)) + .unwrap_or(out); + } + } + + out + } } diff --git a/compiler/rustc_codegen_ssa/src/mir/block.rs b/compiler/rustc_codegen_ssa/src/mir/block.rs index 0173b84a4d9a1..fd806d04d18e8 100644 --- a/compiler/rustc_codegen_ssa/src/mir/block.rs +++ b/compiler/rustc_codegen_ssa/src/mir/block.rs @@ -610,7 +610,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { // // Returning a value with value-dependent padding will instead trigger a lint. let ret_layout = self.fn_abi.ret.layout; - let uninit_ranges = ret_layout.padding_ranges(bx.cx()); + let uninit_ranges = ret_layout.guaranteed_padding_ranges(bx.cx()); self.zero_byte_ranges(bx, llslot, ret_layout.size, &uninit_ranges); } @@ -1878,7 +1878,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { bx, llscratch, Size::from_bytes(copy_bytes), - &arg.layout.padding_ranges(bx.cx()), + &arg.layout.guaranteed_padding_ranges(bx.cx()), ); } diff --git a/compiler/rustc_data_structures/src/range_set.rs b/compiler/rustc_data_structures/src/range_set.rs index 514946a0fb2de..c0b44f4f5e42b 100644 --- a/compiler/rustc_data_structures/src/range_set.rs +++ b/compiler/rustc_data_structures/src/range_set.rs @@ -56,4 +56,73 @@ where v.insert(idx, (offset, size)); } } + + pub fn intersection(&self, other: &Self) -> Self { + let (a, b) = (self, other); + let mut out = RangeSet::new(); + + let (mut i, mut j) = (0, 0); + while let (Some(&(a_offset, a_size)), Some(&(b_offset, b_size))) = (a.0.get(i), b.0.get(j)) + { + let (a_end, b_end) = (a_offset + a_size, b_offset + b_size); + + let start = Ord::max(a_offset, b_offset); + let end = Ord::min(a_end, b_end); + + // Add the intersection if nonempty. + if start < end { + out.add_range(start, end - start); + } + + // Advance past whichever range ends first. + // The other may still overlap a later range. + if a_end < b_end { + i += 1; + } else { + j += 1; + } + } + + out + } + + /// The ranges from `self` with any intersection with `other` removed. + pub fn difference(&self, other: &Self) -> Self { + let (a, b) = (self, other); + let mut out = Vec::new(); + + let mut j = 0; + for &(a_offset, a_size) in a.0.iter() { + let mut cursor = a_offset; + let a_end = a_offset + a_size; + + // Skip ranges of `b` that end before this range of `a` begins. + // both sequences are sorted they cannot overlap any later range of `a` either. + while let Some(&(b_offset, b_size)) = b.0.get(j) + && b_offset + b_size <= cursor + { + j += 1; + } + + // Carve out each range of `b` that overlaps this range of `a`. A range of `b` may extend + // past `a_end` and overlap the next range of `a`, so leave `j` pointing at it. + let mut k = j; + while let Some(&(b_offset, b_size)) = b.0.get(k) + && b_offset < a_end + { + if b_offset > cursor { + out.push((cursor, b_offset)); + } + cursor = Ord::max(cursor, b_offset + b_size); + k += 1; + } + + // Keep the remainder of the `a`'s range. + if cursor < a_end { + out.push((cursor, a_end)); + } + } + + Self(out) + } } diff --git a/compiler/rustc_lint/Cargo.toml b/compiler/rustc_lint/Cargo.toml index 0d327820e23f7..176890bab85f2 100644 --- a/compiler/rustc_lint/Cargo.toml +++ b/compiler/rustc_lint/Cargo.toml @@ -25,7 +25,6 @@ rustc_span = { path = "../rustc_span" } rustc_symbol_mangling = { path = "../rustc_symbol_mangling" } rustc_target = { path = "../rustc_target" } rustc_trait_selection = { path = "../rustc_trait_selection" } -rustc_transmute = { path = "../rustc_transmute" } smallvec = { version = "1.8.1", features = ["union", "may_dangle"] } tracing = "0.1" unicode-security = "0.1.0" diff --git a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs index 43a9397a9a9b1..e156c58edc543 100644 --- a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs +++ b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs @@ -1,7 +1,6 @@ use rustc_abi::ExternAbi; use rustc_hir::{self as hir, Expr, ExprKind}; -use rustc_middle::ty::layout::TyAndLayout; -use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitableExt}; +use rustc_middle::ty::{self, TypeVisitableExt}; use rustc_session::{declare_lint, declare_lint_pass}; use crate::{LateContext, LateLintPass, LintContext, lints}; @@ -86,7 +85,7 @@ fn check_cmse_call_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { continue; }; - if !is_transmutable_to_array_u8(cx.tcx, ty.clone(), layout) { + if !layout.value_dependent_padding_ranges(cx).is_empty() { // Some part of the source type may be uninitialized. cx.emit_span_lint( CMSE_UNINITIALIZED_LEAK, @@ -132,7 +131,7 @@ fn check_cmse_entry_return<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) return; }; - if !is_transmutable_to_array_u8(cx.tcx, return_type.clone(), ret_layout) { + if !ret_layout.value_dependent_padding_ranges(cx).is_empty() { let return_expr_span = if is_implicit_return { match expr.kind { ExprKind::Block(block, _) => match block.expr { @@ -153,29 +152,3 @@ fn check_cmse_entry_return<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) ); } } - -/// Check whether the source type `T` can be safely transmuted to `[u8; size_of::()]`. -/// -/// If the transmute is valid, `T` must be fully initialized. Otherwise, parts of it may be -/// uninitialized, which should trigger the lint defined by this module. -fn is_transmutable_to_array_u8<'tcx>( - tcx: TyCtxt<'tcx>, - ty: Ty<'tcx>, - layout: TyAndLayout<'tcx>, -) -> bool { - use rustc_transmute::{Answer, Assume, TransmuteTypeEnv}; - - let mut transmute_env = TransmuteTypeEnv::new(tcx); - let assume = Assume { alignment: true, lifetimes: true, safety: false, validity: false }; - - let array_u8_ty = Ty::new_array_with_const_len( - tcx, - tcx.types.u8, - ty::Const::from_target_usize(tcx, layout.size.bytes()), - ); - - match transmute_env.is_transmutable(ty, array_u8_ty, assume) { - Answer::Yes => true, - Answer::No(_) | Answer::If(_) => false, - } -} diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs index b07078c1c5875..fe580e737b02e 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs @@ -2,6 +2,7 @@ //@ compile-flags: --target thumbv8m.main-none-eabi --crate-type lib //@ needs-llvm-components: arm //@ check-pass +//@ ignore-backends: gcc #![feature(abi_cmse_nonsecure_call, no_core, lang_items)] #![no_core] #![allow(improper_ctypes_definitions)] @@ -39,6 +40,26 @@ struct PaddedStruct { b: u16, } +#[repr(C)] +enum VariantsSameSize { + A(u16), + B(u16), +} + +#[repr(C)] +enum VariantsDifferentSize { + A(u8), + B(u16), +} + +enum Void {} + +#[repr(C)] +enum UninhabitedVariant { + A(Void), + B(u16), +} + #[no_mangle] fn test_uninitialized( f1: extern "cmse-nonsecure-call" fn(ReprRustUnionU64), @@ -49,6 +70,9 @@ fn test_uninitialized( f6: extern "cmse-nonsecure-call" fn(ReprCAggregate), f7: extern "cmse-nonsecure-call" fn(ReprRustUnionPartiallyUninit), f8: extern "cmse-nonsecure-call" fn(PaddedStruct), + f9: extern "cmse-nonsecure-call" fn(VariantsSameSize), + f10: extern "cmse-nonsecure-call" fn(VariantsDifferentSize), + f11: extern "cmse-nonsecure-call" fn(UninhabitedVariant), ) { // With `repr(Rust)` this union is always initialized. f1(ReprRustUnionU64 { _unused: 1 }); @@ -71,6 +95,15 @@ fn test_uninitialized( f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + // This struct only has no value-dependent padding, the guaranteed padding is zeroed. f8(PaddedStruct { a: 0, b: 0 }); + + // This enum only has no value-dependent padding, the guaranteed padding is zeroed. + f9(VariantsSameSize::A(0)); + + f10(VariantsDifferentSize::A(0)); + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + + f11(UninhabitedVariant::B(0)); //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information } diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr index 3b922b2cb352e..b6a889914b290 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr @@ -1,5 +1,5 @@ warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:56:8 + --> $DIR/params-uninitialized.rs:79:8 | LL | f2(ReprCUnionU64 { _unused: 1 }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -8,7 +8,7 @@ LL | f2(ReprCUnionU64 { _unused: 1 }); = note: `#[warn(cmse_uninitialized_leak)]` on by default warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:59:8 + --> $DIR/params-uninitialized.rs:82:8 | LL | f3(MaybeUninit::uninit()); | ^^^^^^^^^^^^^^^^^^^^^ @@ -16,7 +16,7 @@ LL | f3(MaybeUninit::uninit()); = note: padding or fields not used by the current variant of a union may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:62:8 + --> $DIR/params-uninitialized.rs:85:8 | LL | f4(MaybeUninit::uninit()); | ^^^^^^^^^^^^^^^^^^^^^ @@ -24,7 +24,7 @@ LL | f4(MaybeUninit::uninit()); = note: padding or fields not used by the current variant of a union may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:65:8 + --> $DIR/params-uninitialized.rs:88:8 | LL | f5((0, MaybeUninit::uninit())); | ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -32,7 +32,7 @@ LL | f5((0, MaybeUninit::uninit())); = note: padding or fields not used by the current variant of a union may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:68:8 + --> $DIR/params-uninitialized.rs:91:8 | LL | f6(ReprCAggregate { a: 0, b: ReprCUnionU64 { _unused: 1 } }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -40,7 +40,7 @@ LL | f6(ReprCAggregate { a: 0, b: ReprCUnionU64 { _unused: 1 } }); = note: padding or fields not used by the current variant of a union may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:71:8 + --> $DIR/params-uninitialized.rs:94:8 | LL | f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -48,12 +48,20 @@ LL | f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); = note: padding or fields not used by the current variant of a union may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:74:8 + --> $DIR/params-uninitialized.rs:103:9 | -LL | f8(PaddedStruct { a: 0, b: 0 }); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | f10(VariantsDifferentSize::A(0)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: padding or fields not used by the current variant of a union may contain stale secure data -warning: 7 warnings emitted +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/params-uninitialized.rs:106:9 + | +LL | f11(UninhabitedVariant::B(0)); + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: 8 warnings emitted diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs index f31625a0d004f..a4b0d9626e00b 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs @@ -52,11 +52,6 @@ pub union ReprCUnionU64 { _unused: u64, } -#[repr(C)] -pub union ReprCUnionU64 { - _unused: u64, -} - #[no_mangle] pub fn test_union( f1: extern "cmse-nonsecure-call" fn() -> ReprRustUnionU64, //~ ERROR [E0798] diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.stderr index 65bc9e748333c..6b7446abc8eb8 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.stderr @@ -80,7 +80,7 @@ LL | f1: extern "cmse-nonsecure-call" fn() -> ReprRustUnionU64, = note: the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size error[E0798]: return value of `"cmse-nonsecure-call"` function too large to pass via registers - --> $DIR/return-via-stack.rs:59:46 + --> $DIR/return-via-stack.rs:58:46 | LL | f2: extern "cmse-nonsecure-call" fn() -> ReprCUnionU64, | ^^^^^^^^^^^^^ this type doesn't fit in the available registers diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs index 3a4822980bb86..b7e9d30010691 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs @@ -2,6 +2,7 @@ //@ compile-flags: --target thumbv8m.main-none-eabi --crate-type lib //@ needs-llvm-components: arm //@ check-pass +//@ ignore-backends: gcc #![feature(cmse_nonsecure_entry, no_core, lang_items)] #![no_core] @@ -71,6 +72,44 @@ struct PaddedStruct { #[no_mangle] extern "cmse-nonsecure-entry" fn padded_struct() -> PaddedStruct { + // This struct only has no value-dependent padding, the guaranteed padding is zeroed. PaddedStruct { a: 0, b: 1 } +} + +#[repr(C)] +enum VariantsSameSize { + A(u16), + B(u16), +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn variants_same_size() -> VariantsSameSize { + // This enum only has no value-dependent padding, the guaranteed padding is zeroed. + VariantsSameSize::A(0) +} + +#[repr(C)] +enum VariantsDifferentSize { + A(u8), + B(u16), +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn variants_different_size() -> VariantsDifferentSize { + VariantsDifferentSize::A(0) + //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information +} + +enum Void {} + +#[repr(C)] +enum UninhabitedVariant { + A(Void), + B(u16), +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn uninhabited_variant() -> UninhabitedVariant { + UninhabitedVariant::B(0) //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information } diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr index 5bc6a8292a23f..bc273b263209c 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr @@ -44,12 +44,20 @@ LL | | } = note: padding or fields not used by the current variant of a union may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/return-uninitialized.rs:74:5 + --> $DIR/return-uninitialized.rs:98:5 | -LL | PaddedStruct { a: 0, b: 1 } +LL | VariantsDifferentSize::A(0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: padding or fields not used by the current variant of a union may contain stale secure data -warning: 6 warnings emitted +warning: passing a (partially) uninitialized value across the security boundary may leak information + --> $DIR/return-uninitialized.rs:112:5 + | +LL | UninhabitedVariant::B(0) + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: padding or fields not used by the current variant of a union may contain stale secure data + +warning: 7 warnings emitted From 370f6a94973b80a585fc2e6e1b0617fca0da0028 Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Fri, 26 Jun 2026 09:44:35 +0200 Subject: [PATCH 3/7] tweak the error message --- compiler/rustc_lint/src/lints.rs | 2 +- .../params-uninitialized.stderr | 32 +++++++++---------- .../return-uninitialized.stderr | 28 ++++++++-------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/compiler/rustc_lint/src/lints.rs b/compiler/rustc_lint/src/lints.rs index 5e3abb65e965c..c417ae768a6d9 100644 --- a/compiler/rustc_lint/src/lints.rs +++ b/compiler/rustc_lint/src/lints.rs @@ -1832,7 +1832,7 @@ pub(crate) struct NonLocalDefinitionsCargoUpdateNote { "passing a (partially) uninitialized value across the security boundary may leak information" )] #[note( - "padding or fields not used by the current variant of a union may contain stale secure data" + "enum and union values can have variant-dependent padding that may contain stale secure data" )] pub(crate) struct CmseUninitializedMayLeakInformation; diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr index b6a889914b290..9a89f57b2bf93 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr @@ -1,67 +1,67 @@ warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:79:8 + --> $DIR/params-uninitialized.rs:80:8 | LL | f2(ReprCUnionU64 { _unused: 1 }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data = note: `#[warn(cmse_uninitialized_leak)]` on by default warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:82:8 + --> $DIR/params-uninitialized.rs:83:8 | LL | f3(MaybeUninit::uninit()); | ^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:85:8 + --> $DIR/params-uninitialized.rs:86:8 | LL | f4(MaybeUninit::uninit()); | ^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:88:8 + --> $DIR/params-uninitialized.rs:89:8 | LL | f5((0, MaybeUninit::uninit())); | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:91:8 + --> $DIR/params-uninitialized.rs:92:8 | LL | f6(ReprCAggregate { a: 0, b: ReprCUnionU64 { _unused: 1 } }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:94:8 + --> $DIR/params-uninitialized.rs:95:8 | LL | f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:103:9 + --> $DIR/params-uninitialized.rs:104:9 | LL | f10(VariantsDifferentSize::A(0)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/params-uninitialized.rs:106:9 + --> $DIR/params-uninitialized.rs:107:9 | LL | f11(UninhabitedVariant::B(0)); | ^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: 8 warnings emitted diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr index bc273b263209c..14bf75bb4c374 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr @@ -1,38 +1,38 @@ warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/return-uninitialized.rs:33:5 + --> $DIR/return-uninitialized.rs:34:5 | LL | ReprRustUnionPartiallyUninit { _unused1: 1 } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data = note: `#[warn(cmse_uninitialized_leak)]` on by default warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/return-uninitialized.rs:39:5 + --> $DIR/return-uninitialized.rs:40:5 | LL | MaybeUninit::uninit() | ^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/return-uninitialized.rs:49:5 + --> $DIR/return-uninitialized.rs:50:5 | LL | MaybeUninit::new(3.14) | ^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/return-uninitialized.rs:46:9 + --> $DIR/return-uninitialized.rs:47:9 | LL | return MaybeUninit::new(6.28); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/return-uninitialized.rs:58:5 + --> $DIR/return-uninitialized.rs:59:5 | LL | / match 0 { LL | | @@ -41,23 +41,23 @@ LL | | _ => Wrapper(MaybeUninit::new(1)), LL | | } | |_____^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/return-uninitialized.rs:98:5 + --> $DIR/return-uninitialized.rs:99:5 | LL | VariantsDifferentSize::A(0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: passing a (partially) uninitialized value across the security boundary may leak information - --> $DIR/return-uninitialized.rs:112:5 + --> $DIR/return-uninitialized.rs:113:5 | LL | UninhabitedVariant::B(0) | ^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: padding or fields not used by the current variant of a union may contain stale secure data + = note: enum and union values can have variant-dependent padding that may contain stale secure data warning: 7 warnings emitted From 5e5687d2080b301bed7e156000814d625c45b26f Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Fri, 26 Jun 2026 10:59:39 +0200 Subject: [PATCH 4/7] rename, simplify --- compiler/rustc_abi/src/layout/ty.rs | 22 +++++-------------- compiler/rustc_codegen_ssa/src/mir/block.rs | 4 ++-- .../rustc_lint/src/cmse_uninitialized_leak.rs | 4 ++-- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/compiler/rustc_abi/src/layout/ty.rs b/compiler/rustc_abi/src/layout/ty.rs index 566e635c1129e..f65f52c540826 100644 --- a/compiler/rustc_abi/src/layout/ty.rs +++ b/compiler/rustc_abi/src/layout/ty.rs @@ -291,29 +291,17 @@ impl<'a, Ty> TyAndLayout<'a, Ty> { /// This is the "guaranteed" padding. There may be more bytes that are padding for some /// but not all variants of this type; those are not included. /// (E.g. `Option` has no guaranteed padding so the empty range set is returned, but its `None` value still has padding). - pub fn guaranteed_padding_ranges(&self, cx: &C) -> Vec> + pub fn variant_independent_padding_ranges(&self, cx: &C) -> Vec> where Ty: TyAbiInterface<'a, C> + Copy, { let mut data = RangeSet::new(); self.add_data_ranges(cx, Size::ZERO, &mut data); - // Find gaps between the data ranges. - let mut uninit_ranges = Vec::new(); - let mut covered_until = Size::ZERO; - for &(offset, size) in data.0.iter() { - if offset > covered_until { - uninit_ranges.push(covered_until..offset); - } - covered_until = Ord::max(covered_until, offset + size); - } - - // Add trailing padding. - if self.size > covered_until { - uninit_ranges.push(covered_until..self.size); - } + let mut full = RangeSet::new(); + full.add_range(Size::ZERO, self.size); - uninit_ranges + full.difference(&data).0.into_iter().map(|(a, b)| a..b).collect() } /// The ranges of bytes that are padding for *some, but not all*, valid values of this type. @@ -325,7 +313,7 @@ impl<'a, Ty> TyAndLayout<'a, Ty> { /// /// For example, `Option` has no guaranteed padding, but the byte holding the payload is /// value-dependent padding: it is data for `Some(_)` and padding for `None`. - pub fn value_dependent_padding_ranges(&self, cx: &C) -> Vec> + pub fn variant_dependent_padding_ranges(&self, cx: &C) -> Vec> where Ty: TyAbiInterface<'a, C> + Copy, { diff --git a/compiler/rustc_codegen_ssa/src/mir/block.rs b/compiler/rustc_codegen_ssa/src/mir/block.rs index fd806d04d18e8..60a9a8e2ca5b4 100644 --- a/compiler/rustc_codegen_ssa/src/mir/block.rs +++ b/compiler/rustc_codegen_ssa/src/mir/block.rs @@ -610,7 +610,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { // // Returning a value with value-dependent padding will instead trigger a lint. let ret_layout = self.fn_abi.ret.layout; - let uninit_ranges = ret_layout.guaranteed_padding_ranges(bx.cx()); + let uninit_ranges = ret_layout.variant_independent_padding_ranges(bx.cx()); self.zero_byte_ranges(bx, llslot, ret_layout.size, &uninit_ranges); } @@ -1878,7 +1878,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> { bx, llscratch, Size::from_bytes(copy_bytes), - &arg.layout.guaranteed_padding_ranges(bx.cx()), + &arg.layout.variant_independent_padding_ranges(bx.cx()), ); } diff --git a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs index e156c58edc543..07667669bd950 100644 --- a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs +++ b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs @@ -85,7 +85,7 @@ fn check_cmse_call_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { continue; }; - if !layout.value_dependent_padding_ranges(cx).is_empty() { + if !layout.variant_dependent_padding_ranges(cx).is_empty() { // Some part of the source type may be uninitialized. cx.emit_span_lint( CMSE_UNINITIALIZED_LEAK, @@ -131,7 +131,7 @@ fn check_cmse_entry_return<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) return; }; - if !ret_layout.value_dependent_padding_ranges(cx).is_empty() { + if !ret_layout.variant_dependent_padding_ranges(cx).is_empty() { let return_expr_span = if is_implicit_return { match expr.kind { ExprKind::Block(block, _) => match block.expr { From 137ee210a3d242904026f18089dce4d29388e39f Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Fri, 26 Jun 2026 12:09:37 +0200 Subject: [PATCH 5/7] tweak the error message --- .../rustc_lint/src/cmse_uninitialized_leak.rs | 22 ++++++++++--------- compiler/rustc_lint/src/lints.rs | 2 +- .../params-uninitialized.rs | 16 +++++++------- .../params-uninitialized.stderr | 16 +++++++------- .../return-uninitialized.rs | 14 ++++++------ .../return-uninitialized.stderr | 14 ++++++------ 6 files changed, 43 insertions(+), 41 deletions(-) diff --git a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs index 07667669bd950..f586d3acffa35 100644 --- a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs +++ b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs @@ -20,30 +20,32 @@ declare_lint! { /// This will produce: /// /// ```text - /// warning: passing a union across the security boundary may leak information + /// warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information /// --> lint_example.rs:2:5 /// | /// 2 | MaybeUninit::uninit() /// | ^^^^^^^^^^^^^^^^^^^^^ /// | - /// = note: the bits not used by the current variant may contain stale secure data + /// = note: enum and union values can have variant-dependent padding that may contain stale secure data /// = note: `#[warn(cmse_uninitialized_leak)]` on by default /// ``` /// /// ### Explanation /// /// The cmse calling conventions normally take care of clearing registers to make sure that - /// stale secure information is not observable from non-secure code. Uninitialized memory may - /// still contain secret information, so the programmer must be careful when (partially) - /// uninitialized values cross the secure boundary. This lint fires when a partially - /// uninitialized value (e.g. a `union` value or a type with a niche) crosses the secure - /// boundary, i.e.: + /// stale secure information is not observable from non-secure code. However, arguments and + /// return values that cross the secure boundary can contain stale secure data in their + /// padding bytes. + /// + /// The compiler zeroes all variant-independent padding: bytes that are padding for all valid + /// values of the type. But enum and union values can contain variant-dependent padding: bytes + /// that are padding for some but not all valid values of the type. For instance, `Option` + /// has no padding when `Some(_)` but does have padding when `None`. + /// + /// This lint fires when a type with variant-dependent padding crosses the secure boundary: /// /// - when returned from a `cmse-nonsecure-entry` function /// - when passed as an argument to a `cmse-nonsecure-call` function - /// - /// This lint is a best effort: not all cases of (partially) uninitialized data crossing the - /// secure boundary are caught. pub CMSE_UNINITIALIZED_LEAK, Warn, "(partially) uninitialized value may leak secure information" diff --git a/compiler/rustc_lint/src/lints.rs b/compiler/rustc_lint/src/lints.rs index c417ae768a6d9..93c86e17aba42 100644 --- a/compiler/rustc_lint/src/lints.rs +++ b/compiler/rustc_lint/src/lints.rs @@ -1829,7 +1829,7 @@ pub(crate) struct NonLocalDefinitionsCargoUpdateNote { // cmse_uninitialized_leak.rs #[derive(Diagnostic)] #[diag( - "passing a (partially) uninitialized value across the security boundary may leak information" + "this value crossing a secure boundary may contain (partially) uninitialized data which can leak information" )] #[note( "enum and union values can have variant-dependent padding that may contain stale secure data" diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs index fe580e737b02e..49856e90e69b2 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs @@ -78,22 +78,22 @@ fn test_uninitialized( f1(ReprRustUnionU64 { _unused: 1 }); f2(ReprCUnionU64 { _unused: 1 }); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information f3(MaybeUninit::uninit()); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information f4(MaybeUninit::uninit()); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information f5((0, MaybeUninit::uninit())); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information f6(ReprCAggregate { a: 0, b: ReprCUnionU64 { _unused: 1 } }); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information // This struct only has no value-dependent padding, the guaranteed padding is zeroed. f8(PaddedStruct { a: 0, b: 0 }); @@ -102,8 +102,8 @@ fn test_uninitialized( f9(VariantsSameSize::A(0)); f10(VariantsDifferentSize::A(0)); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information f11(UninhabitedVariant::B(0)); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr index 9a89f57b2bf93..7e8ae87c6caad 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr @@ -1,4 +1,4 @@ -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:80:8 | LL | f2(ReprCUnionU64 { _unused: 1 }); @@ -7,7 +7,7 @@ LL | f2(ReprCUnionU64 { _unused: 1 }); = note: enum and union values can have variant-dependent padding that may contain stale secure data = note: `#[warn(cmse_uninitialized_leak)]` on by default -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:83:8 | LL | f3(MaybeUninit::uninit()); @@ -15,7 +15,7 @@ LL | f3(MaybeUninit::uninit()); | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:86:8 | LL | f4(MaybeUninit::uninit()); @@ -23,7 +23,7 @@ LL | f4(MaybeUninit::uninit()); | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:89:8 | LL | f5((0, MaybeUninit::uninit())); @@ -31,7 +31,7 @@ LL | f5((0, MaybeUninit::uninit())); | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:92:8 | LL | f6(ReprCAggregate { a: 0, b: ReprCUnionU64 { _unused: 1 } }); @@ -39,7 +39,7 @@ LL | f6(ReprCAggregate { a: 0, b: ReprCUnionU64 { _unused: 1 } }); | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:95:8 | LL | f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); @@ -47,7 +47,7 @@ LL | f7(ReprRustUnionPartiallyUninit { _unused1: 0 }); | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:104:9 | LL | f10(VariantsDifferentSize::A(0)); @@ -55,7 +55,7 @@ LL | f10(VariantsDifferentSize::A(0)); | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:107:9 | LL | f11(UninhabitedVariant::B(0)); diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs index b7e9d30010691..851e282240c1f 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs @@ -32,23 +32,23 @@ union ReprRustUnionPartiallyUninit { #[allow(improper_ctypes_definitions)] extern "cmse-nonsecure-entry" fn union_rust_partially_uninit() -> ReprRustUnionPartiallyUninit { ReprRustUnionPartiallyUninit { _unused1: 1 } - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } #[no_mangle] extern "cmse-nonsecure-entry" fn maybe_uninit_32bit() -> MaybeUninit { MaybeUninit::uninit() - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } #[no_mangle] extern "cmse-nonsecure-entry" fn maybe_uninit_64bit() -> MaybeUninit { if true { return MaybeUninit::new(6.28); - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } MaybeUninit::new(3.14) - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } #[repr(transparent)] @@ -57,7 +57,7 @@ struct Wrapper(MaybeUninit); #[no_mangle] extern "cmse-nonsecure-entry" fn repr_transparent_union() -> Wrapper { match 0 { - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information 0 => Wrapper(MaybeUninit::new(0)), _ => Wrapper(MaybeUninit::new(1)), } @@ -97,7 +97,7 @@ enum VariantsDifferentSize { #[no_mangle] extern "cmse-nonsecure-entry" fn variants_different_size() -> VariantsDifferentSize { VariantsDifferentSize::A(0) - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } enum Void {} @@ -111,5 +111,5 @@ enum UninhabitedVariant { #[no_mangle] extern "cmse-nonsecure-entry" fn uninhabited_variant() -> UninhabitedVariant { UninhabitedVariant::B(0) - //~^ WARN passing a (partially) uninitialized value across the security boundary may leak information + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr index 14bf75bb4c374..0c427a78bb3b1 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr @@ -1,4 +1,4 @@ -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:34:5 | LL | ReprRustUnionPartiallyUninit { _unused1: 1 } @@ -7,7 +7,7 @@ LL | ReprRustUnionPartiallyUninit { _unused1: 1 } = note: enum and union values can have variant-dependent padding that may contain stale secure data = note: `#[warn(cmse_uninitialized_leak)]` on by default -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:40:5 | LL | MaybeUninit::uninit() @@ -15,7 +15,7 @@ LL | MaybeUninit::uninit() | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:50:5 | LL | MaybeUninit::new(3.14) @@ -23,7 +23,7 @@ LL | MaybeUninit::new(3.14) | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:47:9 | LL | return MaybeUninit::new(6.28); @@ -31,7 +31,7 @@ LL | return MaybeUninit::new(6.28); | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:59:5 | LL | / match 0 { @@ -43,7 +43,7 @@ LL | | } | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:99:5 | LL | VariantsDifferentSize::A(0) @@ -51,7 +51,7 @@ LL | VariantsDifferentSize::A(0) | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: passing a (partially) uninitialized value across the security boundary may leak information +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:113:5 | LL | UninhabitedVariant::B(0) From 54b0938e444c037c92c95fca8b1b5fa2c25fa9f2 Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Fri, 26 Jun 2026 15:22:06 +0200 Subject: [PATCH 6/7] fix discriminant getting skipped --- compiler/rustc_abi/src/layout/ty.rs | 37 +++++++-- .../rustc_lint/src/cmse_uninitialized_leak.rs | 2 +- tests/assembly-llvm/cmse-clear-padding.rs | 77 +++++++++++++++++++ 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/compiler/rustc_abi/src/layout/ty.rs b/compiler/rustc_abi/src/layout/ty.rs index f65f52c540826..54d3a5671d3e0 100644 --- a/compiler/rustc_abi/src/layout/ty.rs +++ b/compiler/rustc_abi/src/layout/ty.rs @@ -309,7 +309,7 @@ impl<'a, Ty> TyAndLayout<'a, Ty> { /// These bytes contain data for at least one valid value, but are padding for at least one /// other valid value. In other words, whether such a byte is initialized depends on the /// concrete value, hence "value-dependent". This is exactly the padding that is *not* - /// already returned by [`Self::guaranteed_padding_ranges`]. + /// already returned by [`Self::variant_independent_padding_ranges`]. /// /// For example, `Option` has no guaranteed padding, but the byte holding the payload is /// value-dependent padding: it is data for `Some(_)` and padding for `None`. @@ -374,6 +374,16 @@ impl<'a, Ty> TyAndLayout<'a, Ty> { } }, Variants::Multiple { variants, .. } => { + // The variants do not contain e.g. the discriminant or coroutine upvars. + let FieldsShape::Arbitrary { offsets, in_memory_order: _ } = &self.fields else { + unreachable!("a multi-variant layout should have `Arbitrary` fields") + }; + + for (field, &offset) in offsets.iter_enumerated() { + let field = self.field(cx, field.as_usize()); + field.add_data_ranges(cx, base_offset + offset, out); + } + for variant in variants.indices() { let variant = self.for_variant(cx, variant); variant.add_data_ranges(cx, base_offset, out); @@ -441,14 +451,31 @@ impl<'a, Ty> TyAndLayout<'a, Ty> { } }, Variants::Multiple { variants, .. } => { - // A byte is data for every value only when it is data for every value of every - // variant. - out = variants + // The variants do not contain e.g. the discriminant or coroutine upvars. + let FieldsShape::Arbitrary { offsets, in_memory_order: _ } = &self.fields else { + unreachable!("a multi-variant layout should have `Arbitrary` fields") + }; + + // The fields are variant-independent and always data. + for (field, &offset) in offsets.iter_enumerated() { + let field = self.field(cx, field.as_usize()); + let field = field.always_data_ranges(cx, base_offset + offset); + for &(offset, size) in field.0.iter() { + out.add_range(offset, size); + } + } + + // Otherwise a byte is data for every value only when it is data for every variant. + if let Some(common) = variants .indices() .map(|variant| self.for_variant(cx, variant)) .map(|variant| variant.always_data_ranges(cx, base_offset)) .reduce(|acc, f| acc.intersection(&f)) - .unwrap_or(out); + { + for &(offset, size) in common.0.iter() { + out.add_range(offset, size); + } + } } } diff --git a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs index f586d3acffa35..25f7584844aee 100644 --- a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs +++ b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs @@ -37,7 +37,7 @@ declare_lint! { /// return values that cross the secure boundary can contain stale secure data in their /// padding bytes. /// - /// The compiler zeroes all variant-independent padding: bytes that are padding for all valid + /// The compiler clears all variant-independent padding: bytes that are padding for all valid /// values of the type. But enum and union values can contain variant-dependent padding: bytes /// that are padding for some but not all valid values of the type. For instance, `Option` /// has no padding when `Some(_)` but does have padding when `None`. diff --git a/tests/assembly-llvm/cmse-clear-padding.rs b/tests/assembly-llvm/cmse-clear-padding.rs index b092bf304dcca..8406bac74b89f 100644 --- a/tests/assembly-llvm/cmse-clear-padding.rs +++ b/tests/assembly-llvm/cmse-clear-padding.rs @@ -202,3 +202,80 @@ extern "cmse-nonsecure-entry" fn cmse_ret_with_wide_u8_uninit_tuple( ) -> (MaybeUninit, MaybeUninit) { unsafe { (mem::transmute(a), mem::transmute(b)) } } + +// Check that the tag does not get cleared. +enum Enum { + A(u16), + B(u16), +} + +// CHECK-LABEL: cmse_ret_enum: +// CHECK: .fnstart +// CHECK-NEXT: .save {r7, lr} +// CHECK-NEXT: push {r7, lr} +// CHECK-NEXT: .setfp r7, sp +// CHECK-NEXT: mov r7, sp +// CHECK-NEXT: pop.w {r7, lr} +// CHECK-NEXT: mov r1, lr +// CHECK-NEXT: mov r2, lr +// CHECK-NEXT: mov r3, lr +// CHECK-NEXT: mov r12, lr +// CHECK-NEXT: msr apsr_nzcvq, lr +// CHECK-NEXT: bxns lr +#[no_mangle] +extern "cmse-nonsecure-entry" fn cmse_ret_enum(x: Enum) -> Enum { + x +} + +// Check that variant-dependent padding is left alone. +enum VariantPadding { + A(u8), + B(u16), +} + +// CHECK-LABEL: cmse_ret_enum_with_variant_padding: +// CHECK: .fnstart +// CHECK-NEXT: .save {r7, lr} +// CHECK-NEXT: push {r7, lr} +// CHECK-NEXT: .setfp r7, sp +// CHECK-NEXT: mov r7, sp +// CHECK-NEXT: pop.w {r7, lr} +// CHECK-NEXT: mov r1, lr +// CHECK-NEXT: mov r2, lr +// CHECK-NEXT: mov r3, lr +// CHECK-NEXT: mov r12, lr +// CHECK-NEXT: msr apsr_nzcvq, lr +// CHECK-NEXT: bxns lr +#[no_mangle] +extern "cmse-nonsecure-entry" fn cmse_ret_enum_with_variant_padding( + x: VariantPadding, +) -> VariantPadding { + x +} + +// Check that variant-independent padding does get cleared. +enum IndependentPadding { + A(WideU8), + B(WideU8), +} + +// CHECK-LABEL: cmse_ret_enum_with_independent_padding: +// CHECK: .fnstart +// CHECK-NEXT: .save {r7, lr} +// CHECK-NEXT: push {r7, lr} +// CHECK-NEXT: .setfp r7, sp +// CHECK-NEXT: mov r7, sp +// CHECK-NEXT: bic r0, r0, #-16777216 +// CHECK-NEXT: pop.w {r7, lr} +// CHECK-NEXT: mov r1, lr +// CHECK-NEXT: mov r2, lr +// CHECK-NEXT: mov r3, lr +// CHECK-NEXT: mov r12, lr +// CHECK-NEXT: msr apsr_nzcvq, lr +// CHECK-NEXT: bxns lr +#[no_mangle] +extern "cmse-nonsecure-entry" fn cmse_ret_enum_with_independent_padding( + x: IndependentPadding, +) -> IndependentPadding { + x +} From 86edf4881527f11db3c31ca7b2055fa6eeb0c1d5 Mon Sep 17 00:00:00 2001 From: Folkert de Vries Date: Thu, 2 Jul 2026 15:16:04 +0200 Subject: [PATCH 7/7] only check variant-dependent padding in repr(C) enums for enums of other reprs, and all unions, the lint fires --- .../rustc_lint/src/cmse_uninitialized_leak.rs | 88 ++++++++++++++++--- .../params-uninitialized.rs | 2 +- .../params-uninitialized.stderr | 12 ++- .../return-uninitialized.rs | 30 ++++++- .../return-uninitialized.stderr | 65 +++++++++++++- 5 files changed, 177 insertions(+), 20 deletions(-) diff --git a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs index 25f7584844aee..4c97aa9120046 100644 --- a/compiler/rustc_lint/src/cmse_uninitialized_leak.rs +++ b/compiler/rustc_lint/src/cmse_uninitialized_leak.rs @@ -1,6 +1,6 @@ use rustc_abi::ExternAbi; use rustc_hir::{self as hir, Expr, ExprKind}; -use rustc_middle::ty::{self, TypeVisitableExt}; +use rustc_middle::ty::{self, Ty, TypeVisitableExt}; use rustc_session::{declare_lint, declare_lint_pass}; use crate::{LateContext, LateLintPass, LintContext, lints}; @@ -75,7 +75,6 @@ fn check_cmse_call_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { }; let fn_sig = cx.tcx.erase_and_anonymize_regions(sig); - let typing_env = ty::TypingEnv::fully_monomorphized(); for (arg, ty) in arguments.iter().zip(fn_sig.inputs()) { // `impl Trait` is not allowed in the argument types. @@ -83,11 +82,7 @@ fn check_cmse_call_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { continue; } - let Ok(layout) = cx.tcx.layout_of(typing_env.as_query_input(*ty)) else { - continue; - }; - - if !layout.variant_dependent_padding_ranges(cx).is_empty() { + if contains_unstable_or_variant_dependent_padding(cx, *ty) { // Some part of the source type may be uninitialized. cx.emit_span_lint( CMSE_UNINITIALIZED_LEAK, @@ -127,13 +122,7 @@ fn check_cmse_entry_return<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) return; } - let typing_env = ty::TypingEnv::fully_monomorphized(); - - let Ok(ret_layout) = cx.tcx.layout_of(typing_env.as_query_input(return_type)) else { - return; - }; - - if !ret_layout.variant_dependent_padding_ranges(cx).is_empty() { + if contains_unstable_or_variant_dependent_padding(cx, return_type) { let return_expr_span = if is_implicit_return { match expr.kind { ExprKind::Block(block, _) => match block.expr { @@ -154,3 +143,74 @@ fn check_cmse_entry_return<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) ); } } + +/// Traverse `T` for any `union` or `enum`, and check whether it contains any padding that is +/// variant-dependent or unstable. +fn contains_unstable_or_variant_dependent_padding<'tcx>( + cx: &LateContext<'tcx>, + ty: Ty<'tcx>, +) -> bool { + let tcx = cx.tcx; + + // Types cross the secure boundary fully monomorphized. + let typing_env = ty::TypingEnv::fully_monomorphized(); + + match ty.kind() { + ty::Adt(adt_def, args) => { + if adt_def.is_union() { + // It is still unclear whether a union value where all fields are equally + // large and allow the same bit patterns can be considered initialized + // (see rust-lang/unsafe-code-guidelines#438), so for now we just warn on + // any union. + return true; + } + + if adt_def.is_enum() { + let repr = adt_def.repr(); + + // A `repr(C)` enum has a stable layout, and we can check whether + // whether it contains variant-dependent padding. + if repr.c() { + let Ok(layout) = tcx.layout_of(typing_env.as_query_input(ty)) else { + // Just be conservative if the layout cannot be computed. + return true; + }; + return !layout.variant_dependent_padding_ranges(cx).is_empty(); + } + } + + // For structs we recurse into the fields. + // A non-repr(C) struct already triggers `improper_ctypes`. + adt_def.all_fields().any(|field| { + let field_ty = tcx.normalize_erasing_regions(typing_env, field.ty(tcx, args)); + contains_unstable_or_variant_dependent_padding(cx, field_ty) + }) + } + + ty::Tuple(elems) => { + // Element types might contain unions or enums. + // + // Passing a tuple already triggers `improper_ctypes`. + elems.iter().any(|elem| contains_unstable_or_variant_dependent_padding(cx, elem)) + } + ty::Array(elem, _) => { + // The element type might contain unions or enums. + // + // Passing an array already triggers `improper_ctypes`. + contains_unstable_or_variant_dependent_padding(cx, *elem) + } + + _ => { + // Other types are either scalar (hence no padding), or behind some kind of indirection. + // + // Note that the system traps when dereferencing a pointer to secure memory while in + // non-secure mode, so passing a value with indirection is useless in practice. + debug_assert!({ + let layout = tcx.layout_of(typing_env.as_query_input(ty)).unwrap(); + !layout.variant_dependent_padding_ranges(cx).is_empty() + }); + + false + } + } +} diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs index 49856e90e69b2..04f08a3719e47 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.rs @@ -74,8 +74,8 @@ fn test_uninitialized( f10: extern "cmse-nonsecure-call" fn(VariantsDifferentSize), f11: extern "cmse-nonsecure-call" fn(UninhabitedVariant), ) { - // With `repr(Rust)` this union is always initialized. f1(ReprRustUnionU64 { _unused: 1 }); + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information f2(ReprCUnionU64 { _unused: 1 }); //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr index 7e8ae87c6caad..add535190d167 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-call/params-uninitialized.stderr @@ -1,3 +1,12 @@ +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information + --> $DIR/params-uninitialized.rs:77:8 + | +LL | f1(ReprRustUnionU64 { _unused: 1 }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: enum and union values can have variant-dependent padding that may contain stale secure data + = note: `#[warn(cmse_uninitialized_leak)]` on by default + warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:80:8 | @@ -5,7 +14,6 @@ LL | f2(ReprCUnionU64 { _unused: 1 }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: enum and union values can have variant-dependent padding that may contain stale secure data - = note: `#[warn(cmse_uninitialized_leak)]` on by default warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/params-uninitialized.rs:83:8 @@ -63,5 +71,5 @@ LL | f11(UninhabitedVariant::B(0)); | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: 8 warnings emitted +warning: 9 warnings emitted diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs index 851e282240c1f..89ebe46f5c783 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.rs @@ -18,8 +18,8 @@ union ReprRustUnionU32 { #[no_mangle] #[allow(improper_ctypes_definitions)] extern "cmse-nonsecure-entry" fn union_rust() -> ReprRustUnionU32 { - // With `repr(Rust)` value is always fully initialized. ReprRustUnionU32 { _unused: 1 } + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } #[repr(Rust)] @@ -113,3 +113,31 @@ extern "cmse-nonsecure-entry" fn uninhabited_variant() -> UninhabitedVariant { UninhabitedVariant::B(0) //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information } + +#[no_mangle] +extern "cmse-nonsecure-entry" fn variants_same_size_array() -> [VariantsSameSize; 1] { + //~^ WARN improper_ctypes_definitions + // This enum only has no value-dependent padding, the guaranteed padding is zeroed. + [VariantsSameSize::A(0)] +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn variants_different_size_array() -> [VariantsDifferentSize; 1] { + //~^ WARN improper_ctypes_definitions + [VariantsDifferentSize::A(0)] + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn variants_same_size_tuple() -> (VariantsSameSize,) { + //~^ WARN improper_ctypes_definitions + // This enum only has no value-dependent padding, the guaranteed padding is zeroed. + (VariantsSameSize::A(0),) +} + +#[no_mangle] +extern "cmse-nonsecure-entry" fn variants_different_size_tuple() -> (VariantsDifferentSize,) { + //~^ WARN improper_ctypes_definitions + (VariantsDifferentSize::A(0),) + //~^ WARN this value crossing a secure boundary may contain (partially) uninitialized data which can leak information +} diff --git a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr index 0c427a78bb3b1..b8bf310ea5a04 100644 --- a/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr +++ b/tests/ui/cmse-nonsecure/cmse-nonsecure-entry/return-uninitialized.stderr @@ -1,3 +1,12 @@ +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information + --> $DIR/return-uninitialized.rs:21:5 + | +LL | ReprRustUnionU32 { _unused: 1 } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: enum and union values can have variant-dependent padding that may contain stale secure data + = note: `#[warn(cmse_uninitialized_leak)]` on by default + warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:34:5 | @@ -5,7 +14,6 @@ LL | ReprRustUnionPartiallyUninit { _unused1: 1 } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: enum and union values can have variant-dependent padding that may contain stale secure data - = note: `#[warn(cmse_uninitialized_leak)]` on by default warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information --> $DIR/return-uninitialized.rs:40:5 @@ -59,5 +67,58 @@ LL | UninhabitedVariant::B(0) | = note: enum and union values can have variant-dependent padding that may contain stale secure data -warning: 7 warnings emitted +warning: `extern` fn uses type `[VariantsSameSize; 1]`, which is not FFI-safe + --> $DIR/return-uninitialized.rs:118:64 + | +LL | extern "cmse-nonsecure-entry" fn variants_same_size_array() -> [VariantsSameSize; 1] { + | ^^^^^^^^^^^^^^^^^^^^^ not FFI-safe + | + = help: consider passing a pointer to the array + = note: passing raw arrays by value is not FFI-safe + = note: `#[warn(improper_ctypes_definitions)]` on by default + +warning: `extern` fn uses type `[VariantsDifferentSize; 1]`, which is not FFI-safe + --> $DIR/return-uninitialized.rs:125:69 + | +LL | extern "cmse-nonsecure-entry" fn variants_different_size_array() -> [VariantsDifferentSize; 1] { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ not FFI-safe + | + = help: consider passing a pointer to the array + = note: passing raw arrays by value is not FFI-safe + +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information + --> $DIR/return-uninitialized.rs:127:5 + | +LL | [VariantsDifferentSize::A(0)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: enum and union values can have variant-dependent padding that may contain stale secure data + +warning: `extern` fn uses type `(VariantsSameSize,)`, which is not FFI-safe + --> $DIR/return-uninitialized.rs:132:64 + | +LL | extern "cmse-nonsecure-entry" fn variants_same_size_tuple() -> (VariantsSameSize,) { + | ^^^^^^^^^^^^^^^^^^^ not FFI-safe + | + = help: consider using a struct instead + = note: tuples have unspecified layout + +warning: `extern` fn uses type `(VariantsDifferentSize,)`, which is not FFI-safe + --> $DIR/return-uninitialized.rs:139:69 + | +LL | extern "cmse-nonsecure-entry" fn variants_different_size_tuple() -> (VariantsDifferentSize,) { + | ^^^^^^^^^^^^^^^^^^^^^^^^ not FFI-safe + | + = help: consider using a struct instead + = note: tuples have unspecified layout + +warning: this value crossing a secure boundary may contain (partially) uninitialized data which can leak information + --> $DIR/return-uninitialized.rs:141:5 + | +LL | (VariantsDifferentSize::A(0),) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: enum and union values can have variant-dependent padding that may contain stale secure data + +warning: 14 warnings emitted