DF-0847 / dq_race.c
/* * DF-0847 — concurrent dquot hash/free-list race harness (cross-mount). * * NON-DEFAULT-CONFIG CHARACTERIZATION ONLY. * * The global ufs_dqhashtbl / ufs_dqfreelist are mutated with NO lock in * ufs_dqget()/ufs_dqrele()/ufs_dqflush() (ufs_quota.c). This harness drives * concurrent cache-miss/recycle + free-list races to expose it. * * IMPORTANT: vfs_quotactl() takes the per-mount mnt_token (UFS is not * MNTK_MPSAFE), so quotactl calls on a SINGLE mount are serialized and cannot * race. The dquot lists are GLOBAL across mounts, so the race is only * reachable by hammering TWO (or more) quota'd UFS mounts concurrently. * This harness therefore splits its worker threads across two mount points. * * Each worker calls quotactl(Q_GETQUOTA, its-mount, uid=cycling, ...) in a * tight loop. Q_GETQUOTA -> ufs_getquota -> ufs_dqget + ufs_dqrele. With a * small uid range, workers collide on the same cached dquot that is * momentarily on the free list (dq_cnt==0) and both TAILQ_REMOVE it -> list * corruption. With a large range, the cache-miss recycle path * (TAILQ_FIRST + TAILQ_REMOVE + LIST_REMOVE + LIST_INSERT_HEAD) is exercised. * * Privilege: Q_GETQUOTA for arbitrary uid requires root * (caps_priv_check RESTRICTEDROOT). This is a root-driven characterization * of the unlocked list mutation — NOT an unprivileged escalation. An * unprivileged user can only cause a cache-miss for their own uid (a single * dquot), giving a negligible race window. * * Build: cc -O2 -pthread -o dq_race dq_race.c * Run (as root, options-QUOTA kernel, TWO quotaon'd UFS mounts): * ./dq_race /mnt/q0 /mnt/q1 12 60 50 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <pthread.h> #include <sys/syscall.h> #define Q_GETQUOTA 0x0300 #define USRQUOTA 0 #define SUBCMDSHIFT 8 struct dqblk { char _pad[512]; }; static const char *g_mp[2]; static int g_secs; static unsigned uidrange = 50; static volatile int g_stop = 0; static void * worker(void *arg) { unsigned seed = (unsigned)(size_t)arg ^ (unsigned)getpid(); const char *mp = g_mp[(unsigned)(size_t)arg & 1]; /* even->mp0, odd->mp1 */ long iter = 0; struct dqblk db; int cmd = (Q_GETQUOTA << SUBCMDSHIFT) | USRQUOTA; while (!g_stop) { unsigned uid = (rand_r(&seed) % uidrange) + 1; syscall(SYS_quotactl, mp, cmd, uid, &db); iter++; } return (void *)iter; } int main(int argc, char **argv) { int nthreads = (argc > 3) ? atoi(argv[3]) : 12; int secs = (argc > 4) ? atoi(argv[4]) : 60; if (argc > 5) uidrange = (unsigned)atoi(argv[5]); g_mp[0] = (argc > 1) ? argv[1] : "/mnt/q0"; g_mp[1] = (argc > 2) ? argv[2] : "/mnt/q1"; g_secs = secs; fprintf(stderr, "DF-0847 race harness: mp0=%s mp1=%s threads=%d secs=%d uidrange=1..%u\n", g_mp[0], g_mp[1], nthreads, secs, uidrange); fprintf(stderr, "driving CROSS-MOUNT concurrent Q_GETQUOTA cache/freelist races...\n"); pthread_t th[128]; if (nthreads > 128) nthreads = 128; for (int i = 0; i < nthreads; i++) pthread_create(&th[i], NULL, worker, (void *)(size_t)(i + 1)); sleep(secs); g_stop = 1; long total = 0; for (int i = 0; i < nthreads; i++) { long it; pthread_join(th[i], (void **)&it); total += it; } fprintf(stderr, "survived: %ld quotactl iterations across %d threads\n", total, nthreads); fprintf(stderr, "If the kernel panicked, see boot.log for the signature.\n"); return 0; } |