From 6d73218439cde7f2a9773158baef6ac0e62c0460 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 26 Aug 2026 16:25:33 -0500 Subject: [PATCH 1/3] flash protect mechanism --- ports/nordic/mpconfigport.h | 6 ++++ ports/nordic/peripherals/nrf/nvm.c | 48 ++++++++++++++++++++++++++++++ ports/nordic/peripherals/nrf/nvm.h | 4 +++ ports/nordic/supervisor/port.c | 5 ++++ 4 files changed, 63 insertions(+) diff --git a/ports/nordic/mpconfigport.h b/ports/nordic/mpconfigport.h index 33fcfa371e0..00537ce74ad 100644 --- a/ports/nordic/mpconfigport.h +++ b/ports/nordic/mpconfigport.h @@ -117,6 +117,12 @@ // The firmware space is the space left over between the fixed lower and upper regions. #define CIRCUITPY_FIRMWARE_SIZE (CIRCUITPY_BLE_CONFIG_START_ADDR - CIRCUITPY_FIRMWARE_START_ADDR) +// nRF52's access control hardware write-protect every part of +// internal flash that CircuitPython does not own +#ifndef CIRCUITPY_NRF_FLASH_PROTECT +#define CIRCUITPY_NRF_FLASH_PROTECT (0) +#endif + #if BOOTLOADER_START_ADDR % FLASH_ERASE_SIZE != 0 #error BOOTLOADER_START_ADDR must be on a flash erase boundary. #endif diff --git a/ports/nordic/peripherals/nrf/nvm.c b/ports/nordic/peripherals/nrf/nvm.c index 61a517f1152..86e3f099e37 100644 --- a/ports/nordic/peripherals/nrf/nvm.c +++ b/ports/nordic/peripherals/nrf/nvm.c @@ -12,8 +12,29 @@ #include "nrfx_nvmc.h" +#include "supervisor/shared/safe_mode.h" + #define FLASH_PAGE_SIZE (4096) +static bool flash_page_is_ours(uint32_t page_addr) { + if ((page_addr & (FLASH_PAGE_SIZE - 1)) != 0) { + return false; + } + #if CIRCUITPY_INTERNAL_NVM_SIZE > 0 + if (page_addr >= CIRCUITPY_INTERNAL_NVM_START_ADDR && + page_addr < CIRCUITPY_INTERNAL_NVM_START_ADDR + CIRCUITPY_INTERNAL_NVM_SIZE) { + return true; + } + #endif + #if CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE > 0 + if (page_addr >= CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR && + page_addr < CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR + CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) { + return true; + } + #endif + return false; +} + #ifdef BLUETOOTH_SD #include "ble_drv.h" #include "nrf_sdm.h" @@ -68,6 +89,27 @@ bool sd_flash_write_sync(uint32_t *dest_words, uint32_t *src_words, uint32_t num #endif +void nrf_nvm_protect_init(void) { + #if CIRCUITPY_NRF_FLASH_PROTECT + const struct { + uint32_t addr; + uint32_t size; + } regions[] = { + { 0, CIRCUITPY_BLE_CONFIG_START_ADDR }, + { BOOTLOADER_START_ADDR, FLASH_SIZE - BOOTLOADER_START_ADDR }, + }; + + for (size_t i = 0; i < MP_ARRAY_SIZE(regions); i++) { + if (regions[i].size == 0) { + continue; + } + NRF_ACL->ACL[i].ADDR = regions[i].addr; + NRF_ACL->ACL[i].SIZE = regions[i].size; + NRF_ACL->ACL[i].PERM = ACL_ACL_PERM_WRITE_Disable << ACL_ACL_PERM_WRITE_Pos; + } + #endif +} + // The nRF52840 datasheet specifies a maximum of two writes to a flash // location before an erase is necessary, even if the write is all // ones (erased state). So we can't avoid erases even if the page @@ -75,6 +117,12 @@ bool sd_flash_write_sync(uint32_t *dest_words, uint32_t *src_words, uint32_t num // writes to a page. bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data) { + if (!flash_page_is_ours(page_addr)) { + // Out of bounds write that should never have been asked for, + // reset into safe mode + reset_into_safe_mode(SAFE_MODE_FLASH_WRITE_FAIL); + } + #ifdef BLUETOOTH_SD if (sd_is_enabled()) { uint32_t err_code; diff --git a/ports/nordic/peripherals/nrf/nvm.h b/ports/nordic/peripherals/nrf/nvm.h index aff98094469..bd8da08eeec 100644 --- a/ports/nordic/peripherals/nrf/nvm.h +++ b/ports/nordic/peripherals/nrf/nvm.h @@ -15,3 +15,7 @@ bool sd_flash_write_sync(uint32_t *dest_words, uint32_t *src_words, uint32_t num #endif bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data); + +// Hardware write-protect the parts of internal flash CircuitPython does not +// own, using the ACL peripheral. +void nrf_nvm_protect_init(void); diff --git a/ports/nordic/supervisor/port.c b/ports/nordic/supervisor/port.c index 1eabfcbe216..3d8ed914ff3 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -19,6 +19,7 @@ #include "nrf/cache.h" #include "nrf/clocks.h" +#include "nrf/nvm.h" #include "nrf/power.h" #include "nrf/timers.h" @@ -131,6 +132,10 @@ void tick_set_prescaler(uint32_t prescaler_val) { } safe_mode_t port_init(void) { + //lock the flash regions we do not own out of reach of + // NVMC for the rest of this boot. + nrf_nvm_protect_init(); + nrf_peripherals_clocks_init(); // If GPIO voltage is set wrong in UICR, this will fix it, and From b09f7937d17ff322be4ea3d95756ed6dc865e15b Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 27 Aug 2026 07:33:36 -0500 Subject: [PATCH 2/3] fix ACL slot finding logic, re-use existing error string --- locale/circuitpython.pot | 7 ++- ports/nordic/peripherals/nrf/nvm.c | 77 ++++++++++++++++++++++-------- ports/nordic/supervisor/port.c | 16 ++++++- supervisor/shared/safe_mode.c | 3 ++ supervisor/shared/safe_mode.h | 1 + 5 files changed, 80 insertions(+), 24 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 130bb695683..02996c105ce 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1033,6 +1033,7 @@ msgstr "" #: ports/atmel-samd/common-hal/sdioio/SDCard.c #: ports/espressif/common-hal/qspibus/QSPIBus.c +#: shared-bindings/socketpool/SocketPool.c msgid "%q failure: %d" msgstr "" @@ -1153,6 +1154,7 @@ msgstr "" #: ports/espressif/common-hal/_bleio/Adapter.c #: ports/nordic/common-hal/_bleio/Adapter.c +#: ports/zephyr-cp/common-hal/_bleio/Adapter.c msgid "Data too large for advertisement packet" msgstr "" @@ -1817,7 +1819,7 @@ msgstr "" #: ports/raspberrypi/bindings/cyw43/__init__.c py/argcheck.c py/objexcept.c #: shared-bindings/bitmapfilter/__init__.c shared-bindings/canio/CAN.c #: shared-bindings/digitalio/Pull.c shared-bindings/supervisor/__init__.c -#: shared-module/audiofilters/Filter.c shared-module/displayio/__init__.c +#: shared-module/audiofilters/__init__.c shared-module/displayio/__init__.c #: shared-module/synthio/Synthesizer.c msgid "%q must be of type %q or %q, not %q" msgstr "" @@ -1965,6 +1967,7 @@ msgid "Unable to access unaligned IO register" msgstr "" #: ports/raspberrypi/common-hal/memorymap/AddressRange.c +#: supervisor/shared/safe_mode.c msgid "Unable to write to read-only memory" msgstr "" @@ -2353,7 +2356,7 @@ msgstr "" msgid "%q length must be %d" msgstr "" -#: py/argcheck.c shared-module/audiofilters/Filter.c +#: py/argcheck.c shared-module/audiofilters/__init__.c msgid "%q in %q must be of type %q, not %q" msgstr "" diff --git a/ports/nordic/peripherals/nrf/nvm.c b/ports/nordic/peripherals/nrf/nvm.c index 86e3f099e37..7cb7d8dc601 100644 --- a/ports/nordic/peripherals/nrf/nvm.c +++ b/ports/nordic/peripherals/nrf/nvm.c @@ -12,8 +12,6 @@ #include "nrfx_nvmc.h" -#include "supervisor/shared/safe_mode.h" - #define FLASH_PAGE_SIZE (4096) static bool flash_page_is_ours(uint32_t page_addr) { @@ -89,24 +87,65 @@ bool sd_flash_write_sync(uint32_t *dest_words, uint32_t *src_words, uint32_t num #endif -void nrf_nvm_protect_init(void) { - #if CIRCUITPY_NRF_FLASH_PROTECT - const struct { - uint32_t addr; - uint32_t size; - } regions[] = { - { 0, CIRCUITPY_BLE_CONFIG_START_ADDR }, - { BOOTLOADER_START_ADDR, FLASH_SIZE - BOOTLOADER_START_ADDR }, - }; - - for (size_t i = 0; i < MP_ARRAY_SIZE(regions); i++) { - if (regions[i].size == 0) { +#if CIRCUITPY_NRF_FLASH_PROTECT +static bool page_is_acl_protected(uint32_t page_addr) { + for (size_t slot = 0; slot < ACL_REGIONS_COUNT; slot++) { + uint32_t base = NRF_ACL->ACL[slot].ADDR; + uint32_t size = NRF_ACL->ACL[slot].SIZE; + if (size != 0 && page_addr >= base && page_addr < base + size) { + return true; + } + } + return false; +} + +// Claim the next unused ACL region and write-protect addr..addr + size. +static void acl_claim(uint32_t addr, uint32_t size) { + for (size_t slot = 0; slot < ACL_REGIONS_COUNT; slot++) { + if (NRF_ACL->ACL[slot].PERM != 0 || NRF_ACL->ACL[slot].SIZE != 0) { + // Already claimed by the MBR or the bootloader. continue; } - NRF_ACL->ACL[i].ADDR = regions[i].addr; - NRF_ACL->ACL[i].SIZE = regions[i].size; - NRF_ACL->ACL[i].PERM = ACL_ACL_PERM_WRITE_Disable << ACL_ACL_PERM_WRITE_Pos; + NRF_ACL->ACL[slot].ADDR = addr; + NRF_ACL->ACL[slot].SIZE = size; + NRF_ACL->ACL[slot].PERM = ACL_ACL_PERM_WRITE_Disable << ACL_ACL_PERM_WRITE_Pos; + + if (NRF_ACL->ACL[slot].ADDR == addr && NRF_ACL->ACL[slot].SIZE == size) { + return; + } + } +} + +// Write-protect start..end, one ACL region per run of pages that is not +// already protected. +static void acl_write_protect(uint32_t start, uint32_t end) { + uint32_t run_start = 0; + bool in_run = false; + + for (uint32_t page_addr = start; page_addr < end; page_addr += FLASH_PAGE_SIZE) { + if (!page_is_acl_protected(page_addr)) { + if (!in_run) { + run_start = page_addr; + in_run = true; + } + } else if (in_run) { + acl_claim(run_start, page_addr - run_start); + in_run = false; + } + } + if (in_run) { + acl_claim(run_start, end - run_start); } +} +#endif + +void nrf_nvm_protect_init(void) { + #if CIRCUITPY_NRF_FLASH_PROTECT + // Everything below the regions CircuitPython owns: the MBR, the + // SoftDevice, the interrupt vectors and the firmware itself. + acl_write_protect(MBR_START_ADDR, CIRCUITPY_BLE_CONFIG_START_ADDR); + // The bootloader, its copy of the MBR, and its settings page. + acl_write_protect(BOOTLOADER_START_ADDR, FLASH_SIZE); #endif } @@ -118,9 +157,7 @@ void nrf_nvm_protect_init(void) { bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data) { if (!flash_page_is_ours(page_addr)) { - // Out of bounds write that should never have been asked for, - // reset into safe mode - reset_into_safe_mode(SAFE_MODE_FLASH_WRITE_FAIL); + return false; } #ifdef BLUETOOTH_SD diff --git a/ports/nordic/supervisor/port.c b/ports/nordic/supervisor/port.c index 3d8ed914ff3..c68dc05ca44 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -132,8 +132,7 @@ void tick_set_prescaler(uint32_t prescaler_val) { } safe_mode_t port_init(void) { - //lock the flash regions we do not own out of reach of - // NVMC for the rest of this boot. + // Lock the flash regions we do not own nrf_nvm_protect_init(); nrf_peripherals_clocks_init(); @@ -363,6 +362,19 @@ void port_idle_until_interrupt(void) { extern void HardFault_Handler(void); void HardFault_Handler(void) { + #if CIRCUITPY_NRF_FLASH_PROTECT + // A write to an ACL-protected page raises a BusFault, which escalates to + // this HardFault. + if ((SCB->CFSR & SCB_CFSR_BUSFAULTSR_Msk) != 0) { + const bool bfar_in_flash = + (SCB->CFSR & SCB_CFSR_BFARVALID_Msk) != 0 && SCB->BFAR < FLASH_SIZE; + const bool nvmc_armed = + NRF_NVMC->CONFIG != (NVMC_CONFIG_WEN_Ren << NVMC_CONFIG_WEN_Pos); + if (bfar_in_flash || nvmc_armed) { + reset_into_safe_mode(SAFE_MODE_FLASH_WRITE_PROTECTED); + } + } + #endif reset_into_safe_mode(SAFE_MODE_HARD_FAULT); while (true) { asm ("nop;"); diff --git a/supervisor/shared/safe_mode.c b/supervisor/shared/safe_mode.c index fba4c0fb37f..f891b129321 100644 --- a/supervisor/shared/safe_mode.c +++ b/supervisor/shared/safe_mode.c @@ -169,6 +169,9 @@ void print_safe_mode_message(safe_mode_t reason) { message = MP_ERROR_TEXT("You pressed the reset button during boot."); #endif break; + case SAFE_MODE_FLASH_WRITE_PROTECTED: + message = MP_ERROR_TEXT("Unable to write to read-only memory"); + break; case SAFE_MODE_NO_CIRCUITPY: message = MP_ERROR_TEXT("CIRCUITPY drive could not be found or created."); break; diff --git a/supervisor/shared/safe_mode.h b/supervisor/shared/safe_mode.h index 87f65d867ea..19979036e9e 100644 --- a/supervisor/shared/safe_mode.h +++ b/supervisor/shared/safe_mode.h @@ -14,6 +14,7 @@ typedef enum { SAFE_MODE_BROWNOUT, // alphabetical from here down SAFE_MODE_FLASH_WRITE_FAIL, + SAFE_MODE_FLASH_WRITE_PROTECTED, SAFE_MODE_GC_ALLOC_OUTSIDE_VM, SAFE_MODE_HARD_FAULT, SAFE_MODE_INTERRUPT_ERROR, From 1bec5ccc67f500c236c50bfda1c15a47c9f85bca Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 27 Aug 2026 07:54:56 -0500 Subject: [PATCH 3/3] internal_flash bounds check --- ports/nordic/supervisor/internal_flash.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ports/nordic/supervisor/internal_flash.c b/ports/nordic/supervisor/internal_flash.c index 88b6a274d06..84dce25d9e1 100644 --- a/ports/nordic/supervisor/internal_flash.c +++ b/ports/nordic/supervisor/internal_flash.c @@ -36,6 +36,11 @@ static inline uint32_t lba2addr(uint32_t block) { return CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR + block * FILESYSTEM_BLOCK_SIZE; } +static bool blocks_in_range(uint32_t lba, uint32_t num_blocks) { + uint32_t block_count = supervisor_flash_get_block_count(); + return lba <= block_count && num_blocks <= block_count - lba; +} + void supervisor_flash_init(void) { } @@ -61,6 +66,10 @@ void port_internal_flash_flush(void) { } mp_uint_t supervisor_flash_read_blocks(uint8_t *dest, uint32_t block, uint32_t num_blocks) { + if (!blocks_in_range(block, num_blocks)) { + return 1; // failure + } + // Must write out anything in cache before trying to read. supervisor_flash_flush(); @@ -70,6 +79,10 @@ mp_uint_t supervisor_flash_read_blocks(uint8_t *dest, uint32_t block, uint32_t n } mp_uint_t supervisor_flash_write_blocks(const uint8_t *src, uint32_t lba, uint32_t num_blocks) { + if (!blocks_in_range(lba, num_blocks)) { + return 1; // failure + } + while (num_blocks) { uint32_t const addr = lba2addr(lba); uint32_t const page_addr = addr & ~(FLASH_PAGE_SIZE - 1);