fix(cpu): PPCBUG-006/008/018/019/028/029/030/031/033 4a active poisoning

Phase 4 batch 1: 9 PPCBUGs in the active-poisoning sub-section. All
follow the pattern `!val` on u64, which unconditionally flips the upper
32 bits and poisons the GPR even with clean inputs — every execution
corrupts the high 32 bits regardless of upstream state.

Sub/neg family:
- PPCBUG-006 negx: `(!ra).wrapping_add(1)` on u64 + neg_ov_64 checks
  64-bit INT_MIN. Fix: do arithmetic in u32, OE checks PPC[ra32==0x80000000].
- PPCBUG-008 subfex: same shape as above plus 64-bit unsigned CA compare.
  Fix: cast all operands to u32, compute, write `as u64`.
- PPCBUG-018 subfzex: `!ra` on u64. Fix: u32 arithmetic.
- PPCBUG-019 subfmex: `!ra` on u64 + always-true CA edge (`!ra != 0`
  was always true for clean ra<0xFFFFFFFF because high bits of !u64
  are non-zero). Fix: u32 arithmetic; CA predicate now correct.

Logical NOT family:
- PPCBUG-028 orcx: rs | !rb on u64 → high-bit poison.
- PPCBUG-029 norx: !(rs|rb) — the `not` simplified mnemonic. Hot path,
  every `not` corrupted GPR upper 32 bits.
- PPCBUG-030 nandx: !(rs&rb).
- PPCBUG-031 eqvx: !(rs^rb). The common `eqv rA,rA,rA` set-to-all-ones
  idiom now produces 0x00000000_FFFFFFFF instead of 0xFFFFFFFF_FFFFFFFF.
- PPCBUG-033 andcx: rs & !rb.

CR0 update at every Rc=1 path now uses `as u32 as i32 as i64` so a result
with bit 31 set gets classified as negative under the 32-bit ABI (was
positive before because upper bits were ones; will be positive in new
truncated form unless we cast through i32). This pre-emptively addresses
PPCBUG-020 for these specific opcodes; the catch-all sweep in batch 6
covers the remaining sites.

Tests:
- nego_sets_ov_only_on_int_min: updated from i64::MIN → 0x80000000 (32-bit).
- test_subfze_carry_only_when_ra_zero_and_ca_one: result expectations
  updated from u64::MAX → 0xFFFFFFFF (low 32 bits, upper 32 zero).
- New: neg_clean_input_no_upper_bits (PPCBUG-006 regression).
- New: norx_not_simplified_keeps_upper_bits_clean (PPCBUG-029 regression).
- New: eqvx_self_self_self_sets_low32_to_all_ones (PPCBUG-031 regression).
- New: andcx_bit_clear_keeps_upper_clean (PPCBUG-033 regression).
- New: subfex_clean_inputs_no_upper_bits (PPCBUG-008 regression).
- New: subfmex_ra_max_ca_zero_clears_ca (PPCBUG-019 always-true CA fix).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-02 11:35:05 +02:00
parent f424132a5b
commit e18a0a40b8

View File

