DF-3057 / harness.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | /* * 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; } |