Firmware component of the Ashen Open-Source HSM Stack https://yasec.de/projects/ashen/
  • Rust 88.2%
  • Nix 6.2%
  • Python 2.3%
  • Shell 2.1%
  • Linker Script 1.2%
Find a file
jaseg 107e333716
Some checks are pending
CI / Build and Test (push) Waiting to run
Fix HardFault and RX corruption on the LPUART1 DMA link
host.py deterministically killed the firmware on the first echo request:
the device sent a defmt log reading "panicked at 'DMA: user settings
error on DMA@40020000 channel 2'" (GPDMA1 CH2 = LPUART1 RX ring DMA) and
hardfaulted via the panic handler. Later stages of debugging surfaced
two more bugs underneath. All three are fixed here; the embassy-stm32
issues are patched into the vendored crate at nix build time (they are
present on embassy main as of 2026-07 and worth upstreaming).

1) embassy-stm32: GPDMA user-settings-error panic on RX restart.
   On any UART RX error the driver pauses the ring DMA channel
   (CR.SUSP) and restarts it on the next read. But unlike configure(),
   configure_linked_list() writes CR.RESET without first suspending the
   channel and waiting for SUSPF, and the SUSPF interrupt branch of
   on_irq() rewrites the whole CR register, which clears SUSP and
   resumes a channel the restart path just suspended. Either way RESET
   hits an *active* channel, which RM0456 forbids; the GPDMA raises
   USEF and embassy's DMA IRQ handler panics. Fixed by mirroring
   configure()'s suspend-and-wait guard in configure_linked_list() and
   by masking only SUSPIE in the SUSPF branch (which also preserves
   SUSP/SUSPF, and with them is_running() == false, for poll_stop()).

2) embassy-stm32: ring-buffer position race at the LLI switchover
   (this is what produced the RX errors feeding bug 1, and silent
   stale-data reads besides; same family as embassy issue #3900).
   The ping-pong two-LLI ring derives the DMA write position from a
   software LLI index (updated in the IRQ handler) plus the live BNDT
   register. Around a half-buffer switchover the two desync and the
   ring computes a bogus position, yielding garbage reads or phantom
   availability - deterministically whenever a frame straddled the
   boundary (the first echo request landed exactly on byte 128).
   Restarts additionally reused stale software indices against
   hardware that restarts at position 0. Fixed by replacing the
   ping-pong pair with a single self-linked LLI spanning the whole
   buffer (BNDT alone is then the exact, race-free position; HTF/TCF
   still fire at half and wrap), pre-loading the LLI into the channel
   registers so BNDT is valid from the moment of configuration, and
   resetting the software ring state on every start. The half-transfer
   interrupt is forced on because with one LLI the half-buffer wake
   comes from HTF, not a per-half-item TCF.

3) ashen-fw: read_frame() was not cancellation-safe.
   run_session()'s select() drops the in-flight receive future
   whenever a defmt log frame is queued mid-session, and read_frame()
   kept its partial frame in a local Vec - the already-consumed bytes
   were lost, desyncing the COBS stream and poisoning the next
   decrypt (observed as SessionEnd Crypto(Decrypt)). The partial-frame
   accumulator now lives in ResetReader, so a dropped future loses
   nothing and the next call resumes the same frame.

SplitIo now logs UART errors and main logs the session-end reason.
These logs are error-path only by design: a log on a success path is
itself forwarded over the link by run_session(), whose select() then
cancels and re-polls the receive future, re-arming itself forever.

