Implement authenticated HTTPS OTA uploads with bounded streaming, image validation, reboot coordination, and lifecycle exclusion. Add the admin UI, regression tests, and Phase 10 acceptance documentation.
453 lines
24 KiB
C
453 lines
24 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
#include "fakes.h"
|
|
#include "web_auth_parse.h"
|
|
#include "esp_app_format.h"
|
|
#include "esp_app_desc.h"
|
|
#include "esp_image_format.h"
|
|
#include "web_firmware_update.h"
|
|
|
|
static bool httpd_valid_req(httpd_req_t *r) { return r && r->aux; }
|
|
static size_t strlcpy(char *out, const char *in, size_t size)
|
|
{
|
|
size_t length = strlen(in);
|
|
if (size) { size_t n = length < size - 1 ? length : size - 1; memcpy(out, in, n); out[n] = 0; }
|
|
return length;
|
|
}
|
|
/* IDF getters compare ptrdiff_t with size_t; retain their exact source. */
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wsign-compare"
|
|
#include "httpd_production.h"
|
|
#pragma GCC diagnostic pop
|
|
|
|
static int s_lock;
|
|
static bool s_ready;
|
|
static web_cookie_auth_snapshot_t s_counts;
|
|
#include "auth_production.h"
|
|
|
|
static SemaphoreHandle_t s_server_mutex;
|
|
static httpd_handle_t s_server;
|
|
static bool s_transitioning;
|
|
static esp_err_t s_last_error;
|
|
static uint32_t s_generation;
|
|
#include "server_production.h"
|
|
|
|
static void *test_malloc(size_t);
|
|
static void test_free(void *);
|
|
#define malloc test_malloc
|
|
#define free test_free
|
|
#include "web_firmware_update.c"
|
|
#undef malloc
|
|
#undef free
|
|
|
|
static uint8_t image[12000], flash[12000];
|
|
static httpd_req_t request;
|
|
static esp_partition_t running, target;
|
|
static bool missing_target, missing_running;
|
|
static bool lookup_current, final_current, lookup_unavailable, final_unavailable;
|
|
static int role, stage_fail, response_fail, response_step;
|
|
static bool malloc_fail, task_fail, identity_busy, locked, mutex_busy;
|
|
static unsigned allocations, allocations_live, tasks_created, tasks_deleted;
|
|
static unsigned begins, writes, ends, aborts, metadata_reads, commits, receives, wipes;
|
|
static unsigned restarts, response_sends, releases, identity_releases;
|
|
static size_t flash_size, max_chunk, cutoff, stall_at, parsed_size;
|
|
static int64_t now_us, read_us;
|
|
static char response_status[64], response_body[160];
|
|
static uint32_t notification, identity_token;
|
|
static void (*task_entry)(void *);
|
|
static int task_storage, server_storage, mutex_storage;
|
|
static bool receive_race, commit_race, revoke_at_metadata;
|
|
static unsigned cases;
|
|
static bool ota_live;
|
|
static char raw_length[32], header_scratch[2048];
|
|
static struct httpd_req_aux request_aux;
|
|
|
|
static void *test_malloc(size_t size)
|
|
{
|
|
assert(size == BUFFER_SIZE); ++allocations;
|
|
if (malloc_fail) return NULL;
|
|
++allocations_live; return malloc(size);
|
|
}
|
|
static void test_free(void *p) { if (p) { assert(allocations_live == 1); --allocations_live; } free(p); }
|
|
void secure_wipe(void *p, size_t n) { memset(p, 0, n); }
|
|
int64_t esp_timer_get_time(void) { return now_us; }
|
|
int xSemaphoreTake(SemaphoreHandle_t m, unsigned ticks)
|
|
{
|
|
assert(m == &mutex_storage && !locked);
|
|
if (mutex_busy) { assert(ticks == 0); return 0; }
|
|
locked = true; return pdTRUE;
|
|
}
|
|
void xSemaphoreGive(SemaphoreHandle_t m) { assert(m == &mutex_storage && locked); locked = false; }
|
|
|
|
int xTaskCreate(void (*entry)(void *), const char *name, unsigned stack, void *arg,
|
|
unsigned priority, TaskHandle_t *out)
|
|
{
|
|
assert(!strcmp(name, "fw_reboot") && stack == 2048 && !arg && priority == 5);
|
|
assert(s_transitioning && identity_token && !commits && !task_entry);
|
|
if (task_fail) return 0;
|
|
task_entry = entry; *out = &task_storage; ++tasks_created; return pdPASS;
|
|
}
|
|
int xTaskNotify(TaskHandle_t t, uint32_t value, int action)
|
|
{
|
|
assert(t == &task_storage && task_entry && !notification && action == eSetValueWithOverwrite);
|
|
assert(value == 1 || value == 2);
|
|
if (value == 1) assert(commits == 1 && response_sends == 1 && !response_fail && !aborts);
|
|
notification = value; return pdPASS;
|
|
}
|
|
int xTaskNotifyWait(uint32_t clear_in, uint32_t clear_out, uint32_t *value, uint32_t wait)
|
|
{
|
|
assert(!clear_in && clear_out == UINT32_MAX && wait == portMAX_DELAY && notification);
|
|
*value = notification; return pdTRUE;
|
|
}
|
|
void vTaskDelay(unsigned ticks) { assert(ticks == 500 && notification == 1); }
|
|
void vTaskDelete(TaskHandle_t task) { assert(!task); ++tasks_deleted; }
|
|
void esp_restart(void) { assert(!locked); ++restarts; }
|
|
static void finish_owner(void)
|
|
{
|
|
if (task_entry) {
|
|
assert(notification); task_entry(NULL); task_entry = NULL;
|
|
assert(tasks_deleted == tasks_created);
|
|
}
|
|
}
|
|
|
|
|
|
static void set_header(const char *name, const char *value)
|
|
{
|
|
for (size_t i = 0; i < request.header_count; ++i) {
|
|
if (!strcmp(name, request.headers[i].name)) { request.headers[i].value = value; return; }
|
|
}
|
|
assert(request.header_count < 12);
|
|
request.headers[request.header_count++] = (test_header_t){name, value};
|
|
}
|
|
static void set_length(size_t length)
|
|
{
|
|
request.content_len = length;
|
|
snprintf(raw_length, sizeof(raw_length), "%zu", length);
|
|
set_header("Content-Length", raw_length);
|
|
}
|
|
bool web_httpd_headers_valid(httpd_req_t *r)
|
|
{
|
|
request_aux = (struct httpd_req_aux){.scratch = header_scratch};
|
|
for (size_t i = 0; i < r->header_count; ++i) {
|
|
if (!r->headers[i].value) continue;
|
|
size_t used = request_aux.scratch_cur_size;
|
|
int written = snprintf(header_scratch + used, sizeof(header_scratch) - used,
|
|
"%s:%s", r->headers[i].name, r->headers[i].value);
|
|
assert(written >= 0 && (size_t)written + 1 <= sizeof(header_scratch) - used);
|
|
request_aux.scratch_cur_size += (size_t)written + 1;
|
|
++request_aux.req_hdrs_count;
|
|
}
|
|
r->aux = &request_aux;
|
|
return r->headers_valid && adapter_headers_valid(r);
|
|
}
|
|
bool web_httpd_unread_body(httpd_req_t *r) { return r->received < r->content_len; }
|
|
void web_httpd_wipe_request(httpd_req_t *r, bool unread)
|
|
{
|
|
assert(unread == (r->received < r->content_len)); ++wipes;
|
|
}
|
|
static esp_err_t response_result(void) { return ++response_step == response_fail ? ESP_FAIL : ESP_OK; }
|
|
esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *s)
|
|
{
|
|
assert(r == &request && strlen(s) < sizeof(response_status)); strcpy(response_status, s);
|
|
return response_result();
|
|
}
|
|
esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *s)
|
|
{
|
|
assert(r == &request && !strcmp(s, "application/json; charset=utf-8")); return response_result();
|
|
}
|
|
esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *name, const char *value)
|
|
{
|
|
assert(r == &request && name && value); return response_result();
|
|
}
|
|
esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *body)
|
|
{
|
|
assert(r == &request && strlen(body) < sizeof(response_body));
|
|
++response_sends; strcpy(response_body, body); return response_result();
|
|
}
|
|
static void competing_lifecycle(void)
|
|
{
|
|
assert(s_transitioning && !locked);
|
|
assert(web_firmware_update_reserve(request.handle) == ESP_ERR_INVALID_STATE);
|
|
assert(web_server_reboot_current(s_generation) == ESP_ERR_INVALID_STATE);
|
|
assert(!restarts);
|
|
uint32_t token = 0;
|
|
assert(web_security_reserve_identity(0, false, &token) == ESP_ERR_INVALID_STATE && !token);
|
|
}
|
|
int httpd_req_recv(httpd_req_t *r, char *out, size_t want)
|
|
{
|
|
assert(r == &request && want && want <= BUFFER_SIZE && s_transitioning && identity_token);
|
|
assert(web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
|
++receives; now_us += read_us;
|
|
if (receive_race) competing_lifecycle();
|
|
if (r->received >= stall_at) return HTTPD_SOCK_ERR_TIMEOUT;
|
|
if (r->received >= cutoff) return 0;
|
|
size_t count = want < max_chunk ? want : max_chunk;
|
|
if (count > cutoff - r->received) count = cutoff - r->received;
|
|
assert(r->received + count <= sizeof(image));
|
|
memcpy(out, image + r->received, count); r->received += count; return (int)count;
|
|
}
|
|
esp_err_t web_session_store_lookup(const char *token, size_t length, const char *origin,
|
|
size_t origin_length, web_session_view_t *view)
|
|
{
|
|
assert(length == 64 && token[0] == 'a' && origin_length == strlen(origin));
|
|
assert(!strcmp(origin, "https://device"));
|
|
if (lookup_unavailable) return ESP_FAIL;
|
|
if (!lookup_current) return ESP_ERR_NOT_FOUND;
|
|
view->id = 42; view->principal.role = role; view->principal.user_id = 9;
|
|
memset(view->csrf, 'b', 64); view->csrf[64] = 0; return ESP_OK;
|
|
}
|
|
esp_err_t web_session_store_check_principal(web_session_id_t id, const user_principal_t *principal, bool *current)
|
|
{
|
|
assert(id == 42 && principal->role == USER_ROLE_ADMIN && principal->user_id == 9);
|
|
assert(ends == 1 && metadata_reads == 1 && !commits);
|
|
*current = final_current; return final_unavailable ? ESP_FAIL : ESP_OK;
|
|
}
|
|
esp_err_t web_security_reserve_identity(uint32_t expected, bool reset, uint32_t *token)
|
|
{
|
|
assert(!expected && !reset && s_transitioning); *token = 0;
|
|
if (identity_busy || identity_token) return ESP_ERR_INVALID_STATE;
|
|
*token = identity_token = 7; return ESP_OK;
|
|
}
|
|
void web_security_release_identity(uint32_t token)
|
|
{
|
|
assert(token && token == identity_token && s_transitioning);
|
|
identity_token = 0; ++identity_releases;
|
|
}
|
|
const esp_partition_t *esp_ota_get_running_partition(void) { return missing_running ? NULL : &running; }
|
|
const esp_partition_t *esp_ota_get_next_update_partition(const esp_partition_t *p)
|
|
{
|
|
assert(!p); return missing_target ? NULL : ⌖
|
|
}
|
|
esp_err_t esp_ota_begin(const esp_partition_t *p, size_t size, esp_ota_handle_t *handle)
|
|
{
|
|
assert(p == &target && p->address != running.address && size == request.content_len);
|
|
assert(size <= target.size && request.received == PREFIX_SIZE && s_transitioning && identity_token);
|
|
assert(tasks_created == 1 && !locked); ++begins;
|
|
assert(!ota_live && *handle == 0);
|
|
if (stage_fail == 1) return ESP_FAIL;
|
|
*handle = 123; ota_live = true;
|
|
/* IDF publishes the handle before erasing; this models erase failure. */
|
|
return stage_fail == 6 ? ESP_FAIL : ESP_OK;
|
|
}
|
|
esp_err_t esp_ota_write(esp_ota_handle_t handle, const void *data, size_t size)
|
|
{
|
|
assert(handle == 123 && s_transitioning && !locked && size <= BUFFER_SIZE);
|
|
assert(flash_size + size <= sizeof(flash)); ++writes;
|
|
if (stage_fail == 2) return ESP_FAIL;
|
|
memcpy(flash + flash_size, data, size); flash_size += size; return ESP_OK;
|
|
}
|
|
esp_err_t esp_ota_end(esp_ota_handle_t handle)
|
|
{
|
|
assert(handle == 123 && flash_size == request.content_len && !memcmp(flash, image, flash_size));
|
|
assert(!aborts && !locked && s_transitioning && tasks_created == 1 && ota_live); ++ends;
|
|
ota_live = false; /* End consumes on success AND validation failure. */
|
|
return stage_fail == 3 ? ESP_FAIL : ESP_OK;
|
|
}
|
|
esp_err_t esp_ota_abort(esp_ota_handle_t handle)
|
|
{
|
|
assert(handle == 123 && begins == 1 && !ends && !commits && ota_live);
|
|
ota_live = false; ++aborts; return ESP_OK;
|
|
}
|
|
esp_err_t esp_image_get_metadata(const esp_partition_pos_t *p, esp_image_metadata_t *metadata)
|
|
{
|
|
assert(p->offset == target.address && p->size == target.size && ends == 1);
|
|
++metadata_reads; metadata->image_len = (uint32_t)parsed_size;
|
|
if (revoke_at_metadata) final_current = false;
|
|
return stage_fail == 4 ? ESP_FAIL : ESP_OK;
|
|
}
|
|
esp_err_t esp_ota_set_boot_partition(const esp_partition_t *p)
|
|
{
|
|
assert(p == &target && ends == 1 && metadata_reads == 1 && final_current && !final_unavailable);
|
|
assert(tasks_created == 1 && !response_sends && s_transitioning && identity_token && !locked);
|
|
assert(web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
|
if (commit_race) competing_lifecycle();
|
|
if (stage_fail == 5) return ESP_FAIL;
|
|
++commits; return ESP_OK;
|
|
}
|
|
|
|
static void reset(void)
|
|
{
|
|
assert(!allocations_live && !task_entry && !locked && !ota_live);
|
|
atomic_store(&s_reboot_gate, false);
|
|
s_firmware_selected = false;
|
|
++cases;
|
|
memset(&request, 0, sizeof(request));
|
|
request.uri = WEB_FIRMWARE_UPDATE_URI; request.method = HTTP_POST;
|
|
request.content_len = sizeof(image); request.handle = &server_storage; request.headers_valid = true;
|
|
set_header("Host", "device"); set_header("Origin", "https://device");
|
|
set_header("Content-Type", "application/octet-stream");
|
|
set_length(sizeof(image));
|
|
set_header("Cookie", SESSION_COOKIE "=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
set_header("X-CSRF-Token", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
memset(image, 0x55, sizeof(image)); memset(flash, 0, sizeof(flash));
|
|
esp_image_header_t header = {.magic = ESP_IMAGE_HEADER_MAGIC, .chip_id = ESP_CHIP_ID_ESP32S3,
|
|
.segment_count = 3, .hash_appended = 1};
|
|
esp_image_segment_header_t segment = {.data_len = sizeof(esp_app_desc_t)};
|
|
uint32_t magic = ESP_APP_DESC_MAGIC_WORD;
|
|
memcpy(image, &header, sizeof(header)); memcpy(image + sizeof(header), &segment, sizeof(segment));
|
|
memcpy(image + sizeof(header) + sizeof(segment), &magic, sizeof(magic));
|
|
running = (esp_partition_t){.type = 0, .subtype = 0x10, .address = 0x10000, .size = 0x400000};
|
|
target = (esp_partition_t){.type = 0, .subtype = 0x11, .address = 0x410000, .size = 0x400000};
|
|
missing_target = missing_running = false;
|
|
s_ready = lookup_current = final_current = true;
|
|
s_server_mutex = &mutex_storage; s_server = &server_storage;
|
|
s_transitioning = false; s_last_error = ESP_OK; s_generation = 11;
|
|
lookup_unavailable = final_unavailable = malloc_fail = task_fail = identity_busy = mutex_busy = false;
|
|
receive_race = commit_race = revoke_at_metadata = false;
|
|
role = USER_ROLE_ADMIN; stage_fail = response_fail = response_step = 0;
|
|
allocations = allocations_live = tasks_created = tasks_deleted = 0;
|
|
begins = writes = ends = aborts = metadata_reads = commits = receives = wipes = 0;
|
|
restarts = response_sends = releases = identity_releases = 0;
|
|
flash_size = 0; max_chunk = BUFFER_SIZE; cutoff = stall_at = SIZE_MAX; parsed_size = sizeof(image);
|
|
now_us = 0; read_us = 1000; notification = identity_token = 0;
|
|
response_status[0] = response_body[0] = 0;
|
|
}
|
|
static void rejected(const char *status, const char *code)
|
|
{
|
|
bool pre_reserved = s_transitioning;
|
|
bool gate_reserved = atomic_load(&s_reboot_gate);
|
|
esp_err_t result = web_firmware_update_handler(&request);
|
|
assert(!strcmp(response_status, status) && strstr(response_body, code));
|
|
assert(result == (request.received < request.content_len ? ESP_FAIL : ESP_OK));
|
|
assert(!commits && !allocations_live && !restarts && wipes == 1 && !ota_live);
|
|
assert(s_transitioning == pre_reserved && !identity_token);
|
|
assert(atomic_load(&s_reboot_gate) == gate_reserved);
|
|
if (task_entry) assert(notification == 2);
|
|
finish_owner(); assert(!restarts);
|
|
}
|
|
static void success(void)
|
|
{
|
|
assert(web_firmware_update_handler(&request) == ESP_OK);
|
|
assert(!strcmp(response_status, "200 OK") && !strcmp(response_body, "{\"ok\":true,\"rebooting\":true}"));
|
|
assert(commits == 1 && ends == 1 && !aborts && s_transitioning && identity_token);
|
|
assert(!allocations_live && notification == 1 && !restarts && wipes == 1);
|
|
assert(s_firmware_selected && web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
|
assert(request.received == request.content_len && !memcmp(image, flash, sizeof(image)));
|
|
finish_owner(); assert(restarts == 1);
|
|
}
|
|
int main(void)
|
|
{
|
|
reset();
|
|
assert(web_firmware_update_reserve_reboot() == ESP_OK);
|
|
rejected("503 Service Unavailable", "busy");
|
|
assert(!receives && !begins && !allocations);
|
|
reset(); s_server_mutex = NULL; s_server = NULL;
|
|
assert(web_firmware_update_reserve_reboot() == ESP_OK);
|
|
assert(web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
|
reset(); success();
|
|
reset(); max_chunk = 1; receive_race = commit_race = true; success();
|
|
assert(receives == sizeof(image));
|
|
reset(); max_chunk = 17; success();
|
|
puts("PASS bounded streaming, bytewise partial prefix, exact writes, delayed owner restart and reservation races");
|
|
|
|
reset(); lookup_current = false; rejected("401 Unauthorized", "authentication_required"); assert(!receives && !allocations);
|
|
reset(); lookup_unavailable = true; rejected("503 Service Unavailable", "unavailable");
|
|
reset(); role = USER_ROLE_USER; rejected("403 Forbidden", "admin_required"); assert(!receives && !allocations);
|
|
reset(); set_header("X-CSRF-Token", NULL); rejected("403 Forbidden", "csrf");
|
|
reset(); set_header("X-CSRF-Token", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); rejected("403 Forbidden", "csrf");
|
|
reset(); set_header("Origin", NULL); rejected("403 Forbidden", "origin");
|
|
reset(); set_header("Origin", "null"); rejected("403 Forbidden", "origin");
|
|
reset(); set_header("Origin", "https://attacker"); rejected("403 Forbidden", "origin");
|
|
reset(); set_header("Sec-Fetch-Site", "cross-site"); rejected("403 Forbidden", "origin");
|
|
reset(); set_header("Cookie", NULL); rejected("401 Unauthorized", "authentication_required");
|
|
reset(); request.headers_valid = false; rejected("400 Bad Request", "invalid_request");
|
|
reset(); request.uri = "/api/firmware?x=1"; rejected("400 Bad Request", "invalid_request");
|
|
reset(); request.method = HTTP_GET; rejected("400 Bad Request", "invalid_request");
|
|
reset(); s_ready = false; rejected("503 Service Unavailable", "unavailable");
|
|
puts("PASS production cookie/Origin/CSRF/admin admission before receive or flash (SDK getters/adapter real, database doubled)");
|
|
|
|
const char *invalid_lengths[] = {NULL, "", " ", "+12000", "-12000", "12000x", "1.2e4",
|
|
"12 000", "12000,12000", "12000 ", "12000\t", "\t12000", "11999", "12001",
|
|
"000000000000000012000"};
|
|
for (size_t i = 0; i < sizeof(invalid_lengths) / sizeof(invalid_lengths[0]); ++i) {
|
|
reset(); set_header("Content-Length", invalid_lengths[i]);
|
|
rejected("400 Bad Request", "invalid_request");
|
|
assert(!receives && !begins && !allocations && !notification);
|
|
}
|
|
const char *oversized_lengths[] = {"4194305", "4296815136", "4294967296",
|
|
"18446744073709551615", "18446744073709551616", "99999999999999999999"};
|
|
for (size_t i = 0; i < sizeof(oversized_lengths) / sizeof(oversized_lengths[0]); ++i) {
|
|
reset(); set_header("Content-Length", oversized_lengths[i]);
|
|
/* Explicitly model HTTPD's 64-bit -> ESP32 size_t narrowing. */
|
|
request.content_len = i == 1 ? 1847840 : i == 2 ? 0 : sizeof(image);
|
|
rejected("413 Payload Too Large", "firmware_too_large");
|
|
assert(!receives && !begins && !allocations && !notification);
|
|
}
|
|
reset();
|
|
request.headers[request.header_count++] = (test_header_t){"content-length", "12000"};
|
|
rejected("400 Bad Request", "invalid_request"); assert(!receives && !begins && !allocations);
|
|
reset(); set_header("Content-Length", " 12000"); success();
|
|
reset(); set_header("Content-Length", "00000000000000012000"); success();
|
|
puts("PASS raw Content-Length missing/duplicate/malformed/oversized/64-bit wrap and overflow reject before receive/erase; IDF leading-space semantics");
|
|
|
|
reset(); set_header("Content-Type", "multipart/form-data"); rejected("415 Unsupported Media Type", "firmware_content_type");
|
|
reset(); set_header("Content-Type", NULL); rejected("415 Unsupported Media Type", "firmware_content_type");
|
|
reset(); set_length(0); rejected("400 Bad Request", "invalid_firmware");
|
|
reset(); set_length(PREFIX_SIZE - 1); rejected("400 Bad Request", "invalid_firmware");
|
|
reset(); set_length(target.size + 1); rejected("413 Payload Too Large", "firmware_too_large"); assert(!allocations);
|
|
reset(); target.size = sizeof(image); success();
|
|
reset(); missing_target = true; rejected("503 Service Unavailable", "firmware_unavailable");
|
|
reset(); missing_running = true; rejected("503 Service Unavailable", "firmware_unavailable");
|
|
reset(); target.address = running.address; rejected("503 Service Unavailable", "firmware_unavailable");
|
|
reset(); target.type = 1; rejected("503 Service Unavailable", "firmware_unavailable");
|
|
reset(); target.subtype = 0; rejected("503 Service Unavailable", "firmware_unavailable");
|
|
for (unsigned field = 0; field < 6; ++field) {
|
|
reset(); esp_image_header_t header; memcpy(&header, image, sizeof(header));
|
|
if (field == 0) header.magic = 0;
|
|
if (field == 1) header.chip_id = ESP_CHIP_ID_ESP32;
|
|
if (field == 2) header.segment_count = 0;
|
|
if (field == 3) header.hash_appended = 0;
|
|
memcpy(image, &header, sizeof(header));
|
|
if (field == 4) memset(image + sizeof(header) + sizeof(esp_image_segment_header_t), 0, 4);
|
|
if (field == 5) memset(image + sizeof(header), 0, sizeof(esp_image_segment_header_t));
|
|
rejected("400 Bad Request", "invalid_firmware"); assert(!begins && !aborts);
|
|
}
|
|
puts("PASS content type, length/capacity, inactive app selection, format/chip/descriptor/hash header rejection before erase");
|
|
|
|
reset(); s_transitioning = true; rejected("503 Service Unavailable", "busy");
|
|
reset(); identity_busy = true; rejected("503 Service Unavailable", "busy");
|
|
reset(); mutex_busy = true; rejected("503 Service Unavailable", "busy");
|
|
reset(); s_server = NULL; rejected("503 Service Unavailable", "busy");
|
|
reset(); malloc_fail = true; rejected("503 Service Unavailable", "firmware_resources"); assert(!tasks_created && !begins);
|
|
reset(); task_fail = true; rejected("503 Service Unavailable", "firmware_resources"); assert(!tasks_created && !begins);
|
|
for (int stage = 1; stage <= 5; ++stage) {
|
|
reset(); stage_fail = stage;
|
|
rejected(stage == 3 || stage == 4 ? "400 Bad Request" : "500 Internal Server Error",
|
|
stage == 3 || stage == 4 ? "invalid_firmware" : stage == 5 ? "firmware_commit_failed" : "firmware_write_failed");
|
|
assert(aborts == (unsigned)(stage == 2));
|
|
}
|
|
reset(); stage_fail = 6;
|
|
rejected("500 Internal Server Error", "firmware_write_failed");
|
|
assert(begins == 1 && aborts == 1 && !writes && !ends && !ota_live);
|
|
puts("PASS begin failure after handle publication aborts once; unpublished failure and consumed end never double-abort");
|
|
reset(); parsed_size--; rejected("400 Bad Request", "invalid_firmware");
|
|
reset(); parsed_size++; rejected("400 Bad Request", "invalid_firmware");
|
|
reset(); revoke_at_metadata = true; rejected("401 Unauthorized", "authentication_required");
|
|
reset(); final_unavailable = true; rejected("401 Unauthorized", "authentication_required");
|
|
puts("PASS busy/resources/OTA begin-write-end-metadata-commit failures, final auth revocation and exact SDK image length");
|
|
|
|
reset(); cutoff = 10; rejected("400 Bad Request", "firmware_incomplete"); assert(!begins);
|
|
reset(); cutoff = 300; rejected("400 Bad Request", "firmware_incomplete"); assert(aborts == 1);
|
|
reset(); stall_at = 0; read_us = 1000000; rejected("408 Request Timeout", "firmware_timeout"); assert(!begins && receives == 10);
|
|
reset(); stall_at = PREFIX_SIZE; read_us = 1000000; rejected("408 Request Timeout", "firmware_timeout"); assert(aborts == 1);
|
|
reset(); read_us = STALL_US; rejected("408 Request Timeout", "firmware_timeout"); assert(!begins);
|
|
reset(); max_chunk = 1; read_us = 40000; rejected("408 Request Timeout", "firmware_timeout"); assert(aborts == 1 && now_us == TOTAL_US);
|
|
puts("PASS incomplete body, stalled receive and total slow-drip deadline; no selection/reboot, handle abort when live");
|
|
|
|
for (int step = 1; step <= 6; ++step) {
|
|
reset(); response_fail = step;
|
|
assert(web_firmware_update_handler(&request) == ESP_FAIL);
|
|
assert(commits == 1 && !s_transitioning && !identity_token && !allocations_live);
|
|
assert(notification == 2 && !restarts && !aborts); finish_owner(); assert(!restarts);
|
|
assert(s_firmware_selected && !atomic_load(&s_reboot_gate));
|
|
request.received = 0; response_fail = 0;
|
|
assert(web_firmware_update_handler(&request) == ESP_FAIL);
|
|
assert(!strcmp(response_status, "409 Conflict") && strstr(response_body, "firmware_selected_reboot_required"));
|
|
assert(commits == 1 && begins == 1 && !atomic_load(&s_reboot_gate));
|
|
assert(web_firmware_update_reserve_reboot() == ESP_OK);
|
|
assert(web_firmware_update_reserve_reboot() == ESP_ERR_INVALID_STATE);
|
|
}
|
|
puts("PASS postcommit response failure: selected image retained, no automatic reboot, resources/reservations released");
|
|
printf("PASS %u firmware backend cases; SDK validation, flash and scheduling are mocked, not device evidence\n", cases);
|
|
return 0;
|
|
}
|