[Subsystem-fixes] 6 verified ours-vs-canary divergence fixes
From the 2026-06-12 5-subsystem differential audit. All verified against canary as oracle; 660/660 workspace tests green (655 + 5 new). 1. nt_create_event polarity (exports.rs) — `manual_reset = gpr[5] != 0` was INVERTED. Canary xboxkrnl_threading.cc:668 `Initialize(!event_type,..)` + xevent.cc:41 (type 0 = NotificationEvent = manual, type 1 = Sync = auto). Now `== 0`. Was the dormant 2.AI fix on chore/portable-snapshot, never merged. The Ke-path was already correct; only the Nt-path was wrong. 2. 2.AF deadline drain (main.rs coord_pre_round) — expired KeWait/KeDelay deadlines never fired under load because advance_to_next_wake_if_due was only called in coord_idle_advance (no-Ready-threads path). Added a per-round drain loop; covers BOTH lockstep and parallel outer loops since both call coord_pre_round. Was the dormant 2.AF fix, never merged. 3. handle slab-recycle ABA guard (state.rs + scheduler.rs) — release_handle_slot (my round-34 regression) recycled a closed slot even with a thread still parked on it, risking a stale-waiter wake when the slot is re-minted. Added Scheduler::any_thread_waiting_on; decline to recycle a still-waited slot. 4. vpkpx pixel-pack (vmx.rs) — wrong field mapping (~100% mismatch). Now exact canary ppc_emit_altivec.cc:1795 shift/mask (red 6b out[15:10] from w[24:19], green out[9:5] from w[14:10], blue out[4:0] from w[7:3]; no fabricated alpha bit). +unit test. 5. VFS GDFX attribute plumbing (vfs/*, exports.rs query fns) — VfsEntry now carries the real on-disc attribute byte (GDFX dirent +12, canary disc_image_device.cc:136/154) instead of inferring directory-ness from path shape. Query exports report the real FILE_ATTRIBUTE_* bits. Candidate driver of the XamShowDirtyDiscErrorUI gate. +tests. 6. MmGetPhysicalAddress region-aware mirror (exports.rs) — flat 0x1FFFFFFF mask missed canary's +0x1000 host_address_offset for 0xE0000000+ mirror (memory.cc:2317). Read-only query; proven byte-identical 50M digest. +test. Investigated and intentionally NOT changed: - zero-on-recommit: no-op; ours has no region-reuse path (bump allocators, free is a stub). - 32-bit ALU writeback truncation (PPCBUG-020): documented-deliberate; premise (MSR.SF=0) is questionable but flipping it is out of scope here. - KeSetEvent/NtSetEvent return value: ours returns true previous state (hardware-faithful); canary returns constant 1 — NOT an ours bug. sylpheed_n50m golden will need re-baselining (legit behavior change). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -696,9 +696,36 @@ fn mm_create_kernel_stack(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut K
|
||||
}
|
||||
}
|
||||
|
||||
/// Region-aware guest-virtual → physical translation, matching canary's
|
||||
/// `Memory::GetPhysicalAddress` + `PhysicalHeap::GetPhysicalAddress`
|
||||
/// (`xenia-canary/src/xenia/memory.cc:528-545` and `:2317-2326`).
|
||||
///
|
||||
/// Canary `PhysicalHeap::GetPhysicalAddress`:
|
||||
/// ```c
|
||||
/// address -= heap_base_;
|
||||
/// if (heap_base_ >= 0xE0000000) { address += 0x1000; }
|
||||
/// return address;
|
||||
/// ```
|
||||
/// The three physical heap bases (0xA0000000 / 0xC0000000 / 0xE0000000) all
|
||||
/// alias the same 512 MB physical window, so `address - heap_base ==
|
||||
/// address & 0x1FFFFFFF` for each. The only region-specific delta is the
|
||||
/// `+0x1000` host-address-offset for the 0xE0000000+ 4 KB mirror — see
|
||||
/// `memory.h:368-372` (`host_address_offset` for `heap_base >= 0xE0000000`).
|
||||
/// For non-physical / sub-0x1FFFFFFF virtual addresses canary returns the
|
||||
/// address unchanged, which equals `address & 0x1FFFFFFF` there too.
|
||||
pub(crate) fn translate_physical_address(virt: u32) -> u32 {
|
||||
let phys = virt & 0x1FFF_FFFF;
|
||||
if virt >= 0xE000_0000 {
|
||||
phys + 0x1000
|
||||
} else {
|
||||
phys
|
||||
}
|
||||
}
|
||||
|
||||
fn mm_get_physical_address(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = virtual address -> return physical address
|
||||
ctx.gpr[3] &= 0x1FFF_FFFF; // Mask to 512MB physical
|
||||
// r3 = virtual address -> return physical address.
|
||||
// Region-aware, mirroring canary (see `translate_physical_address`).
|
||||
ctx.gpr[3] = translate_physical_address(ctx.gpr[3] as u32) as u64;
|
||||
}
|
||||
|
||||
fn mm_query_address_protect(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
||||
@@ -1480,20 +1507,35 @@ fn nt_query_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mu
|
||||
*size
|
||||
};
|
||||
|
||||
// Root-of-device opens (`game:\`, `cache:\`, `partition0`) strip to
|
||||
// an empty string post-prefix — see `open_vfs_file`'s synth path.
|
||||
// Games query these as directories (DirectoryObject probe), and
|
||||
// reporting `Directory=0` makes Sylpheed treat the open as "found a
|
||||
// non-directory where I expected a directory" and call
|
||||
// `XamShowDirtyDiscErrorUI`. Canary's `NtQueryInformationFile` pulls
|
||||
// the real file-system entry's kind; we key on path shape since we
|
||||
// don't model directory entries.
|
||||
let is_directory = path.is_empty()
|
||||
|| path.ends_with('/')
|
||||
|| path.ends_with(':');
|
||||
// Snapshot what we need from the handle, then drop the borrow so we can
|
||||
// re-resolve the path against the VFS for its real attribute byte.
|
||||
let path = path.clone();
|
||||
let size = live_size;
|
||||
let position = *position;
|
||||
|
||||
// Pull the REAL GDFX attribute byte (canary `disc_image_device.cc:154`)
|
||||
// for disc-backed handles by re-resolving the stored path. Root-of-device
|
||||
// opens (`game:\`, `cache:\`, `partition0`) strip to an empty string and
|
||||
// synth-stub opens have no VFS entry — for those we fall back to the
|
||||
// path-shape heuristic. Games query these as directories (DirectoryObject
|
||||
// probe), and reporting `Directory=0` makes Sylpheed treat the open as
|
||||
// "found a non-directory where I expected a directory" and call
|
||||
// `XamShowDirtyDiscErrorUI`.
|
||||
let vfs_attributes: Option<u32> = if path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
state
|
||||
.vfs
|
||||
.as_ref()
|
||||
.and_then(|vfs| vfs.stat(&path).ok())
|
||||
.map(|e| e.attributes)
|
||||
.filter(|&a| a != 0)
|
||||
};
|
||||
let is_directory = match vfs_attributes {
|
||||
Some(a) => (a & 0x10) != 0,
|
||||
None => path.is_empty() || path.ends_with('/') || path.ends_with(':'),
|
||||
};
|
||||
|
||||
// `FILE_ATTRIBUTE_DIRECTORY` (NT / Xbox) — advertised in
|
||||
// `FileNetworkOpenInformation.FileAttributes`; Sylpheed's async-I/O
|
||||
// worker queries with class=34 and the calling code checks this bit
|
||||
@@ -1532,10 +1574,13 @@ fn nt_query_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mu
|
||||
}
|
||||
mem.write_u64(file_info + 32, size);
|
||||
mem.write_u64(file_info + 40, size);
|
||||
let attrs = if is_directory {
|
||||
FILE_ATTRIBUTE_DIRECTORY
|
||||
} else {
|
||||
FILE_ATTRIBUTE_NORMAL
|
||||
// Prefer the real GDFX attribute byte; fall back to the
|
||||
// DIRECTORY/NORMAL split for root-of-device and synth-stub
|
||||
// handles that have no VFS entry.
|
||||
let attrs = match vfs_attributes {
|
||||
Some(a) => a,
|
||||
None if is_directory => FILE_ATTRIBUTE_DIRECTORY,
|
||||
None => FILE_ATTRIBUTE_NORMAL,
|
||||
};
|
||||
mem.write_u32(file_info + 48, attrs);
|
||||
mem.write_u32(file_info + 52, 0); // pad
|
||||
@@ -1738,7 +1783,18 @@ fn nt_query_full_attributes_file(ctx: &mut PpcContext, mem: &GuestMemory, state:
|
||||
mem.write_u32(out + 28, filetime as u32);
|
||||
mem.write_u64(out + 32, entry.size);
|
||||
mem.write_u64(out + 40, entry.size);
|
||||
let attrs: u32 = if entry.is_directory { 0x10 } else { 0x80 };
|
||||
// Use the REAL GDFX attribute byte forwarded by the VFS
|
||||
// (canary `disc_image_device.cc:154`) instead of a
|
||||
// path-shape guess. Disc rips never carry a 0-attribute
|
||||
// entry, but guard anyway so a synthesised/legacy entry
|
||||
// still advertises a sane DIRECTORY/NORMAL split.
|
||||
let attrs: u32 = if entry.attributes != 0 {
|
||||
entry.attributes
|
||||
} else if entry.is_directory {
|
||||
0x10
|
||||
} else {
|
||||
0x80
|
||||
};
|
||||
mem.write_u32(out + 48, attrs);
|
||||
mem.write_u32(out + 52, 0);
|
||||
}
|
||||
@@ -1859,6 +1915,7 @@ fn nt_query_directory_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut
|
||||
is_directory: e.is_directory,
|
||||
size: e.size,
|
||||
offset: e.offset,
|
||||
attributes: e.attributes,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
@@ -1909,7 +1966,12 @@ fn nt_query_directory_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut
|
||||
mem.write_u64(base + 0x20, 0);
|
||||
mem.write_u64(base + 0x28, entry.size);
|
||||
mem.write_u64(base + 0x30, entry.size);
|
||||
let attrs = if entry.is_directory {
|
||||
// Real GDFX attribute byte (canary `disc_image_device.cc:154`);
|
||||
// fall back to the directory/normal split only for legacy entries
|
||||
// that carry no attribute bits.
|
||||
let attrs = if entry.attributes != 0 {
|
||||
entry.attributes
|
||||
} else if entry.is_directory {
|
||||
FILE_ATTRIBUTE_DIRECTORY
|
||||
} else {
|
||||
FILE_ATTRIBUTE_NORMAL
|
||||
@@ -1985,9 +2047,21 @@ fn nt_close(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
||||
}
|
||||
|
||||
fn nt_create_event(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||
// r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state
|
||||
// r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state.
|
||||
//
|
||||
// Xenon DISPATCHER_HEADER `Type` (NT convention):
|
||||
// 0 = NotificationEvent (manual-reset)
|
||||
// 1 = SynchronizationEvent (auto-reset)
|
||||
// Canary: `xboxkrnl_threading.cc:668` `ev->Initialize(!event_type, !!initial_state)`
|
||||
// with `XEvent::Initialize(bool manual_reset, ...)` (xevent.cc:25) and
|
||||
// `InitializeNative` (xevent.cc:41 `case 0x00: manual_reset_ = true`).
|
||||
// So `manual_reset = (event_type == 0)`. The Ke-path
|
||||
// (`ensure_dispatcher_object`) was already correct; the Nt-path here was
|
||||
// inverted, mis-classifying Sylpheed's per-frame VSync gate (type=1 auto +
|
||||
// initial=1) as manual-reset+signaled → it stayed signaled forever and
|
||||
// tid=1's main loop spun ~2800x canary's 60Hz.
|
||||
let handle_ptr = ctx.gpr[3] as u32;
|
||||
let manual_reset = ctx.gpr[5] != 0;
|
||||
let manual_reset = ctx.gpr[5] == 0;
|
||||
let signaled = ctx.gpr[6] != 0;
|
||||
let handle = state.alloc_handle_for(KernelObject::Event {
|
||||
manual_reset,
|
||||
@@ -4823,12 +4897,14 @@ mod tests {
|
||||
is_directory: false,
|
||||
size: 0x1000,
|
||||
offset: 0,
|
||||
attributes: 0x81, // NORMAL | READONLY
|
||||
},
|
||||
xenia_vfs::VfsEntry {
|
||||
name: "dat".into(),
|
||||
is_directory: true,
|
||||
size: 0,
|
||||
offset: 0,
|
||||
attributes: 0x11, // DIRECTORY | READONLY
|
||||
},
|
||||
// A grandchild — must NOT appear in root enumeration.
|
||||
xenia_vfs::VfsEntry {
|
||||
@@ -4836,6 +4912,7 @@ mod tests {
|
||||
is_directory: false,
|
||||
size: 0x2000,
|
||||
offset: 0,
|
||||
attributes: 0x81,
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -4862,9 +4939,11 @@ mod tests {
|
||||
// NextEntryOffset.
|
||||
let mut cursor: u32 = 0;
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
let mut attrs: Vec<u32> = Vec::new();
|
||||
loop {
|
||||
let entry_base = buf + cursor;
|
||||
let name_len = mem.read_u32(entry_base + 0x3C) as usize;
|
||||
attrs.push(mem.read_u32(entry_base + 0x38));
|
||||
let mut bytes = Vec::with_capacity(name_len);
|
||||
for i in 0..name_len as u32 {
|
||||
bytes.push(mem.read_u8(entry_base + 0x40 + i));
|
||||
@@ -4877,6 +4956,12 @@ mod tests {
|
||||
cursor += next;
|
||||
}
|
||||
assert_eq!(names, vec!["default.xex", "dat"]);
|
||||
// The real GDFX attribute byte must be forwarded verbatim: the file
|
||||
// reports NORMAL|READONLY (no DIRECTORY bit), the directory reports
|
||||
// DIRECTORY|READONLY.
|
||||
assert_eq!(attrs, vec![0x81, 0x11]);
|
||||
assert_eq!(attrs[0] & 0x10, 0, "file must not advertise DIRECTORY");
|
||||
assert_ne!(attrs[1] & 0x10, 0, "dir must advertise DIRECTORY");
|
||||
// A second call on the same handle must return NO_MORE_FILES —
|
||||
// the cursor has advanced past the end.
|
||||
ctx.gpr[3] = handle as u64;
|
||||
@@ -6396,4 +6481,23 @@ mod tests {
|
||||
assert!(resolved.ends_with("etc/foo"));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// `MmGetPhysicalAddress` must be region-aware, matching canary's
|
||||
/// `PhysicalHeap::GetPhysicalAddress`: the 0xE0000000+ 4 KB mirror gets a
|
||||
/// `+0x1000` host-address-offset; every other region is a flat
|
||||
/// `& 0x1FFFFFFF` mask.
|
||||
#[test]
|
||||
fn mm_get_physical_address_region_aware() {
|
||||
// 0xE0000000 mirror: canary `address - heap_base (==addr & 0x1FFFFFFF)`
|
||||
// then `+ 0x1000`.
|
||||
assert_eq!(translate_physical_address(0xE000_0000), 0x0000_1000);
|
||||
assert_eq!(translate_physical_address(0xE000_5000), 0x0000_6000);
|
||||
assert_eq!(translate_physical_address(0xFFFF_F000), 0x1FFF_F000 + 0x1000);
|
||||
// 0xA0000000 / 0xC0000000 physical heaps: flat mask, no offset.
|
||||
assert_eq!(translate_physical_address(0xA000_0000), 0x0000_0000);
|
||||
assert_eq!(translate_physical_address(0xC012_3000), 0x0012_3000);
|
||||
// Virtual / already-physical (< 0x20000000): unchanged.
|
||||
assert_eq!(translate_physical_address(0x0012_3000), 0x0012_3000);
|
||||
assert_eq!(translate_physical_address(0x4012_3000), 0x0012_3000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,10 +721,20 @@ impl KernelState {
|
||||
/// recycle queue. No-op for the synthetic XAudio range (`>= 0xF000_0000`,
|
||||
/// AUDIT-048) and the reserved `< 0x1000` band. Call site: `nt_close`'s
|
||||
/// `objects.remove` branch when refcount reaches zero.
|
||||
///
|
||||
/// ABA guard (subsystem-audit 2026-06-12): never recycle a slot that a
|
||||
/// thread is still parked on. Without this, a closed slot could be
|
||||
/// re-minted for a new object and a signal on that new object would wake
|
||||
/// the stale waiter that was blocked on the OLD object at the same slot.
|
||||
/// Such a slot is simply leaked (it stays out of `free_handles`),
|
||||
/// reproducing the pre-R34 bump-only behaviour for that rare case.
|
||||
pub fn release_handle_slot(&mut self, handle: u32) {
|
||||
if handle < 0x1000 || handle >= 0xF000_0000 {
|
||||
return;
|
||||
}
|
||||
if self.scheduler.any_thread_waiting_on(handle) {
|
||||
return;
|
||||
}
|
||||
self.free_handles.push_back(handle);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user