Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmake/sysbuild/bootconf.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function(setup_bootconf_data)
${ZEPHYR_NRF_MODULE_DIR}/scripts/reglock.py
--output ${CMAKE_BINARY_DIR}/bootconf.hex
--size $<TARGET_PROPERTY:partition_manager,PM_B0_SIZE>
--soc ${SB_CONFIG_SOC}
DEPENDS ${APPLICATION_BINARY_DIR}/pm.config
VERBATIM
)
Expand Down
66 changes: 49 additions & 17 deletions scripts/reglock.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Copyright 2025 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
Expand All @@ -22,20 +22,54 @@
import argparse
import sys
import warnings

SIZE_MAX_KB = 31
import struct

BOOTCONF_ADDR = 0x00FFD080

READ_ALLOWED = 0x01
WRITE_ALLOWED = 0x02
EXECUTE_ALLOWED = 0x04
SECURE = 0x08
OWNER_NONE = 0x00
OWNER_APP = 0x10
OWNER_KMU = 0x20
WRITEONCE = 0x10
LOCK = 0x20
LOCK = 0x2000

SIZE_OFFSET = 16


supported_socs = [
"nrf54l05",
"nrf54l10",
"nrf54l15",
"nrf54lm20a",
"nrf54lv10a",
"nrf54ls05b",
]

def get_max_size_kb(soc):
if soc in ["nrf54l05", "nrf54l10", "nrf54l15"]:
return 31
elif soc in ["nrf54lm20a", "nrf54lv10a"]:
return 127
elif soc in ["nrf54ls05b"]:
return 1023
else:
sys.exit("error: unsupported SoC")

def get_bootconf_reg_32bit_value(soc, size):
value = READ_ALLOWED | EXECUTE_ALLOWED | LOCK

if soc not in ["nrf54ls05b"]:
value |= SECURE

size_kb = size // 1024
max_size_kb = get_max_size_kb(soc)

if size_kb > max_size_kb:
warnings.warn("warning: requested size too big; Setting to allowed maximum")
size_kb = max_size_kb

value |= size_kb << SIZE_OFFSET
return value

