//! Five real EL0 entries: A, B, faulty C, then A and B again. EL0 IRQs masked. //! K536's separate feature unmasks the physical timer only at three EL1 exit //! checkpoints, after Exited publication and before page retirement/reaping. //! Independent ProcessId generations, three private roots, and explicit //! kernel-root/TLBI retirement precede every physical-page return. use super::*; use turkos_kernel::{ elf, process::{ProcessId, ProcessTable, State}, process_copy::{OwnedPage, ProcessPages}, user_copy::EFAULT, }; static mut SPACES: [Option; 3] = [const { None }; 3]; static mut IDS: [Option; 3] = [None; 3]; static mut PROCESSES: ProcessTable = ProcessTable::new(); static mut ALLOCATOR: *mut memory::FrameAllocator = core::ptr::null_mut(); static mut OWNED_ALLOCATOR: core::mem::MaybeUninit = core::mem::MaybeUninit::uninit(); static mut KERNEL: u64 = 0; static mut BEFORE: u64 = 0; static mut STEP: usize = 0; static mut TARGET: u64 = 0; #[cfg(feature = "process-exit-irq-test")] static mut EXIT_IRQ_ACTIVE: bool = false; #[cfg(feature = "process-exit-irq-test")] static mut EXIT_IRQ_INDEX: usize = 0; #[cfg(feature = "process-exit-irq-test")] static mut EXIT_IRQ_CODE: i32 = 0; #[cfg(feature = "process-exit-irq-test")] static mut EXIT_IRQ_SEEN: u64 = 0; #[cfg(feature = "process-exit-irq-test")] static mut EXIT_IRQ_FREE: u64 = 0; unsafe extern "C" { static __stack_top: u8; #[cfg(feature = "process-exit-irq-test")] static __stack_start: u8; static __process_good_start: u8; static __process_good_end: u8; static __process_bad_start: u8; static __process_bad_end: u8; } fn fail(reason: &str) -> ! { write_str("TURKOS/PROCESS-ERROR: "); write_line(reason); park() } fn id(index: usize) -> ProcessId { unsafe { (*core::ptr::addr_of!(IDS))[index] }.unwrap_or_else(|| fail("missing identity")) } fn table() -> &'static mut ProcessTable { // SAFETY: separate single-CPU, IRQ-masked acceptance image; no reentry. unsafe { &mut *core::ptr::addr_of_mut!(PROCESSES) } } fn space(index: usize) -> &'static mmu::UserSpace { unsafe { (&*core::ptr::addr_of!(SPACES))[index].as_ref() } .unwrap_or_else(|| fail("retired process selected")) } pub(super) fn run( allocator: &mut memory::FrameAllocator, tables: &mmu::Tables, layout: mmu::Layout, ) -> ! { let before = allocator.remaining_pages(); if elf::parse_layout(b"bad ELF").is_ok() || allocator.remaining_pages() != before { fail("bad ELF allocation rollback"); } write_line("TURKOS/PROCESS-ELF-REJECT: NO-PROCESS NO-FRAMES PASS"); unsafe { ALLOCATOR = allocator as *mut _; KERNEL = tables.root; BEFORE = before; } let ctr: usize; unsafe { asm!("mrs {v}, CTR_EL0", v = out(reg) ctr, options(nomem, nostack)) }; let line = 4usize << ((ctr >> 16) & 15); for index in 0..3 { let process = table().create().unwrap_or_else(|_| fail("process create")); let user = unsafe { tables.build_user_space(allocator, layout.ram_base, layout.ram_size) } .unwrap_or_else(|_| fail("private space")); if !user.contract_valid(tables.root) { fail("private contract"); } let (start, end) = if index == 2 { ( core::ptr::addr_of!(__process_bad_start) as usize, core::ptr::addr_of!(__process_bad_end) as usize, ) } else { ( core::ptr::addr_of!(__process_good_start) as usize, core::ptr::addr_of!(__process_good_end) as usize, ) }; let bytes = end.checked_sub(start).unwrap_or(0); if bytes == 0 || bytes > memory::PAGE_SIZE as usize { fail("code length"); } unsafe { core::ptr::copy_nonoverlapping(start as *const u8, user.code_frame() as *mut u8, bytes) }; for offset in (0..bytes).step_by(line) { unsafe { asm!("dc cvau, {p}", p = in(reg) (user.code_frame() as usize + offset), options(nostack)) }; } write_str("TURKOS/PROCESS-OWNER: SLOT=0x"); write_hex(index); write_str(" GEN=0x"); write_hex(process.generation as usize); write_str(" ROOT=0x"); write_hex(user.root as usize); write_str(" DATA=0x"); write_hex(user.data_frame() as usize); write_str("\r\n"); unsafe { (*core::ptr::addr_of_mut!(IDS))[index] = Some(process); (*core::ptr::addr_of_mut!(SPACES))[index] = Some(user); } } if allocator.remaining_pages() + 18 != before { fail("18 frame ownership"); } for a in 0..3 { for b in 0..a { if space(a).root == space(b).root || space(a).data_frame() == space(b).data_frame() { fail("process alias"); } } } unsafe { TARGET = space(1).data_frame(); asm!("dsb ish", "ic iallu", "dsb ish", "isb", options(nostack)); // No return to kernel_main: transfer its allocator before reclaiming // the boot stack. Repeated noreturn exception calls must not accumulate // Rust frames and push the 4 KiB copy scratch into protected text. (*core::ptr::addr_of_mut!(OWNED_ALLOCATOR)).write(core::ptr::read(allocator)); ALLOCATOR = (*core::ptr::addr_of_mut!(OWNED_ALLOCATOR)).as_mut_ptr(); }; enter(0) } fn enter(index: usize) -> ! { let process = id(index); table() .run(process) .unwrap_or_else(|_| fail("dead/running process selected")); let root = space(index).root; let target = unsafe { TARGET }; let (pointer, length_and_write) = match unsafe { STEP } { 0 => (mmu::USER_CODE_VA, 4100u64), // actual two-page read 1 => (mmu::USER_DATA_VA + 4094, 4), // guard hole 2 => (target, 0), // peer fault, before SVC 3 => (u64::MAX - 1, 4), // wrapping range 4 => (mmu::USER_CODE_VA + 4094, (1u64 << 32) | 4), // RX -> RW write _ => fail("copy call sequence"), }; write_str("TURKOS/PROCESS-ENTER: SLOT=0x"); write_hex(index); write_str(" STEP=0x"); write_hex(unsafe { STEP }); write_str("\r\n"); unsafe { asm!("dsb ishst", "msr TTBR0_EL1, {root}", "isb", "tlbi vmalle1is", "dsb ish", "isb", "mov sp, {kernel_stack}", "msr SP_EL0, {stack}", "msr ELR_EL1, {entry}", "msr SPSR_EL1, {mode}", "eret", root = in(reg) root, stack = in(reg) mmu::USER_STACK_TOP, entry = in(reg) mmu::USER_CODE_VA, mode = in(reg) 0x3c0u64, kernel_stack = in(reg) core::ptr::addr_of!(__stack_top) as usize, in("x0") process.generation, in("x1") pointer, in("x2") length_and_write, options(noreturn)) } } fn retire(index: usize, code: i32) { let root = unsafe { KERNEL }; // No other CPU, DMA queue or ASID participates. The ordinary probe keeps // IRQ masked; K536 allows one timer only after kernel-root/Exited publication // and masks again before free/reap. No mutable page owner spans that window. // Stop using the root before broadcast invalidation and page erasure. unsafe { asm!("dsb ish", "msr TTBR0_EL1, {root}", "isb", "tlbi vmalle1is", "dsb ish", "isb", root = in(reg) root, options(nostack)) }; table() .exit(id(index), code) .unwrap_or_else(|_| fail("exit transition")); #[cfg(feature = "process-exit-irq-test")] exit_irq_checkpoint(index, code); let user = unsafe { (&mut *core::ptr::addr_of_mut!(SPACES))[index].take() } .unwrap_or_else(|| fail("double retirement")); let data = user.data_frame(); let allocator = unsafe { &mut *ALLOCATOR }; let before = allocator.remaining_pages(); unsafe { user.release_after_tlbi(allocator, root) } .unwrap_or_else(|_| fail("page return after TLBI")); if allocator.remaining_pages() != before + 6 || unsafe { read_volatile(data as *const u64) } != 0 || allocator.release(data).is_ok() || allocator.remaining_pages() != before + 6 { fail("leak/double return/erase"); } if table().reap(id(index)) != Ok(code) || table().state(id(index)).is_ok() { fail("old process identity accepted"); } write_str("TURKOS/PROCESS-REAP: SLOT=0x"); write_hex(index); write_line(" FRAMES=6 ZEROED DOUBLE-RETURN=REJECTED PASS"); } #[unsafe(no_mangle)] pub extern "C" fn kernel_process_probe_exception( esr: usize, elr: usize, user_sp: usize, spsr: usize, root: usize, token: usize, far: usize, pointer: usize, length_and_write: usize, ) -> ! { let step = unsafe { STEP }; let index = match step { 0 | 3 => 0, 1 | 4 => 1, 2 => 2, _ => fail("unexpected exception"), }; let process = id(index); if table().state(process) != Ok(State::Running) || root as u64 != space(index).root || token as u64 != process.generation || user_sp as u64 != mmu::USER_STACK_TOP || spsr & 15 != 0 { fail("EL0 origin/context"); } if index == 2 { if (esr >> 26) & 0x3f != 0x24 || esr & 0x3f != 0x0f || esr & (1 << 6) != 0 || elr as u64 != mmu::USER_CODE_VA || far as u64 != unsafe { TARGET } || unsafe { read_volatile(space(2).data_frame() as *const u64) } != 0 { fail("peer access fault"); } write_str("TURKOS/PROCESS-FAULT: SLOT=2 ESR=0x"); write_hex(esr); write_str(" FAR=0x"); write_hex(far); write_line(" ISOLATED"); retire(2, -11); let replacement = table().create().unwrap_or_else(|_| fail("process reuse")); if replacement.slot != process.slot || replacement.generation <= process.generation || table().run(process).is_ok() { fail("stale identity reuse"); } table().exit(replacement, 0).unwrap(); table().reap(replacement).unwrap(); write_line("TURKOS/PROCESS-REUSE: OLD=REJECTED NEW-GENERATION PASS"); } else { let count = unsafe { read_volatile(space(index).data_frame() as *const u64) }; let expected = if step < 2 { 1 } else { 2 }; if (esr >> 26) & 0x3f != 0x15 || esr & 0xffff != 0x42 || elr as u64 != mmu::USER_CODE_VA + 24 || count != expected { fail("survivor progress"); } check_copy(index, step, pointer as u64, length_and_write as u64); write_str("TURKOS/PROCESS-PROGRESS: SLOT=0x"); write_hex(index); write_str(" COUNT=0x"); write_hex(count as usize); write_str(" AFTER-FAULT="); write_line(if step > 2 { "YES" } else { "NO" }); if step == 0 { table().wait(process).unwrap(); table().wake(process).unwrap(); } else { table().yield_ready(process).unwrap(); } } unsafe { STEP = step + 1; } if step == 4 { retire(0, 0); retire(1, 0); if unsafe { (&*ALLOCATOR).remaining_pages() != BEFORE } { fail("final frame leak"); } write_str("TURKOS/PROCESS-FRAMES: BEFORE=0x"); write_hex(unsafe { BEFORE } as usize); write_str(" AFTER=0x"); write_hex(unsafe { (&*ALLOCATOR).remaining_pages() } as usize); write_str(" PASS\r\n"); write_line("TURKOS/PROCESS-THREE: FAULT-ISOLATED SURVIVORS=2 FRAMES=RESTORED PASS"); #[cfg(feature = "process-exit-irq-test")] { if unsafe { EXIT_IRQ_SEEN } != 3 { fail("missing exit/timer checkpoints"); } write_line( "TURKOS/PROCESS-EXIT-IRQ: CHECKPOINTS=3 DEAD-REJECTED=3 SURVIVORS=2 FRAMES=RESTORED PASS", ); } park() } enter(match step { 0 => 1, 1 => 2, 2 => 0, 3 => 1, _ => fail("selection"), }) } #[cfg(feature = "process-exit-irq-test")] pub(super) fn exit_irq_active() -> bool { unsafe { read_volatile(core::ptr::addr_of!(EXIT_IRQ_ACTIVE)) } } #[cfg(feature = "process-exit-irq-test")] pub(super) fn handle_exit_irq( frame: *const irq::IrqContext, low: usize, top: usize, sp: usize, ) -> usize { if frame.is_null() { fail("exit IRQ null frame"); } let context = unsafe { &*frame }; let index = unsafe { EXIT_IRQ_INDEX }; let dying = id(index); let root: u64; let iar: u64; unsafe { asm!("mrs {root}, TTBR0_EL1", "mrs {iar}, ICC_IAR1_EL1", root = out(reg) root, iar = out(reg) iar, options(nomem, nostack)); } if frame as usize != top - size_of::() || !irq::handler_sp_in_stack(low, frame as usize, sp) || context.vector_slot != 5 || context.spsr & 0x8f != 5 || context.interrupted_sp & 15 != 0 || context.interrupted_sp < core::ptr::addr_of!(__stack_start) as u64 + 16 || context.interrupted_sp > core::ptr::addr_of!(__stack_top) as u64 || context.x[20] != 0x5360 || context.x[21] != 0x5361 || context.q[0] != [0x5a5a_5a5a_5a5a_5a5a; 2] || context.q[31] != [0xa5a5_a5a5_a5a5_a5a5; 2] || unsafe { read_volatile(core::ptr::addr_of!(__irq_stack_guard)) } != IRQ_GUARD || irq::classify_iar(iar, 30) != irq::IarClass::Expected || root != unsafe { KERNEL } || table().state(dying) != Ok(State::Exited(unsafe { EXIT_IRQ_CODE })) || table().run(dying).is_ok() || unsafe { (*ALLOCATOR).remaining_pages() } != unsafe { EXIT_IRQ_FREE } { fail("exit IRQ frame/source/dead identity"); } // Do not context-switch in the middle of page retirement. Only a ready // identity may be queued for later; the ERET below resumes EL1 retirement, // not the dead task's EL0 PC or pages. No mutable owner spans IRQ unmask. #[cfg(not(feature = "process-exit-stale-irq-test"))] let candidate = table().next_ready(); #[cfg(feature = "process-exit-stale-irq-test")] let candidate = Some(dying); if candidate.is_some_and(|id| id == dying || table().state(id) != Ok(State::Ready)) { fail("exit IRQ stale candidate rejected"); } unsafe { asm!("msr CNTP_CTL_EL0, xzr", "isb", "msr ICC_EOIR1_EL1, {iar}", "isb", iar = in(reg) iar, options(nomem, nostack)); let control: u64; asm!("mrs {value}, ICC_CTLR_EL1", value = out(reg) control, options(nomem, nostack)); if control & 2 != 0 { asm!("msr ICC_DIR_EL1, {iar}", "isb", iar = in(reg) iar, options(nomem, nostack)); } } write_str("TURKOS/PROCESS-EXIT-IRQ-VECTOR: SLOT=5 IAR=30 EXITED=0x"); write_hex(index); write_str(" CANDIDATE=0x"); write_hex(candidate.map_or(usize::MAX, |id| id.slot as usize)); write_line(" DEAD=REJECTED PAGES=LIVE RETURN=EL1-RETIREMENT"); unsafe { let seen = core::ptr::addr_of_mut!(EXIT_IRQ_SEEN); write_volatile(seen, read_volatile(seen) + 1); } 1 } #[cfg(feature = "process-exit-irq-test")] fn exit_irq_checkpoint(index: usize, code: i32) { let now: u64; unsafe { EXIT_IRQ_INDEX = index; EXIT_IRQ_CODE = code; EXIT_IRQ_FREE = (*ALLOCATOR).remaining_pages(); write_volatile(core::ptr::addr_of_mut!(__irq_stack_guard), IRQ_GUARD); write_volatile(core::ptr::addr_of_mut!(EXIT_IRQ_ACTIVE), true); asm!("mrs {value}, CNTPCT_EL0", value = out(reg) now, options(nomem, nostack)); } let target = unsafe { EXIT_IRQ_SEEN } + 1; write_str("TURKOS/PROCESS-EXIT-IRQ: ARMED EXITED=0x"); write_hex(index); write_line(" PAGES=LIVE TIMER=ALREADY-DUE"); let seen: u64; let x20: u64; let x21: u64; let q0: u64; let q31: u64; unsafe { asm!( "movi v0.16b, #0x5a", "movi v31.16b, #0xa5", "msr CNTP_CVAL_EL0, {deadline}", "msr CNTP_CTL_EL0, {enable}", "isb", "msr daifclr, #2", "2:", "ldr {seen}, [{pointer}]", "cmp {seen}, {target}", "b.eq 3f", "subs {budget}, {budget}, #1", "b.ne 2b", "3:", "msr daifset, #2", "msr CNTP_CTL_EL0, xzr", "isb", "umov {q0}, v0.d[0]", "umov {q31}, v31.d[0]", deadline = in(reg) now.saturating_sub(1), enable = in(reg) 1u64, target = in(reg) target, pointer = in(reg) core::ptr::addr_of!(EXIT_IRQ_SEEN), budget = inout(reg) 10_000_000u64 => _, seen = out(reg) seen, inout("x20") 0x5360u64 => x20, inout("x21") 0x5361u64 => x21, q0 = lateout(reg) q0, q31 = lateout(reg) q31, out("v0") _, out("v31") _, ); write_volatile(core::ptr::addr_of_mut!(EXIT_IRQ_ACTIVE), false); } if seen != target || x20 != 0x5360 || x21 != 0x5361 || q0 != 0x5a5a_5a5a_5a5a_5a5a || q31 != 0xa5a5_a5a5_a5a5_a5a5 || unsafe { (*ALLOCATOR).remaining_pages() } != unsafe { EXIT_IRQ_FREE } { fail("exit IRQ return/timeout/frame owner"); } write_str("TURKOS/PROCESS-EXIT-IRQ-RESUME: EXITED=0x"); write_hex(index); write_line(" MASKED GP-SIMD=RESTORED BEFORE-FREE PASS"); } fn check_copy(index: usize, step: usize, pointer: u64, encoded: u64) { let user = space(index); let owner = id(index); let pages = [ OwnedPage { virtual_base: mmu::USER_CODE_VA, physical_base: user.code_frame(), writable: false, }, OwnedPage { virtual_base: mmu::USER_DATA_VA, physical_base: user.data_frame(), writable: true, }, OwnedPage { virtual_base: mmu::USER_STACK_VA, physical_base: user.stack_frame(), writable: true, }, ]; let view = ProcessPages::new(owner, &pages).unwrap_or_else(|_| fail("copy owner map")); let (expected_pointer, expected_encoded, name) = match step { 0 => (mmu::USER_CODE_VA, 4100, "MULTIPAGE"), 1 => (mmu::USER_DATA_VA + 4094, 4, "GUARD"), 3 => (u64::MAX - 1, 4, "OVERFLOW"), 4 => (mmu::USER_CODE_VA + 4094, (1u64 << 32) | 4, "READONLY"), _ => fail("invalid copy step"), }; if pointer != expected_pointer || encoded != expected_encoded { fail("untrusted copy arguments"); } let length = (encoded & 0xffff_ffff) as usize; let mut scratch = [0xcc; 4100]; let result = if encoded >> 32 == 1 { unsafe { view.copy_to(table(), owner, pointer, b"FAIL") } } else { unsafe { view.copy_from(table(), owner, pointer, &mut scratch[..length]) } }; if step == 0 { let code = unsafe { core::slice::from_raw_parts(user.code_frame() as *const u8, 4096) }; let data = unsafe { core::slice::from_raw_parts(user.data_frame() as *const u8, 4) }; if result != Ok(4100) || scratch[..4096] != *code || scratch[4096..] != *data { fail("two-page copy content"); } } else if result != Err(EFAULT) || scratch.iter().any(|byte| *byte != 0xcc) || unsafe { read_volatile(user.data_frame() as *const u64) } != if step < 2 { 1 } else { 2 } || unsafe { read_volatile((user.code_frame() + 4094) as *const u16) } != 0 { fail("rejected copy changed prefix or live data"); } write_str("TURKOS/PROCESS-COPY: CASE="); write_str(name); write_str(" PTR=0x"); write_hex(pointer as usize); write_str(" LEN=0x"); write_hex(length); write_line(if step == 0 { " RESULT=4100 PASS" } else { " RESULT=EFAULT ATOMIC=YES PASS" }); }