feat(firmware): QEMU ESP32-S3 testing platform (ADR-061)

Implement full QEMU emulation framework for firmware testing without
physical hardware:

Mock CSI Generator (mock_csi.c):
- 10 test scenarios: empty room, static/walking person, fall, multi-person,
  channel sweep, MAC filter, ring overflow, boundary RSSI, zero-length
- Physics-based signal model with breathing modulation and Doppler
- LFSR pseudo-random noise, CONFIG_CSI_MOCK_ENABLED Kconfig guard
- Scenario 255 runs all sequentially

QEMU Runner & CI:
- qemu-esp32s3-test.sh: build, merge flash image, run QEMU, validate
- validate_qemu_output.py: 14 automated checks (boot, NVS, edge, vitals,
  crash detection) with colored output and severity-based exit codes
- generate_nvs_matrix.py: 14 NVS provisioning configs for matrix testing
- firmware-qemu.yml: GitHub Actions CI with 4-scenario matrix

Fuzz Testing:
- 3 libFuzzer targets: CSI serialize, NVS config validation, ring buffer
- Host-compilable ESP-IDF stubs (no ESP-IDF dependency for fuzzing)
- 6 seed corpus files for guided fuzzing
- Makefile with ASAN + UBSAN sanitizers

Documentation:
- firmware/esp32-csi-node/README.md: comprehensive QEMU testing guide
- Root README.md: collapsed QEMU testing section

Build verified: normal firmware build (RC=0) with mock_csi excluded.

Closes #259

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-03-13 09:17:07 -04:00
parent a467dfed9f
commit ffeaa46bc6
33 changed files with 3274 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
# Makefile for ESP32 CSI firmware fuzz testing targets (ADR-061 Layer 6).
#
# Requirements:
# - clang with libFuzzer support (clang 6.0+)
# - Linux or macOS (host-based fuzzing, no ESP-IDF needed)
#
# Usage:
# make all # Build all fuzz targets
# make fuzz_serialize # Build serialize target only
# make fuzz_edge # Build edge enqueue target only
# make fuzz_nvs # Build NVS config target only
# make run_serialize # Build and run serialize fuzzer (30s)
# make run_edge # Build and run edge fuzzer (30s)
# make run_nvs # Build and run NVS fuzzer (30s)
# make run_all # Run all fuzzers (30s each)
# make clean # Remove build artifacts
#
# Environment variables:
# FUZZ_DURATION=60 # Override fuzz duration in seconds
# FUZZ_JOBS=4 # Parallel fuzzing jobs
CC = clang
CFLAGS = -fsanitize=fuzzer,address,undefined -g -O1 \
-Istubs -I../main \
-DCONFIG_CSI_NODE_ID=1 \
-DCONFIG_CSI_WIFI_CHANNEL=6 \
-DCONFIG_CSI_WIFI_SSID=\"test\" \
-DCONFIG_CSI_TARGET_IP=\"192.168.1.1\" \
-DCONFIG_CSI_TARGET_PORT=5500 \
-DCONFIG_ESP_WIFI_CSI_ENABLED=1 \
-Wno-unused-function
STUBS_SRC = stubs/esp_stubs.c
MAIN_DIR = ../main
# Default fuzz duration (seconds) and jobs
FUZZ_DURATION ?= 30
FUZZ_JOBS ?= 1
.PHONY: all clean run_serialize run_edge run_nvs run_all
all: fuzz_serialize fuzz_edge fuzz_nvs
# --- Serialize fuzzer ---
# Tests csi_serialize_frame() with random wifi_csi_info_t inputs.
# Links against the real csi_collector.c (with stubs for ESP-IDF).
fuzz_serialize: fuzz_csi_serialize.c $(MAIN_DIR)/csi_collector.c $(STUBS_SRC)
$(CC) $(CFLAGS) $^ -o $@ -lm
# --- Edge enqueue fuzzer ---
# Tests the SPSC ring buffer push/pop logic with rapid-fire enqueues.
# Self-contained: reproduces ring buffer logic from edge_processing.c.
fuzz_edge: fuzz_edge_enqueue.c $(STUBS_SRC)
$(CC) $(CFLAGS) $^ -o $@ -lm
# --- NVS config validation fuzzer ---
# Tests all NVS config validation ranges with random values.
# Self-contained: reproduces validation logic from nvs_config.c.
fuzz_nvs: fuzz_nvs_config.c $(STUBS_SRC)
$(CC) $(CFLAGS) $^ -o $@ -lm
# --- Run targets ---
run_serialize: fuzz_serialize
@mkdir -p corpus
./fuzz_serialize corpus/ -max_total_time=$(FUZZ_DURATION) -max_len=2048 -jobs=$(FUZZ_JOBS)
run_edge: fuzz_edge
@mkdir -p corpus
./fuzz_edge corpus/ -max_total_time=$(FUZZ_DURATION) -max_len=4096 -jobs=$(FUZZ_JOBS)
run_nvs: fuzz_nvs
@mkdir -p corpus
./fuzz_nvs corpus/ -max_total_time=$(FUZZ_DURATION) -max_len=256 -jobs=$(FUZZ_JOBS)
run_all: run_serialize run_edge run_nvs
clean:
rm -f fuzz_serialize fuzz_edge fuzz_nvs
rm -rf corpus/
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,203 @@
/**
* @file fuzz_csi_serialize.c
* @brief libFuzzer target for csi_serialize_frame() (ADR-061 Layer 6).
*
* Takes fuzz input and constructs wifi_csi_info_t structs with random
* field values including extreme boundaries. Verifies that
* csi_serialize_frame() never crashes, triggers ASAN, or causes UBSAN.
*
* Build (Linux/macOS with clang):
* make fuzz_serialize
*
* Run:
* ./fuzz_serialize corpus/ -max_len=2048
*/
#include "esp_stubs.h"
/* Provide the globals that csi_collector.c references. */
#include "nvs_config.h"
nvs_config_t g_nvs_config;
/* Pull in the serialization function. */
#include "csi_collector.h"
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
/**
* Helper: read a value from the fuzz data, advancing the cursor.
* Returns 0 if insufficient data remains.
*/
static size_t fuzz_read(const uint8_t **data, size_t *size,
void *out, size_t n)
{
if (*size < n) {
memset(out, 0, n);
return 0;
}
memcpy(out, *data, n);
*data += n;
*size -= n;
return n;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
if (size < 8) {
return 0; /* Need at least a few control bytes. */
}
const uint8_t *cursor = data;
size_t remaining = size;
/* Parse control bytes from fuzz input. */
uint8_t test_case;
int16_t iq_len_raw;
int8_t rssi;
uint8_t channel;
int8_t noise_floor;
uint8_t out_buf_scale; /* Controls output buffer size: 0-255. */
fuzz_read(&cursor, &remaining, &test_case, 1);
fuzz_read(&cursor, &remaining, &iq_len_raw, 2);
fuzz_read(&cursor, &remaining, &rssi, 1);
fuzz_read(&cursor, &remaining, &channel, 1);
fuzz_read(&cursor, &remaining, &noise_floor, 1);
fuzz_read(&cursor, &remaining, &out_buf_scale, 1);
/* --- Test case 0: Normal operation with fuzz-controlled values --- */
wifi_csi_info_t info;
memset(&info, 0, sizeof(info));
info.rx_ctrl.rssi = rssi;
info.rx_ctrl.channel = channel & 0x0F; /* 4-bit field */
info.rx_ctrl.noise_floor = noise_floor;
/* Use remaining fuzz data as I/Q buffer content. */
uint16_t iq_len;
if (iq_len_raw < 0) {
iq_len = 0;
} else if (iq_len_raw > (int16_t)remaining) {
iq_len = (uint16_t)remaining;
} else {
iq_len = (uint16_t)iq_len_raw;
}
int8_t iq_buf[CSI_MAX_FRAME_SIZE];
if (iq_len > 0 && remaining > 0) {
uint16_t copy = (iq_len > remaining) ? (uint16_t)remaining : iq_len;
memcpy(iq_buf, cursor, copy);
/* Zero-fill the rest if iq_len > available data. */
if (copy < iq_len) {
memset(iq_buf + copy, 0, iq_len - copy);
}
info.buf = iq_buf;
} else {
info.buf = iq_buf;
memset(iq_buf, 0, sizeof(iq_buf));
}
info.len = (int16_t)iq_len;
/* Output buffer: scale from tiny (1 byte) to full size. */
uint8_t out_buf[CSI_MAX_FRAME_SIZE + 64];
size_t out_len;
if (out_buf_scale == 0) {
out_len = 0;
} else if (out_buf_scale < 20) {
/* Small buffer: test buffer-too-small path. */
out_len = (size_t)out_buf_scale;
} else {
/* Normal/large buffer. */
out_len = sizeof(out_buf);
}
/* Call the function under test. Must not crash. */
size_t result = csi_serialize_frame(&info, out_buf, out_len);
/* Basic sanity: result must be 0 (error) or <= out_len. */
if (result > out_len) {
__builtin_trap(); /* Buffer overflow detected. */
}
/* --- Test case 1: NULL info pointer --- */
if (test_case & 0x01) {
result = csi_serialize_frame(NULL, out_buf, sizeof(out_buf));
if (result != 0) {
__builtin_trap(); /* NULL info should return 0. */
}
}
/* --- Test case 2: NULL output buffer --- */
if (test_case & 0x02) {
result = csi_serialize_frame(&info, NULL, sizeof(out_buf));
if (result != 0) {
__builtin_trap(); /* NULL buf should return 0. */
}
}
/* --- Test case 3: NULL I/Q buffer in info --- */
if (test_case & 0x04) {
wifi_csi_info_t null_iq_info = info;
null_iq_info.buf = NULL;
result = csi_serialize_frame(&null_iq_info, out_buf, sizeof(out_buf));
if (result != 0) {
__builtin_trap(); /* NULL info->buf should return 0. */
}
}
/* --- Test case 4: Extreme channel values --- */
if (test_case & 0x08) {
wifi_csi_info_t extreme_info = info;
extreme_info.buf = iq_buf;
/* Channel 0 (invalid). */
extreme_info.rx_ctrl.channel = 0;
csi_serialize_frame(&extreme_info, out_buf, sizeof(out_buf));
/* Channel 15 (max 4-bit value, invalid for WiFi). */
extreme_info.rx_ctrl.channel = 15;
csi_serialize_frame(&extreme_info, out_buf, sizeof(out_buf));
}
/* --- Test case 5: Extreme RSSI values --- */
if (test_case & 0x10) {
wifi_csi_info_t rssi_info = info;
rssi_info.buf = iq_buf;
rssi_info.rx_ctrl.rssi = -128;
csi_serialize_frame(&rssi_info, out_buf, sizeof(out_buf));
rssi_info.rx_ctrl.rssi = 127;
csi_serialize_frame(&rssi_info, out_buf, sizeof(out_buf));
}
/* --- Test case 6: Zero-length I/Q --- */
if (test_case & 0x20) {
wifi_csi_info_t zero_info = info;
zero_info.buf = iq_buf;
zero_info.len = 0;
result = csi_serialize_frame(&zero_info, out_buf, sizeof(out_buf));
/* len=0 means frame_size = CSI_HEADER_SIZE + 0 = 20 bytes. */
if (result != 0 && result != CSI_HEADER_SIZE) {
/* Either 0 (rejected) or exactly the header size is acceptable. */
}
}
/* --- Test case 7: Output buffer exactly header size --- */
if (test_case & 0x40) {
wifi_csi_info_t hdr_info = info;
hdr_info.buf = iq_buf;
hdr_info.len = 4; /* Small I/Q. */
/* Buffer exactly header_size + iq_len = 24 bytes. */
uint8_t tight_buf[CSI_HEADER_SIZE + 4];
result = csi_serialize_frame(&hdr_info, tight_buf, sizeof(tight_buf));
if (result > sizeof(tight_buf)) {
__builtin_trap();
}
}
return 0;
}
@@ -0,0 +1,217 @@
/**
* @file fuzz_edge_enqueue.c
* @brief libFuzzer target for edge_enqueue_csi() (ADR-061 Layer 6).
*
* Rapid-fire enqueues with varying iq_len from 0 to beyond
* EDGE_MAX_IQ_BYTES, testing the SPSC ring buffer overflow behavior
* and verifying no out-of-bounds writes occur.
*
* Build (Linux/macOS with clang):
* make fuzz_edge
*
* Run:
* ./fuzz_edge corpus/ -max_len=4096
*/
#include "esp_stubs.h"
/*
* We cannot include edge_processing.c directly because it references
* FreeRTOS task creation and other ESP-IDF APIs in edge_processing_init().
* Instead, we re-implement the SPSC ring buffer and edge_enqueue_csi()
* logic identically to the production code, testing the same algorithm.
*/
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
/* ---- Reproduce the ring buffer from edge_processing.h ---- */
#define EDGE_RING_SLOTS 16
#define EDGE_MAX_IQ_BYTES 1024
#define EDGE_MAX_SUBCARRIERS 128
typedef struct {
uint8_t iq_data[EDGE_MAX_IQ_BYTES];
uint16_t iq_len;
int8_t rssi;
uint8_t channel;
uint32_t timestamp_us;
} fuzz_ring_slot_t;
typedef struct {
fuzz_ring_slot_t slots[EDGE_RING_SLOTS];
volatile uint32_t head;
volatile uint32_t tail;
} fuzz_ring_buf_t;
static fuzz_ring_buf_t s_ring;
/**
* ring_push: identical logic to edge_processing.c::ring_push().
* This is the code path exercised by edge_enqueue_csi().
*/
static bool ring_push(const uint8_t *iq, uint16_t len,
int8_t rssi, uint8_t channel)
{
uint32_t next = (s_ring.head + 1) % EDGE_RING_SLOTS;
if (next == s_ring.tail) {
return false; /* Full. */
}
fuzz_ring_slot_t *slot = &s_ring.slots[s_ring.head];
uint16_t copy_len = (len > EDGE_MAX_IQ_BYTES) ? EDGE_MAX_IQ_BYTES : len;
memcpy(slot->iq_data, iq, copy_len);
slot->iq_len = copy_len;
slot->rssi = rssi;
slot->channel = channel;
slot->timestamp_us = (uint32_t)(esp_timer_get_time() & 0xFFFFFFFF);
__sync_synchronize();
s_ring.head = next;
return true;
}
/**
* ring_pop: identical logic to edge_processing.c::ring_pop().
*/
static bool ring_pop(fuzz_ring_slot_t *out)
{
if (s_ring.tail == s_ring.head) {
return false;
}
memcpy(out, &s_ring.slots[s_ring.tail], sizeof(fuzz_ring_slot_t));
__sync_synchronize();
s_ring.tail = (s_ring.tail + 1) % EDGE_RING_SLOTS;
return true;
}
/**
* Canary pattern: write to a buffer zone after ring memory to detect
* out-of-bounds writes. If the canary is overwritten, we trap.
*/
#define CANARY_SIZE 64
#define CANARY_BYTE 0xCD
static uint8_t s_canary_before[CANARY_SIZE];
/* s_ring is between the canaries (static allocation order not guaranteed,
* but ASAN will catch OOB writes regardless). */
static uint8_t s_canary_after[CANARY_SIZE];
static void init_canaries(void)
{
memset(s_canary_before, CANARY_BYTE, CANARY_SIZE);
memset(s_canary_after, CANARY_BYTE, CANARY_SIZE);
}
static void check_canaries(void)
{
for (int i = 0; i < CANARY_SIZE; i++) {
if (s_canary_before[i] != CANARY_BYTE) __builtin_trap();
if (s_canary_after[i] != CANARY_BYTE) __builtin_trap();
}
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
if (size < 4) return 0;
/* Reset ring buffer state for each fuzz iteration. */
memset(&s_ring, 0, sizeof(s_ring));
init_canaries();
const uint8_t *cursor = data;
size_t remaining = size;
/*
* Protocol: each "enqueue command" is:
* [0..1] iq_len (LE u16)
* [2] rssi (i8)
* [3] channel (u8)
* [4..] iq_data (up to iq_len bytes, zero-padded if short)
*
* We consume commands until data is exhausted.
*/
uint32_t enqueue_count = 0;
uint32_t full_count = 0;
uint32_t pop_count = 0;
while (remaining >= 4) {
uint16_t iq_len = (uint16_t)cursor[0] | ((uint16_t)cursor[1] << 8);
int8_t rssi = (int8_t)cursor[2];
uint8_t channel = cursor[3];
cursor += 4;
remaining -= 4;
/* Prepare I/Q data buffer.
* Even if iq_len > EDGE_MAX_IQ_BYTES, we pass it to ring_push
* which must clamp it internally. We need a source buffer that
* is at least iq_len bytes to avoid reading OOB. */
uint8_t iq_buf[EDGE_MAX_IQ_BYTES + 128];
memset(iq_buf, 0, sizeof(iq_buf));
/* Copy available fuzz data into iq_buf. */
uint16_t avail = (remaining > sizeof(iq_buf))
? (uint16_t)sizeof(iq_buf)
: (uint16_t)remaining;
if (avail > 0) {
memcpy(iq_buf, cursor, avail);
}
/* Advance cursor past the I/Q data portion.
* We consume min(iq_len, remaining) bytes. */
uint16_t consume = (iq_len > remaining) ? (uint16_t)remaining : iq_len;
cursor += consume;
remaining -= consume;
/* The key test: iq_len can be 0, normal, EDGE_MAX_IQ_BYTES,
* or larger (up to 65535). ring_push must clamp to EDGE_MAX_IQ_BYTES. */
bool ok = ring_push(iq_buf, iq_len, rssi, channel);
if (ok) {
enqueue_count++;
} else {
full_count++;
/* When ring is full, drain one slot to make room.
* This tests the interleaved push/pop pattern. */
fuzz_ring_slot_t popped;
if (ring_pop(&popped)) {
pop_count++;
/* Verify popped data is sane. */
if (popped.iq_len > EDGE_MAX_IQ_BYTES) {
__builtin_trap(); /* Clamping failed. */
}
}
/* Retry the enqueue after popping. */
ring_push(iq_buf, iq_len, rssi, channel);
}
/* Periodically check canaries. */
if ((enqueue_count + full_count) % 8 == 0) {
check_canaries();
}
}
/* Drain remaining items and verify each. */
fuzz_ring_slot_t popped;
while (ring_pop(&popped)) {
pop_count++;
if (popped.iq_len > EDGE_MAX_IQ_BYTES) {
__builtin_trap();
}
}
/* Final canary check. */
check_canaries();
/* Verify ring is now empty. */
if (s_ring.head != s_ring.tail) {
__builtin_trap();
}
return 0;
}
@@ -0,0 +1,286 @@
/**
* @file fuzz_nvs_config.c
* @brief libFuzzer target for NVS config validation logic (ADR-061 Layer 6).
*
* Since we cannot easily mock the full ESP-IDF NVS API under libFuzzer,
* this target extracts and tests the validation ranges used by
* nvs_config_load() when processing NVS values. Each validation check
* from nvs_config.c is reproduced here with fuzz-driven inputs.
*
* Build (Linux/macOS with clang):
* clang -fsanitize=fuzzer,address -g -I stubs fuzz_nvs_config.c \
* stubs/esp_stubs.c -o fuzz_nvs_config -lm
*
* Run:
* ./fuzz_nvs_config corpus/ -max_len=256
*/
#include "esp_stubs.h"
#include "nvs_config.h"
#include <stdint.h>
#include <stddef.h>
#include <string.h>
/**
* Validate a hop_count value using the same logic as nvs_config_load().
* Returns the validated value (0 = rejected).
*/
static uint8_t validate_hop_count(uint8_t val)
{
if (val >= 1 && val <= NVS_CFG_HOP_MAX) return val;
return 0;
}
/**
* Validate dwell_ms using the same logic as nvs_config_load().
* Returns the validated value (0 = rejected).
*/
static uint32_t validate_dwell_ms(uint32_t val)
{
if (val >= 10) return val;
return 0;
}
/**
* Validate TDM node count.
*/
static uint8_t validate_tdm_node_count(uint8_t val)
{
if (val >= 1) return val;
return 0;
}
/**
* Validate edge_tier (0-2).
*/
static uint8_t validate_edge_tier(uint8_t val)
{
if (val <= 2) return val;
return 0xFF; /* Invalid. */
}
/**
* Validate vital_window (32-256).
*/
static uint16_t validate_vital_window(uint16_t val)
{
if (val >= 32 && val <= 256) return val;
return 0;
}
/**
* Validate vital_interval_ms (>= 100).
*/
static uint16_t validate_vital_interval(uint16_t val)
{
if (val >= 100) return val;
return 0;
}
/**
* Validate top_k_count (1-32).
*/
static uint8_t validate_top_k(uint8_t val)
{
if (val >= 1 && val <= 32) return val;
return 0;
}
/**
* Validate power_duty (10-100).
*/
static uint8_t validate_power_duty(uint8_t val)
{
if (val >= 10 && val <= 100) return val;
return 0;
}
/**
* Validate wasm_max_modules (1-8).
*/
static uint8_t validate_wasm_max(uint8_t val)
{
if (val >= 1 && val <= 8) return val;
return 0;
}
/**
* Validate CSI channel: 1-14 (2.4 GHz) or 36-177 (5 GHz).
*/
static uint8_t validate_csi_channel(uint8_t val)
{
if ((val >= 1 && val <= 14) || (val >= 36 && val <= 177)) return val;
return 0;
}
/**
* Validate tdm_slot_index < tdm_node_count (clamp to 0 on violation).
*/
static uint8_t validate_tdm_slot(uint8_t slot, uint8_t node_count)
{
if (slot >= node_count) return 0;
return slot;
}
/**
* Test string field handling: ensure NVS_CFG_SSID_MAX length is respected.
*/
static void test_string_bounds(const uint8_t *data, size_t len)
{
char ssid[NVS_CFG_SSID_MAX];
char password[NVS_CFG_PASS_MAX];
char ip[NVS_CFG_IP_MAX];
/* Simulate strncpy with NVS_CFG_*_MAX bounds. */
size_t ssid_len = (len > NVS_CFG_SSID_MAX - 1) ? NVS_CFG_SSID_MAX - 1 : len;
memcpy(ssid, data, ssid_len);
ssid[ssid_len] = '\0';
size_t pass_len = (len > NVS_CFG_PASS_MAX - 1) ? NVS_CFG_PASS_MAX - 1 : len;
memcpy(password, data, pass_len);
password[pass_len] = '\0';
size_t ip_len = (len > NVS_CFG_IP_MAX - 1) ? NVS_CFG_IP_MAX - 1 : len;
memcpy(ip, data, ip_len);
ip[ip_len] = '\0';
/* Ensure null termination holds. */
if (ssid[NVS_CFG_SSID_MAX - 1] != '\0' && ssid_len == NVS_CFG_SSID_MAX - 1) {
/* OK: we set terminator above. */
}
}
/**
* Test presence_thresh and fall_thresh fixed-point conversion.
* nvs_config.c stores as u16 with value * 1000.
*/
static void test_thresh_conversion(uint16_t pres_raw, uint16_t fall_raw)
{
float pres = (float)pres_raw / 1000.0f;
float fall = (float)fall_raw / 1000.0f;
/* Ensure no NaN or Inf from valid integer inputs. */
if (pres != pres) __builtin_trap(); /* NaN check. */
if (fall != fall) __builtin_trap(); /* NaN check. */
/* Range: 0.0 to 65.535 for u16/1000. Both should be finite. */
if (pres < 0.0f || pres > 65.536f) __builtin_trap();
if (fall < 0.0f || fall > 65.536f) __builtin_trap();
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
if (size < 32) return 0;
const uint8_t *p = data;
/* Extract fuzz-driven config field values. */
uint8_t hop_count = p[0];
uint32_t dwell_ms = (uint32_t)p[1] | ((uint32_t)p[2] << 8)
| ((uint32_t)p[3] << 16) | ((uint32_t)p[4] << 24);
uint8_t tdm_slot = p[5];
uint8_t tdm_nodes = p[6];
uint8_t edge_tier = p[7];
uint16_t vital_win = (uint16_t)p[8] | ((uint16_t)p[9] << 8);
uint16_t vital_int = (uint16_t)p[10] | ((uint16_t)p[11] << 8);
uint8_t top_k = p[12];
uint8_t power_duty = p[13];
uint8_t wasm_max = p[14];
uint8_t csi_channel = p[15];
uint16_t pres_thresh = (uint16_t)p[16] | ((uint16_t)p[17] << 8);
uint16_t fall_thresh = (uint16_t)p[18] | ((uint16_t)p[19] << 8);
uint8_t node_id = p[20];
uint16_t target_port = (uint16_t)p[21] | ((uint16_t)p[22] << 8);
uint8_t wasm_verify = p[23];
/* Run all validators. These must not crash regardless of input. */
(void)validate_hop_count(hop_count);
(void)validate_dwell_ms(dwell_ms);
(void)validate_tdm_node_count(tdm_nodes);
(void)validate_edge_tier(edge_tier);
(void)validate_vital_window(vital_win);
(void)validate_vital_interval(vital_int);
(void)validate_top_k(top_k);
(void)validate_power_duty(power_duty);
(void)validate_wasm_max(wasm_max);
(void)validate_csi_channel(csi_channel);
/* Validate TDM slot with validated node count. */
uint8_t valid_nodes = validate_tdm_node_count(tdm_nodes);
if (valid_nodes > 0) {
(void)validate_tdm_slot(tdm_slot, valid_nodes);
}
/* Test threshold conversions. */
test_thresh_conversion(pres_thresh, fall_thresh);
/* Test string field bounds with remaining data. */
if (size > 24) {
test_string_bounds(data + 24, size - 24);
}
/* Construct a full nvs_config_t and verify field assignments don't overflow. */
nvs_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.target_port = target_port;
cfg.node_id = node_id;
uint8_t valid_hop = validate_hop_count(hop_count);
cfg.channel_hop_count = valid_hop ? valid_hop : 1;
/* Fill channel list from fuzz data. */
for (uint8_t i = 0; i < NVS_CFG_HOP_MAX && (24 + i) < size; i++) {
cfg.channel_list[i] = data[24 + i];
}
cfg.dwell_ms = validate_dwell_ms(dwell_ms) ? dwell_ms : 50;
cfg.tdm_slot_index = 0;
cfg.tdm_node_count = valid_nodes ? valid_nodes : 1;
if (cfg.tdm_slot_index >= cfg.tdm_node_count) {
cfg.tdm_slot_index = 0;
}
uint8_t valid_tier = validate_edge_tier(edge_tier);
cfg.edge_tier = (valid_tier != 0xFF) ? valid_tier : 2;
cfg.presence_thresh = (float)pres_thresh / 1000.0f;
cfg.fall_thresh = (float)fall_thresh / 1000.0f;
uint16_t valid_win = validate_vital_window(vital_win);
cfg.vital_window = valid_win ? valid_win : 256;
uint16_t valid_int = validate_vital_interval(vital_int);
cfg.vital_interval_ms = valid_int ? valid_int : 1000;
uint8_t valid_topk = validate_top_k(top_k);
cfg.top_k_count = valid_topk ? valid_topk : 8;
uint8_t valid_duty = validate_power_duty(power_duty);
cfg.power_duty = valid_duty ? valid_duty : 100;
uint8_t valid_wasm = validate_wasm_max(wasm_max);
cfg.wasm_max_modules = valid_wasm ? valid_wasm : 4;
cfg.wasm_verify = wasm_verify ? 1 : 0;
uint8_t valid_ch = validate_csi_channel(csi_channel);
cfg.csi_channel = valid_ch;
/* MAC filter: use 6 bytes from fuzz data if available. */
if (size >= 32) {
memcpy(cfg.filter_mac, data + 24, 6);
cfg.filter_mac_set = (data[30] & 0x01) ? 1 : 0;
}
/* Verify struct is self-consistent — no field should be in an impossible state. */
if (cfg.channel_hop_count > NVS_CFG_HOP_MAX) __builtin_trap();
if (cfg.tdm_slot_index >= cfg.tdm_node_count) __builtin_trap();
if (cfg.edge_tier > 2) __builtin_trap();
if (cfg.wasm_max_modules > 8 || cfg.wasm_max_modules < 1) __builtin_trap();
if (cfg.top_k_count > 32 || cfg.top_k_count < 1) __builtin_trap();
if (cfg.power_duty > 100 || cfg.power_duty < 10) __builtin_trap();
return 0;
}
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_ERR_H_STUB
#define ESP_ERR_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_LOG_H_STUB
#define ESP_LOG_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,65 @@
/**
* @file esp_stubs.c
* @brief Implementation of ESP-IDF stubs for host-based fuzz testing.
*
* Must be compiled with: -Istubs -I../main
* so that ESP-IDF headers resolve to stubs/ and firmware headers
* resolve to ../main/.
*/
#include "esp_stubs.h"
#include "edge_processing.h"
#include "wasm_runtime.h"
#include <stdint.h>
/** Monotonically increasing microsecond counter for esp_timer_get_time(). */
static int64_t s_fake_time_us = 0;
int64_t esp_timer_get_time(void)
{
/* Advance by 50ms each call (~20 Hz CSI rate simulation). */
s_fake_time_us += 50000;
return s_fake_time_us;
}
/* ---- stream_sender stubs ---- */
int stream_sender_send(const uint8_t *data, size_t len)
{
(void)data;
return (int)len;
}
int stream_sender_init(void)
{
return 0;
}
int stream_sender_init_with(const char *ip, uint16_t port)
{
(void)ip; (void)port;
return 0;
}
void stream_sender_deinit(void)
{
}
/* ---- wasm_runtime stubs ---- */
void wasm_runtime_on_frame(const float *phases, const float *amplitudes,
const float *variances, uint16_t n_sc,
const edge_vitals_pkt_t *vitals)
{
(void)phases; (void)amplitudes; (void)variances;
(void)n_sc; (void)vitals;
}
esp_err_t wasm_runtime_init(void) { return ESP_OK; }
esp_err_t wasm_runtime_load(const uint8_t *d, uint32_t l, uint8_t *id) { (void)d; (void)l; (void)id; return ESP_OK; }
esp_err_t wasm_runtime_start(uint8_t id) { (void)id; return ESP_OK; }
esp_err_t wasm_runtime_stop(uint8_t id) { (void)id; return ESP_OK; }
esp_err_t wasm_runtime_unload(uint8_t id) { (void)id; return ESP_OK; }
void wasm_runtime_on_timer(void) {}
void wasm_runtime_get_info(wasm_module_info_t *info, uint8_t *count) { (void)info; if(count) *count = 0; }
esp_err_t wasm_runtime_set_manifest(uint8_t id, const char *n, uint32_t c, uint32_t m) { (void)id; (void)n; (void)c; (void)m; return ESP_OK; }
@@ -0,0 +1,169 @@
/**
* @file esp_stubs.h
* @brief Minimal ESP-IDF type stubs for host-based fuzz testing.
*
* Provides just enough type definitions and macros to compile
* csi_collector.c and edge_processing.c on a Linux/macOS host
* without the full ESP-IDF SDK.
*/
#ifndef ESP_STUBS_H
#define ESP_STUBS_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
/* ---- esp_err.h ---- */
typedef int esp_err_t;
#define ESP_OK 0
#define ESP_FAIL (-1)
#define ESP_ERR_NO_MEM 0x101
#define ESP_ERR_INVALID_ARG 0x102
/* ---- esp_log.h ---- */
#define ESP_LOGI(tag, fmt, ...) ((void)0)
#define ESP_LOGW(tag, fmt, ...) ((void)0)
#define ESP_LOGE(tag, fmt, ...) ((void)0)
#define ESP_LOGD(tag, fmt, ...) ((void)0)
#define ESP_ERROR_CHECK(x) ((void)(x))
/* ---- esp_timer.h ---- */
typedef void *esp_timer_handle_t;
/**
* Stub: returns a monotonically increasing microsecond counter.
* Declared here, defined in esp_stubs.c.
*/
int64_t esp_timer_get_time(void);
/* ---- esp_wifi_types.h ---- */
/** Minimal rx_ctrl fields needed by csi_serialize_frame. */
typedef struct {
signed rssi : 8;
unsigned channel : 4;
unsigned noise_floor : 8;
unsigned rx_ant : 2;
/* Padding to fill out the struct so it compiles. */
unsigned _pad : 10;
} wifi_pkt_rx_ctrl_t;
/** Minimal wifi_csi_info_t needed by csi_serialize_frame. */
typedef struct {
wifi_pkt_rx_ctrl_t rx_ctrl;
uint8_t mac[6];
int16_t len; /**< Length of the I/Q buffer in bytes. */
int8_t *buf; /**< Pointer to I/Q data. */
} wifi_csi_info_t;
/* ---- Kconfig defaults ---- */
#ifndef CONFIG_CSI_NODE_ID
#define CONFIG_CSI_NODE_ID 1
#endif
#ifndef CONFIG_CSI_WIFI_CHANNEL
#define CONFIG_CSI_WIFI_CHANNEL 6
#endif
#ifndef CONFIG_CSI_WIFI_SSID
#define CONFIG_CSI_WIFI_SSID "test_ssid"
#endif
#ifndef CONFIG_CSI_TARGET_IP
#define CONFIG_CSI_TARGET_IP "192.168.1.1"
#endif
#ifndef CONFIG_CSI_TARGET_PORT
#define CONFIG_CSI_TARGET_PORT 5500
#endif
/* Suppress the build-time guard in csi_collector.c */
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
#define CONFIG_ESP_WIFI_CSI_ENABLED 1
#endif
/* ---- sdkconfig.h stub ---- */
/* (empty — all needed CONFIG_ macros are above) */
/* ---- FreeRTOS stubs ---- */
#define pdMS_TO_TICKS(x) ((x))
#define pdPASS 1
typedef int BaseType_t;
static inline int xPortGetCoreID(void) { return 0; }
static inline void vTaskDelay(uint32_t ticks) { (void)ticks; }
static inline BaseType_t xTaskCreatePinnedToCore(
void (*fn)(void *), const char *name, uint32_t stack,
void *arg, int prio, void *handle, int core)
{
(void)fn; (void)name; (void)stack; (void)arg;
(void)prio; (void)handle; (void)core;
return pdPASS;
}
/* ---- WiFi API stubs (no-ops) ---- */
typedef int wifi_interface_t;
typedef int wifi_second_chan_t;
#define WIFI_IF_STA 0
#define WIFI_SECOND_CHAN_NONE 0
typedef struct {
unsigned filter_mask;
} wifi_promiscuous_filter_t;
typedef int wifi_promiscuous_pkt_type_t;
#define WIFI_PROMIS_FILTER_MASK_MGMT 1
#define WIFI_PROMIS_FILTER_MASK_DATA 2
typedef struct {
int lltf_en;
int htltf_en;
int stbc_htltf2_en;
int ltf_merge_en;
int channel_filter_en;
int manu_scale;
int shift;
} wifi_csi_config_t;
typedef struct {
uint8_t primary;
} wifi_ap_record_t;
static inline esp_err_t esp_wifi_set_promiscuous(bool en) { (void)en; return ESP_OK; }
static inline esp_err_t esp_wifi_set_promiscuous_rx_cb(void *cb) { (void)cb; return ESP_OK; }
static inline esp_err_t esp_wifi_set_promiscuous_filter(wifi_promiscuous_filter_t *f) { (void)f; return ESP_OK; }
static inline esp_err_t esp_wifi_set_csi_config(wifi_csi_config_t *c) { (void)c; return ESP_OK; }
static inline esp_err_t esp_wifi_set_csi_rx_cb(void *cb, void *ctx) { (void)cb; (void)ctx; return ESP_OK; }
static inline esp_err_t esp_wifi_set_csi(bool en) { (void)en; return ESP_OK; }
static inline esp_err_t esp_wifi_set_channel(uint8_t ch, wifi_second_chan_t sc) { (void)ch; (void)sc; return ESP_OK; }
static inline esp_err_t esp_wifi_80211_tx(wifi_interface_t ifx, const void *b, int len, bool en) { (void)ifx; (void)b; (void)len; (void)en; return ESP_OK; }
static inline esp_err_t esp_wifi_sta_get_ap_info(wifi_ap_record_t *ap) { (void)ap; return ESP_FAIL; }
static inline const char *esp_err_to_name(esp_err_t code) { (void)code; return "STUB"; }
/* ---- NVS stubs ---- */
typedef uint32_t nvs_handle_t;
#define NVS_READONLY 0
static inline esp_err_t nvs_open(const char *ns, int mode, nvs_handle_t *h) { (void)ns; (void)mode; (void)h; return ESP_FAIL; }
static inline void nvs_close(nvs_handle_t h) { (void)h; }
static inline esp_err_t nvs_get_str(nvs_handle_t h, const char *k, char *v, size_t *l) { (void)h; (void)k; (void)v; (void)l; return ESP_FAIL; }
static inline esp_err_t nvs_get_u8(nvs_handle_t h, const char *k, uint8_t *v) { (void)h; (void)k; (void)v; return ESP_FAIL; }
static inline esp_err_t nvs_get_u16(nvs_handle_t h, const char *k, uint16_t *v) { (void)h; (void)k; (void)v; return ESP_FAIL; }
static inline esp_err_t nvs_get_u32(nvs_handle_t h, const char *k, uint32_t *v) { (void)h; (void)k; (void)v; return ESP_FAIL; }
static inline esp_err_t nvs_get_blob(nvs_handle_t h, const char *k, void *v, size_t *l) { (void)h; (void)k; (void)v; (void)l; return ESP_FAIL; }
/* ---- stream_sender stubs (defined in esp_stubs.c) ---- */
int stream_sender_send(const uint8_t *data, size_t len);
int stream_sender_init(void);
int stream_sender_init_with(const char *ip, uint16_t port);
void stream_sender_deinit(void);
/*
* wasm_runtime stubs: defined in esp_stubs.c.
* The actual prototype comes from ../main/wasm_runtime.h (via csi_collector.c).
* We just need the definition in esp_stubs.c to link.
*/
#endif /* ESP_STUBS_H */
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_TIMER_H_STUB
#define ESP_TIMER_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_WIFI_H_STUB
#define ESP_WIFI_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_WIFI_TYPES_H_STUB
#define ESP_WIFI_TYPES_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef FREERTOS_H_STUB
#define FREERTOS_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef FREERTOS_TASK_H_STUB
#define FREERTOS_TASK_H_STUB
#include "esp_stubs.h"
#endif
+5
View File
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef NVS_H_STUB
#define NVS_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,5 @@
/* Stub: redirect to unified stubs header. */
#ifndef NVS_FLASH_H_STUB
#define NVS_FLASH_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,5 @@
/* Stub: sdkconfig.h — all CONFIG_ macros provided by esp_stubs.h. */
#ifndef SDKCONFIG_H_STUB
#define SDKCONFIG_H_STUB
#include "esp_stubs.h"
#endif