DF-4196 / vblank_toctou.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 | /* * vblank_toctou.c โ DF-4196 model harness. * * Proves the divide-by-zero (#DE => in-kernel panic) reachable through the * lock-ordering hole between: * * drm_crtc_vblank_off() drm_vblank.c:1115-1171 * ... * spin_unlock_irqrestore(&dev->event_lock) :1165 * vblank->hwmode.crtc_clock = 0; :1169 <-- LOCK-FREE * * drm_calc_vbltimestamp_from_scanoutpos() drm_vblank.c:579-693 * if (mode->crtc_clock == 0) return false; :618 <-- check * vbl_status = dev->driver->get_scanout_position(...) :637 (opaque * external call => compiler MUST reload * mode->crtc_clock for the division) * delta_ns = div_s64(1000000LL * (vpos * mode->crtc_htotal + hpos), * mode->crtc_clock); :671-672 <-- divide * * The racing reader path drm_vblank_get() -> drm_vblank_enable() (holds * dev->vbl_lock + dev->vblank_time_lock, :988/:946) -> drm_update_vblank_count * (:961) -> drm_get_last_vbltimestamp -> drm_calc_vbltimestamp_from_scanoutpos * holds NEITHER event_lock NOR any lock the :1169 store takes, so the store * can land between :618 and :671 and the reloaded divisor is 0. * * This harness reproduces the exact interleaving with two threads + a barrier * (the check passes on the old value; the lock-free store lands; the division * reloads). The divide-by-zero manifests as SIGFPE โ the same exception the * kernel takes as #DE (panic: integer divide fault). * * Modes: * ./vblank_toctou race vulnerable interleaving (expect SIGFPE) * ./vblank_toctou race_fixed same interleaving with the fix applied * (crtc_clock cached into a local before the * check, division uses the local) -> clean */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> struct drm_display_mode_like { int crtc_clock; /* kHz */ int crtc_htotal; }; struct vblank_like { struct drm_display_mode_like hwmode; }; static struct vblank_like vblank; static int vpos, hpos; /* filled by the fake get_scanout_position */ static pthread_barrier_t barrier; static volatile int check_passed, store_done; /* stands in for dev->driver->get_scanout_position(): an OPAQUE external * call receiving `mode` โ the compiler cannot assume *mode unchanged */ static __attribute__((noinline)) int get_scanout_position(struct drm_display_mode_like *mode, int *v, int *h) { (void)mode; /* wait until the vblank_off thread executes its lock-free store * (drm_vblank.c:1169) โ models the multi-microsecond MMIO window */ pthread_barrier_wait(&barrier); while (!store_done) __asm__ __volatile__("pause"); *v = 100; *h = 50; return 1; } /* verbatim structure of drm_calc_vbltimestamp_from_scanoutpos() :618-677 * (retry loop elided; vbl_status handling identical) */ static __attribute__((noinline)) long long calc_vbltimestamp_from_scanoutpos(struct drm_display_mode_like *mode, int fixed) { int v, h; long long delta_ns; int crtc_clock_cached; if (fixed) { /* FIX: cache the divisor before the check */ crtc_clock_cached = mode->crtc_clock; if (crtc_clock_cached == 0) { printf("[calc] FIXED path: cached crtc_clock==0 -> graceful failure (return false)\n"); fflush(stdout); return -1; } } else { if (mode->crtc_clock == 0) { /* :618 */ printf("[calc] crtc_clock==0 at check -> graceful failure\n"); fflush(stdout); return -1; } } check_passed = 1; /* reader is past :618, now inside the MMIO window */ if (!get_scanout_position(mode, &v, &h)) /* :637 */ return -1; if (fixed) { /* :671-672 with the cached local */ delta_ns = 1000000LL * ((long long)(v * mode->crtc_htotal + h)) / crtc_clock_cached; } else { /* :671-672 โ reloads mode->crtc_clock (opaque call above) */ delta_ns = 1000000LL * ((long long)(v * mode->crtc_htotal + h)) / mode->crtc_clock; } return delta_ns; } static void *crtc_off_thread(void *arg) { (void)arg; /* drm_crtc_vblank_off() tail: * spin_unlock_irqrestore(&dev->event_lock) :1165 * vblank->hwmode.crtc_clock = 0; :1169 (lock-free) */ pthread_barrier_wait(&barrier); /* reader just passed :618 */ printf("[vblank_off] event_lock released; executing lock-free " "vblank->hwmode.crtc_clock = 0 (drm_vblank.c:1169)\n"); fflush(stdout); vblank.hwmode.crtc_clock = 0; store_done = 1; return NULL; } int main(int argc, char **argv) { int fixed = argc > 1 && strcmp(argv[1], "race_fixed") == 0; pthread_t off; printf("[main] mode=%s (initial hwmode.crtc_clock=148500 kHz, htotal=2200)\n", fixed ? "race_fixed" : "race"); fflush(stdout); vblank.hwmode.crtc_clock = 148500; vblank.hwmode.crtc_htotal = 2200; pthread_barrier_init(&barrier, NULL, 2); pthread_create(&off, NULL, crtc_off_thread, NULL); long long r = calc_vbltimestamp_from_scanoutpos(&vblank.hwmode, fixed); pthread_join(off, NULL); if (r >= 0) printf("[main] SURVIVED: delta_ns=%lld (check passed BEFORE zero store, " "division used a %s value)\n", r, fixed ? "cached-nonzero" : "RELOADED-NONZERO?!"); else printf("[main] SURVIVED: calc failed gracefully\n"); return 0; } |