Module 7 Exercises — Undefined Behavior and unsafe
Back to the Course 2 syllabus. Read first: Module 7 lessons (Seacord 4 and 11, Rust Book 20, and the Rustonomicon remain available as optional deep-dives).
Work in the labs repo’s course2/ folder — c/host/src/ex-4-N/, c/mcu/src/ex-4-N/, and rust/host/src/bin/ex-4-N.rs as each exercise names — and record everything in m7/notes.md. Everything in this module runs on the Mac: the C safaris and the Rust programs on the host (clang with -fsanitize=address,undefined; cargo run, cargo test, cargo +nightly miri test), the Cortex-M questions answered by cross-compiling with clang --target=thumbv7em-none-eabihf (or arm-none-eabi-gcc) and reading the disassembly. No board is needed; the register block in Exercise 7.6 is a fake one at a made-up address, because the subject is what the compiler does. Predicted cells are filled in before building; observed cells at the machine.
Exercises
Exercise 7.1 — The UB safari, in C. Four small programs in c/host/src/ex-7-1/, each built around one hazard from lessons §2 that firmware actually hits: (a) a signed int32_t tick delta that overflows across a wrap, with an if (now - then < 0) check after it; (b) a uint8_t mask built with 1 << n for n in {7, 8, 31, 32}; (c) a float read through a uint32_t * to extract its sign bit, in a function also storing through a uint32_t * argument; (d) a ring-buffer read with an index that reaches capacity. For each, first write your prediction, then record the behavior at clang -O0 and -O2, then under -fsanitize=address,undefined -fno-sanitize-recover=all, then write the correct version (unsigned wrap arithmetic, UINT32_C(1) << n with a checked n, memcpy, a masked index) and confirm the sanitizer is silent. Complete the table:
| Hazard | Predicted | -O0 |
-O2 |
Sanitizer report (which sanitizer, which check) | Correct idiom |
|---|---|---|---|---|---|
| Signed overflow | … | … | … | … | … |
| Oversized / sign shift | … | … | … | … | … |
| Aliasing pun | … | … | … | … | … |
| Out-of-bounds index | … | … | … | … | … |
For (a) and (c), also capture the -O2 disassembly and mark the instruction sequence that shows the assumption being used — the deleted compare, the cached load. In notes.md: one paragraph per hazard on which tool caught it (warning, sanitizer, neither), and why the aliasing case is the one no runtime tool reports.
Exercise 7.2 — The same safari, in Rust. Port the four programs of Exercise 7.1 to rust/host/src/bin/ex-7-2.rs as literally as the language allows. Before compiling, predict for each whether it (i) fails to compile, (ii) compiles and panics in a debug build, (iii) compiles and wraps or masks in a release build, or (iv) cannot be expressed without unsafe. Then verify with cargo run and cargo run --release, and — for the cases that need it — with overflow-checks = true added to the release profile. Where the literal port is impossible, write the idiomatic version (wrapping_sub, checked_shl, to_bits, a slice index) beside it.
| Hazard | Predicted outcome (i–iv) | Debug build | Release build | Release + overflow-checks |
Idiomatic form |
|---|---|---|---|---|---|
| Signed overflow | … | … | … | … | … |
| Oversized / sign shift | … | … | … | … | … |
| Aliasing pun | … | … | … | … | … |
| Out-of-bounds index | … | … | … | … | … |
Deliverable: the table, the compiler diagnostics for the (i) cases quoted verbatim, and a notes.md paragraph on what the profile setting costs — check cargo size of the two release variants — and which the mcu crate should pin.
Exercise 7.3 — A sound packet view. Design a small wire format — a 4-byte header (u8 type, u8 flags, u16 little-endian length) followed by a payload — and implement, in rust/host/src/bin/ex-7-3.rs, a Header type with #[repr(C)] and a function parse(buf: &[u8]) -> Option<(Header, &[u8])> written twice: once entirely in safe Rust (from_le_bytes, slice splitting), once with an unsafe fast path that reads the header through a raw pointer with read_unaligned, exporting a safe public API in both cases. Every unsafe block carries a // SAFETY: comment naming the invariant it relies on and where the caller established it. Write unit tests that cover the empty buffer, a buffer shorter than the header, a length field larger than the buffer, and a well-formed packet; run them under cargo test and cargo +nightly miri test. Then break one precondition deliberately (drop the length check) and record exactly what Miri reports and what the plain test run does not report.
| Case | Safe version | unsafe version, correct |
unsafe version, precondition removed |
|---|---|---|---|
cargo test result |
… | … | … |
cargo miri test result |
… | … | … |
| Diagnostic quoted | — | — | … |
Deliverable: both implementations with their contracts, the test file, and the notes.md verdict on whether the unsafe version earned its keep — compare the two -O2 disassemblies (cargo objdump --release) before answering.
Exercise 7.4 — Aliasing forensics: restrict vs. &mut. Write an in-place biquad or FIR update y[i] = a·x[i] + b·y[i-1] as a C function with plain pointers, then with restrict-qualified pointers, then as a Rust function taking &[f32] and &mut [f32]. Compile the C at -O2 for the host and for the Cortex-M4 (clang --target=thumbv7em-none-eabihf -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard), and the Rust with cargo objdump --release for both targets; count loads and stores in the inner loop and note whether it vectorized. Then call the C restrict version with x == y and the Rust version with the same slice for both arguments, and record what happens in each language.
C, no restrict |
C, restrict |
Rust &[f32] / &mut [f32] |
|
|---|---|---|---|
Inner-loop loads / stores (host, -O2) |
… | … | … |
Inner-loop loads / stores (M4, -O2) |
… | … | … |
| Vectorized on the host? | … | … | … |
| Called with aliased arguments | … | … | … |
In notes.md: why the Rust signature is the restrict promise made checkable, what the borrow checker’s error says when the promise is broken, and which of the three the CMSIS-DSP sources correspond to.
Exercise 7.5 — Alignment on the Cortex-M4. In c/mcu/src/ex-7-5/, declare a __attribute__((packed)) frame struct (uint8_t, uint32_t, uint16_t, in that order) and three ways of reading the uint32_t: through &frame->field passed to a function taking uint32_t *; through memcpy; and byte-wise with shifts. Cross-compile at -O2 and annotate the instruction each produces (LDR at an odd address vs. byte loads, and whether an LDM/LDRD ever appears when two adjacent fields are read together). Then write the same three in rust/host/src/bin/ex-7-5.rs with #[repr(C, packed)] — record which of the three the compiler refuses and its diagnostic, then use ptr::read_unaligned / addr_of! for the raw form and check it with Miri. Predict before building:
| Access | C, M4 -O2: instruction(s) |
C: warning at compile time? | Rust: compiles? | Rust: Miri verdict |
|---|---|---|---|---|
&frame->field → uint32_t * |
… | … | … | … |
memcpy out |
… | … | … | … |
| Byte-wise decode | … | … | … | … |
| Two adjacent fields, one read | … | — | … | … |
Deliverable: the annotated listings plus a notes.md paragraph on why “the M4 tolerates unaligned LDR” is not a defense — name the instructions that do not tolerate it, and the CCR bit that makes even LDR trap.
Exercise 7.6 — Register pointers, three ways. Take a fake GPIO register block at a made-up address (a volatile struct in C; raw pointers with read_volatile/write_volatile in Rust) and implement set_pin, clear_pin, and toggle_pin. C in c/mcu/src/ex-7-6/; Rust in rust/host/src/bin/ex-7-6.rs (host-compiled and --target thumbv7em-none-eabihf-checked — never executed, the address is fake), written first with a static mut handle (record the 2024-edition diagnostic), then with &raw mut, then wrapped in a Gpio type whose public methods are safe and whose single unsafe fn new(base: *mut Regs) states the precondition in a # Safety section. Compare the -O2 cross-compiled output of the C and the Rust toggle: the same read-modify-write sequence should appear.
| Variant | Compiles? | Diagnostic (if any) | -O2 M4 instructions for toggle_pin |
|---|---|---|---|
C, volatile struct |
… | — | … |
Rust, static mut |
… | … | — |
Rust, &raw mut + volatile |
… | … | … |
Rust, safe Gpio wrapper |
… | … | … |
In notes.md: what unsafe is confined to in the final version (count the blocks), what the // SAFETY: comments claim, and which claim the type system cannot check — the one that will become a PAC’s Peripherals::take() singleton in Module 8.
Exercise 7.7 — Static analysis on a real Course 3 module. Run both analyzers over the portable kernels in Course 3’s firmware/shared/ and their host harness: scan-build cmake --build … for clang’s analyzer on the host build, and arm-none-eabi-gcc -fanalyzer (if installed; otherwise clang --analyze with the cross target) on the same sources. Also run the harness’s tests under -fsanitize=address,undefined. Triage every report into true positive, false positive with reason, or needs a test to decide, and for each true positive write the fix and the assertion (_Static_assert, assert, or a configASSERT-style runtime check with a stated field policy) that would have documented the contract.
| Tool | Reports | True | False (reason) | Undecided |
|---|---|---|---|---|
| clang analyzer (host) | … | … | … | … |
GCC -fanalyzer / clang cross |
… | … | … | … |
| ASan + UBSan (tests) | … | … | … | … |
Deliverable: the triage table and a half-page notes.md entry — the course’s quality-gate policy in draft: which tools run on which tier, what a firing assertion does in the field, and what the analyzers found in code that “already worked.” Module 12’s gate starts from this draft.