Small, runnable evidence for two questions in the MISRA C++:2023 → Rust mapping (issue #575, PR #1226).
Everything here compiles and runs. Reproduce it all with:
./run-evidence.sh
cargo test
Verified on rustc 1.96.0 (ac68faa20 2026-05-25), edition 2024, aarch64-apple-darwin.
The mapping rationale currently says:
In safe rust the use of inline assembly is not possible, therefore this rule doesn't apply to safe rust.
Testing the three ways to write assembly in Rust gives a more nuanced picture:
| Mechanism | Needs an unsafe block? |
Rejected by #![forbid(unsafe_code)]? |
|---|---|---|
asm! |
yes | yes — "usage of an unsafe block" |
global_asm! |
no | yes — "using this macro is unsafe even though it does not need an unsafe block" |
#[unsafe(naked)] + naked_asm! |
no | no — compiles cleanly |
Three conclusions:
-
The quoted sentence is false as written.
global_asm!compiles with nounsafeblock anywhere (src/bin/a1_global_asm_no_unsafe_block.rs). -
The unsafe-only classification is still defensible — but the criterion should be
forbid(unsafe_code), not impossibility. The compiler states outright thatglobal_asm!is unsafe code that merely doesn't need the block syntax (negative/n1_global_asm_forbidden.rs). That is a precise, checkable criterion; "not possible" is not. -
global_asm!can run beforemain(src/bin/a3_ctor_runs_before_main.rs). It can emit a constructor entry, so the code executes with no call site anywhere in the Rust source and nounsafekeyword in the file. A reviewer auditing a safe-Rust crate has nothing in the source to look at. -
Naked functions pass
#![forbid(unsafe_code)]entirely (src/bin/a2_naked_passes_forbid.rs). A whole function body of raw machine code compiles inside a crate that has declared itself free of unsafe code. Naked functions were stabilized in Rust 1.88, after the lint gained itsglobal_asm!coverage, so this may simply be a gap inunsafe_codeworth reporting upstream. Either way it means naked functions are assembly that no standard safety gate currently catches, which is relevant both to how 10.4.1 is worded and to the consortium's Clippy lints goal.
src/bin/b1_uninit_heap_read.rs reads heap storage that was never written:
let p = unsafe { alloc(layout) } as *mut u32; // p IS initialized
let v = unsafe { *p }; // never written -> UBTwo points for the rationale:
- It separates 11.6.2 from 11.6.1. No variable is uninitialized here —
pis a perfectly good pointer. What is unset is the object it addresses, which is exactly the noun 11.6.2 uses. (MaybeUninitis a poor example for this, because the ratified 11.6.1 rationale already namesMaybeUninitas the reason 11.6.1 applies to unsafe Rust — both rules are engaged there, not just one.) - It printed
0. Fresh pages from the OS are usually zeroed, so this UB routinely looks like it works. Reading uninitialized memory is UB even for integer types — not "you get a garbage value" — which is the part most often misunderstood, and the reasonMaybeUninitexists.
Verified on the official Playground, rustc 1.98.1 (2026-09-01), edition 2024,
x86_64 Linux. The Mach-O demos in this repo use __DATA,__mod_init_func; these
use the ELF equivalent .init_array.
- Assembly runs before
main, nounsafeanywhere in the file — prints[ctor] I ran first.then[main] main started.Run it global_asm!is rejected by#![forbid(unsafe_code)]— "using this macro is unsafe even though it does not need anunsafeblock" Run it- Naked functions are also rejected by
#![forbid(unsafe_code)]on 1.98.1 (this wasa2, nownegative/n3) — "usage of the unsafe#[naked]attribute" Run it
.github/workflows/evidence.yml rebuilds and re-runs every demo on stable, on
both Linux and macOS, on each push and once a month. Every claim here is a claim
about what one toolchain accepts, so it is worth exactly as much as its last
re-run. A separate toolchain-drift job asserts both halves of the correction
above: that 1.96.0 accepts the naked-function file and that current stable
rejects it. If either flips, CI fails rather than the README quietly going stale.
The constructor demo now covers Mach-O (__DATA,__mod_init_func, aarch64) and
x86_64 ELF (.init_array); CI checks that [ctor] I ran first. really is the
first line of output on both.
Earlier revisions of this repo claimed that #![forbid(unsafe_code)] rejects
global_asm! but lets naked functions through, and suggested that was a lint gap
worth reporting upstream. That was true on rustc 1.96.0 (2026-05-25), where
src/bin/a2_naked_passes_forbid.rs still compiles and runs. It is no longer
true on rustc 1.98.1, which rejects #[unsafe(naked)] under the same lint
with a dedicated diagnostic. Nothing needs reporting upstream; it has been fixed.
This does not affect the Rule 10.4.1 argument, which rests on global_asm!
requiring no unsafe block at all, not on the behaviour of the unsafe_code
lint. It is also a small illustration of the point the argument is about: a
toolchain two releases old gives a different answer about what is and is not
reachable from safe Rust.
MISRA C++:2023 gives 10.4.1 category Required, and its rationale concerns the
asm declaration being conditionally-supported, producing implementation-defined
behaviour, and harming portability, with intrinsics named as the better modern
alternative. None of it concerns memory safety.
Rust's unsafe keyword gates memory safety and nothing else. So "does it need
unsafe" does not track what this rule protects, and the harm the rule describes
is reachable from safe Rust via global_asm!.
(The standard's text is not quoted here. It is commercially licensed and not redistributable; the above is a characterisation, not a reproduction.)
These are facts about rustc, not about MISRA. Whether they change a classification depends on MISRA C++:2023's rationale and amplification text for 10.4.1, which the author does not have access to — only the public MathWorks headline listing. Corrections welcome.
src/bin/a1_global_asm_no_unsafe_block.rs global_asm! with no unsafe block -> compiles
src/bin/a2_naked_passes_forbid.rs naked fn under forbid(unsafe_code) -> compiles
src/bin/a3_ctor_runs_before_main.rs global_asm! ctor runs before main -> compiles
src/bin/b1_uninit_heap_read.rs reads unwritten heap storage -> UB, prints 0
negative/n1_global_asm_forbidden.rs global_asm! under forbid -> must fail
negative/n2_asm_forbidden.rs unsafe block under forbid -> must fail
tests/negative.rs asserts the two negatives fail
run-evidence.sh runs everything, prints the table above