@@ -266,65 +266,71 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::subfex => { PpcOpcode::subfex => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-008: 32-bit ABI. Compute in u32 space — `!ra` on u64 always
let rb = ctx.gpr[instr.rb()]; // pollutes the upper 32 bits, making this an active poisoner.
let ca = ctx.xer_ca as u64; let ra32 = ctx.gpr[instr.ra()] as u32;
let result = (!ra).wrapping_add(rb).wrapping_add(ca); let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.xer_ca = if rb > ra || (rb == ra && ca != 0) { 1 } else { 0 }; let ca = ctx.xer_ca as u32;
ctx.gpr[instr.rd()] = result; let result32 = (!ra32).wrapping_add(rb32).wrapping_add(ca);
ctx.xer_ca = if rb32 > ra32 || (rb32 == ra32 && ca != 0) { 1 } else { 0 };
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
// RT <- !RA + RB + CA == RB - RA - 1 + CA // RT <- !RA + RB + CA == RB - RA - 1 + CA (32-bit semantics).
let true_sum = (rb as i64 as i128) - (ra as i64 as i128) - 1 + (ca as i128); let true_sum = (rb32 as i32 as i128) - (ra32 as i32 as i128) - 1 + (ca as i128);
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64));
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::subfzex => { PpcOpcode::subfzex => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-018: same active-poisoning shape as subfex; operate in u32.
let ca = ctx.xer_ca as u64; let ra32 = ctx.gpr[instr.ra()] as u32;
let result = (!ra).wrapping_add(ca); let ca = ctx.xer_ca as u32;
// RT <- !RA + CA (no -1 term). 64-bit carry-out only when let result32 = (!ra32).wrapping_add(ca);
// !RA = u64::MAX (i.e. RA = 0) AND CA = 1. // RT <- !RA + CA (no -1 term). 32-bit carry-out only when
ctx.xer_ca = if ra == 0 && ca != 0 { 1 } else { 0 }; // !ra32 = u32::MAX (i.e. ra32 = 0) AND ca = 1.
ctx.gpr[instr.rd()] = result; ctx.xer_ca = if ra32 == 0 && ca != 0 { 1 } else { 0 };
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
// RT <- !RA + CA == -RA - 1 + CA let true_sum = -(ra32 as i32 as i128) - 1 + (ca as i128);
let true_sum = -(ra as i64 as i128) - 1 + (ca as i128); overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64));
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result));
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::subfmex => { PpcOpcode::subfmex => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-019: also fixes the always-true CA edge — `!ra` on u64
let ca = ctx.xer_ca as u64; // is non-zero when ra32==0xFFFFFFFF and ca==0, so CA was stuck at 1.
let result = (!ra).wrapping_add(ca).wrapping_sub(1); let ra32 = ctx.gpr[instr.ra()] as u32;
ctx.xer_ca = if (!ra) != 0 || ca != 0 { 1 } else { 0 }; let ca = ctx.xer_ca as u32;
ctx.gpr[instr.rd()] = result; let result32 = (!ra32).wrapping_add(ca).wrapping_sub(1);
ctx.xer_ca = if (!ra32) != 0 || ca != 0 { 1 } else { 0 };
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
// RT <- !RA + CA + (-1) == -RA - 2 + CA let true_sum = -(ra32 as i32 as i128) - 2 + (ca as i128);
let true_sum = -(ra as i64 as i128) - 2 + (ca as i128); overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64));
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result));
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::negx => { PpcOpcode::negx => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-006: 32-bit ABI. `(!ra).wrapping_add(1)` on u64 always
let result = (!ra).wrapping_add(1); // sets upper 32 bits — every neg poisoned the GPR. neg_ov also
ctx.gpr[instr.rd()] = result; // checks at 64-bit INT_MIN; should be 32-bit INT_MIN.
let ra32 = ctx.gpr[instr.ra()] as u32;
let result32 = (!ra32).wrapping_add(1);
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
overflow::apply(ctx, overflow::neg_ov_64(ra)); overflow::apply(ctx, ra32 == 0x8000_0000);
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
@@ -497,8 +503,11 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::andcx => { PpcOpcode::andcx => {
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & !ctx.gpr[instr.rb()]; // PPCBUG-033: !rb on u64 flips upper 32 bits — active poisoning.
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } let rs32 = ctx.gpr[instr.rs()] as u32;
let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.ra()] = (rs32 & !rb32) as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::orx => { PpcOpcode::orx => {
@@ -507,8 +516,11 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::orcx => { PpcOpcode::orcx => {
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | !ctx.gpr[instr.rb()]; // PPCBUG-028: same shape as andcx — operate in u32.
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } let rs32 = ctx.gpr[instr.rs()] as u32;
let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.ra()] = (rs32 | !rb32) as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::xorx => { PpcOpcode::xorx => {
@@ -517,18 +529,28 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::norx => { PpcOpcode::norx => {
ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] | ctx.gpr[instr.rb()]); // PPCBUG-029: `not` simplified mnemonic — every `not` poisoned the GPR.
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } let rs32 = ctx.gpr[instr.rs()] as u32;
let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.ra()] = (!(rs32 | rb32)) as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::nandx => { PpcOpcode::nandx => {
ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] & ctx.gpr[instr.rb()]); // PPCBUG-030: same shape — operate in u32.
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } let rs32 = ctx.gpr[instr.rs()] as u32;
let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.ra()] = (!(rs32 & rb32)) as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::eqvx => { PpcOpcode::eqvx => {
ctx.gpr[instr.ra()] = !(ctx.gpr[instr.rs()] ^ ctx.gpr[instr.rb()]); // PPCBUG-031: `eqv rA, rA, rA` is a common "set to all-ones" idiom;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } // 64-bit form gave 0xFFFFFFFFFFFFFFFF but 32-bit ABI expects 0x00000000FFFFFFFF.
let rs32 = ctx.gpr[instr.rs()] as u32;
let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.ra()] = (!(rs32 ^ rb32)) as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4; ctx.pc += 4;
} }
@@ -5067,17 +5089,115 @@ mod tests {
#[test] #[test]
fn nego_sets_ov_only_on_int_min() { fn nego_sets_ov_only_on_int_min() {
// PPCBUG-006: 32-bit ABI. INT_MIN is 0x80000000 (low 32), not 0x8000000000000000.
let mut ctx = PpcContext::new(); let mut ctx = PpcContext::new();
let mut mem = TestMem::new(); let mut mem = TestMem::new();
// nego r5, r3 (XO=104, OE=1) // nego r5, r3 (XO=104, OE=1)
ctx.gpr[3] = i64::MIN as u64; ctx.gpr[3] = 0x8000_0000;
let raw = (31 << 26) | (5 << 21) | (3 << 16) | (1 << 10) | (104 << 1); let raw = (31 << 26) | (5 << 21) | (3 << 16) | (1 << 10) | (104 << 1);
write_instr(&mut mem, 0, raw); write_instr(&mut mem, 0, raw);
ctx.pc = 0; ctx.pc = 0;
step(&mut ctx, &mut mem); step(&mut ctx, &mut mem);
assert_eq!(ctx.xer_ov, 1); assert_eq!(ctx.xer_ov, 1);
// -INT_MIN wraps to INT_MIN // -INT_MIN wraps to INT_MIN (low 32 bits) with upper 32 bits zero.
assert_eq!(ctx.gpr[5], i64::MIN as u64); assert_eq!(ctx.gpr[5], 0x0000_0000_8000_0000);
}
#[test]
fn neg_clean_input_no_upper_bits() {
// PPCBUG-006 regression: neg r3=5 must produce 0x00000000_FFFFFFFB,
// not 0xFFFFFFFF_FFFFFFFB (the 64-bit !ra-then-add-1 result).
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 5;
let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (104 << 1);
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFB);
}
#[test]
fn norx_not_simplified_keeps_upper_bits_clean() {
// PPCBUG-029: `not rA, rB` (norx with rs==rb) is the canonical not
// simplified mnemonic. 64-bit !val poisons upper 32 bits of every
// execution; under the 32-bit ABI we must truncate.
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0x0000_0000_0000_00FF;
// norx r5, r3, r3 (XO=124)
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (3 << 11) | (124 << 1);
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FF00, "upper 32 bits must be zero");
}
#[test]
fn eqvx_self_self_self_sets_low32_to_all_ones() {
// PPCBUG-031: `eqv rA, rA, rA` is a common "set-to-all-ones" idiom.
// 64-bit !(0^0) gives u64::MAX (0xFFFFFFFF_FFFFFFFF); 32-bit ABI
// expects 0x00000000_FFFFFFFF.
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0;
// eqvx r3, r3, r3 (XO=284)
let raw = (31u32 << 26) | (3 << 21) | (3 << 16) | (3 << 11) | (284 << 1);
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[3], 0x0000_0000_FFFF_FFFF);
}
#[test]
fn andcx_bit_clear_keeps_upper_clean() {
// PPCBUG-033: `andc rA, rS, rB` = rS & !rB. 64-bit !rB poisons.
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0xFFFF_FFFF; // rS
ctx.gpr[4] = 0x000F; // rB (low bits to clear)
// andcx r5, r3, r4 (XO=60)
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (60 << 1);
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFF0);
}
#[test]
fn subfex_clean_inputs_no_upper_bits() {
// PPCBUG-008: 32-bit ABI. RT = !RA + RB + CA. RA=5, RB=10, CA=1
// → !5u32 = 0xFFFFFFFA, +10 = 0x100000004, +1 = 0x100000005, low32 = 5.
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 5;
ctx.gpr[4] = 10;
ctx.xer_ca = 1;
// subfex r5, r3, r4 (XO=136)
let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (136 << 1);
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 5);
assert_eq!(ctx.xer_ca, 1, "rb>=ra → CA=1 (10 > 5)");
}
#[test]
fn subfmex_ra_max_ca_zero_clears_ca() {
// PPCBUG-019: `subfme` with RA=u32::MAX and CA=0 should set CA=0
// (because !u32::MAX = 0). The buggy code's `!ra != 0` predicate
// on u64 was always true (because !u64-cast-of-u32::MAX has high
// bits flipped non-zero), wrongly setting CA=1.
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0xFFFF_FFFFu64;
ctx.xer_ca = 0;
// subfmex r5, r3 (XO=232)
let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (232 << 1);
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.xer_ca, 0, "RA=u32::MAX, CA=0 → !RA32==0, CA=0");
} }
// ---------- Phase 2 fixes: trap TO-field ---------- // ---------- Phase 2 fixes: trap TO-field ----------
@@ -6086,7 +6206,8 @@ mod tests {
ctx.xer_ca = 0; ctx.xer_ca = 0;
step(&mut ctx, &mem); step(&mut ctx, &mem);
assert_eq!(ctx.xer_ca, 0, "ra=0, ca=0 should produce CA=0"); assert_eq!(ctx.xer_ca, 0, "ra=0, ca=0 should produce CA=0");
assert_eq!(ctx.gpr[3], u64::MAX, "result = !0 + 0 = u64::MAX"); // PPCBUG-018: 32-bit ABI. !0u32 + 0 = u32::MAX, with upper 32 bits zero.
assert_eq!(ctx.gpr[3], 0xFFFF_FFFFu64, "result = !0u32 + 0 = u32::MAX");
} }
// Case 3: ra=1, ca=0 → CA=0 (old buggy code reported CA=1) // Case 3: ra=1, ca=0 → CA=0 (old buggy code reported CA=1)
{ {
@@ -6098,21 +6219,20 @@ mod tests {
ctx.xer_ca = 0; ctx.xer_ca = 0;
step(&mut ctx, &mem); step(&mut ctx, &mem);
assert_eq!(ctx.xer_ca, 0, "ra=1, ca=0 should produce CA=0"); assert_eq!(ctx.xer_ca, 0, "ra=1, ca=0 should produce CA=0");
assert_eq!(ctx.gpr[3], u64::MAX - 1, "result = !1 + 0 = u64::MAX - 1"); // PPCBUG-018: 32-bit ABI. !1u32 + 0 = u32::MAX - 1, with upper 32 bits zero.
assert_eq!(ctx.gpr[3], 0xFFFF_FFFEu64, "result = !1u32 + 0 = u32::MAX - 1");
} }
// Case 4: ra=u64::MAX, ca=0 → CA=0 (old buggy code reported CA=1 // Case 4: ra=u32::MAX, ca=1 → CA=0; result = !u32::MAX + 1 = 1.
// because !ra == 0 only here, which the buggy `!ra != 0` predicate
// happened to handle right; flip ca=1 to exercise the other arm)
{ {
let mut ctx = PpcContext::new(); let mut ctx = PpcContext::new();
let mem = TestMem::new(); let mem = TestMem::new();
write_instr(&mem, 0, raw); write_instr(&mem, 0, raw);
ctx.pc = 0; ctx.pc = 0;
ctx.gpr[4] = u64::MAX; ctx.gpr[4] = 0xFFFF_FFFFu64;
ctx.xer_ca = 1; ctx.xer_ca = 1;
step(&mut ctx, &mem); step(&mut ctx, &mem);
assert_eq!(ctx.xer_ca, 0, "ra=u64::MAX, ca=1 should produce CA=0"); assert_eq!(ctx.xer_ca, 0, "ra=u32::MAX, ca=1 should produce CA=0");
assert_eq!(ctx.gpr[3], 1, "result = !u64::MAX + 1 = 1"); assert_eq!(ctx.gpr[3], 1, "result = !u32::MAX + 1 = 1");
} }
} }