Pin PlatformIO packages and toolchains, rebase protected SDK overrides, and add WebSocket receive regression coverage. Document isolated candidate validation, archive provenance, and remaining gates.
216 lines
8.6 KiB
C
216 lines
8.6 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
#define _GNU_SOURCE
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
typedef int esp_err_t;
|
|
typedef void *httpd_handle_t;
|
|
typedef int httpd_ws_type_t;
|
|
#define ESP_OK 0
|
|
#define ESP_FAIL -1
|
|
#define ESP_ERR_INVALID_ARG -2
|
|
#define ESP_ERR_INVALID_STATE -3
|
|
#define ESP_ERR_INVALID_SIZE -4
|
|
#define HTTPD_SOCK_ERR_TIMEOUT -3
|
|
#define HTTPD_WS_FIN_BIT 0x80
|
|
#define HTTPD_WS_MASK_BIT 0x80
|
|
#define HTTPD_WS_LENGTH_BITS 0x7f
|
|
#define HTTPD_WS_OPCODE_BITS 0x0f
|
|
#define HTTPD_WS_TYPE_CLOSE 8
|
|
#define HTTPD_WS_TYPE_PING 9
|
|
#define HTTPD_WS_TYPE_PONG 10
|
|
#define MIN(a,b) ((a) < (b) ? (a) : (b))
|
|
#define ESP_LOGW(...) ((void)0)
|
|
#define ESP_LOGD(...) ((void)0)
|
|
/* SDK_OPTIONS */
|
|
struct sock_db {
|
|
httpd_handle_t handle;
|
|
int fd;
|
|
bool ws_handshake_done, ws_control_frames;
|
|
unsigned char pending_data[32];
|
|
size_t pending_len;
|
|
int (*recv_fn)(httpd_handle_t, int, char *, size_t, int);
|
|
};
|
|
struct httpd_req_aux {
|
|
struct sock_db *sd;
|
|
bool ws_final;
|
|
httpd_ws_type_t ws_type;
|
|
unsigned char before[8], mask_key[4], after[8];
|
|
};
|
|
typedef struct { void *aux; httpd_handle_t handle; } httpd_req_t;
|
|
typedef struct {
|
|
bool final, fragmented;
|
|
httpd_ws_type_t type;
|
|
unsigned char *payload;
|
|
size_t len;
|
|
} httpd_ws_frame_t;
|
|
static unsigned char *wire;
|
|
static size_t position, limit, chunk;
|
|
static int terminal_result, terminal_calls, sends, sent_type;
|
|
static size_t sent_len;
|
|
static unsigned char sent_payload[128];
|
|
static int receive(httpd_handle_t h, int fd, char *buf, size_t len, int flags)
|
|
{
|
|
(void)h; (void)fd; (void)flags;
|
|
assert(len > 0);
|
|
if (position == limit) {
|
|
/* Any continuation past failed framing is an observable failure. */
|
|
assert(terminal_calls++ == 0);
|
|
return terminal_result;
|
|
}
|
|
size_t n = MIN(len, MIN(chunk, limit - position));
|
|
memcpy(buf, wire + position, n);
|
|
position += n;
|
|
return (int)n;
|
|
}
|
|
static int httpd_req_to_sockfd(httpd_req_t *req) { (void)req; return 7; }
|
|
static esp_err_t httpd_ws_send_frame_async(httpd_handle_t h, int fd, httpd_ws_frame_t *f)
|
|
{
|
|
(void)h; assert(fd == 7); assert(f->len <= sizeof(sent_payload));
|
|
sends++; sent_type = f->type; sent_len = f->len;
|
|
if (f->len) memcpy(sent_payload, f->payload, f->len);
|
|
return ESP_OK;
|
|
}
|
|
/* SDK_FUNCTIONS */
|
|
|
|
static struct sock_db sd;
|
|
static struct httpd_req_aux aux;
|
|
static httpd_req_t req;
|
|
static unsigned char packet[32];
|
|
static size_t packet_len, header_len;
|
|
static const unsigned char plain[] = {0, 0xff, 0x80};
|
|
static void setup(int encoding, int opcode, size_t available, int result, size_t split, size_t pending)
|
|
{
|
|
memset(&sd, 0, sizeof(sd)); memset(&aux, 0, sizeof(aux));
|
|
memset(aux.before, 0xa5, sizeof(aux.before));
|
|
memset(aux.mask_key, 0xcc, sizeof(aux.mask_key));
|
|
memset(aux.after, 0x5a, sizeof(aux.after));
|
|
sd.ws_handshake_done = true; sd.recv_fn = receive; aux.sd = &sd;
|
|
aux.ws_type = 2; req.aux = &aux;
|
|
size_t n = 0;
|
|
packet[n++] = 0x80 | opcode;
|
|
packet[n++] = 0x80 | (encoding ? (encoding == 2 ? 126 : 127) : sizeof(plain));
|
|
for (int i = 0; i < encoding; i++) packet[n++] = i == encoding - 1 ? sizeof(plain) : 0;
|
|
for (int i = 0; i < 4; i++) packet[n++] = (unsigned char)(0x10 + i);
|
|
header_len = n;
|
|
for (size_t i = 0; i < sizeof(plain); i++) packet[n++] = plain[i] ^ (0x10 + i);
|
|
packet_len = n;
|
|
limit = MIN(available, n); position = pending; chunk = split;
|
|
assert(pending <= limit && pending <= sizeof(sd.pending_data));
|
|
memcpy(sd.pending_data + sizeof(sd.pending_data) - pending, packet, pending);
|
|
sd.pending_len = pending;
|
|
/* Input ends exactly at an inaccessible page, including partial headers. */
|
|
long page = sysconf(_SC_PAGESIZE);
|
|
wire = mmap(NULL, (size_t)page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
assert(wire != MAP_FAILED);
|
|
assert(mprotect(wire + page, (size_t)page, PROT_NONE) == 0);
|
|
wire += page - limit;
|
|
memcpy(wire, packet, limit);
|
|
terminal_result = result; terminal_calls = sends = 0;
|
|
}
|
|
static void finish(void)
|
|
{
|
|
for (size_t i = 0; i < 8; i++) assert(aux.before[i] == 0xa5 && aux.after[i] == 0x5a);
|
|
long page = sysconf(_SC_PAGESIZE);
|
|
assert(munmap(wire + limit - page, (size_t)page * 2) == 0);
|
|
}
|
|
static unsigned checks;
|
|
static void failure(int encoding, int stage, size_t partial, int error, bool pending, int opcode)
|
|
{
|
|
/* stage: first byte, second byte, extended length, mask. */
|
|
size_t start = stage == 0 ? 0 : stage == 1 ? 1 : stage == 2 ? 2 : 2 + encoding;
|
|
setup(encoding, opcode, start + partial, error, 1, pending ? start + partial : 0);
|
|
unsigned char output[5]; memset(output, 0xab, sizeof(output));
|
|
httpd_ws_frame_t frame = {.payload = output + 1};
|
|
esp_err_t ret = httpd_ws_get_frame_type(&req);
|
|
if (stage == 0) {
|
|
assert(ret == ESP_OK && aux.ws_final && aux.ws_type == HTTPD_WS_TYPE_CLOSE);
|
|
assert(frame.len == 0);
|
|
} else if (opcode == 2) {
|
|
assert(ret == ESP_OK && aux.ws_type == 2 && aux.ws_final);
|
|
assert(httpd_ws_recv_frame(&req, &frame, 3) == ESP_FAIL);
|
|
assert(frame.type == 2 && frame.final);
|
|
assert(frame.len == (stage == 3 ? 3u : 0u));
|
|
} else {
|
|
assert(ret == ESP_ERR_INVALID_STATE);
|
|
assert(aux.ws_type == opcode && aux.ws_final);
|
|
}
|
|
assert(position == limit && terminal_calls == 1 && sends == 0);
|
|
assert(sd.pending_len == 0 && sd.ws_handshake_done && !sd.ws_control_frames);
|
|
assert(req.aux == &aux && aux.sd == &sd);
|
|
for (size_t i = 0; i < sizeof(output); i++) assert(output[i] == 0xab);
|
|
for (size_t i = 0; i < sizeof(aux.mask_key); i++)
|
|
assert(aux.mask_key[i] == (stage == 3 && i < partial ? 0x10 + i : 0xcc));
|
|
finish(); checks++;
|
|
}
|
|
static void valid(int encoding, size_t split, size_t pending, int opcode, bool probe)
|
|
{
|
|
setup(encoding, opcode, 32, 0, split, pending);
|
|
unsigned char output[5]; memset(output, 0xab, sizeof(output));
|
|
httpd_ws_frame_t frame = {.payload = output + 1};
|
|
assert(httpd_ws_get_frame_type(&req) == ESP_OK);
|
|
if (opcode == 2) {
|
|
if (probe) {
|
|
assert(httpd_ws_recv_frame(&req, &frame, 0) == ESP_OK);
|
|
assert(frame.len == 3 && position == header_len);
|
|
assert(output[1] == 0xab);
|
|
}
|
|
assert(httpd_ws_recv_frame(&req, &frame, 3) == ESP_OK);
|
|
assert(frame.len == 3 && frame.type == 2 && frame.final);
|
|
assert(memcmp(output + 1, plain, 3) == 0 && sends == 0);
|
|
} else {
|
|
assert(sends == 1 && sent_type == (opcode == 9 ? 10 : 8));
|
|
assert(sent_len == (opcode == 9 ? 3u : 0u));
|
|
if (opcode == 9) assert(memcmp(sent_payload, plain, 3) == 0);
|
|
}
|
|
assert(output[0] == 0xab && output[4] == 0xab);
|
|
assert(position == packet_len && sd.pending_len == 0 && terminal_calls == 0);
|
|
finish(); checks++;
|
|
}
|
|
static void extended_length(int encoding, uint64_t length)
|
|
{
|
|
setup(encoding, 2, (size_t)(6 + encoding), 0, 1, 0);
|
|
for (int i = 0; i < encoding; i++)
|
|
wire[2 + i] = (unsigned char)(length >> (8 * (encoding - i - 1)));
|
|
httpd_ws_frame_t frame = {0};
|
|
assert(httpd_ws_get_frame_type(&req) == ESP_OK);
|
|
assert(httpd_ws_recv_frame(&req, &frame, 0) == ESP_OK);
|
|
assert(frame.len == length && frame.type == 2 && frame.final);
|
|
assert(position == header_len && sends == 0 && terminal_calls == 0);
|
|
finish(); checks++;
|
|
}
|
|
int main(void)
|
|
{
|
|
const int errors[] = {-1, HTTPD_SOCK_ERR_TIMEOUT, 0};
|
|
const int encodings[] = {0, 2, 8};
|
|
const int opcodes[] = {2, 9, 8};
|
|
for (size_t e = 0; e < 3; e++) {
|
|
int encoding = encodings[e];
|
|
for (size_t o = 0; o < 3; o++) {
|
|
for (int stage = 0; stage < 4; stage++) {
|
|
if (stage == 2 && encoding == 0) continue;
|
|
size_t extent = stage < 2 ? 1 : stage == 2 ? (size_t)encoding : 4;
|
|
for (size_t part = 0; part < extent; part++)
|
|
for (size_t err = 0; err < 3; err++)
|
|
for (int pending = 0; pending < 2; pending++)
|
|
failure(encoding, stage, part, errors[err], pending, opcodes[o]);
|
|
}
|
|
for (size_t split = 1; split <= 16; split *= 2)
|
|
for (size_t pending = 0; pending <= (size_t)(6 + encoding); pending++)
|
|
valid(encoding, split, pending, opcodes[o], false);
|
|
valid(encoding, 1, 0, opcodes[o], true);
|
|
}
|
|
}
|
|
extended_length(2, 126);
|
|
extended_length(2, 65535);
|
|
extended_length(8, 65536);
|
|
extended_length(8, UINT64_C(0x0102030405060708));
|
|
printf("WS actual full receive/blocking/pending functions: %u guard-page/canary cases PASS\n", checks);
|
|
return 0;
|
|
}
|