Signed packets. A GeoNetworking Basic Header NextHeader of 2 means a TS 103 097 (IEEE 1609.2) envelope follows, with the Common Header inside it. gn_unwrap_its rejected all of these, and most real traffic is signed: the 2026-08-17 capture holds 157 signed frames from 15 source MACs against 2 unsecured stations. It now opens a COER-encoded signedData, or a bare unsecuredData, and parses the inner packet as before. The inner packet comes first inside tbsData, so the certificate and signature are never parsed, and the signature is not verified - the firmware has no trust store. Such messages reach the phone with the new V2X_RX flags bit1, signed but not verified. The app reads only bit0 and is unaffected until it learns the flag. Encrypted payloads, nested signing and the legacy v1.2.1 envelope are still rejected. All 157 recorded signed frames have the layout this reads, in all three COER length forms, and asn1tools decodes every envelope to the same inner packet. Payload bounds. Every frame recorded through the ESP32-C5's promiscuous RX, about 15 000 of them, ends in 8 bytes that are not part of the 802.11 frame and not a valid FCS. obu-firmware reads frames through the same API and took the rest of the frame as the message, so it forwarded those 8 bytes to the phone after every message. UPER decoders stop where the message ends, so nothing visibly broke, but the bytes cost serial bandwidth and 8 bytes of the DENM's headroom, and they stayed attached wherever raw payloads were stored or passed on. The payload is now exactly what the Common Header's payload-length field declares, which is also what separates a signed message from its signature. A frame longer than main.c's 800-byte capture buffer is now reported as truncated instead of being forwarded cut off, and counted as an oversize drop through the new serial_link_note_oversize_drop, as it was when the cut-off frame failed serial_link's size check. Host tests in obu-firmware/test/host build the firmware sources unmodified with MSYS2 gcc; `make` runs all three. - test_chain: frames from the firmware's TX code checked byte by byte against EN 302 636-4-1 and parsed back, including hand-built signed frames, the payload-length rule, the RX trailer, and every truncation length against a no-access guard page. 1731 checks, 0 failures. - test_replay and check_replay.py: all 15 145 recorded frames through gn_unwrap_its, cut to 800 bytes as on the board, and re-derived independently in Python with the envelope decoded by asn1tools. They agree on every record; 15 131 accepted, 157 of them signed. 11 043 of the 11 106 distinct messages re-encode byte-identically. The other 63 fail the same way with the old 8 bytes put back, so the boundary is not the cause: 5 are our own CAMs from before the 2026-08-20 yawRateConfidence fix, and the rest, from other stations, are a follow-up in TODO.md. - fuzz_gn_unwrap: random edits of every recorded frame, each run against the guard page. 50 000 000 iterations, no crash. obu-firmware/test/pcap_gn_tally.py tallies GeoNetworking header fields per station over captures; it is how the other stations' lifetimes were measured. TODO.md collects what is still open, including the on-air check for this change: it builds on IDF 6.1 but has not been flashed.
250 lines
6.5 KiB
C
250 lines
6.5 KiB
C
#ifndef _WIN32
|
|
#define _DEFAULT_SOURCE // MAP_ANONYMOUS under -std=c11
|
|
#endif
|
|
|
|
#include "test_util.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <signal.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
// ---- Crash reporting ------------------------------------------------------------------------
|
|
|
|
static char s_context[200] = "startup";
|
|
static const uint8_t *s_crash_bytes;
|
|
static const int *s_crash_len;
|
|
|
|
void tu_set_context(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(s_context, sizeof s_context, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
const char *tu_context(void)
|
|
{
|
|
return s_context;
|
|
}
|
|
|
|
void tu_set_crash_input(const uint8_t *bytes, const int *len)
|
|
{
|
|
s_crash_bytes = bytes;
|
|
s_crash_len = len;
|
|
}
|
|
|
|
static void report_crash(const char *what)
|
|
{
|
|
fprintf(stderr, "CRASH (%s) during: %s\n", what, s_context);
|
|
if (s_crash_bytes && s_crash_len) {
|
|
fprintf(stderr, "input (%d bytes): ", *s_crash_len);
|
|
for (int i = 0; i < *s_crash_len; i++) {
|
|
fprintf(stderr, "%02x", s_crash_bytes[i]);
|
|
}
|
|
fputc('\n', stderr);
|
|
}
|
|
fflush(stderr);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static LONG WINAPI on_crash(EXCEPTION_POINTERS *info)
|
|
{
|
|
char what[40];
|
|
snprintf(what, sizeof what, "exception 0x%08lx",
|
|
(unsigned long)info->ExceptionRecord->ExceptionCode);
|
|
report_crash(what);
|
|
return EXCEPTION_EXECUTE_HANDLER; // terminate, with the exception code as the exit status
|
|
}
|
|
|
|
void tu_install_crash_handler(void)
|
|
{
|
|
SetUnhandledExceptionFilter(on_crash);
|
|
}
|
|
#else
|
|
static void on_crash(int sig)
|
|
{
|
|
char what[40];
|
|
snprintf(what, sizeof what, "signal %d", sig);
|
|
report_crash(what); // not async-signal-safe, but the process is ending anyway
|
|
_exit(2);
|
|
}
|
|
|
|
void tu_install_crash_handler(void)
|
|
{
|
|
signal(SIGSEGV, on_crash);
|
|
signal(SIGBUS, on_crash);
|
|
signal(SIGILL, on_crash); // -fsanitize-undefined-trap-on-error traps with an illegal instruction
|
|
}
|
|
#endif
|
|
|
|
// ---- Guard page -----------------------------------------------------------------------------
|
|
|
|
static uint8_t *s_guard_end; // first byte of the no-access page
|
|
static size_t s_page_size;
|
|
|
|
void tu_guard_init(void)
|
|
{
|
|
#ifdef _WIN32
|
|
SYSTEM_INFO si;
|
|
GetSystemInfo(&si);
|
|
s_page_size = si.dwPageSize;
|
|
uint8_t *base = VirtualAlloc(NULL, 2 * s_page_size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
|
DWORD old;
|
|
if (!base || !VirtualProtect(base + s_page_size, s_page_size, PAGE_NOACCESS, &old)) {
|
|
fprintf(stderr, "guard page setup failed\n");
|
|
exit(2);
|
|
}
|
|
#else
|
|
s_page_size = (size_t)sysconf(_SC_PAGESIZE);
|
|
uint8_t *base = mmap(NULL, 2 * s_page_size, PROT_READ | PROT_WRITE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
if (base == MAP_FAILED || mprotect(base + s_page_size, s_page_size, PROT_NONE) != 0) {
|
|
fprintf(stderr, "guard page setup failed\n");
|
|
exit(2);
|
|
}
|
|
#endif
|
|
s_guard_end = base + s_page_size;
|
|
}
|
|
|
|
uint8_t *tu_guard_buf(int len)
|
|
{
|
|
if (len < 0 || (size_t)len > s_page_size) {
|
|
fprintf(stderr, "tu_guard_buf(%d) exceeds one page\n", len);
|
|
exit(2);
|
|
}
|
|
return s_guard_end - len;
|
|
}
|
|
|
|
const uint8_t *tu_guarded(const uint8_t *frame, int len)
|
|
{
|
|
uint8_t *dst = tu_guard_buf(len);
|
|
if (len > 0) {
|
|
memcpy(dst, frame, (size_t)len);
|
|
}
|
|
return dst;
|
|
}
|
|
|
|
// ---- pcap -----------------------------------------------------------------------------------
|
|
|
|
static uint32_t rd_le32(const uint8_t *p)
|
|
{
|
|
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
|
}
|
|
|
|
static uint32_t rd_be32(const uint8_t *p)
|
|
{
|
|
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3];
|
|
}
|
|
|
|
int tu_pcap_foreach(const char *path, tu_frame_fn fn, void *ctx)
|
|
{
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) {
|
|
return -1;
|
|
}
|
|
fseek(f, 0, SEEK_END);
|
|
const long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
if (size < 24) {
|
|
fclose(f);
|
|
return size == 0 ? 0 : -1; // the recordings include empty files
|
|
}
|
|
uint8_t *d = malloc((size_t)size);
|
|
if (!d || fread(d, 1, (size_t)size, f) != (size_t)size) {
|
|
fclose(f);
|
|
free(d);
|
|
return -1;
|
|
}
|
|
fclose(f);
|
|
|
|
const uint32_t magic = rd_le32(d);
|
|
const bool swapped = magic == 0xD4C3B2A1u || magic == 0x4D3CB2A1u;
|
|
if (!swapped && magic != 0xA1B2C3D4u && magic != 0xA1B23C4Du) {
|
|
free(d);
|
|
return -1;
|
|
}
|
|
uint32_t (*u32)(const uint8_t *) = swapped ? rd_be32 : rd_le32;
|
|
const uint32_t linktype = u32(d + 20);
|
|
if (linktype != 127 && linktype != 105) {
|
|
free(d);
|
|
return -1;
|
|
}
|
|
|
|
long off = 24;
|
|
int index = 0;
|
|
while (off + 16 <= size) {
|
|
const uint32_t incl = u32(d + off + 8);
|
|
if (incl > (uint32_t)(size - off - 16)) {
|
|
break; // last record cut off
|
|
}
|
|
const uint8_t *pkt = d + off + 16;
|
|
int len = (int)incl;
|
|
off += 16 + (long)incl;
|
|
if (linktype == 127) {
|
|
const int rt_len = len >= 4 ? (pkt[2] | (pkt[3] << 8)) : len + 1; // always little-endian
|
|
if (rt_len > len) {
|
|
index++;
|
|
continue;
|
|
}
|
|
pkt += rt_len;
|
|
len -= rt_len;
|
|
}
|
|
fn(pkt, len, index++, ctx);
|
|
}
|
|
free(d);
|
|
return index;
|
|
}
|
|
|
|
static FILE *s_pcap_out;
|
|
|
|
static void put_le32(uint8_t *p, uint32_t v)
|
|
{
|
|
p[0] = (uint8_t)v;
|
|
p[1] = (uint8_t)(v >> 8);
|
|
p[2] = (uint8_t)(v >> 16);
|
|
p[3] = (uint8_t)(v >> 24);
|
|
}
|
|
|
|
bool tu_pcap_open(const char *path)
|
|
{
|
|
// Global header: magic, version 2.4, zone 0, sigfigs 0, snaplen 65535, linktype 105.
|
|
static const uint8_t hdr[24] = {
|
|
0xD4, 0xC3, 0xB2, 0xA1, 0x02, 0x00, 0x04, 0x00, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0xFF, 0xFF, 0x00, 0x00, 105, 0, 0, 0,
|
|
};
|
|
s_pcap_out = fopen(path, "wb");
|
|
return s_pcap_out && fwrite(hdr, 1, sizeof hdr, s_pcap_out) == sizeof hdr;
|
|
}
|
|
|
|
void tu_pcap_add(const uint8_t *frame, int len)
|
|
{
|
|
static uint32_t seq;
|
|
if (!s_pcap_out || len <= 0) {
|
|
return;
|
|
}
|
|
uint8_t rec[16];
|
|
put_le32(rec + 0, seq++); // timestamp seconds: just the frame's sequence number
|
|
put_le32(rec + 4, 0);
|
|
put_le32(rec + 8, (uint32_t)len);
|
|
put_le32(rec + 12, (uint32_t)len);
|
|
fwrite(rec, 1, sizeof rec, s_pcap_out);
|
|
fwrite(frame, 1, (size_t)len, s_pcap_out);
|
|
}
|
|
|
|
void tu_pcap_close(void)
|
|
{
|
|
if (s_pcap_out) {
|
|
fclose(s_pcap_out);
|
|
s_pcap_out = NULL;
|
|
}
|
|
}
|