generic: mikrotik: size RouterBoot partitions so they are usable

Two fixes to the RouterBoot partition handling, both found on a MikroTik
Chateau 5G R17 ax but neither device specific.

hard_config is not guaranteed to fit within RB_BLOCK_SIZE. On this board
the tag list spans 5576 bytes, because the WLAN calibration tag alone
carries 5100 bytes of LZ77 compressed data, so truncating the partition
makes that tag unreadable. Walk the tag list to its terminating null node
and size the partition accordingly. The result never shrinks below
RB_BLOCK_SIZE, so devices whose tag list already fits are unaffected.

soft_config carries the boot settings and is the one RouterBoot segment
meant to be written, but at RB_BLOCK_SIZE (4 KiB) on a flash with 64 KiB
erase blocks MTD always forces it read-only. Grow it to a full erase
block where the space is free, checked against the following partition so
that erasing it cannot destroy a neighbour.

That growth alone would break the checksum, because rb_softconfig derives
everything from the partition size while RouterBoot validates the CRC32
over its own 4 KiB. Introduce RB_SC_CFG_SIZE and keep reads, CRC and write
back within it; the erase still covers the whole partition.

With both in place, writing boot_device followed by commit persists across
a reboot and "ethonce" triggers a real etherboot.

Signed-off-by: André Valentin <avalentin@marcant.net>
Link: https://github.com/openwrt/openwrt/pull/24335
Signed-off-by: Robert Marko <robimarko@gmail.com>
This commit is contained in:
André Valentin authored and Robert Marko committed 2026-09-15 13:21:07 +02:00
1 parent 3a2230382f
commit ada1dcb2b4
2 files changed
+99 -5

No files matched your search