Verified on hardware: 22 consecutive host.py runs pass (previously
failed on run 1, and after partial fixes on run 4, where the echo
frame crosses a ring boundary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:38:39 +02:00
.cargo Uno Q port WIP 2026-07-16 17:36:01 +02:00
.claude Add explicit rule denying claude access to host cargo 2026-06-10 16:42:39 +02:00
.github/workflows Rebase example project onto STM32G474RB blink baseline 2026-06-10 11:24:37 +02:00
.vscode Rebase example project onto STM32G474RB blink baseline 2026-06-10 11:24:37 +02:00
hosttest Fix UART on STM32U5 2026-07-17 15:36:11 +02:00
nix Fix HardFault and RX corruption on the LPUART1 DMA link 2026-07-17 19:38:39 +02:00
scripts WIP 2026-06-10 21:46:42 +02:00
src Fix HardFault and RX corruption on the LPUART1 DMA link 2026-07-17 19:38:39 +02:00
tests WIP add new framing code 2026-07-16 18:55:01 +02:00
.gitignore Init 2025-11-28 23:41:47 -08:00
build.rs Move to simple struct-based protocol 2026-07-16 17:59:39 +02:00
Cargo.lock Move to simple struct-based protocol 2026-07-16 17:59:39 +02:00
Cargo.toml Move to simple struct-based protocol 2026-07-16 17:59:39 +02:00
CLAUDE.md Rework FlashKeyStore to use memory-mapped flash via linker section 2026-06-11 12:41:12 +02:00
flake.lock verus: integrate into nix dev shell, verify cobs encode/decode 2026-06-18 12:58:13 +02:00
flake.nix verus: integrate into nix dev shell, verify cobs encode/decode 2026-06-18 12:58:13 +02:00
forward-uart.sh Fix UART on STM32U5 2026-07-17 15:36:11 +02:00
keystore.x Uno Q port WIP 2026-07-16 17:36:01 +02:00
LICENSE.md Rebase example project onto STM32G474RB blink baseline 2026-06-10 11:24:37 +02:00
README.md cobs: ship the Verus-verified codec and prove the round trip 2026-06-18 15:24:18 +02:00
run-openocd.sh Move to simple struct-based protocol 2026-07-16 17:59:39 +02:00
rust-toolchain.toml Uno Q port WIP 2026-07-16 17:36:01 +02:00

Ashen Firmware

Firmware for a custom STM32G474RB-based board, written in Rust using Embassy.

On boot it blinks the status LED on PA5 and brings up an authenticated, COBS-framed Noise link over UART (see Secure Link). The core COBS codec is formally verified with Verus (see Formal Verification).

Building and Flashing

Prerequisites

Software: Install Nix and enable flages Hardware: STM32G474RB devboard, USB UART adapter, and ST-Link/V2 or compatible SWD debugger

Build

nix build            # cross-compiled firmware ELF -> result/bin/ashen-fw

The flake pins the Rust toolchain (rust-toolchain.toml) and all build tools, so Cargo is run through the dev shell rather than a system-wide install:

nix develop -c cargo build --release --features embedded --target thumbv7em-none-eabihf

Flash

Flashing uses Cargo's runner configuration, which invokes probe-rs run --chip STM32G474RB after building (configured in .cargo/config.toml):

nix develop -c cargo run --release --features embedded --target thumbv7em-none-eabihf

Development

nix develop  # Enter the development shell with all tools, then:

cargo build --release --features embedded --target thumbv7em-none-eabihf
cargo run   --release --features embedded --target thumbv7em-none-eabihf
cargo test                                 # host-side tests (see Secure Link)
cargo doc --open --no-deps
nix fmt                                    # Format all code (Rust, Nix, etc.)

The shell also provides the Verus verifier and Cargo helpers (cargo-expand, cargo-bloat, cargo-binutils).

For automatic environment activation, use direnv:

echo "use flake" > .envrc
direnv allow

src/noise_link.rs implements a Noise_XX handshake (trust-on-first-use of the remote static key) over a byte transport, with an authenticated echo loop once the handshake completes. Messages are framed by a variable-length, formally verified COBS codec (src/cobs.rs) wrapped in a frame I/O layer (src/framing.rs). Frames are delimited by 0x00; if a frame is malformed, the implementation resynchronizes by discarding bytes up to the next delimiter and continuing with the next frame.

The link is built as part of the ashen_fw library so it can be exercised on the host:

nix develop -c cargo test

This builds the link implementation for the host, sets up an in-memory duplex transport, runs the handshake, performs echo round trips (including a multi-frame ~513-byte payload), and exercises frame resynchronization. The embedded feature (off by default) gates the firmware binary and its embedded-only dependencies, which require the thumbv7em-none-eabihf target.

Formal Verification

The core COBS codec in src/cobs.rs is formally verified with Verus. The same file is compiled into the firmware and checked by the verifier — a normal cargo build erases the proof annotations, so there is no separate, drifting copy. The frame I/O layer that depends on the async/transport crates lives apart in src/framing.rs to keep the verified file dependency-light.

Verified properties:

  • encode never emits a 0x00 byte, so 0x00 is an unambiguous frame delimiter;
  • decode never panics, never overflows, and never indexes out of bounds on zero-free input (the bytes it sees off the wire);
  • round trip — decode(encode(s)) == Ok(s) for every input s.

Run the proof:

nix build .#cobs-proof          # runs Verus in the sandbox; fails on regression
# or, inside the dev shell:
nix develop -c verus --crate-type=lib src/cobs.rs

Verus ships an x86_64-linux prebuilt only, so the verus and cobs-proof flake outputs are available on x86_64-linux.

Debug Mode

The debug-mode Cargo feature is reserved for enabling more verbose logging and debug-friendly clock configurations as the firmware grows:

nix develop -c cargo run --release --features debug-mode

Project Structure

ashen-fw/
├── .github/workflows/ci.yml      # CI configuration
├── src/
│   ├── main.rs                   # Firmware entry point (`embedded` feature)
│   ├── lib.rs                    # Host-testable library root
│   ├── cobs.rs                   # Formally verified COBS codec (Verus)
│   ├── framing.rs                # COBS frame I/O over a byte transport
│   ├── noise_link.rs             # Noise_XX secure link
│   ├── protocol.rs               # Application request/response protocol
│   ├── flash_keystore.rs         # Persisted remote static key (embedded-only)
│   ├── hw_rng.rs                 # RNG peripheral driver (embedded-only)
│   ├── defmt_link.rs             # defmt logging (embedded-only)
│   ├── panic_link.rs             # Panic reporting over the link (embedded-only)
│   └── test_util.rs              # Host-side test helpers
├── nix/
│   ├── packages/ashen-fw.nix     # Firmware build derivation
│   ├── packages/cobs-proof.nix   # Verus COBS proof as a build step
│   ├── packages/verus.nix        # Pinned Verus toolchain
│   ├── devShells.nix             # Development environment
│   ├── overlays.nix              # Rust overlay composition
│   └── formatter.nix             # treefmt configuration
├── flake.nix                     # Flake entry point
├── rust-toolchain.toml           # Rust toolchain specification
└── .cargo/config.toml            # Build target and probe-rs runner

License

MIT

References