Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to Latch — embedded failure-capture runtime.

## [Unreleased]

### Added
- **Power-fail seal (Last-Microjoule commit)** — NMI-safe `ls_powerfail_seal()` freezes a fixed retained record plus VBAT backup-RAM mirror; `ls_boot()` promotes it as an `EMERGENCY` reset event with additive LEP TLV 23 (`POWERFAIL_SEAL`); torn writes ignored, clear-after-append preserved. Host-tested; HIL brownout rig not yet run (`not hardware-tested`).

## [1.0.0-rc.2] - 2026-08-19

### Added
Expand Down
8 changes: 4 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ if(ESP_PLATFORM)
src/core/peripheral.c src/core/assert.c src/core/log.c src/core/performance.c
src/core/boot.c src/core/build_id.c src/core/policy.c src/core/blackbox.c
src/core/mission.c src/core/time_sync.c src/core/fingerprint.c src/core/supervisor.c
src/core/trace.c src/core/provisioning.c src/core/selftest.c src/core/fault_injection.c src/core/ota.c src/core/environment.c
src/core/trace.c src/core/provisioning.c src/core/selftest.c src/core/fault_injection.c src/core/ota.c src/core/environment.c src/core/powerfail.c
src/capture/capture.c src/envelope/lep.c src/envelope/compression.c
src/spool/spool.c src/storage/memory_storage.c src/storage/storage_sim.c
src/storage/flash_mirror.c src/storage/wear_level.c src/storage/secure_storage.c
Expand Down Expand Up @@ -65,7 +65,7 @@ include(cmake/GenerateBuildId.cmake)
set(LS_CONFIGURABLE_DEFINITIONS
LS_CONSTRAINED_PROFILE LS_REQUIRE_EXTERNAL_CRYPTO_PROVIDER LS_MIN_CRYPTO_ASSURANCE LS_STORE_STRINGS LS_ENABLE_BREADCRUMBS LS_ENABLE_METRICS LS_ENABLE_LOGS
LS_ENABLE_POWER_SAMPLES LS_ENABLE_PERFORMANCE LS_ENABLE_DUMPS LS_ENABLE_ASSERTS
LS_ENABLE_WIDE_CONTEXT
LS_ENABLE_WIDE_CONTEXT LS_ENABLE_POWERFAIL_SEAL
LS_ENABLE_STACK_SNAPSHOT LS_COMPILED_MIN_LEVEL LS_MAX_EVENT_SIZE
LS_BREADCRUMB_CAPACITY LS_BREADCRUMB_MESSAGE_MAX LS_BREADCRUMB_CATEGORY_MAX
LS_BREADCRUMB_KV_MAX LS_METRIC_CAPACITY LS_METRIC_NAME_MAX LS_METRIC_WINDOW_SIZE
Expand All @@ -92,7 +92,7 @@ if(LS_COMMERCIAL_PROFILE)
add_compile_definitions(LS_REQUIRE_EXTERNAL_CRYPTO_PROVIDER=1 LS_MIN_CRYPTO_ASSURANCE=3)
endif()

set(LS_CORE_SOURCES src/core/runtime.c src/core/util.c src/core/memory.c src/core/health.c src/core/peripheral.c src/core/assert.c src/core/log.c src/core/performance.c src/core/boot.c src/core/build_id.c src/core/policy.c src/core/blackbox.c src/core/mission.c src/core/time_sync.c src/core/fingerprint.c src/core/supervisor.c src/core/trace.c src/core/provisioning.c src/core/selftest.c src/core/fault_injection.c src/core/ota.c src/core/environment.c)
set(LS_CORE_SOURCES src/core/runtime.c src/core/util.c src/core/memory.c src/core/health.c src/core/peripheral.c src/core/assert.c src/core/log.c src/core/performance.c src/core/boot.c src/core/build_id.c src/core/policy.c src/core/blackbox.c src/core/mission.c src/core/time_sync.c src/core/fingerprint.c src/core/supervisor.c src/core/trace.c src/core/provisioning.c src/core/selftest.c src/core/fault_injection.c src/core/ota.c src/core/environment.c src/core/powerfail.c)
set(LS_CAPTURE_SOURCES src/capture/capture.c)
set(LS_ENVELOPE_SOURCES src/envelope/lep.c src/envelope/compression.c)
set(LS_SPOOL_SOURCES src/spool/spool.c)
Expand Down Expand Up @@ -315,7 +315,7 @@ if(LS_BUILD_TESTS)
add_executable(latch-core-observability-tests tests/test_core_observability.c)
target_link_libraries(latch-core-observability-tests PRIVATE latch)
add_test(NAME latch-core-observability-tests COMMAND latch-core-observability-tests)
foreach(test_name security crypto-vectors crypto-negative secure-element secure-storage secure-power-loss wear-level compression flash policy transport envelope-fuzz power-loss spool-stress property storage-bounds transport-policy build-id envelope-errors spool-priority crypto-provider update defensive-paths stream-edges memory-capture spool-edges envelope-capacity runtime-edges auv-runtime auv-edges ota fault-injection-pipeline replay-property)
foreach(test_name security crypto-vectors crypto-negative secure-element secure-storage secure-power-loss wear-level compression flash policy transport envelope-fuzz power-loss powerfail-seal spool-stress property storage-bounds transport-policy build-id envelope-errors spool-priority crypto-provider update defensive-paths stream-edges memory-capture spool-edges envelope-capacity runtime-edges auv-runtime auv-edges ota fault-injection-pipeline replay-property)
string(REPLACE "-" "_" test_source ${test_name})
add_executable(latch-${test_name}-tests tests/test_${test_source}.c)
target_link_libraries(latch-${test_name}-tests PRIVATE latch)
Expand Down
62 changes: 47 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ The embedded runtime is heap-free C11 with bounded buffers: you keep control of
[![C11](https://img.shields.io/badge/C-C11-00599C.svg)](CMakeLists.txt)
[![Rust no_std](https://img.shields.io/badge/Rust-no__std-000000.svg)](rust/README.md)

```text
without Latch HardFault -> reboot -> "could not reproduce"

with Latch HardFault -> retained snapshot -> reboot -> persistent spool -> your transport
+-> CPU context, reset reason, build ID,
breadcrumbs, metrics, and health data
```mermaid
flowchart TB
subgraph without["without Latch"]
direction LR
f1["HardFault"] --> r1["reboot"] --> lost["“could not reproduce”"]
end
subgraph with["with Latch"]
direction LR
f2["HardFault"] --> snap["retained snapshot"] --> r2["reboot"] --> spool["persistent spool"] --> t["your transport"]
end
snap -. "CPU context · reset reason · build ID<br/>breadcrumbs · metrics · health data" .-> spool
```

[![Latch physical ESP32 crash, reboot, recovery, and durable ACK demonstration](docs/assets/latch-esp32-demo.gif)](hil/esp32_relay/README.md)
Expand All @@ -36,6 +41,17 @@ ls_capture_message("sensor timeout", LS_SEVERITY_ERROR);

Architecture ports can capture fault state automatically. On the next boot, Latch promotes the retained snapshot into a transactional spool; normal runtime can then call `ls_flush()` to deliver a deterministic [LEP v1](docs/lep-v1.md) envelope through the best available transport.

## Development status

Latch `v1.0.0` is a stable, host-tested core with one physical HIL configuration (ESP32).
Active development continues on `main`.

> [!CAUTION]
> The power-fail seal (LEP TLV 23, [brownout-proof commit](docs/powerfail-seal.md))
> is implemented and host-tested but has **no physical brownout-HIL evidence
> yet — treat it as experimental**. Check [known limitations](docs/known-limitations.md)
> before treating any unreleased feature as qualified.

## Try it in 60 seconds

You need CMake 3.20+, Ninja, and a C/C++ compiler. The host demo uses the same public in-memory storage, transport, capture, and decoding APIs as an embedded integration.
Expand Down Expand Up @@ -108,13 +124,17 @@ No allocator, scheduler, network stack, or hardware register map is hidden insid

## How it works

```text
fault -> retained minimal snapshot -> reboot -> LEP envelope --+
|
error -----------> bounded state snapshot -> LEP envelope -----+-> persistent spool
|
v
normal runtime -> transport -> ACK
```mermaid
flowchart TB
fault["fault"] --> snap["retained minimal snapshot"]
snap --> reboot["reboot"]
reboot --> env1["LEP envelope"]
error["error"] --> state["bounded state snapshot"]
state --> env2["LEP envelope"]
env1 --> spool["persistent spool"]
env2 --> spool
spool --> runtime["normal runtime"]
runtime --> transport["transport"] --> ack["ACK"]
```

The critical path stays deliberately small. Unknown LEP TLVs are skippable, interrupted spool records are ignored during recovery, and retained fault state is cleared only after it is promoted successfully. Read the [architecture](docs/architecture.md), [concurrency contract](docs/concurrency.md), and [wire format](docs/lep-v1.md) for the invariants.
Expand Down Expand Up @@ -183,9 +203,21 @@ Feature switches and buffer capacities live in [`include/laststate/config.h`](in

## Security and production status

Latch supports XChaCha20-Poly1305 envelopes, HKDF-SHA-256 domain separation, replay windows, authenticated at-rest storage, and hardware-backed key contracts. These mechanisms still require a hardware CSPRNG, per-device provisioning, verified TLS, and an independent review for the product threat model. Latch has not claimed an independent cryptographic audit or MISRA compliance; read [security, audit and compliance status](docs/assurance.md) and the [security policy](SECURITY.md) before enabling encryption or dumps.
Latch supports XChaCha20-Poly1305 envelopes, HKDF-SHA-256 domain separation, replay windows, authenticated at-rest storage, and hardware-backed key contracts. These mechanisms still require a hardware CSPRNG, per-device provisioning, verified TLS, and an independent review for the product threat model.

> [!WARNING]
> Latch has not claimed an independent cryptographic audit or MISRA compliance.
> Read [security, audit and compliance status](docs/assurance.md) and the
> [security policy](SECURITY.md) before enabling encryption or dumps in production.

The portable runtime and wire format are extensively host-tested.

The portable runtime and wire format are extensively host-tested. Hardware fault entry, linker placement, flash geometry, reset registers, vendor networking, TrustZone boundaries, and secure elements **must be qualified on each selected board and toolchain**. Host tests are not hardware certification. The exact release gates are in [production readiness](docs/production-readiness.md) and [implementation status](docs/implementation-status.md).
> [!IMPORTANT]
> Hardware fault entry, linker placement, flash geometry, reset registers,
> vendor networking, TrustZone boundaries, and secure elements **must be
> qualified on each selected board and toolchain**. Host tests are not hardware
> certification. The exact release gates are in [production readiness](docs/production-readiness.md)
> and [implementation status](docs/implementation-status.md).

On 2026-07-29 the complete flash → panic → reboot → UART → durable ACK path
was verified on a physical ESP32-D0WD-V3 with ESP-IDF 5.5.0 against the
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ The thread, ISR and fault-context ownership rules are defined in [Concurrency](c
The LEP encoder places identity, reset metadata, event summary, optional CPU frame, breadcrumbs and metrics into TLVs. Unknown TLVs are skippable by their length. [LEP v1](lep-v1.md) defines canonical little-endian encoding, bounds checks, security layouts, truncation and versioning; the C and Rust decoders share golden vectors.

For Cortex-M, only the entry selection belongs in assembly: the handler tests `EXC_RETURN[2]`, reads the original MSP or PSP, and transfers both to C. The handler switches to a dedicated emergency stack, validates the selected frame against registered stack bounds and writes only a small `.noinit` snapshot. Normal boot promotes a valid retained snapshot into the transactional spool and clears it only after that append succeeds. The application must set fault-handler priorities and reset policy suited to its MCU. Verify an actual target before enabling fault persistence in production.

A PVD/NMI handler may additionally call `ls_powerfail_seal()` to freeze a fixed power-fail record into retained plus backup RAM; `ls_boot()` promotes it first, as an `EMERGENCY` reset event with TLV 23, under the same clear-after-append rule. See [power-fail seal](powerfail-seal.md).
2 changes: 2 additions & 0 deletions docs/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ Interrupt handlers may record only data through APIs documented by the selected

Fault handlers are separate from the normal runtime. The Cortex-M and RISC-V ports switch to an emergency stack, avoid spool, storage, transport and reset callbacks, and write a fixed retained snapshot. A recursive Cortex-M fault writes one recursive snapshot and then stops. Normal boot validates and persists the snapshot before clearing it.

Power-fail NMI handlers follow the same rule: `ls_powerfail_seal()` writes only the fixed retained seal plus the installed backup-RAM mirror with bounded stores. Promotion to the spool happens in `ls_boot()`, never in the NMI path. See [power-fail seal](powerfail-seal.md).

The application owns synchronization for callback implementations, storage drivers, network stacks and secure elements. A callback must not re-enter Latch while it is executing on behalf of Latch.
6 changes: 6 additions & 0 deletions docs/known-limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ an LTS release.
**emulator-tested (Renode) only** — not physical-board qualification.
- **Automatic Xtensa panic-frame capture** is an integration boundary on ESP32 and
has not been re-qualified on physical HIL.
- **Power-fail seal (LEP TLV 23)** is implemented and host-tested only. No
physical brownout run exists yet: no programmable-supply ramp/cut evidence,
no measured `vcap_mv` threshold per board, no SPI-FRAM or single-shot Flash
slot claim. The run procedure is staged at `hil/brownout_seal/README.md`
with evidence marked NOT RUN. Treat the seal as experimental until that
fixture reports PASS on the exact board, revision, SDK, and toolchain.
- Per-board physical HIL, and vendor TLS/BLE/LoRaWAN/CAN/secure-element/TrustZone
qualification remains product-qualification work.

Expand Down
2 changes: 1 addition & 1 deletion docs/lep-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ With AEAD, metadata length is 28 bytes: a 24-byte XChaCha20 nonce followed by a

An unencrypted payload is a sequence of TLVs. Each TLV is a two-byte nonzero type, a two-byte value length and that many value bytes. TLVs are contiguous with no padding. Unknown types are skipped by length. A receiver must reject a zero type, an incomplete TLV header or a value extending past the payload boundary.

Types 1 through 15 are currently assigned to identity, reset, event, CPU, fault, breadcrumb, metric, power, health, assert, peripheral, log, memory, stack and heap data respectively. Type 16 (`CPU64`) is an additive full-width CPU extension. New types may be added in future minor protocol revisions; existing type semantics are immutable in v1.
Types 1 through 15 are currently assigned to identity, reset, event, CPU, fault, breadcrumb, metric, power, health, assert, peripheral, log, memory, stack and heap data respectively. Type 16 (`CPU64`) is an additive full-width CPU extension. Types 17 through 22 carry blackbox, mission, time-sync, provisioning, supervisor, and environment data. Type 23 (`POWERFAIL_SEAL`) is an additive 13-byte power-fail seal: encoding (`1`), reason, tier, `vcap_mv` (`u16` LE), `boot_id` (`u32` LE), fault (`u32` LE). New types may be added in future minor protocol revisions; existing type semantics are immutable in v1.

### CPU64 extension (type 16)

Expand Down
55 changes: 55 additions & 0 deletions docs/no-cmake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Latch without CMake (vendoring + direct compile)

Latch is portable C11 with no mandatory build system. CMake is the tested
path (`cmake --preset host-debug`), but you can vendor the sources and
compile with anything: gcc, clang, MSVC, ESP-IDF, PlatformIO.

## 1. Vendor (copy what you need)

Smallest useful set (host example proves the API surface):

```sh
mkdir -p vendor/latch
cp -r include/ vendor/latch/include
cp -r src/envelope/ src/storage/ src/transport/ src/capture/ vendor/latch/src
# KEEP: include/laststate/config.h — edit the LS_* knobs for your target
```

Verify against the repo: `python tools/minify_sources.py --output /tmp/latch-production --verify`.

## 2. Compile (no CMake)

```sh
# gcc / clang — freestanding-friendly C11, warnings as errors
cc -std=c11 -Wall -Wextra -Werror \
-I vendor/latch/include \
$(find vendor/latch/src -name '*.c') \
-c # then link into your firmware

# MSVC (host tools / latch-dump)
cl /std:clatest /W4 /I vendor\latch\include vendor\latch\src\*.c
```

Link only what you use: storage + transport are swappable — provide your
own `ls_storage_*` / `ls_transport_*` backends (flash, RTC RAM, UART) and
skip the host in-memory ones.

## 3. ESP-IDF / PlatformIO (no CMakeLists edits on your side)

- ESP-IDF: add `vendor/latch/src/*.c` + `vendor/latch/include` to your
component `CMakeLists` `SRCS`/`INCLUDE_DIRS` (or list files explicitly);
set `LS_*` options in a copied `config.h`. Full panic→reboot→ACK walkthrough:
`examples/esp32-first-crash/README.md`, HIL fixture: `hil/esp32_relay/README.md`.
- PlatformIO: same files under `lib/latch/`; add `build_flags = -I lib/latch/include`.

## 4. Validate the vendored copy

```sh
# vectors (no build system needed beyond a C compiler + latch-dump):
latch-dump --hex tests/vectors/lep-v1-basic.hex
latch-dump --hex tests/vectors/lep-v2-basic.hex
```

Keep vectors + `latch-dump` in CI: they catch envelope regressions before
flashing. For crypto posture (built-in vs commercial provider), see
`docs/security/crypto-assurance.md`.
52 changes: 52 additions & 0 deletions docs/powerfail-seal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Power-fail seal (Last-Microjoule commit)

Latch survives brownout resets that erase ordinary evidence. A PVD/NMI
handler seals a fixed 64-byte record into retained memory with bounded,
heap-free stores only. Normal boot promotes a valid seal into the spool
before clearing it. Not hardware-tested.

## Context separation

- NMI/PVD path (`ls_powerfail_seal`): retained memory plus the installed
backup window only. No allocation, storage, transport, crypto, logging,
locks, or reset callbacks. A second NMI during the same brownout seals
once and stops so a dying rail cannot torn-write the record.
- Normal runtime (`ls_powerfail_recover`, called by `ls_boot`): validates
magic, version, reason range, and CRC; emits an `EMERGENCY` reset event
with TLV 23; appends to the spool; clears the seal only after the append
succeeds. Interrupted records stay ignored on recovery.

## Tiers

- Tier RAM: internal `.noinit` record. Survives brownout reset while SRAM
stays powered.
- Tier backup RAM: integrator registers a VBAT-retained window with
`ls_powerfail_install_backup()`. The NMI path mirrors the seal there
with direct bounded stores. Recovery prefers `.noinit` and falls back
to the mirror. True power-loss with dead VBAT is out of scope for the
portable runtime and must be qualified per board.

## Wire format

Additive LEP v1 TLV 23 (`POWERFAIL_SEAL`): encoding `1`, reason, tier,
`vcap_mv` little-endian `u16`, `boot_id` `u32`, fault `u32` (13 bytes).
Legacy decoders skip it by length. `latch-dump` prints it as
`powerfail_seal`.

## Integration

```c
static uint8_t backup_ram[128]; /* VBAT-retained section via linker */

ls_powerfail_install_backup(backup_ram, sizeof(backup_ram));

/* In the PVD/NMI ISR, after switching to a known-good stack: */
ls_powerfail_seal(LS_POWERFAIL_BROWNOUT, vcap_mv);
```

Call `ls_powerfail_install_backup()` once after `ls_init()`, before
`ls_boot()`. `ls_boot()` promotes automatically. `ls_boot_mark_successful()`
clears the in-stream seal flag. SPI FRAM and single-shot Flash slots are
not claimed; they need board-level HIL with a programmable supply before
any production claim. See [HIL](hil.md) and
[hardware compatibility](hardware-compatibility.md).
Loading
Loading