DragonFlyBSD Kernel Audit
DF-0876 / harness.c
← back to finding ↓ download raw
/*
 * DF-0876 — ext2_gd_csum OOB heap read: deterministic harness.
 *
 * Transcribes ext2_gd_csum (sys/vfs/ext2fs/ext2_csum.c:666-704) verbatim,
 * in particular the METADATA_CKSUM branch at ext2_csum.c:683-687:
 *
 *     offset = offsetof(struct ext2_gd, ext4bgd_csum);            // 30
 *     ...
 *     offset += sizeof(dummy_csum);                               // 32
 *     if (offset < le16toh(fs->e2fs->e3fs_desc_size))
 *         csum32 = calculate_crc32c(csum32, (uint8_t *)gd + offset,
 *             le16toh(fs->e2fs->e3fs_desc_size) - offset);
 *
 * With an attacker-controlled on-disk e3fs_desc_size (s_desc_size) of 0xFFFF
 * and the 64-bit INCOMPAT feature cleared, the in-kernel read becomes:
 *
 *     calculate_crc32c(csum32, gd + 32, 0xFFFF - 32)
 *         -> reads 65503 bytes past the 64-byte struct ext2_gd.
 *
 * Because ext2 only allocates sizeof(struct ext2_gd)=64 bytes per in-memory
 * group descriptor (vfsops.c:647, multiplied up only to whole-blocks), the
 * 65503-byte read walks far beyond the e2fs_gd array and into adjacent
 * kernel heap (or unmapped pages, page-faulting on INVARIANTS-ON GENERIC).
 *
 * This harness mirrors that read against a poison-padded buffer so the OOB
 * length is observable deterministically without depending on kernel
 * allocation layout.  It proves:
 *   - the read length when desc_size=0xFFFF is exactly 65503 bytes,
 *   - the read goes well past the 64-byte struct ext2_gd,
 *   - on a "production" mapping (where pages past the GD are mapped), this
 *     leaks whatever bytes happen to be adjacent in memory.
 *
 * Build: cc -O2 -o harness harness.c
 * Run  : ./harness
 */
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/mman.h>
#include <sys/endian.h>

/* Verbatim copy of struct ext2_gd from sys/vfs/ext2fs/ext2fs.h */
struct ext2_gd {
	uint32_t ext2bgd_b_bitmap;
	uint32_t ext2bgd_i_bitmap;
	uint32_t ext2bgd_i_tables;
	uint16_t ext2bgd_nbfree;
	uint16_t ext2bgd_nifree;
	uint16_t ext2bgd_ndirs;
	uint16_t ext4bgd_flags;
	uint32_t ext4bgd_x_bitmap;
	uint16_t ext4bgd_b_bmap_csum;
	uint16_t ext4bgd_i_bmap_csum;
	uint16_t ext4bgd_i_unused;
	uint16_t ext4bgd_csum;
	uint32_t ext4bgd_b_bitmap_hi;
	uint32_t ext4bgd_i_bitmap_hi;
	uint32_t ext4bgd_i_tables_hi;
	uint16_t ext4bgd_nbfree_hi;
	uint16_t ext4bgd_nifree_hi;
	uint16_t ext4bgd_ndirs_hi;
	uint16_t ext4bgd_i_unused_hi;
	uint32_t ext4bgd_x_bitmap_hi;
	uint16_t ext4bgd_b_bmap_csum_hi;
	uint16_t ext4bgd_i_bmap_csum_hi;
	uint32_t ext4bgd_reserved;
};

/* ---- CRC32C (Castagnoli), non-inverting, identical to libkern/icrc32.c ---- */
static uint32_t crc32c_table[256];
static void crc32c_init(void) {
	uint32_t i, j, crc;
	const uint32_t POLY = 0x82F63B78; /* reflected */
	for (i = 0; i < 256; i++) {
		crc = i;
		for (j = 0; j < 8; j++)
			crc = (crc >> 1) ^ (POLY & (-(int32_t)(crc & 1)));
		crc32c_table[i] = crc;
	}
}
static uint32_t calculate_crc32c(uint32_t crc, const uint8_t *buf, size_t len) {
	while (len--)
		crc = (crc >> 8) ^ crc32c_table[(crc ^ *buf++) & 0xff];
	return crc;
}

#include <signal.h>
static volatile sig_atomic_t g_faulted = 0;
static void *g_fault_addr = NULL;
static void sig_segv(int sig, siginfo_t *si, void *uc) {
	(void)sig; (void)uc;
	g_fault_addr = si->si_addr;
	g_faulted = 1;
	/* Cannot safely keep going; _exit now. */
	/* Print to stderr from a signal handler is not strictly portable but
	 * works on BSD/Linux for short messages via write(). */
	const char msg[] = "\n[!] SIGSEGV caught during csum read at addr ";
	char buf[160];
	size_t n = 0;
	memcpy(buf+n, msg, sizeof(msg)-1); n += sizeof(msg)-1;
	/* crude hex of fault addr */
	unsigned long addr = (unsigned long)(uintptr_t)si->si_addr;
	const char hex[] = "0123456789abcdef";
	buf[n++] = '0'; buf[n++] = 'x';
	for (int shift = 60; shift >= 0; shift -= 4)
		buf[n++] = hex[(addr >> shift) & 0xf];
	buf[n++] = '\n';
	write(2, buf, n);
	_exit(133);
}

