โฌข DragonFlyBSD Kernel Audit
DF-3057 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-3057 โ€” dirfs_write discards the bread() error and the bwrite() result,
 * so a failed block read is still modified and persisted, and write() reports
 * success:
 *
 *   sys/vfs/dirfs/dirfs_vnops.c:740-755
 *      740  error = bread(vp, base_offset, BSIZE, &bp);
 *      741  error = uiomovebp(bp, (char *)bp->b_data + offset, len, uio);
 *                         ^^^^^^ the bread() error at :740 is OVERWRITTEN
 *      742  if (error) { brelse(bp); break; }
 *      752  if (ap->a_ioflag & IO_SYNC)
 *      753          bwrite(bp);          <-- return value ignored
 *      754  else
 *      755          bdwrite(bp);         <-- async, unaccounted
 *
 * bread()/breadnx() (sys/kern/vfs_bio.c:900-960) always sets *bpp (getblk)
 * but on a failed strategy the buffer contents are NOT filled: biowait()
 * returns the error and B_CACHE is not set for the data.  dirfs_strategy
 * (dirfs_vnops.c:817-845) marks the buffer B_ERROR and leaves b_data
 * untouched when the host pread() fails (e.g. host I/O error on the backing
 * file, or dn_fd closed/invalid).
 *
 * Consequence: dirfs_write uiomoves the user's bytes into the unfilled block
 * and then flushes the WHOLE 16KB block (dirfs_strategy -> pwrite on
 * dn_fd).  The bytes outside the user-modified span are whatever the
 * buffer previously held (getblk reuses buffers) โ€” i.e. STALE CONTENT OF
 * ANOTHER FILE โ€” and they are persisted into the target file: an
 * information disclosure into an attacker-readable file, plus silent data
 * corruption, plus write() returning success despite the failed read (and
 * despite a failed synchronous flush at :753).
 *
 * This harness transcribes the loop with a real 16KB block, a modelled
 * bread() that fails without filling the buffer (buffer pre-seeded with
 * "another file's" data to model buffer reuse), and a real file as the
 * write target.  It proves: (1) stale bytes are persisted and readable back
 * (leak), (2) write() reports success, (3) the FIXED variant (check bread
 * error; propagate bwrite error) leaks nothing and reports the error.
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>

#define BSIZE 16384

struct buf {
	char *b_data;
	int b_error;
	int filled;
};

/*
 * bread() model (vfs_bio.c:900-960): *bpp always set; on failed strategy the
 * buffer is NOT filled and biowait returns the error.
 */
static int
bread_model(struct buf *bpp)
{
	bpp->b_error = EIO;	/* host pread failed inside dirfs_strategy */
	bpp->filled = 0;
	return (bpp->b_error);
}

/* uiomovebp model: copies exactly len bytes of user data */
static int
uiomovebp_model(struct buf *bp, int offset, const char *udata, int len)
{
	memcpy(bp->b_data + offset, udata, len);
	return 0;
}

/* bdwrite/bwrite model: persists the whole block to the real file */
static int
flush_model(struct buf *bp, int fd, off_t base_offset, int sync)
{
	ssize_t n = pwrite(fd, bp->b_data, BSIZE, base_offset);
	(void)sync;
	return (n == BSIZE) ? 0 : EIO;
}

static int
readback_and_scan(int fd, const char *needle, off_t *where)
{
	char block[BSIZE];
	ssize_t n, i;
	size_t nlen = strlen(needle);

	n = pread(fd, block, BSIZE, 0);
	if (n < 0)
		return 0;
	for (i = 0; i + (ssize_t)nlen <= n; i++) {
		if (memcmp(block + i, needle, nlen) == 0) {
			*where = i;
			return 1;
		}
	}
	return 0;
}