def parse_args():
parser = argparse.ArgumentParser(
Expand All @@ -48,6 +82,9 @@
parser.add_argument("-s", "--size", required=False, default="0x7C00",
type=lambda x: hex(int(x, 0)),
help="Size to lock.")
parser.add_argument("--soc", required=True,
type=str,
help="SoC for which to generate the bootconf.")
return parser.parse_args()


Expand All @@ -56,17 +93,12 @@
size = int(args.size, 16)
if size % 1024:
sys.exit("error: requested size not aligned to 1k")
size = size // 1024
if size > SIZE_MAX_KB:
warnings.warn("warning: requested size too big; Setting to allowed maximum")
size = SIZE_MAX_KB

payload = bytearray([
READ_ALLOWED | EXECUTE_ALLOWED | SECURE | OWNER_NONE,
LOCK,
size,
0x0
])
if args.soc not in supported_socs:
sys.exit("error: unsupported SoC")

reg_value = get_bootconf_reg_32bit_value(args.soc, size)

payload = struct.pack('<I', reg_value)

h = IntelHex()
h.frombytes(bytes=payload, offset=BOOTCONF_ADDR)
Expand Down
13 changes: 10 additions & 3 deletions subsys/bootloader/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,26 @@ config SB_INFINITE_LOOP_AFTER_RAM_CLEANUP
Verification option that keeps execution in infinite loop after
RAM cleanup has been performed.

config SB_DISABLE_SELF_RWX_SUPPORTED
bool
default y if SOC_NRF54L15_CPUAPP || SOC_NRF54L05_CPUAPP || SOC_NRF54L10_CPUAPP
default y if SOC_NRF54LV10A_ENGA_CPUAPP
default y if SOC_NRF54LM20A_ENGA_CPUAPP

config SB_DISABLE_SELF_RWX
bool "Disable read and execution on self NVM"
depends on (SOC_NRF54L15_CPUAPP || SOC_NRF54L05_CPUAPP || SOC_NRF54L10_CPUAPP || SOC_NRF54LV10A_ENGA_CPUAPP) && !FPROTECT_ALLOW_COMBINED_REGIONS
depends on SB_DISABLE_SELF_RWX_SUPPORTED && !FPROTECT_ALLOW_COMBINED_REGIONS
help
Sets RRAMC's BOOTCONF region protection before jumping to application.
It disables reads writes and execution memory area which holds NSIB.

config SB_DISABLE_NEXT_W
bool "Disable writes for next stage"
depends on (SOC_NRF54L15_CPUAPP || SOC_NRF54L05_CPUAPP || SOC_NRF54L10_CPUAPP) && !FPROTECT
depends on SB_DISABLE_SELF_RWX_SUPPORTED && !FPROTECT
help
NSIB disables writes on next stage in bootloading chain.
It uses RRAMC's region 4 and is limited to 31KB.
It uses RRAMC's region 4 and is limited to 31KB for nRF54L15, nRF54L10 and nRF54L05
and to 127KB for nRF54LV10a and nRF54LM20a.

endif # IS_SECURE_BOOTLOADER

Expand Down
64 changes: 50 additions & 14 deletions subsys/bootloader/bl_boot/bl_boot.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,70 @@
#define RRAMC_REGION_FOR_NEXT_W 4
#define NRF_RRAM_REGION_SIZE_UNIT 0x400
#define NRF_RRAM_REGION_ADDRESS_RESOLUTION 0x400
#define NEXT_W_SIZE_KB (PM_MCUBOOT_SIZE / NRF_RRAM_REGION_SIZE_UNIT)

BUILD_ASSERT((PM_MCUBOOT_ADDRESS % NRF_RRAM_REGION_ADDRESS_RESOLUTION) == 0,
"Start of protected region is not aligned");
#if defined(CONFIG_SOC_NRF54L15_CPUAPP) || defined(CONFIG_SOC_NRF54L05_CPUAPP) || \
defined(CONFIG_SOC_NRF54L10_CPUAPP)
#define MAX_NEXT_W_SIZE (31 * 1024)
#elif defined(CONFIG_SOC_NRF54LV10A_ENGA_CPUAPP) || defined(CONFIG_SOC_NRF54LM20A_ENGA_CPUAPP)
#define MAX_NEXT_W_SIZE (127 * 1024)
#elif defined(CONFIG_SOC_NRF54LS05B_ENGA_CPUAPP)
#define MAX_NEXT_W_SIZE (1023 * 1024)
#endif

BUILD_ASSERT((PM_S0_IMAGE_ADDRESS % NRF_RRAM_REGION_ADDRESS_RESOLUTION) == 0,
"Start of S0 image region is not aligned - not possible to protect");

BUILD_ASSERT((PM_S0_IMAGE_SIZE % NRF_RRAM_REGION_SIZE_UNIT) == 0,
"Size of S0 image region is not aligned - not possible to protect");

BUILD_ASSERT(PM_S0_IMAGE_SIZE <= MAX_NEXT_W_SIZE, "Size of S0 partition is too big for protection");

#if defined(PM_S1_IMAGE_ADDRESS)
BUILD_ASSERT((PM_S1_IMAGE_ADDRESS % NRF_RRAM_REGION_ADDRESS_RESOLUTION) == 0,
"Start of S1 image region is not aligned - not possible to protect");

BUILD_ASSERT((PM_MCUBOOT_SIZE % NRF_RRAM_REGION_SIZE_UNIT) == 0,
"Size of protected region is not aligned");
BUILD_ASSERT((PM_S1_IMAGE_SIZE % NRF_RRAM_REGION_SIZE_UNIT) == 0,
"Size of S1 image region is not aligned - not possible to protect");

BUILD_ASSERT(NEXT_W_SIZE_KB < 31,
"Size of requested protection is too big");
BUILD_ASSERT(PM_S1_IMAGE_SIZE <= MAX_NEXT_W_SIZE, "Size of S1 partition is too big for protection");
#endif /* defined(PM_S1_IMAGE_ADDRESS) */

static int disable_next_w(void)
static int disable_next_w(const uint32_t address)
{
uint32_t region_size_kb = 0;

/* Note: the protection is only applied to the image itself, not the header (pad).
* When building with MCUBoot, applying protection to the header is not needed, as the
* header is only used during DFU and is only left for compatibility. Without MCUBoot, the
* header is not present.
*/
Comment on lines +63 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so doesn't that theoretically mean there is a possible attacker entry point if there is some data they can put in this completely unprotected location that causes MCUboot to do something undefined?

Copy link
Contributor

@nvlsianpu nvlsianpu Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, looked into ./cmake/sysbuild/sign.cmake, I saw that material for signature is has over built binary(like set(slot_hex ${${slot}_image_dir}/zephyr/${${slot}_kernel_name}.hex), which doesn't contains this area.
For NSIB is waisted area. Active MCUboot instance doesn't parse it as well.

Copy link
Contributor Author

@ahasztag ahasztag Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear: this is NOT something introduced by this PR, it was already there, this PR only adds a comment which only explains why it is not a great concern and why we do not need to fix it immediately.
This will be solved soon, we plan to remove the s0_pad/s1_pad for the NSIB+MCUBoot configuration - the issue will be non-existent then.

if (address == PM_S0_IMAGE_ADDRESS) {
region_size_kb = PM_S0_IMAGE_SIZE / NRF_RRAM_REGION_SIZE_UNIT;
} else if (address == PM_S1_IMAGE_ADDRESS) {
region_size_kb = PM_S1_IMAGE_SIZE / NRF_RRAM_REGION_SIZE_UNIT;
} else {
return -EINVAL;
}

nrf_rramc_region_config_t config = {
.address = PM_MCUBOOT_ADDRESS,
.address = address,
.permissions = NRF_RRAMC_REGION_PERM_READ_MASK |
NRF_RRAMC_REGION_PERM_EXECUTE_MASK,
.writeonce = false,
.lock = false,
.size_kb = NEXT_W_SIZE_KB,
/* There are no issues with locking the region here,
* as the next stage can still impose more strict
* protection by writing 0 to R/X disable bits.
*/
.lock = true,
.size_kb = region_size_kb,
};

nrf_rramc_region_config_set(NRF_RRAMC, RRAMC_REGION_FOR_NEXT_W, &config);
nrf_rramc_region_config_get(NRF_RRAMC, RRAMC_REGION_FOR_NEXT_W, &config);
if (config.permissions & (NRF_RRAMC_REGION_PERM_WRITE_MASK)) {
return -ENOSPC;
}
if (config.size_kb != NEXT_W_SIZE_KB) {
if (config.size_kb != region_size_kb) {
return -ENOSPC;
}

Expand Down Expand Up @@ -286,8 +322,8 @@ void bl_boot(const struct fw_info *fw_info)
uint32_t *vector_table = (uint32_t *)fw_info->address;

#if defined(CONFIG_SB_DISABLE_NEXT_W)
if (disable_next_w()) {
printk("Unable to disable writes on next stage.");
if (disable_next_w(fw_info->address)) {
printk("Unable to disable writes on next stage");
return;
}
#endif
Expand Down
8 changes: 7 additions & 1 deletion sysbuild/Kconfig.secureboot
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,15 @@ config SECURE_BOOT_PUBLIC_KEY_FILES
empty string then only the public key hash corresponding to the private signing key used
to sign the image is included in provision.hex.

config SECURE_BOOT_BOOTCONF_LOCK_WRITES_SUPPORTED
bool
default y if SOC_NRF54L15_CPUAPP || SOC_NRF54L05_CPUAPP || SOC_NRF54L10_CPUAPP
default y if SOC_NRF54LV10A_ENGA_CPUAPP
default y if SOC_NRF54LM20A_ENGA_CPUAPP

config SECURE_BOOT_BOOTCONF_LOCK_WRITES
bool "Protect bootloader's NVM from writes"
depends on SOC_NRF54L15_CPUAPP || SOC_NRF54L05_CPUAPP || SOC_NRF54L10_CPUAPP
depends on SECURE_BOOT_BOOTCONF_LOCK_WRITES_SUPPORTED
default y
help
Sets RRAMC's BOOTCONF region protection to disable writes.
Expand Down
3 changes: 3 additions & 0 deletions tests/subsys/bootloader/b0_lock_rwx/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ config TEST_B0_LOCK_REGION
help
Region 3 is used for NSIB protection. Other one for MCUBoot.

config TEST_B0_LOCK_USE_S1
bool "MCUBoot is running from S1 image for the test"

menu "Zephyr"
source "Kconfig.zephyr"
endmenu
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#
# Copyright (c) 2025 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#

cmake_minimum_required(VERSION 3.20.0)

if(CONFIG_TEST_B0_LOCK_USE_S1)
zephyr_library()
zephyr_library_sources(src/run_from_s1.c)
endif()
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#
# Copyright (c) 2025 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#

config TEST_B0_LOCK_USE_S1
bool "Ensures the S1 image is started by b0"
help
This option causes b0 to invalidate the S0 image during startup,
which ensures that the main function starts the S1 image.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build:
cmake: zephyr
kconfig: zephyr/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/

#include <zephyr/init.h>
#include <zephyr/sys/printk.h>
#include <fw_info.h>
#include <bl_storage.h>

static int invalidate_s0(void)
{
uint32_t s0_addr = s0_address_read();
const struct fw_info *s0_info = fw_info_find(s0_addr);

printk("Invalidating S0 in order to ensure the S1 image is started\n");
fw_info_invalidate(s0_info);

return 0;
}

SYS_INIT(invalidate_s0, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
37 changes: 31 additions & 6 deletions tests/subsys/bootloader/b0_lock_rwx/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,26 @@
#include <zephyr/ztest.h>
#include <hal/nrf_rramc.h>
#include <nrfx_rramc.h>
#include <pm_config.h>

#define B0_RRAMC_REGION 3
#define MCUBOOT_RRAMC_REGION 4

#define RRAMC_REGION_FOR_TEST CONFIG_TEST_B0_LOCK_REGION

#if RRAMC_REGION_FOR_TEST == B0_RRAMC_REGION
#define RRAMC_REGION_FOR_TEST_SIZE PM_B0_SIZE
#define RRAM_REGION_FOR_TEST_ADDRESS PM_B0_ADDRESS
#elif RRAMC_REGION_FOR_TEST == MCUBOOT_RRAMC_REGION
#if !defined(CONFIG_TEST_B0_LOCK_USE_S1)
#define RRAMC_REGION_FOR_TEST_SIZE PM_MCUBOOT_SIZE
#define RRAM_REGION_FOR_TEST_ADDRESS PM_MCUBOOT_ADDRESS
#else
#define RRAMC_REGION_FOR_TEST_SIZE PM_S1_IMAGE_SIZE
#define RRAM_REGION_FOR_TEST_ADDRESS PM_S1_IMAGE_ADDRESS
#endif
#endif

static uint32_t expected_fatal;
static uint32_t actual_fatal;
static nrf_rramc_region_config_t config;
Expand Down Expand Up @@ -43,24 +60,35 @@ void *get_config(void)
(NRF_RRAMC_REGION_PERM_WRITE_MASK),
"Write permission isn't cleared");
#endif
zassert_true(config.size_kb > 0, "Protected region has zero size.");
zassert_equal(config.size_kb, RRAMC_REGION_FOR_TEST_SIZE / 1024,
"Protected region size doesn't match protected partition size.");
zassert_equal(
config.address, RRAM_REGION_FOR_TEST_ADDRESS,
"Protected region start address doesn't match protected partition start address.");
return NULL;
}

ZTEST(b0_self_lock_test, test_reading_b0_image)
ZTEST(b0_self_lock_test, test_rwx_locked_region)
{
printk("Region %d\n", RRAMC_REGION_FOR_TEST);
uint32_t protected_end_address = 1024 * config.size_kb;
uint32_t protected_end_address = config.address + (1024 * config.size_kb);
volatile uint32_t *unprotected_word = (volatile uint32_t *)protected_end_address;
volatile uint32_t *protected_word =
(volatile uint32_t *)protected_end_address - sizeof(uint32_t);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

additional check? something like that:

uint32_t tmp = nrf_rramc_region_config_raw_get(NRF_RRAMC, RRAMC_REGION_FOR_TEST);
.
.
.
zassert_equal(nrf_rramc_region_config_raw_get(NRF_RRAMC, RRAMC_REGION_FOR_TEST) !=
tmp, "error, managed to change permissions")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, added

uint32_t original_config = nrf_rramc_region_config_raw_get(NRF_RRAMC,
RRAMC_REGION_FOR_TEST);
uint32_t updated_config = 0xFFFFFFFF;

config.permissions = NRF_RRAMC_REGION_PERM_READ_MASK |
NRF_RRAMC_REGION_PERM_WRITE_MASK |
NRF_RRAMC_REGION_PERM_EXECUTE_MASK;
/* Try unlocking. This should take no effect at this point */
nrf_rramc_region_config_set(NRF_RRAMC, RRAMC_REGION_FOR_TEST, &config);

updated_config = nrf_rramc_region_config_raw_get(NRF_RRAMC, RRAMC_REGION_FOR_TEST);
zassert_equal(updated_config, original_config,
"Managed to update the locked config.");

#if defined(CONFIG_TEST_B0_LOCK_READS)
printk("Legal read\n");
int val = *unprotected_word;
Expand All @@ -76,8 +104,6 @@ ZTEST(b0_self_lock_test, test_reading_b0_image)
nrfx_rramc_write_enable_set(true, 0);
/* Next line corrupts application header.
* It is ok since we need to run test once per flashing.
* Moreover after reboot slot will be invalidated and
* application will boot from second one.
*/
nrf_rramc_word_write((uint32_t)unprotected_word, test_value);
zassert_equal(test_value, *unprotected_word,
Expand All @@ -86,7 +112,6 @@ ZTEST(b0_self_lock_test, test_reading_b0_image)
expected_fatal++;
__DSB();
nrf_rramc_word_write((uint32_t)protected_word, test_value);

#endif

}
Expand Down
7 changes: 7 additions & 0 deletions tests/subsys/bootloader/b0_lock_rwx/sysbuild.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#
# Copyright (c) 2025 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#

set(b0_EXTRA_ZEPHYR_MODULES ${CMAKE_CURRENT_LIST_DIR}/modules/run_from_s1 CACHE INTERNAL "")
Loading
Loading