DF-4696 / trigger_df4696.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 | /* * DF-4696 โ trigger + arithmetic proof for divide-by-zero in * sys/dev/drm/amd/display/dc/dcn10/dcn10_dpp_dscl.c:439-441 * (dpp1_dscl_calc_lb_num_partitions). * * SINK (kernel, my audit file): * dcn10_dpp_dscl.c:411-412 line_size = min(viewport.width, recout.width) * dcn10_dpp_dscl.c:416 memory_line_size_y = (line_size*lb_bpc + 71)/72 * dcn10_dpp_dscl.c:439 *num_part_y = lb_memory_size / memory_line_size_y * ^^^^^^^^^^^^^^^^^^^^ 816 / 0 -> #DE -> panic * (also :440 via memory_line_size_c and :441 via memory_line_size_a, * same line_size root cause) * * PRODUCER CHAIN (shared with DF-4373, every link static, same types): * amdgpu_dm.c:2210-2240 fill_rects_from_plane_state: raw crtc_x/y/w/h * copied, only zero-size rejected, clip_rect=dst_rect * (plane rects NEVER clipped to the CRTC window). * amdgpu_dm.c:3585-3603 dm_plane_atomic_check: fill_rects + * dc_resource.c:2891-2900 dc_validate_plane: "validates pixel format only". * dc_resource.c:543 viewport.width = clip.w * src.w / dst.w (SOURCE px) * dc_resource.c:1125-1127 the ONLY degeneracy guard: viewport >= 16 โ * recout is computed AFTER it, never guarded. * dc_resource.c:637-643 recout.width = clip.w * dst.w / src.w, then * clamped to the stream dst window; for a plane * flush off the right edge this yields EXACTLY 0. * dcn10_dpp.c:141-210 dpp_get_optimal_number_of_taps: computes * pixel_width = min(viewport.w, recout.w) at :148-151 * but NEVER USES IT โ returns true unconditionally * (non-FP16) โ the missing guard on DCN10. * validation: dcn10_validate_plane (dcn10_resource.c:1131-1138) checks * video src width only; validate_bandwidth never * looks at recout. Context validates DC_OK. * COMMIT: dcn10_hw_sequencer.c:2101-2104 update_dchubp_dpp (full_update) * -> :2029-2041 update_scaler * -> dpp1_dscl_set_scaler_manual_scale (dscl.c:647) * -> find_lb_memory_config (:463; use_max_lb=false, * dc kzalloc'd so dc->debug zeroed, dc.c:729-732) * -> caps->dscl_calc_lb_num_partitions (:476) * -> 816 / 0 => integer #DE in kernel mode. * * TRIGGER GEOMETRY (UABI-legal atomic commit, session DRM master): * CRTC/mode 640x480; plane buffer 640x480 (src 640 wide); * CRTC_X = 640 (== mode width: flush off the right edge), * CRTC_Y = 0, CRTC_W = 1, CRTC_H = 480; format XRGB8888 (not FP16, so the * DSCL_MODE_DSCL_BYPASS early-return at dscl.c:684-685 is not taken). * * This program: * 1. probes /dev/dri/card* (on the audit QEMU guest there is none: QEMU * std VGA only, no vendor 0x1002 PCI device โ amdgpu DC cannot attach); * 2. replays the producer+sink arithmetic 1:1 with the kernel's integer * types (struct rect fields are signed int, dc_hw_types.h) and executes * the exact division sequence. The attack case dies with SIGFPE * (userland twin of kernel #DE/panic); the control case completes and * prints the clamped partition counts. */ #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> /* mirror of struct rect (sys/dev/drm/amd/display/include/dc_hw_types.h) */ struct rect { int x, y, width, height; }; /* Faithful division executor. The kernel computes * lb_memory_size / memory_line_size_y (dcn10_dpp_dscl.c:439) * with a runtime-unknown divisor, which compiles to a real idiv and faults * with #DE when the divisor is 0. A naive userland replica with the same * constants lets the compiler exploit the C UB of x/0 and skip the fault, * so we hide the divisor behind an asm barrier: the compiler must emit the * actual divide instruction, making SIGFPE the exact userland twin of the * kernel's #DE. (Proof this matters: without the barrier, gcc8 computes * garbage and the process survives โ observed and fixed during verification.) */ static int kdiv(int a, int b) { asm volatile("" : "+r"(b)); return a / b; } static int run_case(const char *name, int crtc_x, int expect_fault) { /* --- fill_rects_from_plane_state (amdgpu_dm.c:2210-2240) --- */ struct rect src_rect = { 0, 0, 640, 480 }; /* 640x480 buffer */ struct rect dst_rect = { crtc_x, 0, 1, 480 }; /* raw UABI crtc_* */ struct rect clip_rect = dst_rect; /* :2240 clip_rect = dst_rect */ /* stream state: mode 640x480, src window == dst window == full mode */ struct rect stream_src = { 0, 0, 640, 480 }; struct rect stream_dst = { 0, 0, 640, 480 }; /* --- calculate_viewport width (dc_resource.c:543): SOURCE pixels --- */ int viewport_w = clip_rect.width * src_rect.width / dst_rect.width; /* --- resource_build_scaling_params guard (dc_resource.c:1125-1127) --- */ if (viewport_w < 16) { printf("%s: viewport.width=%d < 16 -> build returns false " "(config REJECTED, no bug)\n", name, viewport_w); return 0; } /* --- calculate_recout (dc_resource.c:631-657) --- */ int recout_x = stream_dst.x; if (stream_src.x < clip_rect.x) recout_x += (clip_rect.x - stream_src.x) * stream_dst.width / stream_src.width; int recout_w = clip_rect.width * stream_dst.width / stream_src.width; if (recout_w + recout_x > stream_dst.x + stream_dst.width) recout_w = stream_dst.x + stream_dst.width - recout_x; /* --- dcn10_dpp.c:148-151: pixel_width computed, then IGNORED --- * (the dead variable is why validation never rejects this geometry) */ int pixel_width = viewport_w < recout_w ? viewport_w : recout_w; (void)pixel_width; /* kernel computes this and never reads it */ /* --- dpp1_dscl_calc_lb_num_partitions (dcn10_dpp_dscl.c:411-441) --- */ int lb_bpc = 10; /* LB_PIXEL_DEPTH_30BPP, forced by dcn10_hw_sequencer.c:2037 / dc_resource.c:1136 */ int line_size = viewport_w < recout_w ? viewport_w : recout_w; int memory_line_size_y = (line_size * lb_bpc + 71) / 72; /* +71 to ceil */ int memory_line_size_a = (line_size + 5) / 6; /* +5 to ceil */ int lb_memory_size = 816; /* LB_MEMORY_CONFIG_1, dscl.c:422 */ printf("%s: viewport.width=%d recout.x=%d recout.width=%d " "line_size=%d memory_line_size_y=%d memory_line_size_a=%d\n", name, viewport_w, recout_x, recout_w, line_size, memory_line_size_y, memory_line_size_a); if (memory_line_size_y == 0 || memory_line_size_a == 0) { printf("%s: divisor is 0 -> kernel would execute " "816 / 0 at dcn10_dpp_dscl.c:439 -> #DE -> panic. " "Executing userland twin; SIGFPE follows:\n", name); fflush(stdout); if (!expect_fault) abort(); } /* the exact division from dscl.c:439 (and :441) */ volatile int num_part_y = kdiv(lb_memory_size, memory_line_size_y); if (num_part_y > 64) num_part_y = 64; /* dscl.c:447-448 clamp */ printf("%s: no fault: num_part_y clamped to %d\n", name, num_part_y); return 0; } int main(int argc, char **argv) { const char *path = argc > 1 ? argv[1] : "/dev/dri/card0"; int fd = open(path, O_RDWR); printf("DF-4696 device probe: open(%s) = %d errno=%d (%s)\n", path, fd, errno, strerror(errno)); if (fd < 0) { printf(" -> no DRM device: the amdgpu DC path (which compiles\n" " dcn10_dpp_dscl.c into the amdgpu module) cannot\n" " attach on this guest; runtime trigger impossible.\n" " Proving the arithmetic chain instead.\n\n"); } else { close(fd); printf(" -> DRM device present: run the atomic commit described\n" " in the header comment (plane flush off right edge).\n" " NOT attempted automatically (needs fb/object setup).\n\n"); } /* control: plane fully on-screen -> normal path, no fault */ run_case("control (CRTC_X=0, fully visible)", 0, 0); /* attack: plane flush off the right edge of a 640-wide mode */ run_case("attack (CRTC_X=640, flush off right edge)", 640, 1); printf("attack case survived?? โ unexpected, chain broken\n"); return 1; } |