mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 18:31:57 +00:00
Add bounded OTAFIX bootloader version fallback
This commit is contained in:
@@ -4744,8 +4744,12 @@ preview, and vendor/build suffixes. If that string is absent or ambiguous, it
|
||||
uses the existing whole-image CRC and unique BLMF/BLM2 validation to read the
|
||||
embedded OTAFIX version (for example, `> OTAFIX2.4.6`). Some current release
|
||||
images, including MeshTower V2 SD 2.4.6, omit the UF2 text entirely. As a last
|
||||
fallback, the Adafruit core's startup-captured version is explicitly labelled
|
||||
`(base)`; it cannot identify an OTAFIX/fork revision.
|
||||
OTAFIX-specific fallback, it accepts one unambiguous standalone version token
|
||||
such as `v0.11.0-OTAFIX2.4.9` from only the first 40 KiB of the identified
|
||||
bootloader region. A corrupt canonical bootloader manifest cannot be bypassed
|
||||
by that printable token. The Adafruit core's startup-captured version remains
|
||||
the final fallback and is explicitly labelled `(base)`; it cannot identify an
|
||||
OTAFIX/fork revision.
|
||||
|
||||
`unknown` remains possible for a bootloader exposing none of these version
|
||||
sources, invalid geometry/vectors, or unresolved ambiguity. There is no universal
|
||||
|
||||
@@ -68,6 +68,37 @@ inline size_t nrf52BootVersionTokenSize(const uint8_t* token, size_t available)
|
||||
return 0; // unterminated or too long
|
||||
}
|
||||
|
||||
inline bool nrf52BootDecimalComponent(const uint8_t* token, size_t length, size_t& pos) {
|
||||
const size_t begin = pos;
|
||||
while (pos < length && token[pos] >= '0' && token[pos] <= '9') ++pos;
|
||||
return pos != begin;
|
||||
}
|
||||
|
||||
// Recent OTAFIX images embed the INFO_UF2 version value without the older
|
||||
// "UF2 Bootloader " label. Match only that explicit version shape, not an
|
||||
// arbitrary dotted number or a SoftDevice/application string.
|
||||
inline bool nrf52BootStandaloneOtafixToken(const uint8_t* token, size_t length) {
|
||||
if (!token || length == 0u || token[0] != 'v') return false;
|
||||
size_t pos = 1u;
|
||||
for (unsigned component = 0; component < 3u; ++component) {
|
||||
if (!nrf52BootDecimalComponent(token, length, pos)) return false;
|
||||
if (component != 2u && (pos >= length || token[pos++] != '.')) return false;
|
||||
}
|
||||
static const char marker[] = "-OTAFIX";
|
||||
const size_t marker_size = sizeof(marker) - 1u;
|
||||
if (length - pos < marker_size || memcmp(token + pos, marker, marker_size) != 0)
|
||||
return false;
|
||||
pos += marker_size;
|
||||
if (!nrf52BootDecimalComponent(token, length, pos) ||
|
||||
pos >= length || token[pos++] != '.' ||
|
||||
!nrf52BootDecimalComponent(token, length, pos)) return false;
|
||||
if (pos < length && token[pos] == '.') {
|
||||
++pos;
|
||||
if (!nrf52BootDecimalComponent(token, length, pos)) return false;
|
||||
}
|
||||
return pos == length || token[pos] == '-' || token[pos] == '+' || token[pos] == '_';
|
||||
}
|
||||
|
||||
inline bool nrf52BootloaderVersion(const uint8_t* image, size_t size,
|
||||
uint32_t start, uint32_t cached_base_version,
|
||||
char* out, size_t capacity) {
|
||||
@@ -122,6 +153,53 @@ inline bool nrf52BootloaderVersion(const uint8_t* image, size_t size,
|
||||
}
|
||||
if (conflicting) return false;
|
||||
|
||||
// Only as a fallback, inspect at most the first 40 KiB of the already
|
||||
// identified bootloader region. A unique standalone OTAFIX version is useful
|
||||
// on boards whose installed image lacks a reader-recognized BLMF identity.
|
||||
// If a canonical identity is present but corrupt, do not let its printable
|
||||
// version string make that image look healthy.
|
||||
bool allow_standalone = true;
|
||||
if (size == ota::OTA_BOOT_IMAGE_SIZE) {
|
||||
const size_t offset = ota::OTA_BOOT_CANDIDATE_MANIFEST_OFFSET;
|
||||
const bool magic0 = ota::ota_boot_rd32(image + offset) == ota::OTA_BOOT_MANIFEST_MAGIC0;
|
||||
const bool magic1 = ota::ota_boot_rd32(image + offset + 4u) == ota::OTA_BOOT_MANIFEST_MAGIC1;
|
||||
if (magic0 || magic1) {
|
||||
ota::OtaBootloaderIdentity continuity;
|
||||
allow_standalone =
|
||||
magic0 && magic1 &&
|
||||
ota::ota_boot_rd16(image + offset + 8u) == ota::OTA_BOOT_MANIFEST_VERSION &&
|
||||
ota::ota_boot_rd16(image + offset + 10u) == ota::OTA_BOOT_MANIFEST_SIZE &&
|
||||
ota::ota_boot_rd32(image + offset + 12u) == ota::OTA_BOOT_IMAGE_START &&
|
||||
ota::ota_boot_rd32(image + offset + 16u) == ota::OTA_BOOT_IMAGE_SIZE &&
|
||||
ota::ota_bootloader_board_id_valid(ota::ota_boot_rd32(image + offset + 20u)) &&
|
||||
ota::ota_boot_image_crc32(image, size, offset + 40u) ==
|
||||
ota::ota_boot_rd32(image + offset + 40u) &&
|
||||
ota::ota_bootloader_continuity_parse(image + offset, size - offset,
|
||||
continuity) &&
|
||||
continuity.continuity_present;
|
||||
}
|
||||
}
|
||||
const size_t scan_size = size < ota::OTA_BOOT_IMAGE_SIZE ? size : ota::OTA_BOOT_IMAGE_SIZE;
|
||||
const uint8_t* standalone = nullptr;
|
||||
size_t standalone_size = 0;
|
||||
for (size_t offset = 0; allow_standalone && offset < scan_size; ++offset) {
|
||||
if (image[offset] != 'v' ||
|
||||
(offset != 0u && nrf52BootVersionChar(image[offset - 1u]))) continue;
|
||||
const uint8_t* token = image + offset;
|
||||
const size_t length = nrf52BootVersionTokenSize(token, scan_size - offset);
|
||||
if (length == 0u || !nrf52BootStandaloneOtafixToken(token, length)) continue;
|
||||
if (standalone &&
|
||||
(length != standalone_size || memcmp(standalone, token, length) != 0)) return false;
|
||||
standalone = token;
|
||||
standalone_size = length;
|
||||
}
|
||||
if (standalone) {
|
||||
if (standalone_size >= capacity) return false;
|
||||
memcpy(out, standalone, standalone_size);
|
||||
out[standalone_size] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Adafruit's core captures TIMER2->CC[0] at startup, before the timer can
|
||||
// be reused. This is only a base version (no OTAFIX/fork suffix). Never read
|
||||
// the live timer here or reinterpret Nordic settings with a different layout.
|
||||
|
||||
+54
-1
@@ -26,6 +26,11 @@ static void text(std::vector<uint8_t>& bytes, size_t offset, const std::string&
|
||||
assert(offset + line.size() <= bytes.size());
|
||||
memcpy(bytes.data() + offset, line.data(), line.size());
|
||||
}
|
||||
static void standalone(std::vector<uint8_t>& bytes, size_t offset, const std::string& version) {
|
||||
const std::string line = version + " s140 6.1.1\r\n";
|
||||
assert(offset + line.size() <= bytes.size());
|
||||
memcpy(bytes.data() + offset, line.data(), line.size());
|
||||
}
|
||||
static void refreshManifestCrc(std::vector<uint8_t>& bytes) {
|
||||
const size_t crc_offset = OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40;
|
||||
put32(bytes, crc_offset, ota_boot_image_crc32(bytes.data(), bytes.size(), crc_offset));
|
||||
@@ -99,7 +104,55 @@ int main(int argc, char** argv) {
|
||||
auto bytes = image(); text(bytes, offset, version); expect(bytes, version);
|
||||
}
|
||||
}
|
||||
auto bytes = image(); text(bytes, 0x100, "0.11.0"); text(bytes, 0x8000, "0.11.0");
|
||||
auto bytes = image();
|
||||
// OTAFIX 2.4.9 and later board images use a standalone INFO_UF2 value.
|
||||
// Search only the first 40 KiB of the identified bootloader region.
|
||||
for (const char* version : {"v0.11.0-OTAFIX2.4.9", "v0.11.0-OTAFIX2.4.10",
|
||||
"v0.9.2-OTAFIX2.2-BP1.3", "v0.11.0-OTAFIX2.4.9-preview.1"}) {
|
||||
for (size_t offset : {0x100u, 0x8000u, 0x98fbu, 0x9c9du}) {
|
||||
auto bytes = image(); standalone(bytes, offset, version); expect(bytes, version);
|
||||
}
|
||||
}
|
||||
bytes = image(); standalone(bytes, 0x100, "v0.11.0-OTAFIX2.4.9");
|
||||
standalone(bytes, 0x8000, "v0.11.0-OTAFIX2.4.9");
|
||||
expect(bytes, "v0.11.0-OTAFIX2.4.9"); // identical copies are harmless
|
||||
standalone(bytes, 0x8000, "v0.11.0-OTAFIX2.4.10"); expect(bytes, nullptr);
|
||||
bytes = image(); standalone(bytes, 0x100, "v0.11.0-OTAFIX2.4.9");
|
||||
char small[8];
|
||||
assert(!nrf52BootloaderVersion(bytes.data(), bytes.size(), 0xf4000, 0,
|
||||
small, sizeof(small)) && small[0] == 0);
|
||||
for (const char* bad : {"v0.11-OTAFIX2.4.9", "v0.11.0-OTAFIX2.x.9",
|
||||
"v0.11.0-OTAFIX2.4.9junk", "v0.11.0-OTAFIX2.4.",
|
||||
"v0.11.0-OTAFIX", "v0.11.0-notOTAFIX2.4.9"}) {
|
||||
bytes = image(); standalone(bytes, 0x100, bad); expect(bytes, nullptr);
|
||||
}
|
||||
bytes = image(); standalone(bytes, 0x100, "v0.11.0-OTAFIX2.4.9");
|
||||
put32(bytes, 4, 0x27001); expect(bytes, nullptr); // reject application vectors
|
||||
bytes = image(); standalone(bytes, 0x100, "v0.11.0-OTAFIX2.4.9");
|
||||
manifest(bytes, 0x020406ff); expect(bytes, "OTAFIX2.4.6"); // validated metadata wins
|
||||
bytes[0x300] ^= 1; expect(bytes, nullptr); // never mask a corrupt manifest CRC
|
||||
bytes = image(); standalone(bytes, 0x100, "v0.11.0-OTAFIX2.4.9");
|
||||
manifest(bytes, 0x020409ff);
|
||||
put32(bytes, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 20, OTA_XIAO_BOARD_ID_BASE);
|
||||
memset(bytes.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 24, 0, 16);
|
||||
memcpy(bytes.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 24, "SCAP_DFU", 8);
|
||||
refreshManifestCrc(bytes);
|
||||
OtaBootloaderIdentity unexpected_board;
|
||||
assert(!ota_bootloader_identity_from_image(bytes.data(), bytes.size(), unexpected_board));
|
||||
expect(bytes, "v0.11.0-OTAFIX2.4.9"); // diagnostic fallback for an unexpected board name
|
||||
bytes = image(); standalone(bytes, 0x100, "v0.11.0-OTAFIX2.4.9");
|
||||
manifest(bytes, 0x020409ff);
|
||||
put16(bytes, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 8, 0);
|
||||
refreshManifestCrc(bytes); expect(bytes, nullptr); // malformed BLMF is not a fallback
|
||||
bytes = image(); standalone(bytes, 0x100, "v0.11.0-OTAFIX2.4.9");
|
||||
manifest(bytes, 0x020409ff);
|
||||
put16(bytes, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 52, 0);
|
||||
refreshManifestCrc(bytes); expect(bytes, nullptr); // malformed BLM2 is not a fallback
|
||||
bytes = image(0xe000, 0xf0000);
|
||||
standalone(bytes, 0xb000, "v0.11.0-OTAFIX2.4.9"); expect(bytes, nullptr, 0xf0000);
|
||||
standalone(bytes, 0x9800, "v0.11.0-OTAFIX2.4.9");
|
||||
expect(bytes, "v0.11.0-OTAFIX2.4.9", 0xf0000);
|
||||
bytes = image(); text(bytes, 0x100, "0.11.0"); text(bytes, 0x8000, "0.11.0");
|
||||
expect(bytes, "0.11.0"); // identical copies are harmless
|
||||
text(bytes, 0x8000, "0.12.0"); expect(bytes, nullptr, 0xf4000, 0xb00);
|
||||
bytes = image(); memcpy(bytes.data() + 0x100, "UF2 Bootloader ", 15);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Compile the real version reader; optionally verify the pinned 2.4.6 release."""
|
||||
"""Compile the real version reader; optionally verify pinned OTAFIX releases."""
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -78,6 +78,32 @@ class BootloaderVersionTest(unittest.TestCase):
|
||||
self.assertEqual(result.stdout.strip(), "OTAFIX2.4.6")
|
||||
print(f"Verified version reader against {len(names)} signed OTAFIX 2.4.6 release images")
|
||||
|
||||
release_249 = os.environ.get("MESHCORE_BOOTLOADER_MOTA_249_ZIP")
|
||||
if release_249:
|
||||
self.assertEqual(hashlib.sha256(Path(release_249).read_bytes()).hexdigest(),
|
||||
"6fd7d799873ba6016d37679d9ef3610e62105c2477b49a71933eec12756e625c",
|
||||
"unexpected 2.4.9 release archive")
|
||||
with zipfile.ZipFile(release_249) as archive:
|
||||
names = [name for name in archive.namelist() if name.endswith(".mota")]
|
||||
self.assertEqual(len(names), 27)
|
||||
for name in names:
|
||||
with self.subTest(release_image=name):
|
||||
# The pinned bundle holds one exact 40 KiB full image per
|
||||
# board. Isolate the native reader without making this
|
||||
# test depend on package capability-policy changes.
|
||||
container = archive.read(name)
|
||||
self.assertEqual(len(container), 41330)
|
||||
payload = container[-5 - 40960:-5]
|
||||
result = subprocess.run([str(exe), "--hex"], input=payload.hex(),
|
||||
capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("OTAFIX2.4.9", result.stdout)
|
||||
if "sensecap_solar_p1" in name:
|
||||
self.assertEqual(result.stdout.strip(), "v0.11.0-OTAFIX2.4.9")
|
||||
if "wiscore_rak4631_board_bootloader" in name:
|
||||
self.assertEqual(result.stdout.strip(), "OTAFIX2.4.9")
|
||||
print(f"Verified version reader against {len(names)} OTAFIX 2.4.9 board images")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user