nx_mcu_brick.nx source
↩ module page · 92 lines · 4526 B
1// nx_mcu_brick.nx -- Rule-26 reversibility classifier for persistent-state writes on MCU targets.
2//
3// WHY THIS EXISTS: Rule 26 is absolute and brand-critical, and it demands the never-brick guarantee be
4// PROVEN MECHANICALLY, never asserted as a promise. Flashing an ESP32 writes persistent hardware state.
5// So the guard is built BEFORE the gun: this classifier exists before any flashing capability does, and
6// it is the function a write chokepoint must call. A capability gated only inside a gate IS the baseline.
7//
8// THE SILICON FACTS THIS ENCODES (verified 2026-07-31 against Espressif ESP-IDF security docs):
9// - eFuses are ONE-TIME PROGRAMMABLE. Default bit value is 0 after manufacturing and once set they
10// cannot be reverted. So EVERY eFuse burn is irreversible BY CONSTRUCTION, not by policy.
11// - SPI flash writes ARE recoverable: the first-stage bootloader lives in unerasable mask ROM and
12// re-enters UART download mode, so a bad app image is re-flashable -- annoying, not a brick.
13// - THE KEYSTONE, and the whole reason this is a function and not a constant: that recovery path is
14// ITSELF an eFuse. Burning DIS_DOWNLOAD_MODE (CONFIG_SECURE_DISABLE_ROM_DL_MODE, or a runtime
15// esp_efuse_disable_rom_download_mode() call) permanently removes it. The instant download mode is
16// burned, the ENTIRE flash surface converts from reversible to irreversible.
17//
18// Hence flash-class writes are GREEN only while the recovery path is intact -- reversibility is
19// CONDITIONAL, never intrinsic. Anything undeclared is RED, fail-closed, because an unclassified write
20// is precisely the case where we do not know whether it bricks.
21// license_tier: ORIGINAL No hw writes (Rule 26).
22import "nx_syscalls.nx"
23
24// Reversibility classes on the persistent-hardware-write axis.
25const NB_VOLATILE: i64 = 0
26const NB_REVERSIBLE: i64 = 1
27const NB_IRREVERSIBLE: i64 = 2
28const NB_UNKNOWN: i64 = 3
29
30// Verdicts. GREEN means the write may proceed; RED means refuse.
31const NB_GREEN: i64 = 0
32const NB_RED: i64 = 1
33
34// Recovery-path states.
35const NB_DL_INTACT: i64 = 1
36const NB_DL_BURNED: i64 = 0
37
38func nb_streq(a: *u8, b: *u8) -> i64 {
39 var i: i64 = 0
40 var r: i64 = 0 - 1
41 while r < 0 {
42 let ca: i64 = a[i] as i64
43 let cb: i64 = b[i] as i64
44 if ca != cb { r = 0 }
45 else { if ca == 0 { r = 1 } else { i = i + 1 } }
46 }
47 return r
48}
49
50// Classify a write by the namespace it targets. UNKNOWN is a real answer, not an error: it is the
51// answer that makes the verdict fail closed.
52func nb_class_of_ns(ns: *u8) -> i64 {
53 if (ns as i64) == 0 { return NB_UNKNOWN }
54 if nb_streq(ns, "efuse" as *u8) == 1 { return NB_IRREVERSIBLE }
55 if nb_streq(ns, "flash" as *u8) == 1 { return NB_REVERSIBLE }
56 if nb_streq(ns, "ram" as *u8) == 1 { return NB_VOLATILE }
57 if nb_streq(ns, "psram" as *u8) == 1 { return NB_VOLATILE }
58 return NB_UNKNOWN
59}
60
61// Does burning this eFuse field destroy the UART-download recovery path that makes flash reversible?
62// These are the fields that turn every future flash write into a one-way door.
63func nb_efuse_kills_recovery(field: *u8) -> i64 {
64 if (field as i64) == 0 { return 1 }
65 if nb_streq(field, "DIS_DOWNLOAD_MODE" as *u8) == 1 { return 1 }
66 if nb_streq(field, "DIS_LEGACY_SPI_BOOT" as *u8) == 1 { return 1 }
67 if nb_streq(field, "DIS_DIRECT_BOOT" as *u8) == 1 { return 1 }
68 if nb_streq(field, "DIS_USB_SERIAL_JTAG" as *u8) == 1 { return 1 }
69 if nb_streq(field, "DIS_PAD_JTAG" as *u8) == 1 { return 1 }
70 if nb_streq(field, "SECURE_BOOT_EN" as *u8) == 1 { return 1 }
71 if nb_streq(field, "SPI_BOOT_CRYPT_CNT" as *u8) == 1 { return 1 }
72 if nb_streq(field, "WR_DIS" as *u8) == 1 { return 1 }
73 if nb_streq(field, "RD_DIS" as *u8) == 1 { return 1 }
74 return 0
75}
76
77// THE CHOKEPOINT FUNCTION. Every MCU write path must route through this and refuse on NB_RED.
78// dl_state carries whether the ROM download recovery path is still available on THIS device.
79func nb_verdict(ns: *u8, dl_state: i64) -> i64 {
80 let cls: i64 = nb_class_of_ns(ns)
81 if cls == NB_VOLATILE { return NB_GREEN }
82 if cls == NB_IRREVERSIBLE { return NB_RED }
83 if cls == NB_UNKNOWN { return NB_RED }
84 if dl_state == NB_DL_INTACT { return NB_GREEN }
85 return NB_RED
86}
87
88// Verdict for a specific eFuse burn request. Always RED -- kept separate so a caller cannot smuggle an
89// eFuse burn through the namespace path, and so the reason is reportable.
90func nb_verdict_efuse(field: *u8) -> i64 {
91 return NB_RED
92}