hw/display/sm501: unvalidated framebuffer and cursor addresses lead to OOB read

Summary

The SM501 display controller allows a guest to set the CRT/Panel framebuffer base address (dc_crt_fb_addr, dc_panel_fb_addr) and hardware cursor address (dc_crt_hwc_addr, dc_panel_hwc_addr) to values up to ~64 MB via the 0x3FFFFF0 mask. With VRAM sizes of 2-8 MB, these addresses exceed the actual allocation.

sm501_update_display() reads pixel data from local_mem + fb_addr without bounds checking. This crashes the host when the OOB read hits unmapped memory.

The 2D engine path already validates bounds via sm501_rect_outside_vram() (strengthened in commit 422734d5), but the display rendering path has no equivalent check.

CWE: CWE-125 (Out-of-bounds Read) CVSS 3.1: 6.5 (AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:H) Verified at: HEAD commit 3377670e Machines: SH4 R2D-PLUS (-machine r2d), PPC Pegasos2 (-machine pegasos2)

Root Cause

Register write handler (sm501.c:1371,1435):

case SM501_DC_CRT_FB_ADDR:
    s->dc_crt_fb_addr = value & 0x8FFFFFF0;
    break;

get_fb_addr() (sm501.c:593-596):

static ram_addr_t get_fb_addr(SM501State *s, int crt)
{
    return (crt ? s->dc_crt_fb_addr : s->dc_panel_fb_addr) & 0x3FFFFF0;
}

Effective address range: up to 0x03FFFFF0 (67,108,848 bytes, ~64 MB). No validation against local memory size.

get_hwc_address() (sm501.c:630-634) has the same issue with the cursor address.

Display update (sm501.c:1839):

offset = get_fb_addr(s, crt);

Then (sm501.c:1854):

draw_line(d, s->local_mem + offset, width, palette);

With get_fb_addr() returning 64 MB and VRAM only 8 MB (R2D default), the read pointer is 56 MB past the allocation.

Trigger

  1. Guest writes 0x03FFFFF0 to CRT framebuffer address register (MMIO offset 0x080204)
  2. Guest sets CRT width/height/bpp to valid values
  3. Guest enables CRT display
  4. Host display update callback fires
  5. sm501_update_display reads from local_mem + 0x03FFFFF0 -- OOB, SIGSEGV

Hardware cursor path triggers the same way via get_hwc_address(). Additionally, hwc_invalidate() calls memory_region_set_dirty() with the unvalidated offset, corrupting dirty bitmap tracking.

Impact

Host QEMU process crash (SIGSEGV). Limited info leak via rendered pixels. Dirty bitmap corruption may affect other device emulation. SH4 R2D and PPC Pegasos2 machines.

Proposed Fix

Validate addresses in get_fb_addr() and get_hwc_address():

--- a/hw/display/sm501.c
+++ b/hw/display/sm501.c
@@ -593,7 +593,12 @@ static ram_addr_t get_fb_addr(SM501State *s, int crt)
 {
-    return (crt ? s->dc_crt_fb_addr : s->dc_panel_fb_addr) & 0x3FFFFF0;
+    ram_addr_t addr = (crt ? s->dc_crt_fb_addr : s->dc_panel_fb_addr)
+                      & 0x3FFFFF0;
+    if (addr >= get_local_mem_size(s)) {
+        addr = 0;
+    }
+    return addr;
 }

@@ -630,7 +635,12 @@ static inline uint8_t *get_hwc_address(SM501State *state, int crt)
 {
     uint32_t addr = crt ? state->dc_crt_hwc_addr : state->dc_panel_hwc_addr;
-    return state->local_mem + (addr & 0x03FFFFF0);
+    uint32_t offset = addr & 0x03FFFFF0;
+    if (offset >= get_local_mem_size(state)) {
+        offset = 0;
+    }
+    return state->local_mem + offset;
 }