F-4339: widen uart-flash-server bounds checks to 64-bit to stop uint32_t wrap

uart_flash_erase/read/write read attacker-controlled address and len as raw
4-byte words from the UART peer and guard the mmap region with
`address + len > FIRMWARE_PARTITION_SIZE + SWAP_SIZE`. The addition is done in
uint32_t, so a large len (e.g. 0xFFFFFFFF) wraps the sum below 0x21000 and the
guard passes, after which the loop walks far past the 0x21000-byte mapping —
SIGSEGV on a 64-bit host, or wrapped writes over the firmware header on 32-bit.

Compute address + len in uint64_t before the comparison so the sum cannot wrap.
Add a pty-driven regression test (test_overflow) and a `make test` target that
sends an ERASE with a wrapping address+len and asserts the server survives.
pull/792/head
Daniele Lacamera 2026-06-10 13:44:10 +02:00
parent 6a60ea3d53
commit 71545e96de
3 changed files with 121 additions and 4 deletions

View File

@ -16,6 +16,11 @@ $(EXE): $(EXE).o libwolfboot.o
libwolfboot.o: ../../src/libwolfboot.c
$(Q)$(CC) $(CFLAGS) -c -o $(@) $(^)
test_overflow: test_overflow.c
$(Q)$(CC) -Wall -g -o $@ $^ -lutil
test: $(EXE) test_overflow
$(Q)./test_overflow ./$(EXE)
clean:
$(Q)rm -f *.o $(EXE)
$(Q)rm -f *.o $(EXE) test_overflow

View File

@ -0,0 +1,112 @@
/* Regression test for the uint32_t wraparound bounds-check bypass in
* uart_flash_erase()/read()/write() (finding F-4339).
*
* Drives the real ufserver binary over a pseudo-terminal and sends an ERASE
* command with address=0x1000 and len=0xFFFFFFFF. With the buggy uint32_t
* guard, address+len wraps to 0xFFF (< FIRMWARE_PARTITION_SIZE+SWAP_SIZE), the
* guard is bypassed and the erase loop walks far past the 0x21000-byte mmap,
* crashing the server with SIGSEGV. With the 64-bit guard the command is
* rejected and the server stays alive.
*
* Usage: test_overflow ./ufserver
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <pty.h>
#include <sys/wait.h>
#define CMD_HDR_WOLF 'W'
#define CMD_HDR_ERASE 0x03
int main(int argc, char *argv[])
{
int master, slave;
char slavename[256];
pid_t pid;
char tmpl[] = "/tmp/ufserver_fw_XXXXXX";
int fd;
uint8_t buf[64];
int i;
if (argc != 2) {
fprintf(stderr, "usage: %s ./ufserver\n", argv[0]);
return 2;
}
/* Create a small (non-WOLF) firmware file; mmap_firmware() pads it to
* FIRMWARE_PARTITION_SIZE + SWAP_SIZE (0x21000). */
fd = mkstemp(tmpl);
if (fd < 0) { perror("mkstemp"); return 2; }
memset(buf, 0xA5, sizeof(buf));
if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) { perror("write"); return 2; }
close(fd);
if (openpty(&master, &slave, slavename, NULL, NULL) != 0) {
perror("openpty");
return 2;
}
signal(SIGPIPE, SIG_IGN);
pid = fork();
if (pid < 0) { perror("fork"); return 2; }
if (pid == 0) {
/* Child: run the server attached to the slave pty. */
close(master);
close(slave);
fd = open("/dev/null", O_WRONLY);
if (fd >= 0) { dup2(fd, STDOUT_FILENO); }
execl(argv[1], argv[1], tmpl, slavename, (char *)NULL);
perror("execl");
_exit(127);
}
/* Parent: drive the server. */
close(slave);
usleep(200000); /* let the child open and configure the port */
/* STX selecting a flash command, then the ERASE opcode. */
buf[0] = CMD_HDR_WOLF;
buf[1] = CMD_HDR_ERASE;
/* address = 0x00001000 (little-endian raw bytes) */
buf[2] = 0x00; buf[3] = 0x10; buf[4] = 0x00; buf[5] = 0x00;
/* len = 0xFFFFFFFF -> address+len wraps to 0xFFF in uint32_t */
buf[6] = 0xFF; buf[7] = 0xFF; buf[8] = 0xFF; buf[9] = 0xFF;
if (write(master, buf, 10) != 10) { perror("write master"); }
/* Drain ack bytes the server sends back so it never blocks. */
fcntl(master, F_SETFL, O_NONBLOCK);
/* Give the server time to process (and crash, if vulnerable). */
for (i = 0; i < 20; i++) {
uint8_t drain[64];
(void)read(master, drain, sizeof(drain));
usleep(50000);
int status;
pid_t r = waitpid(pid, &status, WNOHANG);
if (r == pid) {
unlink(tmpl);
if (WIFSIGNALED(status)) {
fprintf(stderr, "FAIL: server died from signal %d "
"(OOB access from wrapped bounds check)\n",
WTERMSIG(status));
return 1;
}
fprintf(stderr, "FAIL: server exited unexpectedly (status %d)\n",
status);
return 1;
}
}
/* Still alive after processing the malicious command: guard held. */
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
unlink(tmpl);
printf("PASS: server rejected wrapped address+len and stayed alive\n");
return 0;
}

View File

@ -280,7 +280,7 @@ static void uart_flash_erase(uint8_t *base, int ud)
return;
if (read_word(ud,&len) != 4)
return;
if (address + len > (FIRMWARE_PARTITION_SIZE + SWAP_SIZE))
if ((uint64_t)address + (uint64_t)len > (FIRMWARE_PARTITION_SIZE + SWAP_SIZE))
return;
if (address < FIRMWARE_PARTITION_SIZE) {
printmsg(msgEraseUpdate);
@ -319,7 +319,7 @@ static void uart_flash_read(uint8_t *base, int ud)
#if LOG_FLASH_ADDRESS
printf("Read @%x\n", address);
#endif
if (address + len > (FIRMWARE_PARTITION_SIZE + SWAP_SIZE))
if ((uint64_t)address + (uint64_t)len > (FIRMWARE_PARTITION_SIZE + SWAP_SIZE))
return;
for (i = 0; i < len; i++) {
write(ud, base + address + i, 1);
@ -344,7 +344,7 @@ static void uart_flash_write(uint8_t *base, int ud)
#if LOG_FLASH_ADDRESS
printf("Write @%x\n", address);
#endif
if (address + len > (FIRMWARE_PARTITION_SIZE + SWAP_SIZE))
if ((uint64_t)address + (uint64_t)len > (FIRMWARE_PARTITION_SIZE + SWAP_SIZE))
return;
for (i = 0; i < len; i++) {
read(ud, base + address + i, 1);