DF-3611 / harness.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 | /* * DF-3611 โ double free of rdev->pm.dpm.ps after allocation failure in * si_parse_power_table() (radeon si_dpm.c). Twin of amdgpu DF-3588. * * si_dpm.c:6840 rdev->pm.dpm.ps = kzalloc(sizeof(struct radeon_ps) * ucNumEntries); * si_dpm.c:6845 for (i = 0; i < state_array->ucNumEntries; i++) { * si_dpm.c:6853 ps = kzalloc(sizeof(struct ni_ps), GFP_KERNEL); * si_dpm.c:6854 if (ps == NULL) { * si_dpm.c:6855 kfree(rdev->pm.dpm.ps); <-- freed, NOT NULLed, * si_dpm.c:6856 return -ENOMEM; num_ps stays 0, * si_dpm.c:6857 } earlier ps_priv[] leak * * radeon_pm.c:1448 radeon_dpm_init(rdev); <-- return value IGNORED * radeon_pm.c:1453 ret = radeon_dpm_enable(rdev); <-- fails half-initialized, * dpm_enabled stays false * ...driver unload / GPU reset... * si.c:6945 radeon_pm_fini(rdev) * radeon_pm.c:1705 radeon_dpm_fini(rdev); <-- UNCONDITIONAL * si_dpm.c:7085 kfree(rdev->pm.dpm.ps); <-- DOUBLE FREE * * Between the init failure and fini, the kernel heap recycles the freed * block (same size class, heavy alloc/free churn during modeset). The * fini-time kfree() then frees a LIVE object -> next allocation returns * the same address to two owners -> use-after-free with cross-object * type confusion. * * Harness [A]: exact free sequence + heap recycling simulation: the freed * dpm.ps block is reused by a live "victim" object, si_dpm_fini frees it * AGAIN, and a subsequent allocation aliases the victim โ data written * through the second owner clobbers the first (observed corruption). * Harness [B]: fixed error path (free privs + NULL pointer) -> fini no-op. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <stdint.h> #define NUM_ENTRIES 4 #define PS_BYTES (sizeof(struct rps) * NUM_ENTRIES) struct rps { void *ps_priv; }; struct victim_obj { uint64_t marker; char payload[PS_BYTES - sizeof(uint64_t)]; }; static struct { struct rps *ps; int num_ps; } dpm; /* rdev->pm.dpm */ static int kzalloc_fails_at = 2; /* fault injection: state #2 OOMs */ static int live_allocs; /* leaked allocations still alive */ static void *kzalloc_tracked(size_t n) { live_allocs++; return calloc(1, n ? n : 1); } static void kfree_tracked(void *p) { live_allocs--; free(p); } /* si_dpm.c:6808-6879 โ alloc/failure sequence verbatim */ static int si_parse_power_table(void) { dpm.ps = kzalloc_tracked(PS_BYTES); if (!dpm.ps) return -12; for (int i = 0; i < NUM_ENTRIES; i++) { void *ps; if (i == kzalloc_fails_at) ps = NULL; /* injected kzalloc failure */ else ps = kzalloc_tracked(PS_BYTES); if (ps == NULL) { printf(" state %d: kzalloc(ni_ps) fails -> kfree(dpm.ps); return -ENOMEM\n", i); printf(" dpm.ps block freed; pointer LEFT DANGLING in rdev->pm.dpm.ps\n"); printf(" leaked so far: %d live allocations (ps_priv of states 0..%d)\n", live_allocs - 1, i - 1); kfree_tracked(dpm.ps); /* si_dpm.c:6855 โ bug: not NULLed */ return -12; } dpm.ps[i].ps_priv = ps; } dpm.num_ps = NUM_ENTRIES; return 0; } int main(void) { printf("== DF-3611 harness: kzalloc failure at state 2 of %d in si_parse_power_table ==\n\n", NUM_ENTRIES); printf("[A] Buggy path (si_dpm.c:6853-6856 + 7085):\n"); int rc = si_parse_power_table(); printf(" si_dpm_init rc=%d; radeon_pm.c:1448 IGNORES it; dpm enable fails;\n", rc); printf(" kernel keeps running with dangling rdev->pm.dpm.ps = %p\n", (void *)dpm.ps); /* heap recycles the freed block for a live object (modeset churn) */ struct victim_obj *victim = kzalloc_tracked(sizeof(*victim)); victim->marker = 0xCAFEF00DBEEF1234ULL; printf(" [heap churn] victim object now lives at %p %s\n", (void *)victim, (void *)victim == (void *)dpm.ps ? "(SAME block โ recycled)" : "(different block)"); /* driver unload: radeon_pm_fini -> radeon_dpm_fini -> si_dpm_fini */ printf(" [unload] si_dpm_fini: for(i<num_ps=%d) skipped; kfree(dpm.ps=%p)\n", dpm.num_ps, (void *)dpm.ps); if ((void *)victim == (void *)dpm.ps) { free((void *)dpm.ps); /* si_dpm.c:7085 โ frees LIVE victim */ printf(" >>> DOUBLE FREE of a LIVE object <<<\n"); /* next allocation reuses the victim block -> two owners */ struct victim_obj *other = calloc(1, sizeof(*other)); other->marker = 0x4141414141414141ULL; printf(" next allocation gets same block at %p; write via 2nd owner:\n", (void *)other); printf(" victim->marker = 0x%016llx (owner #1 view)\n", (unsigned long long)victim->marker); printf(" other->marker = 0x%016llx (owner #2 view)\n", (unsigned long long)other->marker); printf(" victim->marker was silently CLOBBERED -> use-after-free\n"); printf(" >>> In kernel: cross-object type confusion; any struct of the\n" " same size class (e.g. radeon bo, gart pages array) is freed\n" " while referenced -> arbitrary UAF <<<\n"); free(other); } printf("\n[B] Fixed path (fix.diff: free privs, kfree + NULL on error; fini guards):\n"); live_allocs = 0; dpm.ps = NULL; dpm.num_ps = 0; dpm.ps = kzalloc_tracked(PS_BYTES); void *p0 = kzalloc_tracked(PS_BYTES); void *p1 = kzalloc_tracked(PS_BYTES); dpm.ps[0].ps_priv = p0; dpm.ps[1].ps_priv = p1; /* fixed error path for state 2 failure */ kfree_tracked(dpm.ps[1].ps_priv); kfree_tracked(dpm.ps[0].ps_priv); kfree_tracked(dpm.ps); dpm.ps = NULL; /* fix.diff */ printf(" error path frees all %d live allocations, NULLs dpm.ps, rc propagated\n", live_allocs == 0 ? 0 : live_allocs); /* fini after fixed error path */ if (dpm.ps == NULL) printf(" si_dpm_fini: dpm.ps == NULL -> no-op (no double free)\n"); printf(" clean: no dangling pointer, 0 bytes leaked\n"); return 0; } |