From 5c4510824981e1a620912b1668ff82a2b388af0e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 2 May 2026 12:09:26 +0200 Subject: [PATCH] chore(audit): mark P4 PPCBUGs applied; append P4 progress section P4 phase merged at d945aea. Update audit-findings.md status fields (43 PPCBUGs marked applied) and append the P4 progress section to audit-report-2026-04-29.md. Co-Authored-By: Claude Sonnet 4.6 --- audit-findings.md | 84 +++++++++++++++++++------------------- audit-report-2026-04-29.md | 35 ++++++++++++++++ 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/audit-findings.md b/audit-findings.md index 8010165..7102fe6 100644 --- a/audit-findings.md +++ b/audit-findings.md @@ -30,7 +30,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-001 — addi sign-extension, no truncation - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:114-118 - **Symptom**: `addi rT, r0, -1` (= `li rT, -1`) writes `0xFFFFFFFF_FFFFFFFF` instead of `0x00000000_FFFFFFFF`. Identical shape to addis. - **Fix**: @@ -41,7 +41,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-002 — addic untruncated writeback + 64-bit CA compare - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:133-140 - **Symptom**: (a) GPR writeback not truncated (same shape as addi). (b) CA computed via 64-bit `result < ra` — Canary's `AddDidCarry` explicitly truncates both operands to int32 first. - **Fix**: @@ -56,7 +56,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-003 — addicx untruncated writeback + 64-bit CA + CR0 regression - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:141-150 - **Symptom**: same as PPCBUG-002 plus a CR0 regression: live code uses `update_cr_signed(0, result as i64)` (64-bit signed). The frozen snapshot in `ppc-manual/alu/addicx.md` shows the previously-correct `result as i32 as i64` form. Live code has drifted. - **Fix**: PPCBUG-002 fix plus `update_cr_signed(0, result32 as i32 as i64)`. @@ -65,7 +65,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-004 — mulli untruncated 64-bit signed product - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:159-164 - **Symptom**: RA read as full `i64`, product stored as `u64` without truncation. Per ISA in 32-bit ABI, both factors should be i32 and product should fit in 32 bits (overflow silently wraps per ISA). - **Fix**: @@ -78,7 +78,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-005 — subficx untruncated writeback + 64-bit CA compare - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:151-158 - **Symptom**: (a) `imm.wrapping_sub(ra)` on 64-bit values writes poisoned upper bits; sign-extended `imm` for negative SIMM has bits 32-63 set. (b) CA `imm >= ra` is 64-bit unsigned compare; wrong relative to Canary's 32-bit form. - **Fix**: @@ -93,7 +93,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-006 — negx active GPR poisoning + 64-bit OE overflow check - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:319-330 - **Symptom**: (a) `(!ra).wrapping_add(1)` unconditionally sets upper 32 bits to all-ones because `!ra` flips them. Even a clean `r3 = 5` produces `0xFFFFFFFF_FFFFFFFB` instead of `0x00000000_FFFFFFFB`. **This is active, not latent — every neg in 32-bit-ABI code poisons the GPR.** (b) `neg_ov_64` overflow predicate tests `ra == 0x8000_0000_0000_0000` (64-bit INT_MIN) instead of `ra == 0x0000_0000_8000_0000` (32-bit INT_MIN). - **Fix**: @@ -109,7 +109,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-007 — subfcx CA via 64-bit unsigned compare - **Severity**: HIGH (defensive — same shape as the compare that broke addis) -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:258 - **Symptom**: `if rb >= ra { 1 } else { 0 }` is the exact 64-bit unsigned compare that the addis bug exploited. Wrong CA when either operand has poisoned upper 32 bits. Apply defensively even if all upstream sources are cleaned, because a wrong CA bit is unrecoverable downstream. - **Fix**: @@ -124,7 +124,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-008 — subfex CA via 64-bit unsigned compare + `!ra` poisons writeback - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:268-284 - **Symptom**: (a) CA `if rb > ra || (rb == ra && ca != 0)` is 64-bit; same shape as PPCBUG-007. (b) Writeback uses `(!ra).wrapping_add(rb).wrapping_add(ca)` — `!ra` always sets upper 32 bits, guaranteed GPR poison even with clean inputs (same shape as PPCBUG-006). - **Fix**: @@ -139,7 +139,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-009 — mullwx untruncated 64-bit signed product - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:331-344 - **Symptom**: 32x32 multiply produces 64-bit signed `i64` product, written to GPR via `as u64` without truncation. When product overflows i32 (which `mullw_ov` correctly detects), upper 32 bits are non-zero and corrupt downstream 64-bit unsigned compares — same class as addis. - **Fix** (one line; OE handler unchanged): @@ -156,66 +156,66 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-011 — divwx CR0 update breaks after PPCBUG-010 fix - **Severity**: MEDIUM (coupled to PPCBUG-010 — must land together) -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:379 - **Symptom**: `update_cr_signed(0, ctx.gpr[instr.rd()] as i64)` accidentally works today because the sign-extended GPR has consistent sign in i64 view. After PPCBUG-010, GPR holds `0x00000000_FFFFFFFD` for `-3` and `as i64` reads positive — CR0.LT will be wrong for negative quotients. - **Fix**: `ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as u32 as i32 as i64);` ### PPCBUG-012 — addx writeback not truncated (latent) - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:167-179 - **Symptom**: 64-bit `wrapping_add` result written to GPR untruncated. Latent: only triggers if upstream operands have poisoned upper 32 bits. With PPCBUG-001 etc. unfixed, that invariant is broken — addx amplifies the poison. - **Fix**: `ctx.gpr[instr.rd()] = result as u32 as u64;` ### PPCBUG-013 — addcx writeback not truncated (latent) - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:180-193 - **Fix**: same shape as PPCBUG-012. ### PPCBUG-014 — addex writeback not truncated (latent) - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:194-209 - **Fix**: same shape as PPCBUG-012. ### PPCBUG-015 — addzex writeback not truncated (latent) - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:210-224 - **Fix**: same shape as PPCBUG-012. ### PPCBUG-016 — addmex writeback not truncated (latent + edge case) - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:225-240 - **Symptom**: same writeback issue plus the `wrapping_sub(1)` produces all-ones upper 32 bits when low 32 bits underflow — guaranteed poison even if inputs are clean (same shape as PPCBUG-006/008). - **Fix**: truncate operands and result to 32 bits. ### PPCBUG-017 — subfx writeback not truncated (latent) - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:241-253 - **Fix**: same shape as PPCBUG-012. ### PPCBUG-018 — subfzex writeback not truncated + `!ra` poisons - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:285-302 - **Symptom**: `(!ra).wrapping_add(ca)` flips upper 32 bits — guaranteed poison. - **Fix**: truncate ra to u32, do arithmetic on u32, write `as u64`. ### PPCBUG-019 — subfmex writeback poisoning + always-true CA edge - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:303-318 - **Symptom**: (a) writeback poisoned via `(!ra)`. (b) CA predicate `(!ra) != 0` is always true when ra has clean upper 32 bits (because `!ra` flips them) — so CA is always 1, even in the documented edge case where 32-bit `ra == 0xFFFFFFFF && ca == 0` should yield CA=0. - **Fix**: operate on u32, then `xer_ca = if (!ra32) != 0 || ca != 0 { 1 } else { 0 }`. ### PPCBUG-020 — CR0 update uses 64-bit signed compare in all sub-register ops - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Locations**: interpreter.rs:250, 264, 281, 299, 315, 327, 341, 379, 396, 410, 419, 428, 445, 462 (every Rc=1 path in groups 2-5) - **Symptom**: `update_cr_signed(0, result as i64)` views result as 64-bit signed. In 32-bit ABI, bit 31 determines LT/GT, not bit 63. A result like `0x00000000_80000000` is negative in 32-bit but positive in 64-bit — CR0.LT inverted. - **Fix (catch-all)**: change to `result as u32 as i32 as i64` everywhere. Once PPCBUG-001..-019 truncate writebacks, the upper 32 bits of `result` are zero and this distinction becomes moot — but applying both is cheap and provides defense in depth. @@ -237,7 +237,7 @@ Per-group reports: `audit-out/group-01-add-imm.md`, `group-02-add-reg.md`, `grou ### PPCBUG-023 — andisx CR0 update uses 64-bit signed compare; should use 32-bit - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:475 - **Symptom**: `update_cr_signed(0, ctx.gpr[instr.ra()] as i64)` interprets the result as 64-bit signed. The `andisx` result is bounded by `0x0000_0000_FFFF_0000`, which is always non-negative in 64-bit view. In 32-bit ABI, bit 31 is the sign bit — results with bit 31 set (e.g. `andis. rA, rS, 0x8000` with rS=0x80000000 → result=0x80000000) should yield CR0.LT=1, but xenia-rs gives CR0.GT=1. The ppc-manual frozen snapshot for `andisx` shows the correct `as i32 as i64` form; the live code has drifted. Common trigger: `andis. rA, rS, 0x8000` to test the sign bit of a 32-bit word. - **Fix**: @@ -264,7 +264,7 @@ Group 9 summary: core arithmetic is clean — `rlw_mask`, rotate logic, and resu ### PPCBUG-024 — rlwinmx CR0 update uses 64-bit signed compare; should use 32-bit - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:667 - **Symptom**: `update_cr_signed(0, ctx.gpr[instr.ra()] as i64)` — result is a zero-extended u32, so bit 31 set yields +2147483648 in 64-bit signed view but -2147483648 in 32-bit ABI. CR0.LT/GT inverted for results with bit 31 set. `rlwinm.` is the most common dot-form instruction in compiler output (all `slwi.`, `srwi.`, `clrlwi.`, bitfield-test-and-branch idioms). - **Fix**: `ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64);` @@ -272,7 +272,7 @@ Group 9 summary: core arithmetic is clean — `rlw_mask`, rotate logic, and resu ### PPCBUG-025 — rlwimix CR0 update uses 64-bit signed compare; should use 32-bit - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:679 - **Symptom**: same class as PPCBUG-024. `rlwimi.` is compiler-generated for struct bitfield writes; when the inserted value occupies or sets bit 31 of RA, CR0.LT is wrong. - **Fix**: `ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64);` @@ -280,7 +280,7 @@ Group 9 summary: core arithmetic is clean — `rlw_mask`, rotate logic, and resu ### PPCBUG-026 — rlwnmx CR0 update uses 64-bit signed compare; should use 32-bit - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:690 - **Symptom**: same class as PPCBUG-024. `rlwnm.` is less frequent but used in variable-shift normalisation patterns. - **Fix**: `ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64);` @@ -304,7 +304,7 @@ The group 7 subagent also flagged a CR0 regression across all 8 opcodes — that ### PPCBUG-028 — orcx active GPR poisoning - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:509-513 - **Symptom**: writes `rs | !rb`. Rust's `!` on `u64` flips all 64 bits — the upper 32 bits of `!rb` are unconditionally all-ones, OR'd into the result. With clean inputs `orc r5, r3, r4` writes `0xFFFFFFFF_xxxxxxxx`. Active poisoning, same shape as PPCBUG-006/008. - **Fix**: operate on u32, write `as u64`: @@ -316,35 +316,35 @@ The group 7 subagent also flagged a CR0 regression across all 8 opcodes — that ### PPCBUG-029 — norx active GPR poisoning (the `not` simplified mnemonic) - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:519-523 - **Symptom**: writes `!(rs | rb)` — outer `!` flips upper 32 bits unconditionally. **`nor rA, rS, rS` is the canonical `not` simplified mnemonic** used pervasively in PPC code; every `not` in 32-bit-ABI Xbox 360 binaries actively poisons the GPR. - **Fix**: u32 arithmetic, write `as u64`. ### PPCBUG-030 — nandx active GPR poisoning - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:524-528 - **Symptom**: writes `!(rs & rb)` — same shape as norx. The simplified mnemonic `nand` is also `nand rA, rS, rS` (= `nor . . .` in some assemblers). - **Fix**: u32 arithmetic. ### PPCBUG-031 — eqvx active GPR poisoning - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:529-533 - **Symptom**: writes `!(rs ^ rb)` — same shape. The idiom `eqv rA, rS, rS` "set rA to all-ones (i.e. -1 in 32-bit ABI)" produces `0xFFFFFFFF_FFFFFFFF` instead of `0x00000000_FFFFFFFF`. - **Fix**: u32 arithmetic. ### PPCBUG-032 — andx / orx / xorx writeback not truncated (latent) - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Locations**: interpreter.rs:494-498 (andx), 504-508 (orx), 514-518 (xorx) - **Symptom**: 64-bit bitwise on full GPR values. Latent — clean if both operands are clean; pollutes if either is poisoned upstream. - **Fix**: `as u32 as u64` truncation at writeback. Once all upstream poison sources are fixed, these become unnecessary; until then, defensive truncation. ### PPCBUG-033 — andcx active poisoning via `!rb` sub-expression - **Severity**: MEDIUM (the `!rb` always poisons; outer `&` masks it away when rs is clean — fully active when rs is poisoned) -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:499-503 - **Symptom**: writes `rs & !rb`. The `!rb` always has all-ones upper bits; if rs has clean upper bits (zero), the result is clean. If rs is poisoned upstream, the poison propagates AND the always-set bits in `!rb` make it look "guaranteed". This is closer to active than latent. - **Fix**: `(rs as u32) & !(rb as u32)` then `as u64`. @@ -355,7 +355,7 @@ Per-group report: `audit-out/group-08-extend-clz.md` (report uses local IDs PPCB ### PPCBUG-034 — extsbx writeback sign-extends to 64 bits - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:537 - **Symptom**: `as i8 as i64 as u64` — a byte with high bit set (0x80) writes `0xFFFFFFFF_FFFFFF80` instead of `0x00000000_FFFFFF80`. Active poisoning on every negative byte. `extsb` is emitted by compilers to canonicalize signed-byte arguments — common code path. - **Fix**: `ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i8 as i32 as u32 as u64;` @@ -364,21 +364,21 @@ Per-group report: `audit-out/group-08-extend-clz.md` (report uses local IDs PPCB ### PPCBUG-035 — extshx writeback sign-extends to 64 bits - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:542 - **Symptom**: `as i16 as i64 as u64` — same shape as PPCBUG-034 for halfwords. - **Fix**: `ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i16 as i32 as u32 as u64;` ### PPCBUG-036 — extsbx CR0 coupling - **Severity**: MEDIUM (must land in same commit as PPCBUG-034) -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:538 - **Symptom**: `update_cr_signed(0, ra as i64)` — currently latent because the unfixed sign-extended value's i64 sign matches bit 7 of the byte. After PPCBUG-034 lands, the truncated value's i64 view becomes always non-negative — CR0.LT will never fire for negative byte results. - **Fix**: `ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64);` — must land with PPCBUG-034. ### PPCBUG-037 — extshx CR0 coupling - **Severity**: MEDIUM (must land with PPCBUG-035) -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:543 - **Symptom**: same coupling shape as PPCBUG-036 for halfwords. @@ -425,7 +425,7 @@ Per-group report: `audit-out/group-11-shift.md` (uses local IDs PPCBUG-050..055; ### PPCBUG-041 — srawx writeback sign-extends to 64 bits - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Locations**: interpreter.rs:583, 588 (two writeback paths for the count<32 and count>=32 branches) - **Symptom**: `result as i64 as u64` violates the 32-bit-ABI zero-extension convention. A negative shifted value writes `0xFFFFFFFF_xxxxxxxx` instead of `0x00000000_xxxxxxxx`. - **Fix**: `result as u32 as u64` in both writeback paths. @@ -433,20 +433,20 @@ Per-group report: `audit-out/group-11-shift.md` (uses local IDs PPCBUG-050..055; ### PPCBUG-042 — srawix writeback sign-extends to 64 bits - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Locations**: interpreter.rs:600, 605 (same shape as PPCBUG-041 for srawi) - **Fix**: `result as u32 as u64`. ### PPCBUG-043 — srawx / srawix CR0 coupling - **Severity**: MEDIUM (must land with PPCBUG-041 and PPCBUG-042) -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Locations**: interpreter.rs:593, 607 - **Symptom**: currently masked by the sign-extended writeback (sign-extension makes the 64-bit and 32-bit sign agree). After truncating the writeback, `as i64` will misread the sign for negative results. - **Fix**: `as u32 as i32 as i64` in both Rc=1 paths, applied with PPCBUG-041/042. ### PPCBUG-044 — slwx / srwx CR0 misclassifies negative 32-bit results - **Severity**: LOW (zero-extended results have bit 31 set in low 32, but always positive in i64 view → CR0.LT never fires for slw/srw with bit-31-set results) -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Locations**: interpreter.rs:568, 576 - **Fix**: `as u32 as i32 as i64`. @@ -994,7 +994,7 @@ and zero unit tests for all nine opcodes. ### PPCBUG-095 — `lha`: GPR writeback sign-extends to 64 bits - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:990 - **Symptom**: `mem.read_u16(ea) as i16 as i64 as u64` — memory `0x8000` writes `0xFFFFFFFF_FFFF8000` instead of `0x00000000_FFFF8000`. Active GPR poisoning for every @@ -1008,7 +1008,7 @@ and zero unit tests for all nine opcodes. ### PPCBUG-096 — `lhax`: GPR writeback sign-extends to 64 bits - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:996 - **Symptom**: identical to PPCBUG-095. Indexed form emitted for array access with GPR index. - **Fix**: `mem.read_u16(ea) as i16 as i32 as u32 as u64` @@ -1016,7 +1016,7 @@ and zero unit tests for all nine opcodes. ### PPCBUG-097 — `lhau`: GPR writeback sign-extends to 64 bits - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:1007 - **Symptom**: identical to PPCBUG-095. Update form emitted for auto-incrementing `short[]` loops; poison accumulates across all iterations. @@ -1025,7 +1025,7 @@ and zero unit tests for all nine opcodes. ### PPCBUG-098 — `lhaux`: GPR writeback sign-extends to 64 bits - **Severity**: HIGH -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Location**: interpreter.rs:1013 - **Symptom**: identical to PPCBUG-095, update+indexed form. - **Fix**: `mem.read_u16(ea) as i16 as i32 as u32 as u64` @@ -1072,7 +1072,7 @@ any store instruction, breaking multi-threaded `lwarx`/`stwcx.` atomicity under ### PPCBUG-105 — lwa / lwax / lwaux sign-extend to 64 bits; 32-bit-ABI hazard - **Severity**: MEDIUM -- **Status**: open +- **Status**: applied (P4 d945aea, 2026-05-02) - **Locations**: interpreter.rs:1032 (lwa), 1038 (lwax), 1043 (lwaux) - **Symptom**: `mem.read_u32(ea) as i32 as i64 as u64` — a word with high bit set (e.g. `0x8000_0000`) writes `0xFFFF_FFFF_8000_0000` to rD. ISA-correct for 64-bit-mode `lwa`. In 32-bit ABI, the poisoned diff --git a/audit-report-2026-04-29.md b/audit-report-2026-04-29.md index 75a6b12..734bb40 100644 --- a/audit-report-2026-04-29.md +++ b/audit-report-2026-04-29.md @@ -365,6 +365,41 @@ After applying Phase 1 alone, run `xenia-rs check sylpheed.iso -n 4B --parallel` --- +### P4 — 32-bit ABI writeback truncation sweep (merged 2026-05-02, HEAD d945aea) + +**PPCBUGs fixed**: ~43 IDs across the 4a/4b/4c/4d sub-sections. +- 4a active poisoning: 006 (negx), 008 (subfex), 018 (subfzex), 019 (subfmex), 028 (orcx), 029 (norx), 030 (nandx), 031 (eqvx), 033 (andcx) +- 4a/4d coupled: 034+035+036+037 (extsbx/extshx writeback + CR0) +- 4b immediate ALU: 001 (addi), 002 (addic), 003 (addicx), 004 (mulli), 005 (subficx), 007 (subfcx CA) +- 4b mul/div + srawx coupled: 009 (mullwx), 010+011 (divwx + CR0), 041+042+043 (srawx/srawix + CR0) +- 4b loads: 095-098 (lha/lhax/lhau/lhaux), 105 (lwa/lwax/lwaux) +- 4c latent: 012-017 (addx/addcx/addex/addzex/addmex/subfx), 032 (andx/orx/xorx CR0) +- 4d CR0 catch-all: 020 (in mulhwx/mulhwux/divwux/andx/orx/xorx/cntlzwx etc.), 023 (andisx), 024 (rlwinmx), 025 (rlwimix), 026 (rlwnmx), 044 (slwx/srwx) + +**Batches**: +- Batch 1 (e18a0a4): 4a active poisoning NOT/SUB family — 9 PPCBUGs +- Batch 2 (145a7a4): 4a/4d coupled extsbx+extshx+CR0 — 4 PPCBUGs (must land together) +- Batch 3 (bf8208e): 4b immediate ALU — 6 PPCBUGs +- Batch 4 (82a9bff): 4b mul/div + srawx coupled — 6 PPCBUGs (two coupling groups) +- Batch 5 (20a730d): 4b halfword + lwa loads — 5 PPCBUGs +- Batch 6 (16993bb): 4c latent + 4d CR0 catch-all — ~13 PPCBUGs +- Review-fix (49103bb): subfx/subfcx OE predicate + mulli test rigor + +**Phase invariants restored**: every 32-bit ABI GPR write zero-extends from a u32 result, every CR0 update views the result as i32, every CA bit comes from a 32-bit unsigned compare. Downstream 64-bit unsigned compares (the addis-incident shape) can no longer be fed polluted upper bits from any of the 40+ touched ALU sites. The frozen-snapshot drift detected in PPCBUG-003 (addicx CR0) and PPCBUG-023 (andisx CR0) is also resolved. + +**Review findings**: +- BLOCKING issue caught: subfx and subfcx OE handlers in batch 6 still used the legacy `sum_overflow_64` helper. The helper compares the 32-bit `true_diff` against a u64 view of the result; any legitimate i32::MIN result (bit 31 set) spuriously triggered OV=1. Fixed in 49103bb with two new discriminating regression tests. +- Minor caught: `mulli_overflow_wraps_to_32` rubber-stamped — both pre/post fix wrote 0 for the chosen inputs. Redesigned to use polluted-upper-bits inputs that genuinely discriminate. + +**Gate results**: +- `cargo test --workspace --release`: **494 passed, 0 failed** (up from 470 at P3 merge; 24 new regression tests across the batches) +- 64-bit ABI ops verified untouched: rldicl/rldicr/rldic/rldimi/rldcl/rldcr, sldx/srdx/sradx/sradix, mulhdx/mulhdux/mulldx, divdx/divdux, cntlzdx, extswx +- **Acid test** `-n 4B --parallel --reservations-table`: deferred per user direction + +**Conclusion**: P4 is the largest ABI-correctness sweep of the audit. The systemic invariant is restored. Next: P5 — FPU correctness (~30 IDs). + +--- + ## Index — every PPCBUG referenced (in numerical order) This list intentionally includes every ID found in `audit-findings.md` so nothing is dropped. For each entry's full description / file:line / fix snippet / test recommendation, see the corresponding `### PPCBUG-NNN` heading in `audit-findings.md`.