Skip to content

DSP-HLE AX voice update OOB stack write leads to host code execution

High
OatmealDome published GHSA-4q28-hhjv-hf3f Aug 11, 2026

Package

dolphin-emu

Affected versions

< 2606a, < 2606-235

Patched versions

2606a, 2606-235

Description

DSP-HLE AX voice update OOB stack write

Summary

Dolphin's default DSP-HLE audio backend trusts a guest-controlled AX parameter-block update offset as an index into a host-stack array. Guest code can write a chosen 16-bit value outside that array and corrupt the host stack, which can enable code execution in the Dolphin process. Stack canaries do not reliably mitigate this because the write is indexed, not a linear overflow.

Root cause

ApplyUpdatesForMs copies the parameter block into a fixed-size host-stack array, then applies guest-authored update records without validating the destination offset:

void ApplyUpdatesForMs(int curr_ms, PB_TYPE& pb, u16* num_updates, const PBUpdateData& updates)
{
  // Host-stack copy of the AX parameter block:
  //   AXPB    -> 122 u16 words
  //   AXPBWii -> 155 u16 words
  auto pb_mem = Common::BitCastToArray<u16>(pb);

  // Guest-controlled per-subframe counts select which update records are used.
  u32 start_idx = 0;
  for (int i = 0; i < curr_ms; ++i)
    start_idx += num_updates[i];

  for (u32 i = start_idx; i < start_idx + num_updates[curr_ms]; ++i)
  {
    u16 update_off = updates[i].pb_offset;  // guest-controlled index, 0..0xffff
    u16 update_val = updates[i].new_value;  // guest-controlled 16-bit value

    // BUG: update_off is not checked against pb_mem.size().
    // This is an out-of-bounds write on the host stack when update_off >= size.
    pb_mem[update_off] = update_val;
  }

  pb = std::bit_cast<PB_TYPE>(pb_mem);
}

Reachable call paths:

// GameCube AX: reached during normal AXUCode::ProcessPBList command handling.
PBUpdateData updates = LoadPBUpdates(memory, pb);
for (int curr_ms = 0; curr_ms < 5; ++curr_ms)
  ApplyUpdatesForMs(curr_ms, pb, pb.updates.num_updates, updates);

// Wii old AXWii ucodes: same sink, gated only on old-ucode update counts.
if (m_old_axwii &&
    (pb.updates.num_updates[0] | pb.updates.num_updates[1] | pb.updates.num_updates[2]))
{
  PBUpdateData updates = LoadPBUpdates(memory, pb);
  for (int curr_ms = 0; curr_ms < 3; ++curr_ms)
    ApplyUpdatesForMs(curr_ms, pb, pb.updates.num_updates, updates);
}

The GameCube path is enough for default-config reachability: malicious guest code submits a normal AX command list and supplies a crafted parameter block plus update table.

Impact and exploitability

A malicious GameCube/Wii title, homebrew binary, or guest PPC payload can trigger host stack corruption in the Dolphin process.

The primitive is a repeated stack write:

destination = &pb_mem[0] + 2 * guest_pb_offset
value       = guest_new_value
width       = 16 bits
range       = up to ~128 KB from the stack buffer

Because the offset is an absolute word index, the write does not have to pass through intervening stack bytes. An attacker can target a saved return address directly instead of performing a linear stack smash.

On non-PIE builds, including the Dolphin package in the Ubuntu repository tested here, this primitive alone is enough for code execution. On PIE/ASLR builds, the same primitive still gives guest-controlled host stack corruption, but code execution may require an additional bug to use as an information leak.

Fix guidance

Minimal fix: validate the update-table range before reading updates[i], then validate the PB destination offset before writing pb_mem[update_off].

size_t start_idx = 0;
for (int i = 0; i < curr_ms; ++i)
  start_idx += num_updates[i];

const size_t count = num_updates[curr_ms];

// Current code immediately enters the loop and reads updates[i].
// First ensure the selected half-open range stays inside PBUpdateData.
if (start_idx > updates.size() || count > updates.size() - start_idx)
  return;  // malformed guest counts; ignore this update batch

const size_t end_idx = start_idx + count;
for (size_t i = start_idx; i < end_idx; ++i)
{
  const u16 update_off = updates[i].pb_offset;
  const u16 update_val = updates[i].new_value;

  // Current code writes pb_mem[update_off] unconditionally.
  // Ignore invalid guest offsets; do not clamp them to a valid PB word.
  if (update_off >= pb_mem.size())
    continue;

  pb_mem[update_off] = update_val;
}

This is intentionally small: one range check for updates, one bounds check for pb_mem, shared by both the GameCube AX and Wii AXWii paths.

Minimal reproduction

minimal_repro.zip

minimal_repro.dol is a GameCube DOL that Dolphin can load directly. It drives the real DSP-HLE/AX path from guest code:

guest PPC -> DSP mailbox -> AX command list -> ProcessPBList -> ApplyUpdatesForMs

The repro performs one crafted PB update:

pb_offset = 0xffff
new_value = 0x1234

For the GameCube AXPB path, pb_mem has only 122 valid u16 entries. Offset 0xffff therefore writes 0x1234 roughly 128 KB past the host-stack buffer and is expected to crash a vulnerable Dolphin build.

Example run:

$ dolphin-emu-nogui -p headless -v Null -e minimal_repro.dol
ALSA lib pcm_dmix.c:1000:(snd_pcm_dmix_open) unable to open slave
Dolphin 5.0-19368-dadbeb4bae7e7fa23 | JIT64 DC | Null | HLE
Segmentation fault (core dumped)

Severity

High

CVE ID

No known CVE

Weaknesses

Improper Validation of Array Index

The product uses untrusted input when calculating or using an array index, but the product does not validate or incorrectly validates the index to ensure the index references a valid position within the array. Learn more on MITRE.

Credits