Skip to content

CMSE calling conventions#3884

Open
folkertdev wants to merge 8 commits intorust-lang:masterfrom
folkertdev:cmse-calling-conventions
Open

CMSE calling conventions#3884
folkertdev wants to merge 8 commits intorust-lang:masterfrom
folkertdev:cmse-calling-conventions

Conversation

@folkertdev
Copy link
Copy Markdown

@folkertdev folkertdev commented Nov 24, 2025

Support for the cmse-nonsecure-entry and cmse-nonsecure-call calling conventions on ArmV8m (thumbv8*) targets, and a lint preventing (partially) uninitialized values from crossing the security boundary.

extern "cmse-nonsecure-entry" fn entry(callback: extern "cmse-nonsecure-call" fn(_: u64)) { 
    callback(42);
}

The implementation is tracked in:

Important

When responding to RFCs, try to use inline review comments (it is possible to leave an inline review comment for the entire file at the top) instead of direct comments for normal comments and keep normal comments for procedural matters like starting FCPs.

This keeps the discussion more organized.

@rustbot label +T-lang

Rendered

@rustbot rustbot added the T-lang Relevant to the language team, which will review and decide on the RFC. label Nov 24, 2025
@folkertdev folkertdev changed the title RCF for CMSE calling conventions RFC for CMSE calling conventions Nov 24, 2025
@folkertdev folkertdev force-pushed the cmse-calling-conventions branch from f30d3be to 21f5b58 Compare November 24, 2025 12:14
@folkertdev folkertdev changed the title RFC for CMSE calling conventions CMSE calling conventions Nov 24, 2025
Copy link
Copy Markdown

@thejpster thejpster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had one quibble with the architecture terminology, but everything else looks good to me.

Co-authored-by: Jonathan 'theJPster' Pallant <github@thejpster.org.uk>
@folkertdev
Copy link
Copy Markdown
Author

This has now been reviewed by several people from the rust embedded community and from Arm, so I think this is ready for T-lang to take a look.

@rustbot label +I-lang-nominated

@rustbot rustbot added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Nov 30, 2025
@traviscross traviscross added the P-lang-drag-2 Lang team prioritization drag level 2. label Dec 3, 2025
Co-authored-by: Jacob Lifshay <programmerjake@gmail.com>
@joshtriplett
Copy link
Copy Markdown
Member

This looks reasonable to me, including the lint.

@rfcbot merge lang

@rust-rfcbot
Copy link
Copy Markdown
Collaborator

rust-rfcbot commented Jan 21, 2026

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rust-rfcbot rust-rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Jan 21, 2026
type T5 = extern "cmse-nonsecure-call" fn(_: i64, _: i64) -> WrappedI64;
```

An error is emitted when the program contains a signature that violates the calling convention's constraints:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please explain the exact algorithm used for this check. There are potential semver concerns here so it'd be good to be clear about what is being checked here.

For instance, if I have a single-field struct like U64 without the repr(transparent), is that accepted or not?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll clarify that.

The current implementation is to look at the BackendRepr, and accept anything passed as Primitive::Int(Integer::I64, _) | Primitive::Float(Float::F64). Because we're working with a subset of AAPCS, I don't think how values are passed can ever change?

https://github.com/rust-lang/rust/blob/b765963267a390d817d0b519b15cfa0d6311d0dc/compiler/rustc_hir_analysis/src/hir_ty_lowering/cmse.rs#L161-L181

We test for the case you mention, it emits an error (clang would emit an error in this case too):

https://github.com/rust-lang/rust/blob/b765963267a390d817d0b519b15cfa0d6311d0dc/tests/ui/cmse-nonsecure/cmse-nonsecure-call/return-via-stack.rs#L13-L27

Copy link
Copy Markdown
Member

@RalfJung RalfJung Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BackendRepr is an internal implementation detail of rustc and prone to changes. It seems risky to get ourselves into a corner where changing unspecified layout computation details is a breaking change.

(We do rely on BackendRepr for the SIMD vector ABI check, but that was a case of fixing a previous oversight which caused soundness trouble, so we had to minimize breakage. I wouldn't take that as a template for a new feature where we have the chance to do it right. Also, we don't rely just on BackendRepr there, we also check the PassMode, so really what we are checking there is the de-facto underlying ABI, at least on the level of detail that is represented in LLVM IR.)

Copy link
Copy Markdown
Member

@RalfJung RalfJung Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We test for the case you mention

That is not the case I mentioned. I suggested to remove repr(transparent), but I didn't suggest adding repr(C). (That will probably trigger an FFI lint, but since we are talking about hard errors here, whether or not there is a lint is largely irrelevant.)

Also, the fact that repr(C) inhibits BackendRepr::Scalar is a limitation we'd like to lift (rust-lang/rust#119183). We should not add new logic that relies on how repr(C) and BackendRepr correlate.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So would a check based on the type (peeling off layers of repr(transparent) work better?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably, there can be at most 4 arguments.

module zero-sized types, yes.


The check is based just on the size of the arguments, each rounded up to the next multiple of 4. If that exceeds 16, the arguments do not fit.

I've updated the text.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

module zero-sized types, yes.

The fact that zero-sized arguments are skipped in some ABIs is an implementation detail. I would be hesistant to enshrine it in the language.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZSTs are used a lot in embedded Rust. It would be very nice to be able to pass ZSTs as an argument without it taking up the register budget.

Often hardware peripherals or certain state conditions are modeled using ZSTs.

For example, say we want a secure memcpy, that could look like:

fn secure_memcpy(dma: DMA, src: *const u8, src_len: usize, target: *mut u8, target_len: usize) -> DMA

If the DMA ZST were counted as one of the 4 possible arguments we couldn't make this work, even when there's not technical reason for it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could store the arguments into a repr(C) struct...

But that is just a weird workaround. Given that the calling convention inherits from aapcs, and it does ignore zero-sized types, it feels like it should be OK to allow them here?

Copy link
Copy Markdown
Member

@RalfJung RalfJung Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does ignore zero-sized types

Is that documented in the aapcs ABI description? C does not have zero-sized types, so this would surprise me. So it would be more correct to say that rustc happens to ignore ZST on aapcs calls at the moment, but that's not a deliberate choice (as in, t-lang approved or so) or a stable promise by any means. Generally we should be careful whenever we pass things over an ABI that do not exist in C, since if we make a stable choice here and C later makes a different choice, we are in trouble.

Our general ABI rules do not allow ignoring ZST, and in fact there are ABIs where they currently are not being ignored (specifically on Windows). That might be partially because we make some types ZST that shouldn't be ZST, but last time I checked it looked like we could not get away with a blanket "ZST are ignored" rule. We'd have to start making ABI-specific promises, which is a rabbit hole that we avoided so far.

@folkertdev folkertdev force-pushed the cmse-calling-conventions branch from f795ce0 to ae12919 Compare January 28, 2026 13:30
An error is emitted when the program contains a signature that violates the calling convention's constraints:
The arguments fit if:

- the sum of their sizes, each rounded up to the next multiple of 4, is 16 bytes or less
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, a single argument of type (i32, (i8, i16), i64) is allowed?

That's... very strange and not at all what I expected. In the end I don't care that strongly as long as it is clearly specified, but implementation-wise this seems like it could become quite messy.

Is this our ABI to define or is it a standard ABI we have to follow? If it is the latter, allowing repr(Rust) types (this includes tuples) might be a mistake -- it would be the first time we make any kind of ABI guarantees for them.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is really just aapcs, so the 32-bit arm C calling convention. It already lints on passing repr(Rust) types, I'd be fine with actually disallowing them though. This is meant to be an FFI boundary, the user is meant to think about the constraints.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is that for normal aapcs, if we change repr(Rust) things won't fail to build. They might "just" stop working properly if you ignored the lints. This RFC turns things into a hard error, which typically has a different par for the lang team. But this feels worth explicitly calling out as an open question to them to get their vibes on it. After all, changing the size of a type can already lead to compilation failures due to repr(transparent).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Feb 6, 2026
…, r=davidtwco

cmse: don't use `BackendRepr` when checking return type

tracking issue: rust-lang#81391
tracking issue: rust-lang#75835

r? davidtwco
cc @RalfJung

context: rust-lang/rfcs#3884 (comment)

I believe this is more reliable, and no longer relies on `BackendRepr`. I also added a test specifically for using `repr(Rust)`.
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Feb 6, 2026
…, r=davidtwco

cmse: don't use `BackendRepr` when checking return type

tracking issue: rust-lang#81391
tracking issue: rust-lang#75835

r? davidtwco
cc @RalfJung

context: rust-lang/rfcs#3884 (comment)

I believe this is more reliable, and no longer relies on `BackendRepr`. I also added a test specifically for using `repr(Rust)`.
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Feb 6, 2026
…, r=davidtwco

cmse: don't use `BackendRepr` when checking return type

tracking issue: rust-lang#81391
tracking issue: rust-lang#75835

r? davidtwco
cc @RalfJung

context: rust-lang/rfcs#3884 (comment)

I believe this is more reliable, and no longer relies on `BackendRepr`. I also added a test specifically for using `repr(Rust)`.
rust-timer added a commit to rust-lang/rust that referenced this pull request Feb 6, 2026
Rollup merge of #151590 - folkertdev:cmse-unwrap-transparent, r=davidtwco

cmse: don't use `BackendRepr` when checking return type

tracking issue: #81391
tracking issue: #75835

r? davidtwco
cc @RalfJung

context: rust-lang/rfcs#3884 (comment)

I believe this is more reliable, and no longer relies on `BackendRepr`. I also added a test specifically for using `repr(Rust)`.

### No Generics

No generics are allowed. That includes both standard generics, const generics, and any `impl Trait` in argument or return position. By extension, `async` cannot be used in combination with the cmse ABIs.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about lifetime generics? (Also, why forbid RPIT?)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are not relevant for layout computation, so they are fine to accept.

Is there a better term to only talk about type/const generics?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Monomorphized generics"?

Comment on lines +334 to +351
## An "initialize padding" attribute

The current lint for partially uninitialized values crossing the security boundary does not have a proper workaround: the advice is to just not send such values over the secure boundary, and essentially treat the warning as an error.

A suggestion that was floated is to provide some mechanism to ensure that a value is fully initialized, e.g. by zeroing any potentially uninitialized parts.

One potential method is to extend the`repr` attribute with an option that adds fields where padding is needed internally. These user-hidden padding fields would be zeroed upon creation.
```rust
#[repr(C, align(8), initialized)]
struct Foo {
a: u8,
// implicit _padding0: [u8; 1],
b: u16,
// implicit _padding1: [u8; 4],
}
```

This feature still has many open design questions. We don't think such an attribute is required for practical Trustzone development, so we defer it for now.
Copy link
Copy Markdown
Contributor

@Jules-Bertholet Jules-Bertholet Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems excessively complicated. Rust does not preserve padding bytes on typed copy. The compiler/ABI is free to erase them across a function call. (A padding byte isn't like e.g. MaybeUninit<u8>—that initialization state is guaranteed to be preserved.) There is no need to lint on passing values with padding, the compiler should just zero it when necessary.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to lint on passing values with padding

For now there is, and the phrasing is such that when the value no longer contains uninitialized bytes we could stop linting in those cases.

the compiler should just zero it when necessary.

By what mechanism? This would be some sort of layout flag on the abi? This feels like it might have broader use, which is neat but also I'd rather not block on it. Lints are deliberately a bit more flexible wrt when changes are considered breaking.

Copy link
Copy Markdown
Contributor

@Jules-Bertholet Jules-Bertholet Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the mechanism of Just Doing It. Try running this program under Miri:

use std::ptr::{addr_of, addr_of_mut};

#[repr(C)]
struct HasPadding(u8, u16);

fn foo(hp: HasPadding) {
    unsafe { addr_of!(hp).cast::<[u8; 4]>().read() };
}

fn main() {
    let mut hp = HasPadding(0, 0);
    // Initialize padding
    unsafe { addr_of_mut!(hp).cast::<[u8; 4]>().write([0, 0, 0, 0]) };
    foo(hp);
}

It will fail:

error: Undefined Behavior: constructing invalid value at [1]: encountered uninitialized memory, but expected an integer
 --> src/main.rs:7:14
  |
7 |     unsafe { addr_of!(hp).cast::<[u8; 4]>().read() };
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
  |
  = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
  = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
  = note: stack backtrace:
          0: foo
              at src/main.rs:7:14: 7:51
          1: main
              at src/main.rs:14:5: 14:12

We initialized the padding, but it wasn't preserved across the call to foo.

Therefore, zeroing any padding bytes on the machine-code level is perfectly consistent with Rust's semantics for function calls, as these do not promise to preserve the value of padding bytes in arguments. (And it would be impossible to promise it, because certain standard ABIs are incompatible with it.) There is no need for types to opt in.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but there would be a performance cost I'd think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It#s definitely non-trivial. In particular, once enum are involved, determining padding byte becomes runtime-dependent -- you have to generate LLVM IR that recursively walks the value, figures out the active variant of each enum, and zeros padding accordingly. For unions, there's nothing you can do, but I guess those are forbidden here anyway.

Copy link
Copy Markdown
Contributor

@Jules-Bertholet Jules-Bertholet Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems Clang does exactly what I suggest (overwrites padding with 0). For example: https://godbolt.org/z/8xEqjM3Kj

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-2 Lang team prioritization drag level 2. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.