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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ uboot_clearenv(my_uboot_env) | 0.10.0 | Initialize a clean, variable
uboot_recover(my_uboot_env) | 0.15.0 | If the U-Boot environment is corrupt, reinitialize it. If not, then do nothing
uboot_setenv(my_uboot_env, name, value) | 0.10.0 | Set the specified U-boot variable
uboot_unsetenv(my_uboot_env, name) | 0.10.0 | Unset the specified U-boot variable
ubi_volume_write(device_path) | 1.17.0 | Write a resource atomically to a UBI volume (e.g. `/dev/ubi0_3`). Requires the `--unsafe` flag. Linux-only

## Minimizing writes to the destination

Expand Down
473 changes: 473 additions & 0 deletions src/3rdparty/mtd-utils/include/mtd/ubi-user.h

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fwup_SOURCES=\
simple_string.c \
sparse_file.c \
uboot_env.c \
ubi_linux.c \
util.c \
archive_open.h \
block_cache.h \
Expand Down Expand Up @@ -63,13 +64,15 @@ fwup_SOURCES=\
simple_string.h \
sparse_file.h \
uboot_env.h \
ubi.h \
util.h \
3rdparty/base64.c \
3rdparty/base64.h \
3rdparty/monocypher-3.1.3/src/monocypher.c \
3rdparty/monocypher-3.1.3/src/monocypher.h \
3rdparty/monocypher-3.1.3/src/optional/monocypher-ed25519.c \
3rdparty/monocypher-3.1.3/src/optional/monocypher-ed25519.h \
3rdparty/mtd-utils/include/mtd/ubi-user.h \
3rdparty/semver.c/semver.c \
3rdparty/semver.c/semver.h \
3rdparty/mbedtls/mbedtls_aes.c \
Expand Down Expand Up @@ -112,6 +115,7 @@ endif
fwup_CFLAGS = -Wall -D_FILE_OFFSET_BITS=64 \
-I$(srcdir)/3rdparty/monocypher-3.1.3/src \
-I$(srcdir)/3rdparty/monocypher-3.1.3/src/optional \
-I$(srcdir)/3rdparty/mtd-utils/include \
$(CONFUSE_CFLAGS) \
$(ARCHIVE_CFLAGS) \
$(PTHREAD_CFLAGS)
Expand Down
1 change: 1 addition & 0 deletions src/cfgfile.c
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ static cfg_opt_t uboot_environment_opts[] = {
CFG_FUNC("info", CB), \
CFG_FUNC("path_write", CB), \
CFG_FUNC("pipe_write", CB), \
CFG_FUNC("ubi_volume_write", CB), \
CFG_FUNC("execute", CB), \
CFG_FUNC("reboot_param", CB)

Expand Down
108 changes: 108 additions & 0 deletions src/functions.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "fwfile.h"
#include "block_cache.h"
#include "uboot_env.h"
#include "ubi.h"
#include "sparse_file.h"
#include "progress.h"
#include "pad_to_block_writer.h"
Expand Down Expand Up @@ -66,6 +67,7 @@ DECLARE_FUN(error);
DECLARE_FUN(info);
DECLARE_FUN(path_write);
DECLARE_FUN(pipe_write);
DECLARE_FUN(ubi_volume_write);
DECLARE_FUN(execute);
DECLARE_FUN(reboot_param);

Expand Down Expand Up @@ -103,6 +105,7 @@ static struct fun_info fun_table[] = {
FUN_INFO(info),
FUN_INFO(path_write),
FUN_INFO(pipe_write),
FUN_INFO(ubi_volume_write),
FUN_INFO(execute),
FUN_INFO(reboot_param)
};
Expand Down Expand Up @@ -1227,6 +1230,111 @@ int pipe_write_run(struct fun_context *fctx)
return rc;
}

