at main
3 folders
5 files
Right-size the flash layout and write the index once per import
Two capacity items that were blocking the raised per-variant caps.
The partition tables gave the app far more room than the largest image
needs while starving the vault: the 8 MB boards now run 2 x 0x2C0000 app
slots with a 0x260000 LittleFS, the 4 MB boards a 0x290000 app with
0x160000 of filesystem, and the 16 MB tables keep their geometry. The
loaded 8 MB build sits at 78 % of its app slot and the 4 MB ones near
70 %, so the headroom is real rather than asserted. Two rationales that
had gone stale — a ">1500 credentials" claim and an Arduino/ESP-TEE
headroom note — are gone.
Changing the layout means a device must be fully re-flashed and its vault
repopulated, which is the standing pre-release policy; the flashing and
variant docs now say so where someone upgrading would look.
The bulk import used to rewrite index.bin for every row, and each rewrite
materialized the whole capacity-scaled working set: at 500 credentials
that is a flash rewrite and ~89 KB of transient per credential. A batch
guard now defers the rows and rebuilds the index once at the end.
Deferral is only safe if a half-finished batch can never be mistaken for
a finished one, and review found three ways it could be:
- A transient key-derivation failure returned without closing the batch.
Every later index mutation then reported success while writing nothing,
favorites and ordering survived only in RAM, and the portal's
index-repair button reported success while doing nothing — until a
reboot. The batch now closes before the write, so a failed write cannot
leave mutators deferring into a buffer nobody will flush, and the
key-derivation path aborts the batch explicitly.
- If the pre-batch unlink failed — lfs_remove needs a metadata block, so
it fails on a full filesystem — the old index survived and the batch
deferred every row into oblivion, leaving a MAC-valid, complete-looking
index missing every imported credential with no path back. The batch
now refuses to open and per-row writes stand: slower, not wrong.
- The batch depth was atomic to survive the vault worker's inline-
execution fallback but the snapshot pointer was not, leaving a
use-after-free window in exactly that case.
Also: the capacity guard's ceiling search could two-cycle and return
whichever value the iteration budget's parity landed on, one credential
above the self-consistent ceiling — failing open in the direction it
exists to prevent. It now iterates to a real fixpoint and takes the lower
value on a cycle.
Verified: native 2860/2860, the variant-config guard over all 38
environments, and every one of the eight boards builds — including
m5core_ink, which had never been built directly before.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Feed the caller's watchdog while the vault worker runs a flash-length job
A vault wipe erases the whole 1.9 MB partition and, since the capacity
grew to 150 slots, takes 6-8 s on a populated vault. Every synchronous
caller of the vault worker hard-blocked on the completion semaphore for
that whole time — and the Core-1 loop task is TWDT-subscribed, so a wipe
from PIN-entry brute-force, the portal commit, or the debug console
panicked the device MID-WIPE at the 5 s watchdog, leaving LittleFS
half-formatted. Verified on silicon: a 13-credential wipe now completes
in 7.7 s where it previously rebooted at 5.
The fix lives at the one seam every caller shares: dispatchRequest waits
in one-second slices and feeds the calling task's watchdog between them.
resetCurrentTask() is a silent no-op for unsubscribed callers, so paths
that were never at risk pay nothing.
The BLE QA that exposed this gets three rig-truth fixes of its own:
- The vault-wipe bond test erases the DUT side of a bond by design, which
orphans the LTK BlueZ keeps; it now forgets the host side symmetrically
(setup and finally) via a new LinuxBleHarness.forget_dut() that also
sweeps RPA-keyed stray records, so the test cannot poison its own
reruns or the tests after it.
- ble_normal.py canonicalizes to the identity address after a fresh
pairing: BlueZ re-keys the device record once the IRK arrives, and the
RPA the scan found stops resolving — the uhid lookup and the reconnect
both used the dead address.
- The test now asserts on nimble_bonds (the keystore factoryResetBonds
actually erases) instead of the lazily-flushed bond shadow, and stages
itself on AUTH rather than the HOGP typing step, which races BlueZ
5.66's HoG input plugin and is covered by test_happy_path anyway.
The pairing prerequisite of that test remains flaky on the mastodon rig
(uhid/HoG race); the wipe-erases-bonds property itself is verified over
serial: BONDS? reports count=0 nimble_bonds=0 after a populated wipe.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Add pytest QA system bootstrap (Phase 0, no hardware)
Introduce the qa/ pytest system that will become the single entry-point for
all Kleidos QA (HIL + a thin wrapper over the native Unity suite), replacing the
~60 loose scripts. Phase 0 lands the no-hardware skeleton:
- pyproject.toml: strict markers/config, live logging, marker taxonomy (layer /
requirement / capability / cost), Python-only ruff+black config.
- requirements.txt + constraints.txt: pinned stack on Python 3.13. Drop
pytest-repeat — its --count collides with pytest-embedded's --count (the
mandated transport foundation); soak uses in-test loops instead.
- qa_lib: config resolver (CLI > env > .env > yaml > default) + MAC-keyed device
and host inventories; central redaction filter (logs + Allure attachments);
per-test redacting logging; transport interfaces + mock DUT/host for the
no-hardware mode; redacting Allure evidence helpers.
- inventory: devices.yaml (all 9 DUTs, derived from docs/device-identifiers.md,
MAC = identity), hosts.yaml (mastodon Linux + Plus1 embedded, coords by
env-var name), .env.example placeholders.
- tests: redaction guard (seeded secrets never leak on log/attachment/plain
paths), config + inventory-drift guard (devices.yaml vs the doc), mock
transport, and the native Unity wrapper (skips cleanly without PlatformIO).
Gate: `pytest -m "not needs_hardware"` green (20 passed, native suite included);
`pytest --collect-only`/`--markers` show the taxonomy; redaction self-test
proves no seeded secret leaks; ruff + black clean.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Harden BLE bond lifecycle and land full on-device QA coverage
Defensive BLE audit of src/ble/ (audit dossier: docs/security/audit-ble.md).
The stack was sound; this closes the one actionable finding plus the QA gaps.
BLE-L1 (the finding): BLE bonds survived a vault wipe / factory reset — the
NimBLE keystore (nimble_bond NVS), the kleidos_blnames name blob, and its RTC
mirror all live in NVS, which wipeAll() never touched, so a previous owner's
bonded hosts and the device IRK persisted across a factory reset. Add
BondStore::factoryReset() (erases all three backing stores, radio OFF) exposed
via Facade::factoryResetBonds() and wired into every wipe path (pin_state
brute-force wipe, settings wipe, boot recovery, debug WIPEVAULT/lockout). Clearing
the RTC mirror is essential — reconcile() would otherwise resurrect the bonds
after the post-wipe reboot. Validated on-device. Native regression in
test_bond_store / test_name_store.
QA coverage: implement and validate the eight proposed BLE QA tests —
nc-reject (QA-1), conn-param (QA-2), disconnect (QA-4), factory-reset regression
(QA-5), adv-minimization (QA-7) on the mastodon Linux/BlueZ central; redaction
(QA-6) + UTF-8 robustness (QA-8) in CI; and N4 eviction (QA-3) end-to-end on the
DUT via the ESP32 Plus1 central. New mastodon scenarios (adv-scan,
disconnect-mid-type, conn-param, evict, restore) in ble_attack.sh + ble_probe.py.
Plus1 harness: unify the separate _blehost/_wifihost binaries into one
command-switchable BLE+WiFi binary (m5stickc_plus1_host, MODE BLE|WIFI|NONE).
Add an OWNADDR command that rotates the central's LE random-static identity
(fills the DUT bond table for N4 eviction, no BlueZ). Fix two firmware bugs: the
harness was mute without KLEIDOS_USB_SERIAL_CONSOLE (serial facade compiled to
no-op stubs), and identity rotation broke discovery until OWNADDR/FORGET also
clear the resolving list (ble_hs_resolv_list_clear_all).
CVE maintenance: register CVE-2025-53470/53477 (NimBLE HCI, low-reachability)
and record the advertising-report/Mesh/BlueDroid/BluFi CVEs as not-applicable;
2026 re-check found no new NimBLE CVEs. Plus readability + file:line doc-drift
fixes across ble.md / cve-register.md.
Gates: native 233/233, sticks3 + m5stickc_plus2 + m5stickc_plus1_host build
clean, pio check no defects, clang-format/check_layout/check_variants/typos/
gitleaks/ruff/editorconfig all pass.
Claude-Session: https://claude.ai/code/session_01MyjYtwfLCwUUfGVYuTQqSv