From 94d9c2fc9f282cbe9a5b7ff4694a58205630b995 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:59:24 -0800 Subject: [PATCH 1/4] cpu: R10000 index cache ops address both ways (full cache model) On the R10000, Index_WB_Inv, Index_Load/Store_Tag and Index_Load/Store_Data take the index within one way from the low address bits and the way from bit 0. The full (data-carrying) R10000 model in mips_cache_v2 is direct-mapped at the full cache size and ignored bit 0, so a whole-cache flush -- every index of way 0, then of way 1 -- reached only the lower half of L1I, L1D and L2. Dirty lines in the upper half outlived the flush and were written back later, over pages that had been reused in the meantime. Map way 1 to the upper half of each cache, for the tag ops and the data ops alike, so the two still name the same line. Only R10000Cache is affected. IP28 machines use R10000ShadowCache, which is out of the data path and already decodes bit 0 as the way. Found running OpenBSD/sgi 6.9 on an emulated O2 (R10000) that used R10000Cache: forked ksh processes died with SIGBUS and "write after free" as stale lines landed on their copy-on-write pages. With the fix, 3 x 300 subshells run clean. Test: r10000_index_flush_reaches_both_ways (fails without the fix). Co-Authored-By: Claude Opus 5.5 --- src/cpu/mips_cache_v2.rs | 78 ++++++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/src/cpu/mips_cache_v2.rs b/src/cpu/mips_cache_v2.rs index 62383e50..1fb5b33c 100644 --- a/src/cpu/mips_cache_v2.rs +++ b/src/cpu/mips_cache_v2.rs @@ -1825,6 +1825,23 @@ impl usize { + let half = 1usize << (lines_shift - 1); + (((addr >> line_shift) as usize) & (half - 1)) | ((addr as usize & 1) << (lines_shift - 1)) + } + + /// The same for an R10000 Index_Load/Store_Data: a doubleword slot in a + /// data array of `slots` doublewords. + #[inline] + fn r10k_way_slot(addr: u64, slots: usize) -> usize { + let half = slots / 2; + ((addr as usize >> 3) & (half - 1)) | ((addr as usize & 1) * half) + } + fn pidx(&self, virt_addr: u64) -> u32 { ((virt_addr >> L2_PIDX_VADDR_SHIFT) & L2_PIDX_VADDR_MASK as u64) as u32 } @@ -3973,20 +3990,17 @@ impl return 0, C_R10K_ILD if matches!(sel, CACH_PI | CACH_PD | CACH_SD) => { let is_l2 = sel == CACH_SD; - // The index is a byte offset into the data array. Bit 0 - // selects the way on real silicon; this model is - // direct-mapped, and bit 0 falls below the u64 slot index, - // so it drops out without any special case. - let slot = (virt_addr as usize) >> 3; + // The index is a byte offset into one way's data, and bit + // 0 selects the way: the same folding as the tag ops + // below, so data and tags name the same line. if is_l2 && HAS_L2 { - return self.l2.data()[slot & (L2_DATA - 1)]; + return self.l2.data()[Self::r10k_way_slot(virt_addr, L2_DATA)]; } - return self.dc.data()[slot & (DC_DATA - 1)]; + return self.dc.data()[Self::r10k_way_slot(virt_addr, DC_DATA)]; } C_R10K_ISD if matches!(sel, CACH_SI | CACH_SD) => { - let slot = (virt_addr as usize) >> 3; if HAS_L2 { - self.l2.data_mut()[slot & (L2_DATA - 1)] = phys_addr; + self.l2.data_mut()[Self::r10k_way_slot(virt_addr, L2_DATA)] = phys_addr; } return 0; } @@ -4027,8 +4041,24 @@ impl> 14) as usize & 1) << Self::IC_NUM_LINES_SHIFT) } else { set } @@ -5262,6 +5292,34 @@ mod tests { // Flush a single L2 set to memory. `phys` is any address within the L2 line. // Only valid when L2 is present (r4k or r5k+r5ksc). + /// A whole-cache flush the way an R10000 kernel does it -- every index of + /// one way, then of the other, bit 0 picking the way -- reaches every line, + /// including those this direct-mapped model keeps in its upper half. + #[test] + fn r10000_index_flush_reaches_both_ways() { + let mem = Arc::new(Memory::new(MEM_MB)); + let cache: R10000Cache = make_cache_of(mem.clone()); + // Upper half of the L1D (VA bit 14) and of the 1 MB L2 (PA bit 19). + let cases = [(0x9800_0000_0000_6040u64, 0x2040u32), (0x9800_0000_0008_2080, 0x8_2080)]; + for (i, &(va, pa)) in cases.iter().enumerate() { + let _ = cache.write::<4>(va, pa as u64, 0xa5a5_0000 + i as u64); + } + for va in (0..16 * 1024u64).step_by(32) { + for way in 0..2 { + cache.cache_op(C_IWBINV | CACH_PD, 0x8000_0000 | va | way, 0); + } + } + for va in (0..512 * 1024u64).step_by(128) { + for way in 0..2 { + let a = 0x8000_0000 | va | way; + cache.cache_op(C_IWBINV | CACH_SD, a, a); + } + } + for (i, &(_, pa)) in cases.iter().enumerate() { + assert_eq!(mem_read(&mem, pa), 0xa5a5_0000 + i as u32, "line at {pa:#x} not flushed"); + } + } + fn flush_l2_to_mem(cache: &R4400Cache, phys: u32) { let l2_set = (phys as usize >> R4400Cache::L2_LINE_SHIFT as usize) & (R4400Cache::L2_SIZE / R4400Cache::L2_LINE - 1); let lp = (l2_set * R4400Cache::L2_LINE) as u64; From b03b8848184915b8f042f81667e1d0883db57be5 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:11:40 -0800 Subject: [PATCH 2/4] cpu: the full R10000 cache model has 44 virtual address bits CpuCache took CpuModel's default of 40 for every model, while the shadow model says 44 for the R10000, as the part implements. XContext's fields are laid out around that width, so a 64-bit kernel's TLB refill handler on a full-model R10000 would find the wrong page-table entry. Latent: no machine runs the full R10000 model (IP28 uses the shadow). Co-Authored-By: Claude Opus 5.5 --- src/cpu/mips_cache_v2.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/cpu/mips_cache_v2.rs b/src/cpu/mips_cache_v2.rs index 1fb5b33c..d88437ab 100644 --- a/src/cpu/mips_cache_v2.rs +++ b/src/cpu/mips_cache_v2.rs @@ -3499,6 +3499,9 @@ impl "R5000", model::R10000 => "R10000", @@ -5320,6 +5323,16 @@ mod tests { } } + /// The R10000 implements 44 virtual address bits; XContext's fields are + /// laid out around them, so the full model must say so too. + #[test] + fn the_r10000_full_model_has_44_va_bits() { + assert_eq!(::VA_BITS, 44); + assert_eq!(::VA_BITS, 44); + assert_eq!(::VA_BITS, 40); + assert_eq!(::VA_BITS, 40); + } + fn flush_l2_to_mem(cache: &R4400Cache, phys: u32) { let l2_set = (phys as usize >> R4400Cache::L2_LINE_SHIFT as usize) & (R4400Cache::L2_SIZE / R4400Cache::L2_LINE - 1); let lp = (l2_set * R4400Cache::L2_LINE) as u64; From a8147425ad8f185f767111eed82b9fc7df9ac0ba Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:13:13 -0800 Subject: [PATCH 3/4] cpu: no debug print on every R10000 secondary Index_Store_Tag The full R10000 model printed a line for every secondary-cache Index_Store_Tag: 32768 of them each time firmware initialises the cache. Co-Authored-By: Claude Opus 5.5 --- src/cpu/mips_cache_v2.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cpu/mips_cache_v2.rs b/src/cpu/mips_cache_v2.rs index d88437ab..cbba95ef 100644 --- a/src/cpu/mips_cache_v2.rs +++ b/src/cpu/mips_cache_v2.rs @@ -4185,9 +4185,6 @@ impl> 13) & L2_PTAG_MASK; let state = (tag_lo >> 10) & 0x7; From d25a38c03a2e44b1575c8d54ea8b07d6be714d9c Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:17:32 -0800 Subject: [PATCH 4/4] cpu: the R10000 resolves a virtual colour mismatch instead of raising VCE Virtual coherency exceptions are an R4000/R4400 feature. The R10000 keeps the virtual index in its secondary tags but, on a hit under a different index, writes back and drops the primary copies made under the old one and records the new one. The full model raised the R4400's VCEI/VCED there, which an R10000 kernel never expects (OpenBSD dies on the first page it maps at two colours). Test: one line under two colours, VCE on the R4400, coherent on the R10000. Co-Authored-By: Claude Opus 5.5 --- src/cpu/mips_cache_v2.rs | 94 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/src/cpu/mips_cache_v2.rs b/src/cpu/mips_cache_v2.rs index cbba95ef..362dc580 100644 --- a/src/cpu/mips_cache_v2.rs +++ b/src/cpu/mips_cache_v2.rs @@ -1375,6 +1375,10 @@ impl(va1, pa).status, BUS_OK); + assert_eq!(r4k.read::<4>(va2, pa).status, BUS_VCE, "the R4400 raises VCE"); + + let mem = Arc::new(Memory::new(MEM_MB)); + mem_write(&mem, pa as u32, 0x1111_2222); + let r10k: R10000Cache = make_cache_of(mem.clone()); + assert_eq!(r10k.read::<4>(va1, pa).data, 0x1111_2222); + let r = r10k.read::<4>(va2, pa); + assert_eq!((r.status, r.data), (BUS_OK, 0x1111_2222), "the R10000 carries on"); + assert_eq!(r10k.write::<4>(va2, pa, 0x3333_4444), BUS_OK); + assert_eq!(r10k.read::<4>(va1, pa).data, 0x3333_4444, "the old colour's copy was dropped"); + } + /// The R10000 implements 44 virtual address bits; XContext's fields are /// laid out around them, so the full model must say so too. #[test]