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) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-01 17:44:48 +02:00
parent d4e227eeab
commit a107ac9ae7
2 changed files with 101 additions and 2 deletions

View File

@@ -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,

View File

@@ -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() {