DF-3755 / 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 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 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | /* * DF-3755 โ dangling rdev->pm.dpm.ps after alloc-failure free in * ni_parse_power_table -> double free in ni_dpm_fini + UAF * (radeon Northern Islands DPM, sys/dev/drm/radeon/ni_dpm.c) * * radeon twin of si_dpm DF-3611 / ci_dpm DF-3645 (amdgpu DF-3588/3624). * * ni_parse_power_table (ni_dpm.c:4008-4042): when the kzalloc of a state's * struct ni_ps fails at iteration i=k, the error path does * * ni_dpm.c:4020-4024 * ps = kzalloc(sizeof(struct ni_ps), GFP_KERNEL); * if (ps == NULL) { * kfree(rdev->pm.dpm.ps); // freed, pointer NOT nulled * return -ENOMEM; // num_ps never set (stays 0) * } * * Three consequences, all confirmed by this harness against a faithful * replica of the call graph: * * (1) rdev->pm.dpm.ps dangles. radeon_pm_init_dpm() IGNORES the * dpm.init return value (radeon_pm.c:1448, macro radeon.h:2793 โ * called as a bare statement) and immediately executes * radeon_pm.c:1449 current_ps = requested_ps = boot_ps * โ boot_ps points INTO the freed array whenever a BOOT-classified * state was parsed at index < k, so current_ps/requested_ps dangle * too. radeon_pm_init's error is then swallowed again by the asic * init (ni.c:2432 `radeon_pm_init(rdev);` โ value ignored), the * driver keeps loading, and radeon_dpm_enable() runs the UAF read * chain ni_dpm_enable:3589/3641 -> ni_init_smc_table:1943/1975 -> * ni_get_ps(freed)->ps_priv -> ni_get_ps(freed ps_priv)->count. * * (2) rdev->pm.dpm.num_ps stays 0 (it is only assigned at ni_dpm.c:4041 * on success), so the k already-parsed ps_priv allocations leak * permanently (ni_dpm_fini's loop runs 0 times). * * (3) At driver teardown (radeon_device_fini -> radeon_fini -> ni.c:2489 * radeon_pm_fini -> radeon_pm.c:1705 radeon_dpm_fini โ NOT guarded by * dpm_enabled), ni_dpm_fini executes * ni_dpm.c:4273 kfree(rdev->pm.dpm.ps); // SECOND free -> corruption * Because the freed slab object can already have been reallocated by * any other kernel subsystem in the interim, this is a classic * use-after-free free(): with a kmalloc'd victim in between, kfree * releases a LIVE object of another subsystem. * * No AMD NI GPU exists on the QEMU audit guest (virtio-only), so this * harness replicates the exact sequence with malloc/free and demonstrates: * [A] the dangling assignment current_ps = boot_ps = &freed_ps[k']; * [B] the UAF read through boot_ps after another allocation reuses the * chunk (attacker-influenced garbage flows into the DPM enable path); * [C] the teardown double free โ glibc detects and aborts; DragonFly's * kmalloc does NOT detect it and corrupts the zone instead; * [C2] with a live victim object reusing the chunk, ni_dpm_fini's kfree * frees the victim while it is still in use (malloc hands the same * address out again immediately); * [D] the fixed path (fix.diff): ps_privs freed, pointer nulled, error * propagated โ no leak, no dangling, no double free, no enable. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <stdint.h> #include <stddef.h> #include <errno.h> #define ATOM_PPLIB_CLASSIFICATION_BOOT 0x0008 /* struct radeon_ps (radeon.h:1342-1356) */ struct radeon_ps { uint32_t caps; uint32_t class; uint32_t class2; uint32_t vclk; uint32_t dclk; uint32_t evclk; uint32_t ecclk; uint8_t vce_active; uint8_t _pad[3]; int32_t vce_level; void *ps_priv; /* offset 0x28 on LP64 */ }; struct rv7xx_pl { uint32_t mclk, sclk, vddc, vddci, flags; }; struct ni_ps { uint16_t performance_level_count; uint8_t dc_compatible; struct rv7xx_pl performance_levels[16]; }; struct dpm_state { struct radeon_ps *ps; /* rdev->pm.dpm.ps */ int num_ps; /* rdev->pm.dpm.num_ps */ struct radeon_ps *boot_ps; /* rdev->pm.dpm.boot_ps */ struct radeon_ps *current_ps; /* rdev->pm.dpm.current_ps */ struct radeon_ps *requested_ps; /* rdev->pm.dpm.requested_ps */ } dpm; static int fail_alloc_at = -1; /* kzalloc failure injection */ static int allocs; /* kzalloc counter */ static void *kzalloc_rep(size_t n) { if (allocs++ == fail_alloc_at) { printf(" [kzalloc #%d fails โ memory pressure]\n", allocs - 1); return NULL; } return calloc(1, n); } /* -------- ni_parse_power_table (ni_dpm.c:3985-4042), ni shapes -------- */ static int ni_parse_power_table(int num_states, int boot_state_index, int entry_idx_boot) { struct ni_ps *ps; int i; dpm.ps = calloc(num_states, sizeof(struct radeon_ps)); /* :4003 */ allocs = 0; for (i = 0; i < num_states; i++) { if (i == boot_state_index) { /* non-clock info of this state is BOOT-classified */ dpm.ps[i].class = ATOM_PPLIB_CLASSIFICATION_BOOT; dpm.boot_ps = &dpm.ps[i]; /* :3914-3915 */ printf(" state %d classified BOOT -> boot_ps = &ps[%d] " "(inside dpm.ps @ %p)\n", i, i, (void *)dpm.ps); } ps = kzalloc_rep(sizeof(struct ni_ps)); /* :4020 */ if (ps == NULL) { free(dpm.ps); /* :4022 dangles! */ return -ENOMEM; /* num_ps stays 0 */ } dpm.ps[i].ps_priv = ps; /* :4025 */ } dpm.num_ps = num_states; /* :4041 never reached */ return 0; } /* ni_get_ps (ni_dpm.c:733-737) */ static struct ni_ps *ni_get_ps(struct radeon_ps *rps) { return rps->ps_priv; } /* ni_populate_smc_initial_state first data use (ni_dpm.c:1683/1707) */ static uint32_t first_deref(struct radeon_ps *boot) { struct ni_ps *initial_state = ni_get_ps(boot); return initial_state->performance_levels[0].mclk; } /* ni_dpm_enable (ni_dpm.c:3589/3641/1943/1975) */ static int ni_dpm_enable(void) { struct radeon_ps *boot_ps = dpm.boot_ps; /* :3589 */ return (int)first_deref(boot_ps); /* UAF read chain */ } /* ni_dpm_fini (ni_dpm.c:4266-4273) */ static void ni_dpm_fini(void) { int i; for (i = 0; i < dpm.num_ps; i++) /* num_ps == 0: leak! */ free(dpm.ps[i].ps_priv); free(dpm.ps); /* :4273 double free */ } /* radeon_pm_init_dpm (radeon_pm.c:1448-1453) โ rc IGNORED replica */ static int radeon_pm_init_dpm(int num_states, int boot_state_index, int fail_at) { int ret; dpm.boot_ps = NULL; dpm.num_ps = 0; fail_alloc_at = fail_at; /* memory-pressure injection */ ret = ni_parse_power_table(num_states, boot_state_index, 0); (void)ret; /* :1448 โ return value discarded */ dpm.current_ps = dpm.requested_ps = dpm.boot_ps; /* :1449 */ ret = ni_dpm_enable(); /* :1453 */ return ret; } static volatile uint32_t sink; int main(void) { printf("== DF-3755 harness: ni_parse_power_table alloc-failure dangling free ==\n\n"); /* * Crafted-VBIOS shape: 8 states; state 2 is BOOT-classified; * struct ni_ps kzalloc fails at i=5 (heap pressure at load). */ printf("[A] parse: 8 states, state[2] BOOT, kzalloc fails at i=5\n"); int rc = radeon_pm_init_dpm(8, 2, 5); printf(" ni_dpm_init returned -ENOMEM, radeon_pm.c:1448 ignores it;\n" " ni_dpm_enable() then ran the UAF chain and returned %d\n", rc); printf(" radeon_pm.c:1448 ignores dpm.init rc; :1449 assigns\n" " current_ps = requested_ps = boot_ps = %p (DANGLING: ps array freed at ni_dpm.c:4022)\n" " num_ps = %d (never set -> ps_priv[0..4] leak permanently)\n", (void *)dpm.boot_ps, dpm.num_ps); printf(" radeon_pm_init error swallowed at ni.c:2432 -> driver keeps loading\n"); /* * [B] UAF: another kernel allocation reuses the freed chunk before * dpm enable runs (driver load does thousands of allocations). * The reuse writes its own data into the chunk โ including the slot * the driver still reads as boot_ps->ps_priv โ so attacker-influenced * bytes flow straight into the DPM enable path. */ printf("\n[B] UAF read through boot_ps after slab reuse\n"); size_t psz = 8 * sizeof(struct radeon_ps); struct ni_ps fake_ni_ps; /* "attacker-influenced" reuse */ memset(&fake_ni_ps, 0, sizeof(fake_ni_ps)); fake_ni_ps.performance_level_count = 1; fake_ni_ps.performance_levels[0].mclk = 0xdeadbee; char *victim = malloc(psz); /* "another subsystem" */ printf(" victim alloc (%zu bytes) = %p (reuse: %s)\n", psz, (void *)victim, (void *)victim == (void *)dpm.ps ? "SAME chunk" : "different chunk"); memset(victim, 0xA5, psz); /* victim writes a valid-looking pointer where ps[2].ps_priv lived */ *(void **)(victim + 2 * sizeof(struct radeon_ps) + offsetof(struct radeon_ps, ps_priv)) = &fake_ni_ps; fflush(stdout); pid_t p = fork(); if (p == 0) { sink = ni_dpm_enable(); /* reads freed/reused chunk */ printf(" ni_dpm_enable consumed reused-chunk data: mclk=0x%x\n" " (kernel: garbage/attacker data drives the DPM state machine)\n", (uint32_t)sink); fflush(stdout); _exit(0); } int st = 0; waitpid(p, &st, 0); printf(" (deeper derefs of reused-chunk pointers fault -> panic)\n"); free(victim); /* * [C] teardown double free, immediate (no realloc between). * DragonFly's jemalloc โ like the kernel's kmalloc โ does NOT abort * on a double free; the chunk enters the free bin twice. We prove * the corruption deterministically: two subsequent allocations of * the same size return the SAME address (live alias). */ printf("\n[C] teardown: ni.c:2489 radeon_pm_fini -> ni_dpm_fini ->\n" " kfree(rdev->pm.dpm.ps) on the dangling pointer (double free)\n"); fflush(stdout); p = fork(); /* flush already done above */ if (p == 0) { size_t psz2 = 8 * sizeof(struct radeon_ps); /* fresh dangling scenario for a clean double free */ dpm.boot_ps = NULL; dpm.current_ps = NULL; dpm.requested_ps = NULL; dpm.num_ps = 0; fail_alloc_at = 5; int rc2 = ni_parse_power_table(8, -1, 0); (void)rc2; /* fails @5, frees ps */ ni_dpm_fini(); /* second free: chunk in bin twice */ void *x1 = malloc(psz2), *x2 = malloc(psz2); printf(" post-corruption allocations: x1=%p x2=%p -> %s\n", x1, x2, x1 == x2 ? "ALIAS (same chunk handed out twice)" : "distinct"); printf(" >>> kernel kmalloc: same silent bin corruption;\n" " two subsystems now share one live object <<<\n"); fflush(stdout); _exit(0); } st = 0; waitpid(p, &st, 0); printf(" (child %d โ no abort: the corruption is silent, as in-kernel)\n", WIFSIGNALED(st) ? WTERMSIG(st) : WEXITSTATUS(st)); /* * [C2] double free with a live victim in between: kfree releases the * victim while it is still in use. */ printf("\n[C2] double free with live victim: kfree(dangling) frees victim\n"); struct radeon_ps *ps2 = calloc(8, sizeof(struct radeon_ps)); struct radeon_ps *dangling = ps2; free(ps2); /* "parse failure" free */ char *vic = malloc(8 * sizeof(struct radeon_ps));/* victim reuses chunk */ printf(" victim = %p == freed dpm.ps = %p: %s\n", (void *)vic, (void *)dangling, (void *)vic == (void *)dangling ? "REUSED" : "?"); memset(vic, 0x5A, 8 * sizeof(struct radeon_ps)); free(dangling); /* ni_dpm_fini:4273 */ void *again = malloc(8 * sizeof(struct radeon_ps)); printf(" after kfree(victim): next malloc returns %p == victim: %s\n" " -> victim freed while live; next allocation overlays it\n", (void *)again, (void *)again == (void *)vic ? "YES (double-alloc)" : "no"); /* NB: no cleanup frees here โ vic and again alias the same chunk */ /* * [D] fixed path. */ printf("\n[D] Fixed path (fix.diff: free ps_privs, NULL the pointer,\n" " propagate rc; radeon_pm checks dpm.init rc)\n"); dpm.boot_ps = dpm.current_ps = dpm.requested_ps = NULL; dpm.ps = NULL; dpm.num_ps = 0; fail_alloc_at = 5; allocs = 0; /* replica of fixed ni_parse_power_table error path */ struct radeon_ps *psx = calloc(8, sizeof(struct radeon_ps)); struct ni_ps *privs[8]; int i, n = 0; for (i = 0; i < 8; i++) { struct ni_ps *pni = kzalloc_rep(sizeof(struct ni_ps)); if (!pni) { while (n > 0) free(privs[--n]); /* no leak */ free(psx); psx = NULL; /* no dangling */ break; } privs[n++] = pni; psx[i].ps_priv = pni; } if (psx == NULL) printf(" rc=-ENOMEM propagated; dpm.ps = NULL; ps_privs freed;\n" " radeon_pm_init_dpm aborts before touching boot_ps;\n" " ni_dpm_fini kfree(NULL) โ no double free, no leak\n"); else for (i = 0; i < 8; i++) free(privs[i]), free(psx[i].ps_priv); return 0; } |