/*
 * Verbatim transcription of ext2_gd_csum (ext2_csum.c:666-704), the
 * METADATA_CKSUM branch only.  The two args that matter for the bug are
 * `csum_seed` and `desc_size_le` (the attacker-controlled on-disk u16).
 *
 * Two entry points:
 *   - ext2_gd_csum_harness:   computes the csum length WITHOUT touching OOB
 *                             memory; just reports the requested length.
 *   - ext2_gd_csum_faulting:  performs the full read (and will SIGSEGV if
 *                             the read crosses into PROT_NONE pages).
 */
static uint16_t
ext2_gd_csum_harness(uint32_t csum_seed, uint16_t desc_size_le,
    uint32_t block_group, const struct ext2_gd *gd,
    uint8_t *oob_first, size_t *oob_len, uint16_t *csum_out)
{
	size_t offset;
	uint32_t csum32;
	uint16_t crc, dummy_csum = 0;

	offset = offsetof(struct ext2_gd, ext4bgd_csum);   /* 30 */

	block_group = htole32(block_group);
	csum32 = calculate_crc32c(csum_seed,
	    (const uint8_t *)&block_group, sizeof(block_group));
	csum32 = calculate_crc32c(csum32, (const uint8_t *)gd, offset);
	csum32 = calculate_crc32c(csum32, (const uint8_t *)&dummy_csum,
	    sizeof(dummy_csum));
	offset += sizeof(dummy_csum);                       /* 32 */

	/* THE BUG: ext2_csum.c:684-686 -- computes the read LENGTH only. */
	if (offset < desc_size_le) {
		*oob_first = *((const uint8_t *)gd + offset);
		*oob_len = desc_size_le - offset;
		/* For the harness we sum only up to end-of-struct so we don't
		 * SIGSEGV.  The faulting variant below does the full read. */
		size_t safe = *oob_len;
		if (offset + safe > sizeof(struct ext2_gd))
			safe = sizeof(struct ext2_gd) - offset;
		csum32 = calculate_crc32c(csum32,
		    (const uint8_t *)gd + offset, safe);
	} else {
		*oob_len = 0;
	}

	crc = csum32 & 0xFFFF;
	if (csum_out) *csum_out = crc;
	return crc;
}

/* Faulting variant: does the full ext2_csum.c:684-686 read.
 * Will SIGSEGV when the read crosses a PROT_NONE page (just like the kernel
 * page-faults when it walks past the e2fs_gd slab allocation). */
static uint32_t
ext2_gd_csum_faulting(uint32_t csum_seed, uint16_t desc_size_le,
    uint32_t block_group, const struct ext2_gd *gd)
{
	size_t offset;
	volatile uint32_t csum32;
	uint16_t dummy_csum = 0;

	offset = offsetof(struct ext2_gd, ext4bgd_csum);   /* 30 */
	block_group = htole32(block_group);
	csum32 = calculate_crc32c(csum_seed,
	    (const uint8_t *)&block_group, sizeof(block_group));
	csum32 = calculate_crc32c(csum32, (const uint8_t *)gd, offset);
	csum32 = calculate_crc32c(csum32, (const uint8_t *)&dummy_csum,
	    sizeof(dummy_csum));
	offset += sizeof(dummy_csum);                       /* 32 */

	if (offset < desc_size_le) {
		/* THE BUG: ext2_csum.c:684-686 -- full read, will page-fault. */
		csum32 = calculate_crc32c(csum32,
		    (const uint8_t *)gd + offset, desc_size_le - offset);
	}
	return csum32;
}

