- Rust 88.2%
- Nix 6.2%
- Python 2.3%
- Shell 2.1%
- Linker Script 1.2%
|
Some checks are pending
CI / Build and Test (push) Waiting to run
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> |
||
|---|---|---|
| .cargo | ||
| .claude | ||
| .github/workflows | ||
| .vscode | ||
| hosttest | ||
| nix | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| flake.lock | ||
| flake.nix | ||
| forward-uart.sh | ||
| keystore.x | ||
| LICENSE.md | ||
| README.md | ||
| run-openocd.sh | ||
| rust-toolchain.toml | ||
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
Secure Link
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:
encodenever emits a0x00byte, so0x00is an unambiguous frame delimiter;decodenever 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 inputs.
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