mirror of
https://github.com/ruvnet/RuView
synced 2026-07-17 16:33:18 +00:00
fix(qemu): resolve 23 bugs from deep code review
CRITICAL: - inject_fault.py: make nvs_corrupt write actual bytes via --flash arg; heap_exhaust and corrupt_frame now pause VM with honest WARNING about GDB stub requirement for real memory writes - firmware-qemu.yml: remove github.run_id from cache key (was causing 100% cache miss rate, rebuilding QEMU every run) - mock_csi.c: change scenario_elapsed_ms() to int64_t (uint32 wrapped at ~49 days) HIGH: - qemu-mesh-test.sh: pass --results flag to validate_mesh_test.py (was passing positional arg to named-only parameter) - test/Makefile: separate corpus directories per fuzz target (corpus_serialize/, corpus_edge/, corpus_nvs/) - qemu-snapshot-test.sh: replace log truncation with tail-based extraction (truncation created sparse file while QEMU held fd) MEDIUM: - mock_csi.c: reset s_mac_filter_initialized in mock_csi_init() - mock_csi.c: fix LFSR polynomial comment (32,31,29,1 not 32,22,2,1) - sdkconfig.coverage: add FreeRTOS timer stack 4096 and WDT tuning - firmware-qemu.yml: replace continue-on-error with FUZZER_CRASH env - qemu-chaos-test.sh: rename heap_pressure to heap_exhaust for consistency - validate_qemu_output.py: fix docstring "14 checks" -> "16 checks" - generate_nvs_matrix.py: deduplicate temp file cleanup paths LOW: - mock_csi.c: remove M_PI float suffix, fix overflow burst flag - qemu-snapshot-test.sh: fix now_ms() for macOS date +%s%N - ADR-061: fix scenario 8 RSSI range to -90...-10 dBm - launch.json: remove contradictory compound debug config Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
+61
-59
@@ -18,6 +18,8 @@ Usage:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
@@ -123,31 +125,24 @@ def fault_ring_flood(s: socket.socket) -> None:
|
||||
print(f"[ring_flood] Injected: {sent}/1000 rapid NMI triggers")
|
||||
|
||||
|
||||
def fault_heap_exhaust(s: socket.socket) -> None:
|
||||
"""Write to heap tracking metadata to simulate memory pressure.
|
||||
def fault_heap_exhaust(s: socket.socket, flash_path: str = None) -> None:
|
||||
"""Simulate memory pressure by pausing VM to trigger watchdog/heap checks.
|
||||
|
||||
ESP32-S3 DRAM starts at 0x3FC88000. We write a pattern to the
|
||||
heap control block area to simulate low-memory conditions. The
|
||||
firmware's heap_caps checks should detect the anomaly.
|
||||
Actual heap memory writes require a GDB stub (-gdb tcp::1234).
|
||||
This function probes the heap region and pauses the VM to stress
|
||||
heap management as a realistic simulation.
|
||||
"""
|
||||
# ESP32-S3 internal DRAM heap region
|
||||
heap_base = 0x3FC88000
|
||||
# Write a pattern that looks like an exhausted free-list
|
||||
# (all zeros in the next-free pointer)
|
||||
print(f"[heap_exhaust] Writing to heap metadata at 0x{heap_base:08X}...")
|
||||
# Use QEMU monitor 'memsave' and 'pmemsave' aren't writable;
|
||||
# use 'xp' to read and 'poke' (if available) or GDB memory write
|
||||
# Fallback: use the monitor 'x' command to at least probe the region
|
||||
print("[heap_exhaust] Probing heap region...")
|
||||
resp = send_cmd(s, f"xp /4xw 0x{heap_base:08x}")
|
||||
print(f"[heap_exhaust] Current heap header: {resp.strip()}")
|
||||
|
||||
# Attempt to write garbage via 'write' monitor command (QEMU 8.x+)
|
||||
# Format: write <addr> <size> <data>
|
||||
garbage = "DEADBEEF" * 4 # 16 bytes of garbage
|
||||
resp = send_cmd(s, f"pmemsave 0x{heap_base:08x} 16 /dev/null")
|
||||
# Try direct memory write if supported
|
||||
resp = send_cmd(s, f"x /1xw 0x{heap_base:08x}")
|
||||
print(f"[heap_exhaust] Injected: heap metadata perturbation at 0x{heap_base:08X}")
|
||||
print(f"[heap_exhaust] Heap header: {resp.strip()}")
|
||||
# Pause VM to stress memory management
|
||||
print("[heap_exhaust] Pausing VM for 3s to stress heap management...")
|
||||
send_cmd(s, "stop")
|
||||
time.sleep(3.0)
|
||||
send_cmd(s, "cont")
|
||||
print("[heap_exhaust] WARNING: Actual heap corruption requires GDB stub (-gdb tcp::1234)")
|
||||
print("[heap_exhaust] Injected: 3s VM pause (simulates memory pressure)")
|
||||
|
||||
|
||||
def fault_timer_starvation(s: socket.socket) -> None:
|
||||
@@ -159,51 +154,47 @@ def fault_timer_starvation(s: socket.socket) -> None:
|
||||
print("[timer_starvation] Injected: 500ms execution pause")
|
||||
|
||||
|
||||
def fault_corrupt_frame(s: socket.socket) -> None:
|
||||
"""Write bad magic bytes to CSI frame buffer area.
|
||||
def fault_corrupt_frame(s: socket.socket, flash_path: str = None) -> None:
|
||||
"""Simulate CSI frame corruption by pausing VM during frame processing.
|
||||
|
||||
Mock CSI frames use a magic prefix (0xCSIF or similar). We write
|
||||
an invalid magic to the frame staging buffer so the parser
|
||||
encounters corruption on the next read.
|
||||
Actual memory writes to the frame buffer require a GDB stub
|
||||
(-gdb tcp::1234). This function probes the frame buffer region
|
||||
and pauses the VM mid-frame to simulate corruption effects.
|
||||
"""
|
||||
# Mock CSI buffer is typically in .bss — use a known SRAM region
|
||||
# ESP32-S3 SRAM1: 0x3FC88000 - 0x3FCF0000
|
||||
# Pick an offset likely to hit the frame staging area
|
||||
frame_buf_addr = 0x3FCA0000
|
||||
print(f"[corrupt_frame] Writing bad magic to 0x{frame_buf_addr:08X}...")
|
||||
|
||||
# Write 0xDEADCAFE where the frame magic should be 0x43534946 ("CSIF")
|
||||
# QEMU monitor: attempt memory write
|
||||
print(f"[corrupt_frame] Probing frame buffer at 0x{frame_buf_addr:08X}...")
|
||||
resp = send_cmd(s, f"xp /4xb 0x{frame_buf_addr:08x}")
|
||||
print(f"[corrupt_frame] Before: {resp.strip()}")
|
||||
|
||||
# Use GDB-style memory write if available, otherwise log the attempt
|
||||
# The actual write depends on QEMU version and GDB stub availability
|
||||
resp = send_cmd(s, f"x /1xw 0x{frame_buf_addr:08x}")
|
||||
print(f"[corrupt_frame] Injected: bad magic bytes at 0x{frame_buf_addr:08X}")
|
||||
print(f"[corrupt_frame] Frame buffer: {resp.strip()}")
|
||||
# Pause VM briefly to disrupt frame processing timing
|
||||
print("[corrupt_frame] Pausing VM for 1s to disrupt frame processing...")
|
||||
send_cmd(s, "stop")
|
||||
time.sleep(1.0)
|
||||
send_cmd(s, "cont")
|
||||
print("[corrupt_frame] WARNING: Actual frame corruption requires GDB stub (-gdb tcp::1234)")
|
||||
print(f"[corrupt_frame] Injected: 1s VM pause during frame processing")
|
||||
|
||||
|
||||
def fault_nvs_corrupt(s: socket.socket) -> None:
|
||||
"""Write garbage to the NVS flash region.
|
||||
def fault_nvs_corrupt(s: socket.socket, flash_path: str = None) -> None:
|
||||
"""Write garbage to the NVS flash region on disk.
|
||||
|
||||
NVS partition is at flash offset 0x9000. Under QEMU, the flash is
|
||||
memory-mapped. We write garbage to the NVS page header to trigger
|
||||
NVS corruption detection on next read.
|
||||
When a flash image path is provided, writes random bytes directly
|
||||
to the NVS partition offset (0x9000) in the flash image file.
|
||||
Without a flash path, falls back to a read-only probe via monitor.
|
||||
"""
|
||||
# ESP32-S3 flash is mapped at 0x3C000000 (instruction) / 0x3D000000 (data)
|
||||
# NVS at flash offset 0x9000 maps to 0x3C009000 in QEMU memory
|
||||
nvs_flash_addr = 0x3C009000
|
||||
print(f"[nvs_corrupt] Writing garbage to NVS region 0x{nvs_flash_addr:08X}...")
|
||||
|
||||
# Read current NVS header
|
||||
resp = send_cmd(s, f"xp /8xb 0x{nvs_flash_addr:08x}")
|
||||
print(f"[nvs_corrupt] NVS header before: {resp.strip()}")
|
||||
|
||||
# Attempt to corrupt the NVS page header (first 32 bytes)
|
||||
# NVS page magic is 0xFE (active) or 0xFC (full)
|
||||
# Writing 0x00 makes it appear as an uninitialized page
|
||||
resp = send_cmd(s, f"x /1xw 0x{nvs_flash_addr:08x}")
|
||||
print(f"[nvs_corrupt] Injected: NVS region corruption at 0x{nvs_flash_addr:08X}")
|
||||
if flash_path and os.path.isfile(flash_path):
|
||||
nvs_offset = 0x9000
|
||||
garbage = bytes(random.randint(0, 255) for _ in range(16))
|
||||
with open(flash_path, "r+b") as f:
|
||||
f.seek(nvs_offset)
|
||||
f.write(garbage)
|
||||
print(f"[nvs_corrupt] Wrote 16 garbage bytes at flash offset 0x{nvs_offset:X}")
|
||||
print(f"[nvs_corrupt] Flash image: {flash_path}")
|
||||
else:
|
||||
# Fallback: attempt via monitor (read-only probe)
|
||||
resp = send_cmd(s, f"xp /8xb 0x3C009000")
|
||||
print(f"[nvs_corrupt] NVS region (read-only probe): {resp.strip()}")
|
||||
print(f"[nvs_corrupt] WARNING: No --flash path provided; NVS corruption was NOT injected")
|
||||
print(f"[nvs_corrupt] Pass --flash /path/to/flash.bin for actual corruption")
|
||||
|
||||
|
||||
# Map fault names to injection functions
|
||||
@@ -235,6 +226,10 @@ def main():
|
||||
"--timeout", type=float, default=CMD_TIMEOUT,
|
||||
help=f"Per-command timeout in seconds (default: {CMD_TIMEOUT})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--flash", default=None,
|
||||
help="Path to flash image (for nvs_corrupt direct file writes)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"[inject_fault] Connecting to {args.socket}...")
|
||||
@@ -242,7 +237,14 @@ def main():
|
||||
|
||||
print(f"[inject_fault] Injecting fault: {args.fault}")
|
||||
try:
|
||||
FAULT_MAP[args.fault](s)
|
||||
fault_fn = FAULT_MAP[args.fault]
|
||||
# Pass flash_path to faults that accept it
|
||||
import inspect
|
||||
sig = inspect.signature(fault_fn)
|
||||
if "flash_path" in sig.parameters:
|
||||
fault_fn(s, flash_path=args.flash)
|
||||
else:
|
||||
fault_fn(s)
|
||||
except Exception as e:
|
||||
print(f"ERROR: Fault injection failed: {e}", file=sys.stderr)
|
||||
s.close()
|
||||
|
||||
Reference in New Issue
Block a user