Skip to content

Add ubi_volume_write action for UBI volumes - #293

Merged
fhunleth merged 4 commits into
fwup-home:mainfrom
Hermanverschooten:ubi-volume-write
Jul 23, 2026
Merged

Add ubi_volume_write action for UBI volumes#293
fhunleth merged 4 commits into
fwup-home:mainfrom
Hermanverschooten:ubi-volume-write

Conversation

@Hermanverschooten

Copy link
Copy Markdown
Contributor

Closes #292.

Adds a new ubi_volume_write(device_path) action so fwup can write
to UBI volume character devices (/dev/ubi0_3, etc.). UBI rejects
pwrite() with EPERM; updates have to be initiated via the
UBI_IOCVOLUP ioctl, which puts the volume in atomic-update mode for
a declared number of bytes. Subsequent sequential write()s fill the
volume, and close() commits the update atomically. Previously
raw_write / path_write / pipe_write all use pwrite(), so none
of them worked for OTA updates targeting individual UBI volumes.

on-resource fit_a.itb {
    ubi_volume_write("/dev/ubi0_3")
}

How it works

  • Issues UBI_IOCVOLUP with the resource's full size (sparse holes
    included — they're materialized as zero writes since UBI can't seek).
  • Tracks a write cursor and rejects out-of-order callbacks; fills
    gaps before sparse data with zeros.
  • Requires --unsafe (consistent with path_write / pipe_write
    since the destination is a user-supplied path, not the -d device).
  • Always compiled in. Per Feature request: support writing to UBI volumes (/dev/ubi*_*) #292's discussion, vendored
    mtd/ubi-user.h (GPL-2.0+ WITH Linux-syscall-note, the same
    exemption that lets non-GPL programs include kernel UAPI) under
    src/3rdparty/mtd-utils/. No autoconf header check, no Linux-only
    guards — fails hard at run time if anything goes wrong.

Tests

Smoke test (tests/225_ubi_volume_write.test) covers:

  1. ubi_volume_write() with no args fails at config-parse time.
  2. Without --unsafe, the action refuses to apply.
  3. With --unsafe, applying to /dev/null cleanly errors with the
    ioctl-failure message rather than silently succeeding.

Real ioctl behaviour is the kernel's responsibility and isn't
exercised in CI (would need nandsim + ubi in a VM).

End-to-end hardware verification

Validated on real OpenWRT One hardware (MediaTek MT7981B, SPI NAND)
with this binary cross-compiled for aarch64:

  • Built a 558-byte .fw containing a 2828-byte payload.bin resource.
  • Ran fwup --unsafe -a -d /dev/null -i test.fw -t complete (the
    fwup.conf has ubi_volume_write("/dev/ubi0_3")).
  • fwup reported Success! with 558 bytes in / 2.83 KB out.
  • Read back via dd if=/dev/ubi0_3 bs=1 count=2828 | sha256sum
    hash matched the source payload.bin exactly.
  • UBI volume state remained OK after the atomic update.

I'm using this on real OpenWRT One hardware with nerves_system_openwrt_one
A/B kernel slots and U-Boot env all live in UBI volumes. With this
action plus a future tweak to uboot_setenv to detect UBI device
targets, my custom OTA-script glue can be retired.

@fhunleth

Copy link
Copy Markdown
Collaborator

@Hermanverschooten Just wanted to let you know that this is on my list for my next pass on fwup changes. I'm heading on vacation soon, so it will take longer than usual.

@fhunleth

fhunleth commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

First of all, I'm so sorry about this taking a terribly long time to get to.

There are a few easy things to fix - mostly trimming some of the Claude verbosity down, deleting the CHANGELOG.md entry completely and moving the actual UBI code to a Linux-only file (similar to mmc_linux.c).

The hard part is that I really need a regression test for the success case. It could be something that hooks into a shim or the verify_syscalls.c. I don't think it needs to be airtight, but something that I can run if I ever need to move code around in the future.

Could you see if you and your Claude can address the hard part? I'm not worried about the easy parts since those would take me no time at all to manually update if that turns out to be easiest.

@fhunleth

Copy link
Copy Markdown
Collaborator

Also, if you're running on macOS, I think the CI build failures on mac should be easy to fix.

Hermanverschooten added a commit to Hermanverschooten/fwup that referenced this pull request Jul 23, 2026
Review feedback from fwup-home#293:

- ubi_volume_write's open/UBI_IOCVOLUP/write/close now live in
  ubi_linux.c behind a small API in ubi.h, similar to mmc_linux.c.
  functions.c keeps the portable sequencing/zero-fill logic. This
  also fixes the macOS and Windows builds, which failed because
  mtd/ubi-user.h pulls in linux/types.h.
