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->fielduint32_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.