int main(void)
{
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);

	/* Install SIGSEGV handler so the OOB read in case C becomes a clean
	 * diagnostic instead of an uncaught crash. */
	struct sigaction sa;
	memset(&sa, 0, sizeof(sa));
	sa.sa_sigaction = sig_segv;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGSEGV, &sa, NULL);

	crc32c_init();

	const size_t GD_SIZE = sizeof(struct ext2_gd);     /* 64 */
	printf("[*] sizeof(struct ext2_gd)        = %zu bytes\n", GD_SIZE);
	printf("[*] offsetof(ext4bgd_csum)        = %zu bytes\n",
	    offsetof(struct ext2_gd, ext4bgd_csum));

	/* Allocate a 64-byte struct followed by a poison page (PROT_NONE).
	 * simulate the slab layout: the GD object is bounded; everything past
	 * it is either adjacent slab metadata or unmapped. */
	const size_t PAGE = 4096;
	uint8_t *base = mmap(NULL, 3 * PAGE, PROT_READ | PROT_WRITE,
	    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (base == MAP_FAILED) { perror("mmap"); return 1; }
	/* guard the page after our small window */
	if (mprotect(base + PAGE, PAGE, PROT_NONE) != 0) {
		perror("mprotect"); return 1;
	}
	/* place the GD object at the end of the first page so that
	 * gd + 64 is exactly the PROT_NONE boundary. */
	uint8_t *gd_addr = base + PAGE - GD_SIZE;
	struct ext2_gd *gd = (struct ext2_gd *)gd_addr;
	memset(gd, 0, GD_SIZE);

	/* Fill the legitimate fields with a marker pattern. */
	gd->ext2bgd_b_bitmap = 0xDEADBEEF;
	gd->ext4bgd_csum     = 0x0000;

	/* -- CASE A: legitimate desc_size=0 (rev0) -- no OOB -- */
	uint8_t oob_first;
	size_t  oob_len;
	uint16_t csum_ok = 0;
	ext2_gd_csum_harness(0xCAFEF00D, 0 /* desc_size=0 */,
	    0, gd, &oob_first, &oob_len, &csum_ok);
	printf("\n[A] desc_size=0     (legitimate rev0 GD):\n");
	printf("    csum=0x%04x   OOB read length = %zu bytes (expected 0)\n",
	    csum_ok, oob_len);
	if (oob_len != 0) {
		printf("    !! BUG: OOB read even with desc_size=0\n");
		return 1;
	}

	/* -- CASE B: legitimate desc_size=64 (64bit GD) -- */
	csum_ok = 0;
	ext2_gd_csum_harness(0xCAFEF00D, 64,
	    0, gd, &oob_first, &oob_len, &csum_ok);
	printf("\n[B] desc_size=64    (legitimate 64bit GD):\n");
	printf("    csum=0x%04x   read length    = %zu bytes (32..64 within GD)\n",
	    csum_ok, oob_len);
	if (oob_len != 32) {
		printf("    !! Unexpected length %zu (expected 32)\n", oob_len);
		return 1;
	}

	/* -- CASE C: attacker-controlled desc_size=0xFFFF -- OOB -- */
	csum_ok = 0;
	ext2_gd_csum_harness(0xCAFEF00D, 0xFFFF,
	    0, gd, &oob_first, &oob_len, &csum_ok);
	printf("\n[C] desc_size=0xFFFF (attacker-controlled, METADATA_CKSUM only):\n");
	printf("    csum=0x%04x   read length    = %zu bytes\n",
	    csum_ok, oob_len);

	if (oob_len != 65503) {
		printf("    !! Unexpected OOB length %zu (expected 65503)\n", oob_len);
		return 1;
	}
	size_t struct_end = GD_SIZE;            /* 64 */
	size_t read_end    = offsetof(struct ext2_gd, ext4bgd_csum) + 2 + oob_len; /* 32 + 65503 = 65535 */
	printf("    OOB read: gd+%zu .. gd+%zu (length %zu)\n",
	    (size_t)32, read_end, oob_len);
	printf("    struct ext2_gd ends at gd+%zu\n", struct_end);
	printf("    -> read extends %zu bytes PAST the 64-byte struct ext2_gd.\n",
	    read_end - struct_end);

	/* Demonstrate the page-boundary fault that the kernel will take.
	 * GD object sits at the end of page 1 (last 64 bytes); page 2 is
	 * PROT_NONE.  The faulting csum read will SIGSEGV at the start of
	 * page 2 — exactly what the kernel will page-fault on when it reads
	 * past the e2fs_gd slab allocation.  The SIGSEGV handler prints the
	 * fault address and exits. */
	printf("\n[D] Invoking ext2_gd_csum_faulting (full ext2_csum.c:684-686 read):\n");
	printf("    GD at %p (end of page 1); next page PROT_NONE at %p\n",
	    (void *)gd, (void *)(base + PAGE));
	printf("    Expecting SIGSEGV at %p (= gd+%zu = start of PROT_NONE page).\n",
	    (void *)(base + PAGE), (size_t)((base + PAGE) - (uint8_t *)gd));
	fflush(stdout);
	volatile uint32_t got = ext2_gd_csum_faulting(0xCAFEF00D, 0xFFFF, 0, gd);
	(void)got;

	/* If we get here, no fault happened (would mean PROT_NONE didn't fire). */
	printf("\n[!] Harness did NOT fault -- PROT_NONE guard ineffective.\n");

	/* Final summary */
	printf("\n=== DF-0876 VERDICT ===\n");
	printf("ext2_csum.c:684-686 reads `e3fs_desc_size - offset` bytes from\n");
	printf("`gd + offset`.  With attacker-controlled e3fs_desc_size=0xFFFF and\n");
	printf("offset=32, the read length is 65503 bytes -- %d bytes past the\n",
	    65503 - (GD_SIZE - 32));
	printf("64-byte struct ext2_gd allocation.  In-kernel (where the next page\n");
	printf("may be mapped slab metadata), this is a heap over-read / info leak;\n");
	printf("on INVARIANTS-ON GENERIC it page-faults and panics.\n");

	munmap(base, 3 * PAGE);
	return 0;
}