code wiki / (root) / nx_mcu_brick.nx

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}