@@ -92,12 +92,13 @@ struct routerboot_dynpart {
};
static int routerboot_dtbsfixup(struct mtd_info *, struct routerboot_dynpart *);
static int routerboot_hardfixup(struct mtd_info *, struct routerboot_dynpart *);
static struct routerboot_dynpart rb_dynparts[] = {
{
.name = "hard_config",
.magic = RB_MAGIC_HARD, // stored in CPU-endianness on flash
.size_fixup = NULL,
.size_fixup = routerboot_hardfixup,
.offset = 0x0,
.size = RB_BLOCK_SIZE,
.found = false,
@@ -118,6 +119,49 @@ static struct routerboot_dynpart rb_dynparts[] = {
}
};
/*
* hard_config is not guaranteed to fit within RB_BLOCK_SIZE. On the MikroTik
* Chateau 5G R17 ax the tag list spans 5576 bytes, because the WLAN calibration
* tag alone carries 5100 bytes of LZ77-compressed data. Truncating the partition
* to RB_BLOCK_SIZE makes that tag unreadable ("Out of bounds field").
*
* Walk the tag list to its terminating null node and size the partition to match.
* Tag nodes are u32: low 16 bits are the id, high 16 bits the payload length;
* payloads are 32bit-aligned. The result is rounded up to RB_BLOCK_SIZE and never
* shrinks below it, so devices whose tag list already fits are unaffected.
*/
static int routerboot_hardfixup(struct mtd_info *master, struct routerboot_dynpart *rbdpart)
{
size_t bytes_read, pos;
u32 node;
int err;
pos = rbdpart->offset + sizeof(node); /* skip magic */
while (pos < master->size) {
err = mtd_read(master, pos, sizeof(node), &bytes_read, (u8 *)&node);
if (err)
return err;
if (bytes_read != sizeof(node))
return -EIO;
if (!node) /* tag list ends with a null node */
break;
pos += sizeof(node) + ALIGN((size_t)(node >> 16), sizeof(node));
}
if (pos >= master->size)
return -EINVAL;
pos += sizeof(node); /* account for the null node itself */
rbdpart->size = max_t(size_t, ALIGN(pos - rbdpart->offset, RB_BLOCK_SIZE),
RB_BLOCK_SIZE);
return 0;
}
static int routerboot_dtbsfixup(struct mtd_info *master, struct routerboot_dynpart *rbdpart)
{
int err;
@@ -336,6 +380,43 @@ static int routerboot_partitions_parse(struct mtd_info *master,
np++;
}
/*
* MTD refuses writes to a partition that does not end on an erase block
* boundary. soft_config is RB_BLOCK_SIZE (4 KiB) while NOR erase blocks
* are typically 64 KiB, so it would always end up read-only - even
* though it is the one RouterBoot segment meant to be written, as it
* carries the boot settings.
*
* Grow it to a full erase block where the space is actually free, so
* that erasing it cannot destroy a neighbouring partition. The config
* data itself keeps its original size; see rb_softconfig.c, which
* erases the block but reads, checksums and writes only RB_BLOCK_SIZE.
*/
for (int idx = 0; idx < np; idx++) {
size_t grown, limit;
if (strcmp(parts[idx].name, "soft_config"))
continue;
if (!master->erasesize || parts[idx].size >= master->erasesize)
break;
if (parts[idx].offset % master->erasesize)
break; /* not block aligned to begin with */
grown = ALIGN(parts[idx].size, master->erasesize);
limit = (idx + 1 < np) ? parts[idx + 1].offset : master->size;
if (parts[idx].offset + grown > limit)
break; /* would overlap the next partition */
pr_info("%s: growing \"soft_config\" from 0x%llX to 0x%llX to make it writable\n",
master->name, (unsigned long long)parts[idx].size,
(unsigned long long)grown);
parts[idx].size = grown;
break;
}
*pparts = parts;
return np;
@@ -151,6 +151,17 @@
#define RB_CPU_FREQ_IDX_ATH79_7X_AR724X_MAX 3 // stops at D
#define RB_CPU_FREQ_IDX_ATH79_7X_AR7161_MAX 7 // stops at H - check if applies to all AR71xx devices
/*
* Size of the soft_config data itself, as RouterBoot lays it out and as the
* partition parser reports it. This is deliberately NOT the MTD partition
* size: routerbootpart.c may grow the partition to a full erase block so that
* MTD permits writes at all. Reads, the CRC32 and the write back all have to
* stay within the original size, otherwise RouterBoot sees a checksum over a
* region it does not know about, treats the segment as invalid and restores
* factory defaults.
*/
#define RB_SC_CFG_SIZE 0x1000
#define RB_SC_CRC32_OFFSET 4 // located right after magic
static struct kobject *sc_kobj;
@@ -739,7 +750,7 @@ static ssize_t sc_commit_store(struct kobject *kobj, struct kobj_attribute *attr
write_lock(&sc_bufrwl);
if (!flush) // reread
ret = mtd_read(mtd, 0, mtd->size, &bytes_rw, sc_buf);
ret = mtd_read(mtd, 0, sc_buflen, &bytes_rw, sc_buf);
else { // crc32 + commit
/*
* CRC32 is computed on the entire buffer, excluding the CRC
@@ -754,14 +765,16 @@ static ssize_t sc_commit_store(struct kobject *kobj, struct kobj_attribute *attr
/*
* The soft_config partition is assumed to be entirely contained
* in a single eraseblock.
* in a single eraseblock, so the whole partition is erased. Only
* the config itself is written back; anything beyond it stays
* erased, which is how RouterBoot leaves it too.
*/
ei.addr = 0;
ei.len = mtd->size;
ret = mtd_erase(mtd, &ei);
if (!ret)
ret = mtd_write(mtd, 0, mtd->size, &bytes_rw, sc_buf);
ret = mtd_write(mtd, 0, sc_buflen, &bytes_rw, sc_buf);
/*
* Handling mtd_write() failure here is a tricky situation. The
@@ -811,7 +824,7 @@ int rb_softconfig_init(struct kobject *rb_kobj, struct mtd_info *mtd)
if (ret)
return -ENODEV;
sc_buflen = mtd->size;
sc_buflen = min_t(size_t, mtd->size, RB_SC_CFG_SIZE);
sc_buf = kmalloc(sc_buflen, GFP_KERNEL);
if (!sc_buf) {
__put_mtd_device(mtd);