fix(kernel): KRNBUG-IO-004 — real XamNotifyCreateListener + XNotifyGetNext per canary
Canary's RegisterNotifyListener (kernel_state.cc:1013-1033) auto-enqueues four
startup notifications on the first listener whose mask covers kXNotifySystem
(SystemUI=0x09 + SystemSignInChanged=0x0A) and kXNotifyLive
(LiveConnectionChanged=0x02000001 + LiveLinkStateChanged=0x02000003). XNotifyGetNext
(xam_notify.cc:22-96) pops the queue with mask + version filtering on enqueue per
xnotifylistener.cc:38-51. Our prior stubs returned 0 forever; the dispatch loop
at 0x822f1be8 in sub_822F1AA8 was thus bypassed indefinitely.
Implementation:
- KernelObject::NotifyListener { mask, max_version, queue, waiters } variant.
- KernelState::has_notified_startup + has_notified_live_startup gates.
- xam_notify_create_listener: mask=r3 (qword), max_version=r4 (clamped <=10),
alloc handle, conditional 4-tuple startup enqueue.
- xnotify_get_next: handle/match_id/id_ptr/param_ptr in r3..r6; pop_front
(or scan-by-id), with mask + version filter applied at enqueue time.
- 5 unit tests covering: full-mask 4 startup notifications, second-listener
no re-fire, system-only mask filtering, max_version=0 too-new drop,
unknown handle returning 0.
Tests: 594 -> 599. Lockstep `-n 100M` instructions=100000012 deterministic
across 2 reruns; bit-identical run-to-run diff.
Cascade (verified at -n 500M):
- dispatch arm 0x822f1be8 fires; sub_82173DC8 entered.
- 3/21 renderer-cluster L1 PCs newly reached: 0x822c6870 (2 workers),
0x824563e0, 0x823ddb50.
- canary-only export delta 7 -> 3 (reclassified to fired:
KeResetEvent, ObCreateSymbolicLink, XamTaskCloseHandle, XamTaskSchedule).
- worker thread count 18 -> 20.
- signal_attempts on handle 0x15e0 = 1 (primary=1), was 0.
- draws=0 still expected at this step.
LOC: 119 (97 impl + 22 scaffolding pattern matches across main.rs / objects.rs
/ state.rs) <= 120.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3230,6 +3230,9 @@ fn dump_thread_diagnostic(
|
|||||||
KernelObject::Mutex { owner, recursion, waiters } => {
|
KernelObject::Mutex { owner, recursion, waiters } => {
|
||||||
(format!("Mutex(owner={:?}, rec={})", owner, recursion), waiters)
|
(format!("Mutex(owner={:?}, rec={})", owner, recursion), waiters)
|
||||||
}
|
}
|
||||||
|
KernelObject::NotifyListener { mask, queue, waiters, .. } => {
|
||||||
|
(format!("NotifyListener(mask={:#x}, pending={})", mask, queue.len()), waiters)
|
||||||
|
}
|
||||||
KernelObject::File { .. } => continue,
|
KernelObject::File { .. } => continue,
|
||||||
};
|
};
|
||||||
if waiters.is_empty() {
|
if waiters.is_empty() {
|
||||||
@@ -3281,7 +3284,8 @@ fn dump_thread_diagnostic(
|
|||||||
| KernelObject::Semaphore { waiters, .. }
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
| KernelObject::Thread { waiters, .. }
|
| KernelObject::Thread { waiters, .. }
|
||||||
| KernelObject::Timer { waiters, .. }
|
| KernelObject::Timer { waiters, .. }
|
||||||
| KernelObject::Mutex { waiters, .. } => Some(waiters.len()),
|
| KernelObject::Mutex { waiters, .. }
|
||||||
|
| KernelObject::NotifyListener { waiters, .. } => Some(waiters.len()),
|
||||||
KernelObject::File { .. } => None,
|
KernelObject::File { .. } => None,
|
||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
@@ -3357,7 +3361,8 @@ fn dump_thread_diagnostic(
|
|||||||
| KernelObject::Semaphore { waiters, .. }
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
| KernelObject::Thread { waiters, .. }
|
| KernelObject::Thread { waiters, .. }
|
||||||
| KernelObject::Timer { waiters, .. }
|
| KernelObject::Timer { waiters, .. }
|
||||||
| KernelObject::Mutex { waiters, .. } => Some(waiters.len()),
|
| KernelObject::Mutex { waiters, .. }
|
||||||
|
| KernelObject::NotifyListener { waiters, .. } => Some(waiters.len()),
|
||||||
KernelObject::File { .. } => None,
|
KernelObject::File { .. } => None,
|
||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
@@ -3515,7 +3520,8 @@ fn dump_thread_diagnostic(
|
|||||||
| KernelObject::Semaphore { waiters, .. }
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
| KernelObject::Timer { waiters, .. }
|
| KernelObject::Timer { waiters, .. }
|
||||||
| KernelObject::Thread { waiters, .. }
|
| KernelObject::Thread { waiters, .. }
|
||||||
| KernelObject::Mutex { waiters, .. } => waiters.iter().copied().collect(),
|
| KernelObject::Mutex { waiters, .. }
|
||||||
|
| KernelObject::NotifyListener { waiters, .. } => waiters.iter().copied().collect(),
|
||||||
KernelObject::File { .. } => Vec::new(),
|
KernelObject::File { .. } => Vec::new(),
|
||||||
};
|
};
|
||||||
for w in waiters {
|
for w in waiters {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Kernel object tracking for HLE.
|
//! Kernel object tracking for HLE.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use xenia_cpu::ThreadRef;
|
use xenia_cpu::ThreadRef;
|
||||||
@@ -74,6 +75,12 @@ pub enum KernelObject {
|
|||||||
recursion: u32,
|
recursion: u32,
|
||||||
waiters: Vec<ThreadRef>,
|
waiters: Vec<ThreadRef>,
|
||||||
},
|
},
|
||||||
|
NotifyListener {
|
||||||
|
mask: u64,
|
||||||
|
max_version: u32,
|
||||||
|
queue: VecDeque<(u32, u32)>,
|
||||||
|
waiters: Vec<ThreadRef>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KernelObject {
|
impl KernelObject {
|
||||||
@@ -87,7 +94,8 @@ impl KernelObject {
|
|||||||
| KernelObject::Semaphore { waiters, .. }
|
| KernelObject::Semaphore { waiters, .. }
|
||||||
| KernelObject::Thread { waiters, .. }
|
| KernelObject::Thread { waiters, .. }
|
||||||
| KernelObject::Timer { waiters, .. }
|
| KernelObject::Timer { waiters, .. }
|
||||||
| KernelObject::Mutex { waiters, .. } => Some(waiters),
|
| KernelObject::Mutex { waiters, .. }
|
||||||
|
| KernelObject::NotifyListener { waiters, .. } => Some(waiters),
|
||||||
KernelObject::File { .. } => None,
|
KernelObject::File { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,12 @@ pub struct KernelState {
|
|||||||
/// privilege numbers have already produced a `tracing::info!` line so
|
/// privilege numbers have already produced a `tracing::info!` line so
|
||||||
/// the import-hot path doesn't spam at -n 500M.
|
/// the import-hot path doesn't spam at -n 500M.
|
||||||
pub xex_priv_logged: std::collections::HashSet<u32>,
|
pub xex_priv_logged: std::collections::HashSet<u32>,
|
||||||
|
/// Whether the first listener whose mask covers `kXNotifySystem` has
|
||||||
|
/// been registered — gate for the two startup system notifications
|
||||||
|
/// per `kernel_state.cc:1020-1025`.
|
||||||
|
pub has_notified_startup: bool,
|
||||||
|
/// Same for `kXNotifyLive` per `kernel_state.cc:1026-1032`.
|
||||||
|
pub has_notified_live_startup: bool,
|
||||||
/// Next thread ID. M2.4: atomic.
|
/// Next thread ID. M2.4: atomic.
|
||||||
pub next_thread_id: std::sync::atomic::AtomicU32,
|
pub next_thread_id: std::sync::atomic::AtomicU32,
|
||||||
/// Virtual file system for NtCreateFile/NtReadFile/etc. The app mounts
|
/// Virtual file system for NtCreateFile/NtReadFile/etc. The app mounts
|
||||||
@@ -269,6 +275,8 @@ impl KernelState {
|
|||||||
image_base: 0,
|
image_base: 0,
|
||||||
xex_system_flags: 0,
|
xex_system_flags: 0,
|
||||||
xex_priv_logged: std::collections::HashSet::new(),
|
xex_priv_logged: std::collections::HashSet::new(),
|
||||||
|
has_notified_startup: false,
|
||||||
|
has_notified_live_startup: false,
|
||||||
next_thread_id: AtomicU32::new(1),
|
next_thread_id: AtomicU32::new(1),
|
||||||
vfs: None,
|
vfs: None,
|
||||||
ui: None,
|
ui: None,
|
||||||
|
|||||||
@@ -355,14 +355,113 @@ fn xam_get_system_version(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut
|
|||||||
|
|
||||||
// ===== Notify =====
|
// ===== Notify =====
|
||||||
|
|
||||||
fn xam_notify_create_listener(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
|
const K_X_NOTIFY_SYSTEM: u64 = 0x00000001;
|
||||||
let handle = state.alloc_handle();
|
const K_X_NOTIFY_LIVE: u64 = 0x00000002;
|
||||||
|
const K_X_NOTIFICATION_SYSTEM_UI: u32 = 0x09;
|
||||||
|
const K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED: u32 = 0x0A;
|
||||||
|
const K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED: u32 = 0x02000001;
|
||||||
|
const K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED: u32 = 0x02000003;
|
||||||
|
|
||||||
|
fn notification_mask_index(id: u32) -> u32 {
|
||||||
|
(id >> 25) & 0x3F
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notification_version(id: u32) -> u32 {
|
||||||
|
(id >> 16) & 0x1FF
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enqueue_notification(obj: &mut KernelObject, id: u32, data: u32) {
|
||||||
|
if let KernelObject::NotifyListener { mask, max_version, queue, .. } = obj {
|
||||||
|
let idx = notification_mask_index(id);
|
||||||
|
if (*mask & (1u64 << idx)) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if notification_version(id) > *max_version {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue.push_back((id, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xam_notify_create_listener(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
|
let mask = ctx.gpr[3];
|
||||||
|
let mut max_version = ctx.gpr[4] as u32;
|
||||||
|
if max_version > 10 {
|
||||||
|
max_version = 10;
|
||||||
|
}
|
||||||
|
let handle = state.alloc_handle_for(KernelObject::NotifyListener {
|
||||||
|
mask,
|
||||||
|
max_version,
|
||||||
|
queue: std::collections::VecDeque::new(),
|
||||||
|
waiters: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut startup_pending: Vec<(u32, u32)> = Vec::new();
|
||||||
|
if !state.has_notified_startup && (mask & K_X_NOTIFY_SYSTEM) != 0 {
|
||||||
|
state.has_notified_startup = true;
|
||||||
|
startup_pending.push((K_X_NOTIFICATION_SYSTEM_UI, 0));
|
||||||
|
startup_pending.push((K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1));
|
||||||
|
}
|
||||||
|
if !state.has_notified_live_startup && (mask & K_X_NOTIFY_LIVE) != 0 {
|
||||||
|
state.has_notified_live_startup = true;
|
||||||
|
startup_pending.push((K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED, 0x001510F1));
|
||||||
|
startup_pending.push((K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED, 0));
|
||||||
|
}
|
||||||
|
if let Some(obj) = state.objects.get_mut(&handle) {
|
||||||
|
for (id, data) in startup_pending {
|
||||||
|
enqueue_notification(obj, id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = mem;
|
||||||
ctx.gpr[3] = handle as u64;
|
ctx.gpr[3] = handle as u64;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xnotify_get_next(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) {
|
fn xnotify_get_next(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
|
||||||
// r3 = handle, r4 = id_ptr, r5 = param_ptr
|
let handle = ctx.gpr[3] as u32;
|
||||||
ctx.gpr[3] = 0; // FALSE (no notifications)
|
let match_id = ctx.gpr[4] as u32;
|
||||||
|
let id_ptr = ctx.gpr[5] as u32;
|
||||||
|
let param_ptr = ctx.gpr[6] as u32;
|
||||||
|
|
||||||
|
if param_ptr != 0 {
|
||||||
|
mem.write_u32(param_ptr, 0);
|
||||||
|
}
|
||||||
|
if id_ptr == 0 {
|
||||||
|
ctx.gpr[3] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mem.write_u32(id_ptr, 0);
|
||||||
|
|
||||||
|
let Some(KernelObject::NotifyListener { queue, .. }) = state.objects.get_mut(&handle) else {
|
||||||
|
ctx.gpr[3] = 0;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let dequeued = if match_id != 0 {
|
||||||
|
let pos = queue.iter().position(|&(id, _)| id == match_id);
|
||||||
|
match pos {
|
||||||
|
Some(p) => {
|
||||||
|
let (id, data) = queue.remove(p).unwrap();
|
||||||
|
Some((id, data))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queue.pop_front()
|
||||||
|
};
|
||||||
|
|
||||||
|
match dequeued {
|
||||||
|
Some((id, data)) => {
|
||||||
|
mem.write_u32(id_ptr, id);
|
||||||
|
if param_ptr != 0 {
|
||||||
|
mem.write_u32(param_ptr, data);
|
||||||
|
}
|
||||||
|
ctx.gpr[3] = 1;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
ctx.gpr[3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Session =====
|
// ===== Session =====
|
||||||
@@ -472,4 +571,99 @@ mod tests {
|
|||||||
xget_avpack(&mut ctx, &mem, &mut state);
|
xget_avpack(&mut ctx, &mem, &mut state);
|
||||||
assert_eq!(ctx.gpr[3], 8);
|
assert_eq!(ctx.gpr[3], 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn drain_notifications(state: &mut KernelState, mem: &GuestMemory, handle: u32) -> Vec<(u32, u32)> {
|
||||||
|
let id_ptr = SCRATCH_BASE + 0x100;
|
||||||
|
let param_ptr = SCRATCH_BASE + 0x104;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
loop {
|
||||||
|
let mut ctx = PpcContext::default();
|
||||||
|
ctx.gpr[3] = handle as u64;
|
||||||
|
ctx.gpr[4] = 0;
|
||||||
|
ctx.gpr[5] = id_ptr as u64;
|
||||||
|
ctx.gpr[6] = param_ptr as u64;
|
||||||
|
xnotify_get_next(&mut ctx, mem, state);
|
||||||
|
if ctx.gpr[3] == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let id = mem.read_u32(id_ptr);
|
||||||
|
let data = mem.read_u32(param_ptr);
|
||||||
|
out.push((id, data));
|
||||||
|
if out.len() > 16 {
|
||||||
|
panic!("runaway dequeue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_listener(state: &mut KernelState, mem: &GuestMemory, mask: u64, max_version: u32) -> u32 {
|
||||||
|
let mut ctx = PpcContext::default();
|
||||||
|
ctx.gpr[3] = mask;
|
||||||
|
ctx.gpr[4] = max_version as u64;
|
||||||
|
xam_notify_create_listener(&mut ctx, mem, state);
|
||||||
|
ctx.gpr[3] as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_listener_with_full_mask_gets_4_startup_notifications() {
|
||||||
|
let (_ctx, mem, mut state) = fresh();
|
||||||
|
let h = create_listener(&mut state, &mem, 0x2F, 10);
|
||||||
|
let drained = drain_notifications(&mut state, &mem, h);
|
||||||
|
assert_eq!(
|
||||||
|
drained,
|
||||||
|
vec![
|
||||||
|
(K_X_NOTIFICATION_SYSTEM_UI, 0),
|
||||||
|
(K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1),
|
||||||
|
(K_X_NOTIFICATION_LIVE_CONNECTION_CHANGED, 0x001510F1),
|
||||||
|
(K_X_NOTIFICATION_LIVE_LINK_STATE_CHANGED, 0),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn second_listener_does_not_re_fire_startup() {
|
||||||
|
let (_ctx, mem, mut state) = fresh();
|
||||||
|
let h1 = create_listener(&mut state, &mem, 0x2F, 10);
|
||||||
|
let _ = drain_notifications(&mut state, &mem, h1);
|
||||||
|
let h2 = create_listener(&mut state, &mem, 0x2F, 10);
|
||||||
|
assert!(drain_notifications(&mut state, &mem, h2).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn system_only_mask_filters_live() {
|
||||||
|
let (_ctx, mem, mut state) = fresh();
|
||||||
|
let h = create_listener(&mut state, &mem, K_X_NOTIFY_SYSTEM, 10);
|
||||||
|
let drained = drain_notifications(&mut state, &mem, h);
|
||||||
|
assert_eq!(
|
||||||
|
drained,
|
||||||
|
vec![
|
||||||
|
(K_X_NOTIFICATION_SYSTEM_UI, 0),
|
||||||
|
(K_X_NOTIFICATION_SYSTEM_SIGN_IN_CHANGED, 1),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_filter_drops_too_new() {
|
||||||
|
let (_ctx, mem, mut state) = fresh();
|
||||||
|
let h = create_listener(&mut state, &mem, 0x2F, 0);
|
||||||
|
let drained = drain_notifications(&mut state, &mem, h);
|
||||||
|
let kept_versions: Vec<u32> = drained.iter().map(|&(id, _)| notification_version(id)).collect();
|
||||||
|
assert!(kept_versions.iter().all(|&v| v == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xnotify_get_next_returns_zero_for_unknown_handle() {
|
||||||
|
let (mut ctx, mem, mut state) = fresh();
|
||||||
|
let id_ptr = SCRATCH_BASE + 0x100;
|
||||||
|
let param_ptr = SCRATCH_BASE + 0x104;
|
||||||
|
ctx.gpr[3] = 0xDEAD_BEEF;
|
||||||
|
ctx.gpr[4] = 0;
|
||||||
|
ctx.gpr[5] = id_ptr as u64;
|
||||||
|
ctx.gpr[6] = param_ptr as u64;
|
||||||
|
xnotify_get_next(&mut ctx, &mem, &mut state);
|
||||||
|
assert_eq!(ctx.gpr[3], 0);
|
||||||
|
assert_eq!(mem.read_u32(id_ptr), 0);
|
||||||
|
assert_eq!(mem.read_u32(param_ptr), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user