DF-5829 / msync_poc.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 205 206 | /* * DF-5829 - vm_object_page_clean() partial-range silent no-op proof. * * The kernel marks pages PG_CLEANCHK for the cleaning pass only in * vm_object_page_clean_pass1(), and pass1 runs ONLY when the requested * range covers the whole object ("wholescan", i.e. start==0 and * end_pindex == object->size-1). pass2 and vm_object_page_collect_flush() * both skip any page without PG_CLEANCHK, so ANY partial-range * vm_object_page_clean() - e.g. msync(MS_SYNC) of a subrange, or of a * whole mapping that is smaller than the file - flushes NOTHING and * still returns success. * * Observable: vm.stats.vm.v_vnodeout / v_vnodepgsout ("vnode pager * pageouts") are incremented in vnode_pager_putpages() * (sys/vm/vnode_pager.c:764), which is reached ONLY from * vm_pageout_flush() <- vm_object_page_collect_flush() <- pass2. * A real MS_SYNC flush MUST bump them; the no-op leaves them flat. * * Arms (each in a child that is SIGKILLed before teardown so exit * cannot flush anything): * A partial : 8-page file, mmap 8, dirty page 6, msync [5p,7p) -> BUG: expect delta 0 * B wholefile : 8-page file, mmap 8, dirty page 6, msync [0p,8p) -> ctrl: expect delta >=1 * C wholemap : 8-page file, mmap 4, dirty page 1, msync [0p,4p) -> BUG: expect delta 0 * D tinyfile : 1-page file, mmap 1, dirty page 0, msync [0p,1p) -> ctrl: expect delta >=1 * * Build: cc -O2 -o msync_poc msync_poc.c * Run (unprivileged): ./msync_poc [reps] */ #include <sys/types.h> #include <sys/sysctl.h> #include <sys/mman.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <signal.h> #define PGSZ 4096L #define BASEDIR_D "/home/maxx/df5829" static char path8[256], path1[256]; static unsigned long ctr(const char *name) { unsigned int v = 0; size_t l = sizeof(v); if (sysctlbyname(name, &v, &l, NULL, 0) < 0) { perror(name); exit(1); } return (v); } static void makefile(const char *path, int npages) { int fd; fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror(path); exit(1); } if (ftruncate(fd, (off_t)npages * PGSZ)) { perror("ftruncate"); exit(1); } if (fsync(fd)) { perror("fsync"); exit(1); } close(fd); } /* * mmap map_pages of path, dirty exactly one page (dirty_idx), msync the * window [off_pages, off_pages+len_pages), print marker, then sleep so * the parent can sample counters while the mapping is still alive. */ static void arm_child(const char *path, int map_pages, int dirty_idx, long off_pages, long len_pages, int sleepsec) { int fd, rc; char *p; fd = open(path, O_RDWR); if (fd < 0) { perror("open"); _exit(9); } p = mmap(NULL, map_pages * PGSZ, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (p == MAP_FAILED) { perror("mmap"); _exit(9); } memset(p + (long)dirty_idx * PGSZ, 0xAB, PGSZ); rc = msync(p + off_pages * PGSZ, len_pages * PGSZ, MS_SYNC); printf(" msync pages [%ld,%ld) rc=%d errno=%d\n", off_pages, off_pages + len_pages, rc, errno); fflush(stdout); sleep(sleepsec); munmap(p, map_pages * PGSZ); close(fd); _exit(0); } static void run_arm(const char *label, const char *path, int map_pages, int dirty_idx, long off_pages, long len_pages, unsigned long *d_out, unsigned long *d_pgs) { unsigned long b1, b2, a1, a2; pid_t pid; sync(); sleep(2); /* quiesce syncer/bio noise */ b1 = ctr("vm.stats.vm.v_vnodeout"); b2 = ctr("vm.stats.vm.v_vnodepgsout"); pid = fork(); if (pid == 0) arm_child(path, map_pages, dirty_idx, off_pages, len_pages, 20); sleep(6); /* msync of <=2 pages is instant; child sleeps */ a1 = ctr("vm.stats.vm.v_vnodeout"); a2 = ctr("vm.stats.vm.v_vnodepgsout"); printf("%-34s vnodeout delta = %lu vnodepgsout delta = %lu\n", label, a1 - b1, a2 - b2); fflush(stdout); kill(pid, SIGKILL); /* teardown must not flush */ waitpid(pid, NULL, 0); *d_out = a1 - b1; *d_pgs = a2 - b2; } int main(int argc, char **argv) { const char *dir = (argc > 2) ? argv[2] : BASEDIR_D; int reps = (argc > 1) ? atoi(argv[1]) : 3; int infile_flushed, wholeobj_flushed; int r, fail = 0; unsigned long dA1, dA2, dB1, dB2, dC1, dC2, dD1, dD2, dE1, dE2, dF1, dF2; printf("DF-5829 msync partial-range no-op rig (pid %d, uid %d)\n", getpid(), getuid()); printf("baseline vnodeout=%lu vnodepgsout=%lu\n\n", ctr("vm.stats.vm.v_vnodeout"), ctr("vm.stats.vm.v_vnodepgsout")); snprintf(path8, sizeof(path8), "%s/df5829_8p.bin", dir); snprintf(path1, sizeof(path1), "%s/df5829_1p.bin", dir); printf("test files under: %s\n\n", dir); makefile(path8, 8); makefile(path1, 1); for (r = 1; r <= reps; r++) { printf("== rep %d/%d ==\n", r, reps); run_arm("A partial-range msync[5p,7p)", path8, 8, 6, 5, 2, &dA1, &dA2); run_arm("B whole-file ctrl msync[0p,8p)", path8, 8, 6, 0, 8, &dB1, &dB2); run_arm("C whole-mapping-of-8p-file msync[0p,4p)", path8, 4, 1, 0, 4, &dC1, &dC2); run_arm("D whole-file ctrl msync[0p,1p)", path1, 1, 0, 0, 1, &dD1, &dD2); /* * E/F (block-rounded filesystems like hammer2 only): * 1-page file, mmap/mysnc BEYOND EOF up to the block-rounded * object size. If the msync suddenly starts flushing, that * proves the gate is exactly (end_pindex==object->size-1) * with object->size rounded beyond EOF by nvnode_pager_setsize. */ run_arm("E 1p-file msync[0p,4p)-beyond-EOF", path1, 4, 0, 0, 4, &dE1, &dE2); run_arm("F 1p-file msync[0p,16p)-beyond-EOF", path1, 16, 0, 0, 16, &dF1, &dF2); printf("rep %d: A=%lu/%lu B=%lu/%lu C=%lu/%lu D=%lu/%lu " "E=%lu/%lu F=%lu/%lu\n\n", r, dA1, dA2, dB1, dB2, dC1, dC2, dD1, dD2, dE1, dE2, dF1, dF2); if (dA1 != 0 || dA2 != 0) { printf("!! rep %d arm A unexpectedly flushed\n", r); } if (dB1 == 0 && dB2 == 0) { printf(" rep %d control B did not flush (expected on block-rounded fs)\n", r); } if (dC1 != 0 || dC2 != 0) { printf("!! rep %d arm C unexpectedly flushed\n", r); } if (dD1 == 0 && dD2 == 0) { printf(" rep %d control D did not flush (expected on block-rounded fs)\n", r); } } /* * BUG CONFIRMED if no in-file-range msync (A,C and, on block-rounded * filesystems, also B,D) performed any pageout, while a whole-object * msync (B/D on UFS, or E/F beyond-EOF on hammer2) did. */ infile_flushed = (dA1|dA2|dC1|dC2) != 0; wholeobj_flushed = (dB1|dB2|dD1|dD2|dE1|dE2|dF1|dF2) != 0; if (!infile_flushed && wholeobj_flushed) printf("VERDICT: msync ranges that do not exactly cover " "object->size performed ZERO pageout while the " "whole-object msync did -> CONFIRMED\n"); else if (infile_flushed) printf("VERDICT: partial range unexpectedly flushed -> " "NOT REPRODUCED\n"); else printf("VERDICT: no control flushed -> inconclusive\n"); unlink(path8); unlink(path1); return (fail ? 1 : 0); } |