diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index 9d4d5f61c..a2c4749c2 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -597,11 +597,14 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board return true; } #elif defined(OTA_RAK_AUTO_STORE) - if (c.fetch_store.usesExternal() && !(bl.storage_flags & OTA_BL_STORAGE_QSPI)) { + // Bootloader packages passed their separate privileged checks above and + // select internal staging when their manifest arrives. The currently + // selected application backend must not veto that transition. + if (!selboot && c.fetch_store.usesExternal() && !(bl.storage_flags & OTA_BL_STORAGE_QSPI)) { strcpy(reply, "ERR bootloader cannot apply from detected QSPI storage"); return true; } - if (!c.fetch_store.usesExternal() && !c.fetch_store.usesInternal()) { + if (!selboot && !c.fetch_store.usesExternal() && !c.fetch_store.usesInternal()) { snprintf(reply, 160, "ERR OTA storage unsafe: %s", c.fetch_store.selectionReason()); return true; } diff --git a/test/test_rak_adaptive_bootloader.py b/test/test_rak_adaptive_bootloader.py index c60270e65..284387ef9 100644 --- a/test/test_rak_adaptive_bootloader.py +++ b/test/test_rak_adaptive_bootloader.py @@ -10,6 +10,49 @@ ROOT = Path(__file__).resolve().parents[1] class AdaptiveBootloaderTest(unittest.TestCase): + def test_cli_uses_application_storage_gate_only_for_applications(self): + cli = (ROOT / "src/helpers/ota/OtaCli.cpp").read_text() + error = cli.index('ERR bootloader cannot apply from detected QSPI storage') + start = cli.rfind('#elif defined(OTA_RAK_AUTO_STORE)', 0, error) + end = cli.index('#elif defined(OTA_QSPI_STORE)', error) + gate = cli[cli.index('\n', start) + 1:end] + source = r''' +#include +#include +#include +#include +constexpr uint8_t OTA_BL_STORAGE_QSPI = 4; +struct Store { + int mode; + bool usesExternal() { return mode == 1; } + bool usesInternal() { return mode == 0; } + const char* selectionReason() { return "unsafe wiring"; } +}; +bool rejected(bool selboot, int mode, uint8_t flags) { + struct { Store fetch_store; } c = {{mode}}; + struct { uint8_t storage_flags; } bl = {flags}; + char reply[160]; +''' + gate + r''' + return false; +} +int main() { + // The CLI has already required exact identity and privileged boot caps. + assert(!rejected(true, 1, 0x0A)); // canonical bootloader, external app store + assert(!rejected(true, 0, 0x0A)); + assert(!rejected(true, 2, 0x0A)); // optional NOR is irrelevant to internal boot staging + assert(rejected(false, 1, 0x0A)); // app still needs the external capability + assert(rejected(false, 2, 0x1E)); // unsafe application store remains rejected + assert(!rejected(false, 1, 0x1E)); + assert(!rejected(false, 0, 0x0A)); +} +''' + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / "cli.cpp").write_text(source) + subprocess.run(["g++", "-std=c++17", "-Wall", "-Wextra", "-Werror", + str(path / "cli.cpp"), "-o", str(path / "cli")], check=True) + subprocess.run([str(path / "cli")], check=True) + def test_storage_switch_and_manual_resume(self): header = (ROOT / "src/helpers/ota/OtaStoreAdaptiveNrf52.h").read_text() body = header[header.index("class OtaStoreAdaptiveNrf52"):