DF-2802 / jailchurn.c
/* * DF-2799 โ AB-BA deadlock: jail_lock <-> per-CPU gd_sysctllock. * * Lock cycle (stock kernel): * * CPU A (root, jail(2)): sys_jail takes jail_lock EXCLUSIVE * (kern_jail.c:275) and holds it across kern_jail() -> * prison_sysctl_create() (kern_jail.c:223, called under the same lock) * -> SYSCTL_ADD_NODE/SYSCTL_ADD_BIT64 -> sysctl_add_oid() -> * SYSCTL_XLOCK() == _sysctl_xlock() which takes EVERY CPU's * gd_sysctllock EXCLUSIVE in order 0..ncpu-1 (kern_sysctl.c:442,1641-51). * * CPU B (unprivileged, `sysctl kern.jail.list`): userland_sysctl takes * its own CPU's gd_sysctllock SHARED across the whole sysctl_root() * dispatch INCLUDING the handler (kern_sysctl.c:1572-73,1480-81); the * sysctl_jail_list handler then blocks acquiring jail_lock * (kern_jail.c:696) โ while still holding CPU B's gd_sysctllock. * * A holds jail_lock, waits for CPU B's gd lock (inside _sysctl_xlock). * B holds its gd lock, waits for jail_lock. Neither lockmgr ever * times out -> permanent kernel deadlock. Cascades: jail_lock is * global (all jail(2)/jail_attach(2)/prison_free/jailed_ip callers * wedge) and CPU A already holds gd locks 0..B-1 exclusively (sysctl * readers on those CPUs wedge too). * * This program is the root side: churn jail(2) forever; every child * jail()s itself then exits. The create-window inside each jail() is * milliseconds wide (nlookup + chroot + prison_sysctl_create), so the * cycle closes almost immediately once a reader is parked on jail_lock. */ #include <sys/param.h> #include <sys/jail.h> #include <sys/syscall.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> int main(int argc, char **argv) { int iters = argc > 1 ? atoi(argv[1]) : 100000; if (getuid() != 0) { printf("must run as root\n"); return (2); } for (int i = 0; i < iters; i++) { pid_t pid = fork(); if (pid == 0) { struct jail j; memset(&j, 0, sizeof(j)); j.version = 1; j.path = "/tmp"; j.hostname = "churn"; j.n_ips = 0; j.ips = NULL; int jid = syscall(SYS_jail, &j); if (jid < 0 && errno != EPERM) { /* failed jail() (e.g. ENOTDIR path) still * exercises prison_sysctl_create + out2 */ } _exit(0); } /* reap, but slowly โ let a few children overlap */ if (i % 64 == 63) { int st; while (waitpid(-1, &st, WNOHANG) > 0) ; } } int st; while (waitpid(-1, &st, 0) > 0) ; printf("churn done\n"); return (0); } |