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)
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
ApplyUpdatesForMscopies the parameter block into a fixed-size host-stack array, then applies guest-authored update records without validating the destination offset:Reachable call paths:
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:
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 writingpb_mem[update_off].This is intentionally small: one range check for
updates, one bounds check forpb_mem, shared by both the GameCube AX and Wii AXWii paths.Minimal reproduction
minimal_repro.zip
minimal_repro.dolis a GameCube DOL that Dolphin can load directly. It drives the real DSP-HLE/AX path from guest code:The repro performs one crafted PB update:
For the GameCube
AXPBpath,pb_memhas only 122 validu16entries. Offset0xfffftherefore writes0x1234roughly 128 KB past the host-stack buffer and is expected to crash a vulnerable Dolphin build.Example run: