From a107ac9ae73031eb5f371c0994885c4f103007b6 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Fri, 1 May 2026 17:44:48 +0200 Subject: [PATCH] fix(cpu): PPCBUG-151 add reservation_width discriminator to stwcx./stdcx. Track lwarx vs ldarx reservation width in PpcContext as a u8 (4 = word, 8 = doubleword, 0 = none). stwcx. requires width==4; stdcx. requires width==8. Cross-width pairs (lwarx + stdcx., ldarx + stwcx.) now fail deterministically with CR0.EQ=0 instead of spuriously succeeding. The width is held per-thread; the cross-thread reservation table keeps its existing slot encoding because each host thread consults its own ctx.reservation_width before committing. Affected: PPCBUG-151 stwcx./stdcx. shared the same reservation slot without width discriminator; cross-width commits silently succeeded Tests: lwarx_then_stdcx_cross_width_fails, ldarx_then_stwcx_cross_width_fails Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/xenia-cpu/src/context.rs | 7 +++ crates/xenia-cpu/src/interpreter.rs | 96 ++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/crates/xenia-cpu/src/context.rs b/crates/xenia-cpu/src/context.rs index 26b9733..5826120 100644 --- a/crates/xenia-cpu/src/context.rs +++ b/crates/xenia-cpu/src/context.rs @@ -101,6 +101,12 @@ pub struct PpcContext { pub reserved_line: u32, pub reserved_val: u64, pub has_reservation: bool, + /// PPCBUG-151 — width of the active reservation: 4 = `lwarx` (word), + /// 8 = `ldarx` (doubleword), 0 = no reservation. `stwcx.` requires + /// width==4; `stdcx.` requires width==8. Cross-width pairs fail + /// deterministically with CR0.EQ=0. Cleared alongside `has_reservation` + /// on every `stwcx.`/`stdcx.` exit (success or failure). + pub reservation_width: u8, /// M3.7 — generation stamp returned by [`crate::ReservationTable::reserve`] /// at the most recent `lwarx`/`ldarx`. Paired with `reserved_line`; /// `stwcx.`/`stdcx.` pass this back to `try_commit`. Meaningful only @@ -159,6 +165,7 @@ impl PpcContext { reserved_line: 0, reserved_val: 0, has_reservation: false, + reservation_width: 0, reserved_generation: 0, reservation_table: None, hw_id: 0, diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index c4694b5..63db6a4 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -1124,6 +1124,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.reserved_line = ea & !RESERVATION_MASK; ctx.reserved_val = val as u64; ctx.has_reservation = true; + ctx.reservation_width = 4; // PPCBUG-151: word reservation if let Some(t) = &ctx.reservation_table { if t.is_enabled() { ctx.reserved_generation = t.reserve(ea, ctx.hw_id); @@ -1140,17 +1141,21 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - .as_ref() .filter(|t| t.is_enabled()) .cloned(); + // PPCBUG-151: stwcx. requires a word (lwarx) reservation; + // a doubleword (ldarx) reservation must not commit here. + let width_ok = ctx.reservation_width == 4; let success = if let Some(t) = &table_route { // Table-routed: success iff the slot still holds our // reservation AND the per-ctx flag agrees (the per-ctx // flag would be cleared by an intervening write or // context switch). ctx.has_reservation + && width_ok && ctx.reserved_line == line && t.try_commit(ea, ctx.reserved_generation, ctx.hw_id) } else { // Legacy per-ctx path (M2 default). - ctx.has_reservation && ctx.reserved_line == line + ctx.has_reservation && width_ok && ctx.reserved_line == line }; if success { mem.write_u32(ea, ctx.gpr[instr.rs()] as u32); @@ -1176,6 +1181,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - } } ctx.has_reservation = false; + ctx.reservation_width = 0; // PPCBUG-151: always clear on exit ctx.pc += 4; } @@ -4282,6 +4288,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.reserved_line = ea & !RESERVATION_MASK; ctx.reserved_val = val; ctx.has_reservation = true; + ctx.reservation_width = 8; // PPCBUG-151: doubleword reservation if let Some(t) = &ctx.reservation_table { if t.is_enabled() { ctx.reserved_generation = t.reserve(ea, ctx.hw_id); @@ -4297,12 +4304,16 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - .as_ref() .filter(|t| t.is_enabled()) .cloned(); + // PPCBUG-151: stdcx. requires a doubleword (ldarx) reservation; + // a word (lwarx) reservation must not commit here. + let width_ok = ctx.reservation_width == 8; let success = if let Some(t) = &table_route { ctx.has_reservation + && width_ok && ctx.reserved_line == line && t.try_commit(ea, ctx.reserved_generation, ctx.hw_id) } else { - ctx.has_reservation && ctx.reserved_line == line + ctx.has_reservation && width_ok && ctx.reserved_line == line }; if success { mem.write_u64(ea, ctx.gpr[instr.rs()]); @@ -4324,6 +4335,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - } } ctx.has_reservation = false; + ctx.reservation_width = 0; // PPCBUG-151: always clear on exit ctx.pc += 4; } PpcOpcode::ldbrx => { @@ -6077,6 +6089,86 @@ mod tests { /// Regression: `lvebx` must preserve the prior contents of the /// destination VR for lanes other than the loaded byte. Previously + // ---------- PPCBUG-151: cross-width reservation pairs must fail ---------- + + /// PPCBUG-151: `lwarx` (width=4) followed by `stdcx.` (requires width=8) + /// must fail with CR0.EQ=0. Memory must remain unchanged. + #[test] + fn lwarx_then_stdcx_cross_width_fails() { + let table = std::sync::Arc::new(crate::ReservationTable::new()); + table.enable(); + + let mut ctx = PpcContext::new(); + ctx.reservation_table = Some(table.clone()); + ctx.hw_id = 0; + let mut mem = TestMem::new(); + + // r4=0x1000 (target addr), r5=0 (index), r6=value to (attempt to) store. + ctx.gpr[4] = 0x1000; + ctx.gpr[5] = 0; + ctx.gpr[6] = 0xDEAD_BEEF_CAFE_BABEu64; + + // Instr 0: lwarx r3, r4, r5 (opcode 31, XO 20, Rc=0) + let lwarx = (31u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (20 << 1); + write_instr(&mut mem, 0, lwarx); + // Instr 1: stdcx. r6, r4, r5 (opcode 31, XO 214, Rc=1) + let stdcx = (31u32 << 26) | (6 << 21) | (4 << 16) | (5 << 11) | (214 << 1) | 1; + write_instr(&mut mem, 4, stdcx); + + // Execute lwarx — must set a word reservation (width=4). + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(ctx.has_reservation, "lwarx must set has_reservation"); + assert_eq!(ctx.reservation_width, 4, "lwarx must set reservation_width=4"); + + // Execute stdcx. — width mismatch (needs 8, got 4); must fail. + step(&mut ctx, &mut mem); + assert!(!ctx.cr[0].eq, "stdcx. must fail when reservation was set by lwarx (cross-width)"); + // Memory at 0x1000-0x1007 must be unchanged (still zero). + assert_eq!(mem.read_u64(0x1000), 0, "stdcx. must not write on cross-width failure"); + // Width must be cleared on exit. + assert_eq!(ctx.reservation_width, 0, "stdcx. must clear reservation_width on exit"); + } + + /// PPCBUG-151: `ldarx` (width=8) followed by `stwcx.` (requires width=4) + /// must fail with CR0.EQ=0. Memory must remain unchanged. + #[test] + fn ldarx_then_stwcx_cross_width_fails() { + let table = std::sync::Arc::new(crate::ReservationTable::new()); + table.enable(); + + let mut ctx = PpcContext::new(); + ctx.reservation_table = Some(table.clone()); + ctx.hw_id = 0; + let mut mem = TestMem::new(); + + // r4=0x1000 (target addr), r5=0 (index), r6=value to (attempt to) store. + ctx.gpr[4] = 0x1000; + ctx.gpr[5] = 0; + ctx.gpr[6] = 0xCCCC_CCCCu64; + + // Instr 0: ldarx r3, r4, r5 (opcode 31, XO 84, Rc=0) + let ldarx = (31u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (84 << 1); + write_instr(&mut mem, 0, ldarx); + // Instr 1: stwcx. r6, r4, r5 (opcode 31, XO 150, Rc=1) + let stwcx = (31u32 << 26) | (6 << 21) | (4 << 16) | (5 << 11) | (150 << 1) | 1; + write_instr(&mut mem, 4, stwcx); + + // Execute ldarx — must set a doubleword reservation (width=8). + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert!(ctx.has_reservation, "ldarx must set has_reservation"); + assert_eq!(ctx.reservation_width, 8, "ldarx must set reservation_width=8"); + + // Execute stwcx. — width mismatch (needs 4, got 8); must fail. + step(&mut ctx, &mut mem); + assert!(!ctx.cr[0].eq, "stwcx. must fail when reservation was set by ldarx (cross-width)"); + // Memory at 0x1000 must be unchanged (still zero). + assert_eq!(mem.read_u32(0x1000), 0, "stwcx. must not write on cross-width failure"); + // Width must be cleared on exit. + assert_eq!(ctx.reservation_width, 0, "stwcx. must clear reservation_width on exit"); + } + /// the handler started from a zeroed buffer. #[test] fn test_lvebx_preserves_other_lanes() {