int ubi_volume_write_validate(struct fun_context *fctx)
{
if (fctx->type != FUN_CONTEXT_FILE)
ERR_RETURN("ubi_volume_write only usable in on-resource");

if (fctx->argc != 2)
ERR_RETURN("ubi_volume_write requires a UBI volume path (e.g. /dev/ubi0_3)");

return 0;
}
int ubi_volume_write_compute_progress(struct fun_context *fctx)
{
// count_holes=true since holes are written as zeros
return process_resource_compute_progress(fctx, true);
}

struct ubi_volume_write_cookie {
const char *device;
int fd;
off_t cursor; // bytes written so far; UBI requires sequential writes
};

static int ubi_volume_write_seq(struct ubi_volume_write_cookie *uvc,
const void *buf, size_t count)
{
OK_OR_RETURN(ubi_volume_update_write(uvc->device, uvc->fd, buf, count));
uvc->cursor += count;
return 0;
}

static int ubi_volume_write_fill_zeros(struct ubi_volume_write_cookie *uvc, off_t up_to)
{
static const char zeros[FWUP_BLOCK_SIZE] = {0};
while (uvc->cursor < up_to) {
size_t chunk = (size_t) (up_to - uvc->cursor);
if (chunk > sizeof(zeros))
chunk = sizeof(zeros);
OK_OR_RETURN(ubi_volume_write_seq(uvc, zeros, chunk));
}
return 0;
}

static int ubi_volume_write_pwrite_callback(void *cookie, const void *buf, size_t count, off_t offset)
{
struct ubi_volume_write_cookie *uvc = (struct ubi_volume_write_cookie *) cookie;

// UBI volumes can't seek, so fill in holes with zeros.
if (offset < uvc->cursor)
ERR_RETURN("unexpected out-of-order write to '%s' (offset=%lld, cursor=%lld)",
uvc->device, (long long) offset, (long long) uvc->cursor);
if (offset > uvc->cursor)
OK_OR_RETURN(ubi_volume_write_fill_zeros(uvc, offset));

return ubi_volume_write_seq(uvc, buf, count);
}

static int ubi_volume_write_final_hole_callback(void *cookie, off_t hole_size, off_t file_size)
{
struct ubi_volume_write_cookie *uvc = (struct ubi_volume_write_cookie *) cookie;
(void) hole_size;
return ubi_volume_write_fill_zeros(uvc, file_size);
}

int ubi_volume_write_run(struct fun_context *fctx)
{
OK_OR_RETURN(check_unsafe(fctx));

int rc = 0;
struct ubi_volume_write_cookie uvc;
uvc.device = fctx->argv[1];
uvc.cursor = 0;
uvc.fd = -1;

// UBI's atomic-update ioctl needs the total update size up front.
struct sparse_file_map sfm;
sparse_file_init(&sfm);
OK_OR_CLEANUP(sparse_file_get_map_from_config(fctx->cfg, fctx->on_event->title, &sfm));
int64_t total_bytes = (int64_t) sparse_file_size(&sfm);
sparse_file_free(&sfm);

uvc.fd = ubi_volume_update_start(uvc.device, total_bytes);
if (uvc.fd < 0)
ERR_CLEANUP();

OK_OR_CLEANUP(process_resource(fctx,
true,
ubi_volume_write_pwrite_callback,
ubi_volume_write_final_hole_callback,
&uvc));

if (uvc.cursor != total_bytes)
ERR_CLEANUP_MSG("wrote %lld bytes to '%s' but declared %lld",
(long long) uvc.cursor, uvc.device, (long long) total_bytes);

cleanup:
if (uvc.fd >= 0) {
// close() commits the update when all bytes were written and
// invalidates the volume otherwise.
int close_rc = ubi_volume_update_finish(uvc.device, uvc.fd);
if (rc == 0)
rc = close_rc;
}
return rc;
}

int execute_validate(struct fun_context *fctx)
{
if (fctx->argc != 2)
Expand Down
35 changes: 35 additions & 0 deletions src/ubi.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2026 Herman verschooten
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#ifndef UBI_H
#define UBI_H

#include <stdint.h>
#include <stddef.h>

// Start an atomic update of total_bytes on a UBI volume character
// device (e.g. /dev/ubi0_3). Returns the fd or -1 on error.
int ubi_volume_update_start(const char *path, int64_t total_bytes);

// Write the next count bytes of the update. UBI only supports
// sequential writes while updating.
int ubi_volume_update_write(const char *path, int fd, const void *buf, size_t count);

// Close the volume. UBI commits the update if all declared bytes were
// written and marks the volume corrupted otherwise.
int ubi_volume_update_finish(const char *path, int fd);

#endif
93 changes: 93 additions & 0 deletions src/ubi_linux.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2026 Herman verschooten
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "ubi.h"
#include "util.h"

#ifdef __linux__

#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>

#include <mtd/ubi-user.h>

int ubi_volume_update_start(const char *path, int64_t total_bytes)
{
int fd = open(path, O_WRONLY);
if (fd < 0)
ERR_RETURN("ubi_volume_write can't open '%s': %s", path, strerror(errno));

if (ioctl(fd, UBI_IOCVOLUP, &total_bytes) < 0) {
close(fd);
ERR_RETURN("UBI_IOCVOLUP failed on '%s': %s. Check that it's a UBI volume (see /sys/class/ubi)",
path, strerror(errno));
}

return fd;
}

int ubi_volume_update_write(const char *path, int fd, const void *buf, size_t count)
{
const char *p = buf;
while (count > 0) {
ssize_t n = write(fd, p, count);
if (n < 0) {
if (errno == EINTR)
continue;
ERR_RETURN("write to '%s' failed: %s", path, strerror(errno));
}
p += n;
count -= n;
}
return 0;
}

int ubi_volume_update_finish(const char *path, int fd)
{
if (close(fd) < 0)
ERR_RETURN("close('%s') failed: %s", path, strerror(errno));
return 0;
}

#else

int ubi_volume_update_start(const char *path, int64_t total_bytes)
{
(void) path;
(void) total_bytes;
ERR_RETURN("ubi_volume_write is only supported on Linux");
}

int ubi_volume_update_write(const char *path, int fd, const void *buf, size_t count)
{
(void) path;
(void) fd;
(void) buf;
(void) count;
ERR_RETURN("ubi_volume_write is only supported on Linux");
}

int ubi_volume_update_finish(const char *path, int fd)
{
(void) path;
(void) fd;
ERR_RETURN("ubi_volume_write is only supported on Linux");
}

#endif // __linux__
41 changes: 41 additions & 0 deletions tests/225_ubi_volume_write.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/sh

#
# Test that ubi_volume_write rejects misuse. See
# 226_ubi_volume_write_success.test for the success path.
#

. "$(cd "$(dirname "$0")" && pwd)/common.sh"

# 1. ubi_volume_write with no path argument must fail at create time.
cat >"$CONFIG" <<EOF
file-resource TEST { host-path = "${TESTFILE_1K}" }
task no_args {
on-resource TEST { ubi_volume_write() }
}
EOF
if $FWUP_CREATE -c -f "$CONFIG" -o "$FWFILE" 2>/dev/null; then
echo "Expected ubi_volume_write() with no path argument to fail at create time."
exit 1
fi

# 2. ubi_volume_write with a valid-looking path must require --unsafe.
cat >"$CONFIG" <<EOF
file-resource TEST { host-path = "${TESTFILE_1K}" }
task complete {
on-resource TEST { ubi_volume_write("/dev/null") }
}
EOF
$FWUP_CREATE -c -f "$CONFIG" -o "$FWFILE"

if $FWUP_APPLY -a -d "$IMGFILE" -i "$FWFILE" -t complete 2>/dev/null; then
echo "Expected ubi_volume_write to require --unsafe."
exit 1
fi

# 3. With --unsafe, applying to /dev/null must fail since it's not a
# UBI volume.
if $FWUP_APPLY --unsafe -a -d "$IMGFILE" -i "$FWFILE" -t complete 2>/dev/null; then
echo "Expected ubi_volume_write to fail on a non-UBI target."
exit 1
fi
Loading
Loading