- Non-Linux builds get stubs that error at run time.
- Removed the CHANGELOG entry and trimmed the README row.
@Hermanverschooten

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Claude and I addressed everything:

  • CHANGELOG entry deleted, README row and code comments trimmed
  • UBI syscalls moved to a Linux-only ubi_linux.c (mmc_linux.c-style) behind a small ubi.h API. This also turned out to be the cause of the macOS and Windows build failures — mtd/ubi-user.h pulls in linux/types.h — so those should be green now.
  • The hard part: tests/fixture/ubi_shim.c is an LD_PRELOAD shim that emulates the UBI update API on a regular file — UBI_IOCVOLUP starts update mode and records the declared size, write() before the ioctl fails, and pwrite() fails with EPERM like a real UBI volume. 226_ubi_volume_write_success.test uses it to verify a byte-for-byte round-trip for both a regular and a sparse resource (holes written as zeros and counted in the declared size). It's not airtight, but it should catch any future refactoring that breaks the ioctl/sequential-write sequence. Skips on non-Linux.

Full test suite passes locally on Linux. CI needs a workflow approval to run on the new commits.

Hermanverschooten added a commit to Hermanverschooten/fwup that referenced this pull request Jul 23, 2026
Review feedback from fwup-home#293:

- ubi_volume_write's open/UBI_IOCVOLUP/write/close now live in
  ubi_linux.c behind a small API in ubi.h, similar to mmc_linux.c.
  functions.c keeps the portable sequencing/zero-fill logic. This
  also fixes the macOS and Windows builds, which failed because
  mtd/ubi-user.h pulls in linux/types.h.
- Non-Linux builds get stubs that error at run time.
- Removed the CHANGELOG entry and trimmed the README row.
UBI volumes (/dev/ubi0_N) reject pwrite() with EPERM. Updates have to
be initiated via the UBI_IOCVOLUP ioctl, which puts the volume in
atomic-update mode for a declared number of bytes; subsequent
sequential write()s fill the volume, and close() commits the update
atomically. raw_write/path_write/pipe_write all use pwrite, so none
of them work for OTA updates targeting individual UBI volumes.

ubi_volume_write(device_path) does the right thing:

  on-resource fit_a.itb {
      ubi_volume_write("/dev/ubi0_3")
  }

* Issues UBI_IOCVOLUP with the resource's full size (sparse holes
  included — they are materialized as zero writes since UBI can't
  seek).
* Tracks a write cursor and rejects out-of-order callbacks; fills
  gaps before sparse data with zeros.
* Requires --unsafe (consistent with path_write / pipe_write since
  the destination is a user-supplied path, not the -d device).
* Falls back to a build-time guard error on non-Linux hosts where
  <mtd/ubi-user.h> isn't available.

Includes a smoke test (225_ubi_volume_write.test) covering arg
validation, --unsafe enforcement, and clean failure when the path
isn't actually a UBI volume. Real ioctl behaviour belongs to the
kernel and isn't exercised here.
Per Frank's review on fwup-home#292: ship ubi-user.h with fwup so the
ubi_volume_write action is always compiled in, and let it fail
hard at runtime if anything goes wrong rather than silently
no-op'ing on hosts without mtd-utils headers.

Header copy comes from mtd-utils' own UAPI version, license
"GPL-2.0+ WITH Linux-syscall-note" (the same exemption that lets
non-GPL programs include kernel UAPI). Lives at
src/3rdparty/mtd-utils/include/mtd/ubi-user.h, included via a new
-I flag in src/Makefile.am.

Removed:
- mtd/ubi-user.h from configure.ac AC_CHECK_HEADERS
- #ifdef HAVE_MTD_UBI_USER_H / non-Linux stub from src/functions.c

Build clean, existing test (225_ubi_volume_write.test) still
passes.
Review feedback from fwup-home#293:

- ubi_volume_write's open/UBI_IOCVOLUP/write/close now live in
  ubi_linux.c behind a small API in ubi.h, similar to mmc_linux.c.
  functions.c keeps the portable sequencing/zero-fill logic. This
  also fixes the macOS and Windows builds, which failed because
  mtd/ubi-user.h pulls in linux/types.h.
- Non-Linux builds get stubs that error at run time.
- Removed the CHANGELOG entry and trimmed the README row.
An LD_PRELOAD shim emulates the UBI volume update API on a regular
file: UBI_IOCVOLUP starts update mode and records the declared size,
write() is only allowed while updating, and pwrite() fails with EPERM
like a real UBI volume. The test verifies that both a regular and a
sparse resource round-trip byte-for-byte (holes written as zeros) and
that the declared size matches the bytes written.

Linux-only; the test skips elsewhere and when the shim can't be
preloaded.
@fhunleth
fhunleth merged commit 6f3bf79 into fwup-home:main Jul 23, 2026
18 of 20 checks passed
@fhunleth

Copy link
Copy Markdown
Collaborator

Thanks for all of the updates. This looks good and like something I can maintain long-term.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: support writing to UBI volumes (/dev/ubi*_*)

2 participants