mirror of https://github.com/wolfSSL/wolfBoot.git
401 lines
13 KiB
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;
|
|
}
|