A flash virtualization layer for Zephyr RTOS — global wear-leveling, runtime-resizable named volumes, and self-healing bad-block management on raw NOR/NAND, with an optional secure variant providing AEAD over every on-flash structure.
Inspired by Linux's drivers/mtd/ubi, written from scratch for Zephyr's flash_area API and resource constraints.
Zephyr's storage stack has a missing middle layer:
flash_areais too low — raw partitions, no wear-leveling, no bad-block handling.- LittleFS, NVS, ZMS, FCB are too high — each bakes a specific abstraction (filesystem, key-value, circular log) and consumes raw flash directly. None provide multi-volume layout sharing one wear-leveling pool, and none scale cleanly to large external NOR/NAND with mixed write workloads.
UBI fills that gap. It provides multiple independent named and runtime-resizable volumes — sharing one global wear-leveling pool, with bad-block handling and crash-safe metadata. Higher-level abstractions — a future UBIFS-style filesystem, an LSM-tree-based store, custom indexed databases — can build on top of UBI rather than reinventing wear-leveling each time.
UBI is not a filesystem. It is a block virtualization layer. That is the point.
- Multi-volume — N independent named volumes per partition, created and resized at runtime.
- Global wear-leveling — one wear budget across the whole device, regardless of which volume is hot.
- Block-level access — direct LEB addressing, without filesystem or record overhead; suitable as a substrate for any higher-level structure (filesystems, indexed databases, encrypted vaults).
- Self-healing — automatic bad-block detection, torture-test confirmation, and isolation; transparent to the application.
- Crash-safe metadata — reserved PEBs for redundancy, monotonic sequence numbers, replay-safe recovery.
- Thread-safe — per-device mutex; safe for use from multiple Zephyr threads.
- Coexistence — multiple
ubi_devicehandles per application, each on its own partition. Plain and secure devices can run side by side, e.g. a plain device on internal flash for hot configuration alongside a secure device on external NOR for firmware images and secrets. - Optional secure backend — opt-in via
CONFIG_UBI_SECURE. AEAD over every on-flash byte, anti-rollback, key rotation, sticky read-only on failure. See Optional secure backend below.
With CONFIG_UBI_SECURE=y, every commit-visible on-flash structure — device header, volume headers, EC headers, VID headers, and LEB payloads — is wrapped in AES-128-CCM via PSA Crypto, with location and identity bound into the AAD. On top of bulk authenticated encryption you get:
- Versioned keys with an allowlist and per-block refcounting. Key-lifecycle events (
KEY_ROTATE_SOON,KEY_ROTATE_NOW,KEY_RETIRABLE) are delivered to the application. - Anti-rollback via an application-supplied freshness callback bound to the device-header revision and the VID-header global sequence number (attach-time check + post-commit sync).
- Fail-closed read-only mode on AEAD failure, RNG failure, or write-budget exhaustion. Reads remain available; writes and erases are refused until reset.
Threat model and the application contract are in Secure Architecture.
| Need | Reach for |
|---|---|
| Files and directories | LittleFS |
| Small key-value config | NVS or ZMS |
| Circular log of small records | FCB |
| Multiple volumes / large external flash / mixed workloads | UBI |
| Tamper-evident, rollback-detectable storage substrate | UBI secure |
| Small internal flash, single workload, fixed layout forever | NVS / ZMS — UBI is overkill |
Full side-by-side: comparison page.
#include <ubi.h>
#include <zephyr/sys/util.h>
int main(void)
{
struct ubi_device *ubi = NULL;
int err = ubi_device_init(&flash_desc, NULL, &ubi);
if (err) {
return err;
}
const struct ubi_volume_config cfg = {
.name = "my_vol",
.type = UBI_VOLUME_TYPE_DYNAMIC,
.leb_count = 4,
};
int vol_id = -1;
ubi_volume_create(ubi, &cfg, &vol_id);
const char msg[] = "Hello, UBI!";
ubi_leb_write(ubi, vol_id, 0, msg, sizeof(msg));
char buf[ARRAY_SIZE(msg)] = { 0 };
ubi_leb_read(ubi, vol_id, 0, 0, buf, sizeof(buf));
ubi_device_deinit(ubi);
return 0;
}Full error handling and the flash_desc setup (partition lookup, erase / write block sizes) are in the runnable sample/. All API functions return 0 on success or a negative errno code on failure.
UBI library only, -Os, STM32U585 (b_u585i_iot02a):
- Plain build: ~9.5 KB flash, ~1.5 KB BSS
- Secure build: ~28.6 KB flash, ~1.8 KB BSS
PSA Crypto and mbedTLS are provided by the platform and not counted. See Architecture — Resource profile for what the secure delta pays for.
v1.0.0 — public API and on-flash format (plain + secure) are stable; breaking changes require a major bump.
- 55 test suites, 609 tests total (270 plain + 339 secure).
- Validated on Zephyr
native_sim(flash simulator), STM32U585 (b_u585i_iot02a), and nRF5340 (nrf5340dk). - Live coverage on every push to
main— see the Codecov badge at the top.
Full documentation: https://kamil-kielbasa.github.io/ubi/
| Start here | What is UBI? · Comparison vs LittleFS / NVS / ZMS |
| Integrate | Quick Start · Cookbook |
| Production with secure | Secure Architecture · Secure Workflow · On-Flash Format Spec |
| Reference | API · Configuration · Plain Architecture · Glossary · Test Strategy · Contributing |
For vulnerability reporting and the supported-version policy, see SECURITY.md.
MIT License. See LICENSE for details.
The plain UBI design — PEBs, LEBs, EC/VID headers, dual-bank reserved
metadata, sequence-number recovery — is adapted from the Linux UBI
subsystem (drivers/mtd/ubi). All credit for the underlying model
belongs to its original authors and maintainers; this project ports the
idea to Zephyr's resource constraints (smaller in-RAM footprint, no
filesystem layer, simpler scan/recovery).
Secure UBI is original work — an extension of the plain UBI concept where every commit-visible on-flash structure (device / volume / EC / VID headers and LEB payloads) is AEAD-wrapped through PSA Crypto, so the same wear-leveling, dual-bank, and runtime-resizable-volume guarantees apply to ciphertext rather than plaintext, with one set of on-flash invariants for both modes.
Kamil Kielbasa — kamkie1996@gmail.com