Status: implemented. The host flasher that carries the Suicide/Dead Man's Switch path is Cyber Controller (
LxveAce/cyber-controller), which vendors this repo as a git submodule atdeadmans-switch/and importshost/provision.pydirectly. This document is the reference for that shipped integration — the real symbols, the real call contract, and where each piece lives. Additive by design: the plain-Marauder flash path is unchanged, and every duress control is behind an opt-in that is off by default. Owner-only DEFENSIVE tooling for hardware you own — a disarmed or unprovisioned board can never wipe (fail-safe). Plaintext passwords are hashed host-side and the buffer is zeroized; they are never stored, logged, or placed on a CLI argv.
Historical note. An earlier draft of this plan targeted
headless-marauder-gui(marauder_core/flasher.py,gui_qt/app.py). That is not where the integration landed — headless-marauder-gui stays a standalone flasher with no dead-man coupling. The provisioning + bundle flashing shipped in Cyber Controller instead, with the symbols below. Any doc still namingmarauder_core/flasher.py/gui_qt/app.pyFlasherDialogfor this integration is describing the abandoned target — read this file as the source of truth.
| Half | Repo / file | Entry point |
|---|---|---|
Provision (mint guardcfg.bin + bundle.json) |
this repo — host/provision.py |
build_bundle(args, pw_buf) |
| Flash (write a provisioned bundle to the board) | cyber-controller — src/core/flash_core.py |
read_bundle_manifest(dir) + flash_suicide(...) |
Cyber Controller wraps the provision half in src/core/suicide_setup.py (a thin, validated adapter)
so the GUI/CLI never touch argparse internals, and drives the flash half from its normal flasher.
The real core entry point is:
def build_bundle(args, pw_buf):
"""args: an argparse.Namespace (build it with build_arg_parser() or by hand);
pw_buf: a bytearray of the plaintext password, which is CONSUMED + ZEROIZED here.
Returns (out_dir, manifest, file_warnings)."""- It is positional
(args, pw_buf)— not the keyword formbuild_bundle(out_dir=…, password=…, tier=…)a stale draft once showed. There is nopassword=string parameter and notier=parameter; the password arrives as abytearray(pw_buf) and duress fields live onargs. argsmust carry:partitions,out,variant,chip,build_dir,nvs_gen_dir,kdf_iter,armed,arm_pin,arm_level,arm_pull,deadman,max_att,wipe_ota,wipe_nvs,wipe_spiffs,wipe_sd,brick,sd_passes,flash_passes,fast_wipe.validate_args(args)runs insidebuild_bundleas defense-in-depth (SPEC §4.1 clamps:max_att ≥ 1, fail-safe arm level/pull pairing, kdf floors, strapping-pin rejection).- It hashes with PBKDF2-HMAC-SHA256 (
salt = os.urandom(16)), zeroizespw_buf, writesguardcfg.bin(NVS image viaesp-idf-nvs-partition-gen), the variant's single otadata seed (GUARDIAN mintsotadata_blank.bin; FORK references the build'sboot_app0.bin), andbundle.json— the COMPLETE flash manifest whose offsets are read from the partition CSV, never hardcoded. main(argv)is the standalone CLI (reads the password viagetpass, never argv).
The GUI/CLI call this, not build_bundle directly:
def build(cfg: SuicideConfig, password: str, out_dir) -> tuple[str, dict, list]:
# canonicalizes + range-checks cfg (independent domain check the hand-built Namespace skips),
# imports provision from the submodule, constructs the argparse.Namespace, then:
# return prov.build_bundle(args, pw_buf) # pw_buf is zeroized by the provisioner_load_provision()insertsdeadmans-switch/hostonsys.pathandimport provision. If the submodule isn't checked out it raises with the exact fix:git submodule update --init deadmans-switch._validate_cfg()range-checks everyguardcfgfield before the bake, so a mistypedarm_level=5fails loud host-side instead of writing a nonsense u8 to NVS.- Partition tables resolve CC-bundled-first (
src/config/dms_partitions/) then the submodule's, so CC can ship a layout (e.g. the 8 MB guardian table) the pinned submodule doesn't carry.
read_bundle_manifest(bundle_dir) -> dict # parses bundle.json; path-traversal-hardened;
# each entry needs "file" + "offset_hex"/"offset"
flash_suicide(port, chip, bundle_dir, on_line, baud=921600, profile=None) -> int
# reads the manifest, validates each .bin is a safe in-bundle basename that exists,
# verifies each image's SHA-256 against the manifest, stages verified copies into a fresh
# 0700 tempdir and re-hashes (TOCTOU-safe), then writes all offset/path pairs in ONE
# `write_flash -z --flash_size detect`. Never sees a plaintext password (guardcfg.bin is
# already a hashed NVS image).- CLI:
cyber-controller --deadman-setup→src/app.py→suicide_setup.run_cli()(interactive, password viagetpass, prints next steps; guardcfg-only when nobuild_diris given = a PREVIEW, re-provision with a build dir to get a flashable bundle). - GUI:
src/ui/qt/suicide_dialog.py::SuicideSetupDialog(built onsuicide_setup.build), reachable fromsrc/ui/qt/main_window.py(menu action, keyboard shortcut, and the "Dead Man's Switch Setup" command-palette entry) and fromsrc/ui/qt/flash_tab.py. - T2 / eFuse (Secure Boot v2 + Flash Encryption) stays a separate, explicitly type-to-confirm, owner-gated step — it is IRREVERSIBLE and is never armed silently.
Cyber Controller records this repo as a submodule gitlink. The pin must point at a commit reachable
from origin/master. The public-history PII rewrite orphaned earlier hashes, so a stale pin makes a
fresh git submodule update --init deadmans-switch fail to fetch — which is exactly what breaks the
whole Dead Man's Switch path on a clean clone (the import in §2 then raises). When this repo advances,
bump the pin in cyber-controller to the new origin/master HEAD and re-run CC's suicide test suite.
- Suicide off → byte-for-byte the current flash path (regression).
suicide_setup.build(cfg, pw, out)mintsguardcfg.bin(≥ 0x3000) +bundle.json+ honest incomplete-bundle warnings when nobuild_diris given.- Mismatched/empty password → blocked before any subprocess.
- Out-of-range cfg / non-fail-safe arm pair /
max_att < 1→ rejected before any image is written. read_bundle_manifestrejects path-traversal names and missing offsets;flash_suicidere-verifies SHA-256 atomically with the flash.- The plaintext password never reaches a log line, a file, or argv.