wolfBoot/src/ubootenv.c

401 lines
13 KiB
C

/* ubootenv.c
*
* U-Boot environment based A/B slot selection, RAUC-compatible.
* See include/ubootenv.h for the format and the flow it replicates.
*
* Copyright (C) 2026 wolfSSL Inc.
*
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "ubootenv.h"
/* Standard (zlib) CRC32 - self-contained so this module has no dependencies and
* is trivially unit-testable. Matches the CRC32 U-Boot uses for its env. */
static uint32_t ubootenv_crc32(const uint8_t *p, size_t len)
{
uint32_t crc;
size_t i;
int b;
crc = 0xFFFFFFFFUL;
for (i = 0; i < len; i++) {
crc ^= (uint32_t)p[i];
for (b = 0; b < 8; b++) {
if ((crc & 1U) != 0U)
crc = (crc >> 1) ^ 0xEDB88320UL;
else
crc >>= 1;
}
}
return crc ^ 0xFFFFFFFFUL;
}
static uint32_t rd_le32(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static void wr_le32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v);
p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16);
p[3] = (uint8_t)(v >> 24);
}
/* Longest counter accepted by env_atol(). Chosen so the value always fits a
* 16-bit-int platform's 32-bit long (999999999 < LONG_MAX), which also keeps
* env_ltoa()'s output well inside the caller's buffer. RAUC try counters are
* single digits, so this is far above any real value. */
#define ENV_DIGITS_MAX 9
/* Parse a non-negative decimal counter. Returns -1 unless the value is
* ENTIRELY digits (leading/trailing spaces allowed): a trailing-garbage value
* such as "1x" is a malformed counter, not 1, and a value longer than
* ENV_DIGITS_MAX digits is refused rather than silently wrapping. A -1 makes
* the caller treat the slot as having no tries left and skip it; if that leaves
* no candidate, pass 2 re-arms every counter, so a corrupt value degrades to a
* re-arm rather than to an arbitrary try count. */
static long env_atol(const char *s)
{
long v;
int n;
if (s == NULL)
return -1;
while (*s == ' ')
s++;
if (*s < '0' || *s > '9')
return -1;
v = 0;
n = 0;
while (*s >= '0' && *s <= '9') {
if (++n > ENV_DIGITS_MAX)
return -1; /* too long to represent or to round-trip */
v = (v * 10) + (long)(*s - '0');
s++;
}
while (*s == ' ')
s++;
if (*s != '\0')
return -1; /* trailing non-digit: malformed, not a partial parse */
return v;
}
/* Format a non-negative decimal into out, never writing more than out_max
* bytes including the terminator. The bound is explicit rather than an implied
* contract with the caller's buffer size: previously this assumed >= 16 bytes
* and relied on env_atol()'s input having been clamped elsewhere, so a longer
* value would have written one byte past the buffer. Digits that do not fit are
* dropped from the most significant end, which cannot happen for any value
* env_atol() accepts (ENV_DIGITS_MAX). */
static void env_ltoa(long v, char *out, size_t out_max)
{
char tmp[ENV_DIGITS_MAX + 1];
int i;
int j;
if (out_max == 0)
return;
i = 0;
j = 0;
if (v <= 0) {
if (out_max > 1)
out[j++] = '0';
out[j] = '\0';
return;
}
while (v > 0 && i < (int)sizeof(tmp)) {
tmp[i++] = (char)('0' + (v % 10));
v /= 10;
}
while (i > 0 && (size_t)j + 1 < out_max)
out[j++] = tmp[--i];
out[j] = '\0';
}
/* Build "BOOT_<name>_LEFT" into out. Returns 0 on success, -1 if it would not
* fit - refusing rather than silently dropping the "_LEFT" suffix (which would
* look up the wrong / a colliding counter key). The caller skips such a slot. */
static int env_leftkey(const char *name, char *out, size_t out_max)
{
const char *pre = "BOOT_";
const char *suf = "_LEFT";
size_t prel = 5; /* strlen("BOOT_") */
size_t sufl = 5; /* strlen("_LEFT") */
size_t n = 0;
if (prel + strlen(name) + sufl + 1 > out_max)
return -1;
while (*pre != '\0')
out[n++] = *pre++;
while (*name != '\0')
out[n++] = *name++;
while (*suf != '\0')
out[n++] = *suf++;
out[n] = '\0';
return 0;
}
int uboot_env_verify(const uint8_t *env, size_t env_len)
{
if (env == NULL || env_len < 5)
return 0;
return (rd_le32(env) == ubootenv_crc32(env + 4, env_len - 4)) ? 1 : 0;
}
/* U-Boot's *redundant* environment layout is [crc32:4][flags:1][data]; its
* CRC covers the data starting at offset 5, not 4. Detect it so a valid
* redundant env (a common RAUC choice) is refused rather than misread as
* corruption and wiped. Returns 1 if the buffer validates as a redundant env. */
static int uboot_env_is_redundant(const uint8_t *env, size_t env_len)
{
if (env == NULL || env_len < 6)
return 0;
return (rd_le32(env) == ubootenv_crc32(env + 5, env_len - 5)) ? 1 : 0;
}
void uboot_env_reseal(uint8_t *env, size_t env_len)
{
if (env == NULL || env_len < 5)
return;
wr_le32(env, ubootenv_crc32(env + 4, env_len - 4));
}
int uboot_env_get(const uint8_t *env, size_t env_len, const char *key,
char *val_out, size_t val_max)
{
const char *data;
const char *end;
const char *p;
size_t keylen;
if (env == NULL || key == NULL || val_out == NULL || env_len < 5 ||
val_max == 0)
return -1;
data = (const char *)(env + 4);
end = data + (env_len - 4);
keylen = strlen(key);
p = data;
while (p < end && *p != '\0') {
const char *entry = p;
size_t elen;
while (p < end && *p != '\0')
p++;
elen = (size_t)(p - entry);
if (p < end)
p++; /* skip the entry NUL */
if (elen > keylen && entry[keylen] == '=' &&
memcmp(entry, key, keylen) == 0) {
const char *v = entry + keylen + 1;
size_t vlen = elen - keylen - 1;
if (vlen >= val_max)
vlen = val_max - 1;
memcpy(val_out, v, vlen);
val_out[vlen] = '\0';
return (int)vlen;
}
}
return -1;
}
int uboot_env_set(uint8_t *env, size_t env_len, const char *key,
const char *val)
{
uint8_t *data;
uint8_t *end;
uint8_t *p;
uint8_t *list_end;
size_t keylen;
size_t vlen;
size_t entrylen;
size_t old_total;
size_t avail;
if (env == NULL || key == NULL || val == NULL || env_len < 5)
return -1;
data = env + 4;
end = env + env_len;
keylen = strlen(key);
vlen = strlen(val);
/* 1) Measure the space the existing entries for key occupy plus the free
* space, WITHOUT modifying anything, so a would-not-fit result below is
* non-destructive (never deletes the old value on failure). */
old_total = 0;
p = data;
while (p < end && *p != '\0') {
uint8_t *entry = p;
size_t elen;
while (p < end && *p != '\0')
p++;
elen = (size_t)(p - entry);
if (p < end)
p++; /* include the NUL */
if (elen > keylen && entry[keylen] == '=' &&
memcmp(entry, key, keylen) == 0)
old_total += (size_t)(p - entry);
}
list_end = p; /* start of the terminating NUL run */
entrylen = keylen + 1 + vlen + 1;
avail = (size_t)(end - list_end) + old_total; /* freed by removal + tail */
if (entrylen + 1 > avail) /* +1 keeps the terminator */
return -1;
/* 2) Remove any existing entry for key (shift the remainder down). */
p = data;
while (p < end && *p != '\0') {
uint8_t *entry = p;
size_t elen;
while (p < end && *p != '\0')
p++;
elen = (size_t)(p - entry);
if (p < end)
p++; /* include the NUL */
if (elen > keylen && entry[keylen] == '=' &&
memcmp(entry, key, keylen) == 0) {
size_t tail = (size_t)(end - p);
memmove(entry, p, tail);
memset(entry + tail, 0, (size_t)(end - (entry + tail)));
p = entry; /* continue scanning from the compacted position */
}
}
/* 3) Append "key=val\0"; the following byte stays NUL (list terminator). */
list_end = data;
while (list_end < end && *list_end != '\0') {
uint8_t *e = list_end;
while (e < end && *e != '\0')
e++;
if (e < end)
e++;
list_end = e;
}
memcpy(list_end, key, keylen);
list_end[keylen] = '=';
memcpy(list_end + keylen + 1, val, vlen);
list_end[keylen + 1 + vlen] = '\0';
return 0;
}
/* Copy the next space-separated token of BOOT_ORDER into name; advance *po.
* Returns the token length (0 when the list is exhausted). */
static int env_next_name(const char **po, char *name, size_t name_max)
{
const char *o = *po;
int n = 0;
while (*o == ' ')
o++;
while (*o != '\0' && *o != ' ' && n < (int)name_max - 1)
name[n++] = *o++;
name[n] = '\0';
*po = o;
return n;
}
int uboot_env_select_slot(uint8_t *env, size_t env_len, struct uboot_slot *out)
{
char order[UBOOT_ENV_VAL_MAX];
char leftkey[UBOOT_ENV_VAL_MAX];
char leftval[16];
char name[UBOOT_ENV_VAL_MAX];
const char *o;
long left;
int n;
if (env == NULL || out == NULL || env_len < 5)
return -1;
memset(out, 0, sizeof(*out));
/* Corrupt / blank env: reinitialize with defaults - but never destroy a
* valid-but-different layout. */
if (!uboot_env_verify(env, env_len)) {
/* A redundant-format env ([crc32][flags][data]) validates at offset 5.
* Refuse rather than wipe: overwriting it would destroy live RAUC
* state. The caller falls back to the static command line. */
if (uboot_env_is_redundant(env, env_len))
return -1;
/* Genuinely invalid: reinitialize defaults IN MEMORY so this boot can
* proceed, but flag it (out->reinitialized) so the caller does NOT
* persist the reset over on-disk state it could not validate. */
env_ltoa(UBOOT_ENV_DEFAULT_TRIES, leftval, sizeof(leftval));
memset(env, 0, env_len);
(void)uboot_env_set(env, env_len, "BOOT_ORDER", "A B");
(void)uboot_env_set(env, env_len, "BOOT_A_LEFT", leftval);
(void)uboot_env_set(env, env_len, "BOOT_B_LEFT", leftval);
uboot_env_reseal(env, env_len);
out->reinitialized = 1;
}
if (uboot_env_get(env, env_len, "BOOT_ORDER", order, sizeof(order)) < 0) {
memcpy(order, "A B", 4);
(void)uboot_env_set(env, env_len, "BOOT_ORDER", order);
uboot_env_reseal(env, env_len);
}
/* Malformed (empty / whitespace-only) BOOT_ORDER yields no tokens: report
* failure per the header contract rather than "success, no slot". */
o = order;
if (env_next_name(&o, name, sizeof(name)) == 0)
return -1;
/* Pass 1: first slot with tries left -> select + decrement. */
o = order;
while ((n = env_next_name(&o, name, sizeof(name))) > 0) {
/* Skip a name too long to form a valid "BOOT_<name>_LEFT" key rather
* than looking up a truncated / colliding counter. */
if (env_leftkey(name, leftkey, sizeof(leftkey)) != 0)
continue;
left = UBOOT_ENV_DEFAULT_TRIES;
if (uboot_env_get(env, env_len, leftkey, leftval, sizeof(leftval)) >= 0)
left = env_atol(leftval);
if (left > 0) {
env_ltoa(left - 1, leftval, sizeof(leftval));
/* If the decrement cannot be stored (env full), do NOT select this
* slot: booting it with an un-decremented counter would retry the
* same hung slot forever. Fail so the caller uses the static
* command line. (uboot_env_set is non-destructive on failure.) */
if (uboot_env_set(env, env_len, leftkey, leftval) != 0)
return -1;
uboot_env_reseal(env, env_len);
memcpy(out->name, name, (size_t)(n + 1));
out->selected = 1;
return 0;
}
}
/* Pass 2: none left -> re-arm every counter, ask the caller to reboot. */
env_ltoa(UBOOT_ENV_DEFAULT_TRIES, leftval, sizeof(leftval));
o = order;
while (env_next_name(&o, name, sizeof(name)) > 0) {
if (env_leftkey(name, leftkey, sizeof(leftkey)) != 0)
continue;
(void)uboot_env_set(env, env_len, leftkey, leftval);
}
uboot_env_reseal(env, env_len);
out->rearmed = 1;
out->selected = 0;
return 0;
}