int
main(void)
{
	struct buf bp;
	char userbytes[100];
	char victimdir[] = "/tmp/df3057XXXXXX";
	char targetpath[256], otherpath[256];
	int fd, error;
	off_t where = -1;

	setvbuf(stdout, NULL, _IONBF, 0);

	if (mkdtemp(victimdir) == NULL) { perror("mkdtemp"); return 1; }
	snprintf(targetpath, sizeof(targetpath), "%s/target", victimdir);
	snprintf(otherpath, sizeof(otherpath), "%s/otherfile", victimdir);

	/* model the getblk buffer previously holding ANOTHER file's block */
	bp.b_data = malloc(BSIZE);
	memset(bp.b_data, 0, BSIZE);
	memcpy(bp.b_data + 7000,
	       "STALE-SECRET-DATA-FROM-ANOTHER-DIRECTORY-ENTRY-BLOCK", 53);
	{
		int tfd = open(otherpath, O_RDWR | O_CREAT, 0600);
		if (tfd >= 0) { write(tfd, bp.b_data, BSIZE); close(tfd); }
	}

	fd = open(targetpath, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd < 0) { perror(targetpath); return 1; }

	memset(userbytes, 'U', sizeof(userbytes));

	/* ---- VULNERABLE transcription (dirfs_vnops.c:729-756) ---- */
	printf("== VULNERABLE dirfs_write transcription (vnops.c:740-755)\n");
	error = 0;
	{
		int offset = 0;			/* uio_offset & BMASK */
		int len = (int)sizeof(userbytes);

		error = bread_model(&bp);			/* :740 returns EIO */
		error = uiomovebp_model(&bp, offset, userbytes, len); /* :741 CLOBBER -> 0 */
		if (error) {						/* :742 not taken */
			printf("  (not reached)\n");
		}
		/* kflags |= NOTE_WRITE; ... */
		if (1 /* IO_SYNC */) {
			(void)flush_model(&bp, fd, 0, 1);	/* :753 bwrite ret ignored */
		}
	}
	printf("  bread failed with EIO but write() returns: %d "
	       "(SUCCESS โ€” error swallowed at :741)\n", error);

	if (readback_and_scan(fd, "STALE-SECRET-DATA", &where)) {
		printf("  read back target file: STALE DATA FROM ANOTHER FILE "
		       "present at offset %jd\n", (intmax_t)where);
		printf("  => INFO LEAK INTO ATTACKER-READABLE FILE CONFIRMED "
		       "(CWE-909: uninitialized/stale buffer persisted)\n");
	} else {
		printf("  read back target file: no stale data (unexpected)\n");
		return 1;
	}

	/* ---- FIXED transcription: check bread error, propagate bwrite ---- */
	printf("\n== FIXED dirfs_write transcription\n");
	ftruncate(fd, 0);
	memset(bp.b_data, 0, BSIZE);
	memcpy(bp.b_data + 7000, "STALE-SECRET-DATA-FROM-ANOTHER-DIRECTORY-ENTRY-BLOCK", 53);
	error = 0;
	{
		int offset = 0;
		int len = (int)sizeof(userbytes);
		int berr;

		berr = bread_model(&bp);			/* :740 */
		if (berr) {
			error = berr;				/* brelse(bp); break; */
		} else {
			error = uiomovebp_model(&bp, offset, userbytes, len);
			if (error == 0) {
				error = flush_model(&bp, fd, 0, 1);	/* :753 checked */
			}
		}
	}
	printf("  write() returns: %d (EIO=%d) โ€” failure reported to user, "
	       "nothing persisted\n", error, EIO);
	if (!readback_and_scan(fd, "STALE-SECRET-DATA", &where)) {
		printf("  read back target file: no stale data => NO LEAK, "
		       "FIX VALIDATED\n");
	} else {
		printf("  stale data still present (fix invalid)\n");
		return 1;
	}

	printf("\nRESULT: vulnerable loop persists stale buffer contents and "
	       "reports success; fixed loop reports EIO and persists nothing "
	       "=> BUG CONFIRMED, FIX VALIDATED\n");

	close(fd);
	unlink(targetpath); unlink(otherpath); rmdir(victimdir);
	return 2;
}