AArch64 stage 4 on one CPU: the kernel's own tables, the GICv3 and the timer, and user mode - #589
Conversation
…e timer, and user mode The port's stage 4 (issues/kernel/toyos-runs-on-arm64.md), ahead of small-kernel stage 6 on the owner's word, with the drivers that track moves out of the kernel refused at the narrowest seam rather than ported. AArch64: - paging: TTBR1_EL1 holds the kernel's direct map of every 4 KiB page firmware's map calls memory, and nothing else; each user space is a TTBR0_EL1 root under a 16-bit ASID from toyos-pcid. Live entries are replaced break-before-make, a page is never retyped, and executable pages are made coherent with the instruction stream before they map. - control registers: CPACR lets EL1 and EL0 use FP/SIMD, CNTKCTL gives EL0 the virtual count, TCR asks for 16-bit ASIDs (checked), ICC_SRE puts the GICv3 CPU interface in system registers (ICC_SRE_EL2 first at EL2), and a CPU without one halts in a named refusal. - per-CPU state through TPIDR_EL1; the GICv3 distributor and this CPU's redistributor, SGIs and the virtual timer's PPI; the vectors dispatch an SVC to the syscall dispatcher, a translation fault to the demand pager, a user fault to the end of its process, and an interrupt to the tick or kick that preempts at EL0; the context switch carries FP/SIMD, since the soft-float kernel never touches it; trampolines to EL0 zero every register but the argument. - irq-storm: a boot actuator that ticks the timer under an SGI flood and says whether any tick was lost. Shared: - KernelCtx and the spawn paths name the stack pointer and thread pointer by role, which closes issues/kernel/the-saved-kernel-context-names-x86-registers.md. - The exit-to-user epilogue moves from x86's idt into scheduler.rs; the region bookkeeping from x86's AddressSpace into vma::Regions; the idle stack arena from x86's percpu into sched::idle_stack. - arch::msi_message may refuse, and on AArch64 does: the GICv3 ITS moves to stage 6, where a claimed function behind the SMMUv3 is its only consumer the small-kernel track leaves. - gop::init refuses a scanout that is not whole 2 MiB pages of its own. - paging::init is handed the scanout, which AArch64 maps before its switch. Tests (Tier::Local, VirtEl2): virt_user_mode, virt_timer_preempts (a new tests/virtpreemptcase), virt_irq_storm. Filed: issues/isolation/a-new-x86-thread-enters-ring-3-holding-kernel-register-values.md, issues/kernel/the-x86-address-space-keeps-a-page-map-nothing-fills.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…events, not time The orchestrator's guest run at 57382f7 had four reds. virt_early_panic and virt_early_fault (HVF, entered at EL1) went silent after the loader's handoff, where main is green: this branch put two GIC instructions ahead of the first record on the EL1 path, the ID_AA64PFR0_EL1.GIC gate that halts silently in refused_no_gicv3 and an ICC_SRE_EL1 write under firmware's vectors. QEMU 11.1.1's HVF sets that register's GIC field from env->gicv3state at vCPU creation (target/arm/hvf/hvf.c:1481-1483), which the GIC's realize fills in only after virt.c has realized every CPU (hw/arm/virt.c:3140 before 3158), so under HVF the field is whatever Hypervisor.framework reports for an Apple core; which of the two ended the boot is not measured. Both leave the entry: ICC_SRE_EL1 is written and its SRE read back in irqchip::init, under this kernel's vectors, where a CPU interface that is not there is an undefined instruction the kernel reports. The EL2 path still writes ICC_SRE_EL2, skipped where the ID register names no interface, so that case too fails under this kernel's vectors rather than firmware's. virt_irq_storm and virt_timer_preempts judged rates under TCG: ticks per 2000 ms and a tick's lateness, and a job's CPU time against a 45 s bound. No QEMU test measures time. The storm now floods SGIs until the timer has fired a thousand times and then waits for every SGI it sent, so a tick lost or never re-armed, or an SGI lost, leaves it unsaid; irqchip::lateness goes with the lateness verdict. virt_timer_preempts runs toybox's new `preempt`: a thread counting with no syscall, and a thread that yields until it has seen the count move twice, which on one CPU happens only when an interrupt took the CPU from the counter. The old test's red was its own ordering: it drained to spin's exit line, and the runner's line reaches the console after it. The track records that no QEMU test can show the EL2 deletions red: QEMU resets CNTHCTL_EL2, CNTVOFF_EL2 and CPTR_EL2 to values the declaration agrees with, and ICC_SRE_EL2 is a constant; each deletion stayed green in virt_user_mode at 57382f7. The interrupts-off window is metal's to measure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No earlier review is posted on this PR, so this round covers the whole branch. The gate holds. CI Growth: production +2836 −585 (net +2251), tests +194, issues +69 −24. The deletion owed is the set of siblings in B2. m5, m6 and m7 each revert what they claim. m7 going red only on BLOCKER
NOTE
REMOVE
SEND BACK |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of #589 at 9b7ed0c, B1, B2, N2, N3, N8, N9, N10, N12: - B1: `irqchip::arm_within` armed `want.min(remaining).max(1)`, as little as one counter tick, and stored it as what `rearm` repeats on every EL1 fire. The floor is now enforced once, in `arm_ticks`, the only write of the comparator, as x86's `OneShot::ticks` enforces it. `timer-floor` is the boot actuator that makes the timer due, calls `arm_within`, and takes a hundred EL1 fires with interrupts open. - B2: `KernelHw`, its `Kicker` and `Machine` impls (with `need_resched`, `idle_wait` and `DIAG_TICK_NS`), `RUNNING_CTX`, `report_contexts` and `MIN_ONE_SHOT` move to the portable `kernel/src/hw.rs`; each architecture's `hw.rs` keeps its halt and its `Hw::switch`. The unified `report_contexts` is x86's, which said more. - B2/N8: `toyos-gicv3`, a pure crate with host tests, holds the MPIDR affinity packing (`cpu::hardware_id` and the MADT match both use it), the `ICC_SGI1R_EL1` encoding with its `RS` field, and the redistributor walk with its `VLPIS` stride. - N2: `clock::tsc_ticks` is `counter_ticks`; the percpu timer-fire accessors are `kernel_timer_fires` and kin; `leave_ring3_if_due` is `leave_user_if_due`, and `exit_to_user`'s comments speak of user mode and interrupts rather than Ring 3 and `IF`. - N3: `SYS_DEBUG`'s double fault and TLB acknowledgement delay answer `NotSupported` on AArch64 instead of panicking the kernel. - N9: one `irqchip::Intid` enum declares the kick, log-nest and storm SGIs and the two MSI identities, in one INTID space. - N10: the storm sends each SGI once the last is taken, since an SGI sent while one is pending merges with it on hardware. - N12: `trampoline_entry` calls `scheduler::exit_to_user` itself, and the AArch64 forwarder is gone. - REMOVE: the storm actuator's false "whole period untaken" clause, and "a thousandth of QUANTUM_NS" from the floor's reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unmap and the refusals The review of #589 at 9b7ed0c, B1, B3, B4, B5, N3, N7, N11 and the REMOVEs: - `tests/virtpreemptcase` becomes `tests/virtjobcase`, whose test-runner runs five toybox applets on one CPU under the EL2 profile, each judged by its own `virt_` test through `virt_job`: `preempt` (`virt_timer_preempts`), `fp_isolation` (B3), `first_entry` (B4), `unmap_touch` (B5) and `debug_refused` (N3). The boots carry `TEST_KERNEL`, for `SYS_DEBUG`, and a job bound of four minutes. - `fp_isolation`: pins v0-v31, FPCR and FPSR and holds them until a sibling that loads another state has been seen to run three times, which on one CPU means three switches away and back. - `first_entry`: a raw thread whose first instruction stores x1-x30. - `unmap_touch`: four children each write a page, unmap it and read it; each must die on the read. - `debug_refused`: `SYS_DEBUG`'s TLB acknowledgement delay answers `NotSupported`. - `virt_timer_floor` (B1) boots the `timer-floor` actuator; `virt_selftest` judges it and `virt_irq_storm` alike. - The assembly lives in `userland/toybox/src/arch/`, placed in the source gate as metalprobe's is; x86-64's module names where its own probes are. - N7: two host tests for `direct_map_end`'s start-off-page and end-below-start refusals. - N11: the crash dump says whether TP+0 holds variant II's self-pointer again, where the TLS variant is II. - Filed: `issues/isolation/aarch64-el1-runs-without-pan.md` (N1), `issues/kernel/an-aarch64-crash-report-reads-through-any-user-leaf.md` (N4), `issues/kernel/portable-kernel-code-names-the-tsc.md` (N2's remainder). - N6: the track's stage 4 records the instruction-cache, break-before-make and ASID-reclaim claims as owed, with the first HVF run as their exit. - REMOVE: the track's "no QEMU test can do" sentence and its provenance, the isolation issue's "shape to copy", `aarch64/mod.rs`'s status paragraph, `CPACR`'s "across every entry from EL0", and `virt_timer_preempts`' restated comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of #589, round 3, at 27f08d4 Gate: MERGEABLE. CI Growth: production +3035 −801, tests +680, issues +131 −24. Earlier blockers
BLOCKER
NOTE
REMOVE
SEND BACK |
…off the comparator B5. Every AArch64 munmap was invalidated twice: `AddressSpace::unmap` issues `TLBI VAE1IS`/`ASIDE1IS` + `DSB ISH`, which already reaches every CPU, and then `Unmapped`'s drop called `tlb::shootdown`, a `TLBI VMALLE1IS` that also dropped every other ASID's and every global entry. So deleting the unmap's TLBI (b5) left `virt_unmap_touch` green on any oracle. The page-table edit is the single place now: it is precise, it covers every caller of `unmap` and `replace`, and it is what the module header already promised. `arch::tlb::shootdown` does nothing on AArch64, since every portable caller (`Unmapped`, the pipe window revoke, dlopen) follows an `AddressSpace` edit that has already broadcast; the ASID pool's reclaim calls `tlb::all` directly, and the census counts only that. `unmap_touch`'s child now reads the page, calls munmap and reads again in one assembly block after its last print (`arch::read_unmap_read`), so the only switch that could drop the cached translation is the unmap itself. The parent requires exit status -1, the status `kill_process(-1)` gives a fault; a refused munmap or a panic no longer passes. B7. `timer-floor` judged `percpu::armed_ticks()`, a software copy, and its 100-fire loop could not fail: the b1 log shows all 100 fires done at armed=1. It now reads the counter, calls `arm_within(QUANTUM_NS)` on a due timer, reads `CNTV_CVAL_EL0` back and requires the comparator to be at least `floor_ticks()` past that counter reading. The reading tells floored from unfloored only while the ask took less than the floor, so a wider window is a FAIL as well. The fire loop is deleted. NOTEs. `debug_refused` also asks for SYS_DEBUG's double fault. `virt_job` waits with `await_marker`, whose kernel-death reading (`serial::died`, `kernel_died_here`, `WaitVerdict`) is the x86 harness's: a panicked guest ends the wait once it goes quiet, with the panic report in the verdict, instead of after the whole drain ceiling (633 s for a panic at 2.58 s under n3). The two x86 toybox applets that only panic are filed as issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md. The screen-test doc comment `virt_job` had split from `run_screen_test` goes back to it. REMOVE. The "never gets back to say anything" comment on `virt_timer_floor`, the selftest's matching "no progress to say anything with", and `unmap_touch`'s "Several children" line are deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #562 forbids a time verdict in a QEMU selftest: under TCG load a slow call must not fail a test that measures a value, not a duration. The timer-floor selftest's `window < floor` clause was exactly that — it failed whenever the arm_within call itself ran long, for no defect. The verdict is now only CVAL >= counter_before + floor_ticks(), which holds however long the call took. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…framing read PR #562's rule against a timing verdict in a QEMU selftest made `floor_selftest` compare CVAL to a counter read taken before the call to `arm_within`. Under TCG that call itself can run tens of thousands of ticks long — far past `floor_ticks()` — so `span >= floor` held whether or not the floor clamp fired: a QEMU host under load hid a missing or bypassed clamp rather than catching it. `arm_ticks` (and `arm_within`, which ends in it) now return the counter value they read to compute CVAL, so the selftest relates CVAL to the exact `now` the arm used — a value relation, not a second, independent read framed around however long the call took. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
arm_ticks now returns the counter value it armed the comparator from, for floor_selftest. rearm's match still had a bare `arm_ticks(ticks)` arm against `stop_timer_hardware()`'s `()`, which no longer type-checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, kernel/src/arch/aarch64/boot.rs, both hunks kept: main's (#583) new `report_counter_origin`, empty on AArch64 because no register says where the generic timer counts from, which main.rs's `report_power_on` calls on both architectures; and this branch's `timer()`, its doc and its body (the EL1 virtual timer, logged, stopped until the scheduler arms it) in place of main's `owed!` stub. #583's KernelArgs layout word needs nothing on the AArch64 side: the loader writes it in the portable bootloader/src/main.rs, the kernel refuses a foreign one in the portable `kernel_main`, which AArch64's `_start` reaches, and that `_start` reads its four fields by `offset_of!`, so the layout moves under it by construction. Auto-merged, each checked against the branch's own hunk: actuator.rs (main's layout actuator beside this branch's irq-storm and timer-floor), main.rs (main's layout refusal and power-on report beside this branch's `mod hw` and headless GOP), x86_64/boot.rs (main's UTC `clock` and `report_counter_origin` beside this branch's irq-storm/timer-floor refusal), aarch64/mod.rs (#586 drops log-shared-reservation's window from `percpu_fetch_add`; this branch's percpu.rs still calls `log::nested::reserve_window`, which main keeps), clock.rs, sched/kthread.rs, src/build.rs, src/sourcegate.rs, tests/toyos.rs. kernel/src/hw.rs is this branch's alone: main touched neither it nor either architecture's hw.rs. The rust gitlink takes main's 9c3eea44. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #611 (keystore: each record writer has its own temp name) and #593 (user copies are scatter-gather). #593 replaced `AddressSpace::translate_writable` with `AddressSpace::leaf(vaddr, access) -> Option<(phys, bytes to the leaf's end)>` and left AArch64's as `match self.never {}`. This branch made the AArch64 `AddressSpace` real, so the conflict in kernel/src/arch/aarch64/paging.rs resolves to this branch's body with `translate_writable` replaced by a real `leaf`: `walk` now also answers the size of the leaf it found (2 MiB block or 4 KiB page), a `Read` is granted wherever `walk` finds a user leaf, and a `Write` only where the leaf's `AP[2:1]` is EL0 read-write, the check `translate_writable` made. No table descriptor this file writes sets `APTable`, so the leaf's `AP` is the whole walk's answer, as every level's `USER|WRITE` is on x86-64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs
Mutation patches at b0db9fb (merge of origin/main d1d83f6)Regenerated against the merged tree.
b6 is 219549 bytes, past a comment's limit, so it is the command: # b1-no-floor
diff --git a/kernel/src/arch/aarch64/irqchip.rs b/kernel/src/arch/aarch64/irqchip.rs
index 676f0e80..bef603e2 100644
--- a/kernel/src/arch/aarch64/irqchip.rs
+++ b/kernel/src/arch/aarch64/irqchip.rs
@@ -305,7 +305,7 @@ fn floor_ticks() -> u64 {
/// EL1 fire re-arms with. Returns the counter value the comparator was set
/// from, for a caller that must relate CVAL back to it without a second read.
fn arm_ticks(ticks: u64) -> u64 {
- let ticks = ticks.max(floor_ticks());
+ let ticks = ticks.max(1);
percpu::set_armed_ticks(ticks);
let now = cpu::counter();
// SAFETY: the EL1 virtual timer's comparator and control; CPACR has
# b3-no-fp-restore
diff --git a/kernel/src/arch/aarch64/switch.rs b/kernel/src/arch/aarch64/switch.rs
index c7f98cac..6c49361f 100644
--- a/kernel/src/arch/aarch64/switch.rs
+++ b/kernel/src/arch/aarch64/switch.rs
@@ -67,26 +67,6 @@ pub(crate) unsafe extern "C" fn context_switch(old_sp: *mut u64, new_sp: u64) {
"str x9, [x0]",
"mov sp, x1",
"add x9, sp, #{fp}",
- "ldp q0, q1, [x9, #0]",
- "ldp q2, q3, [x9, #32]",
- "ldp q4, q5, [x9, #64]",
- "ldp q6, q7, [x9, #96]",
- "ldp q8, q9, [x9, #128]",
- "ldp q10, q11, [x9, #160]",
- "ldp q12, q13, [x9, #192]",
- "ldp q14, q15, [x9, #224]",
- "ldp q16, q17, [x9, #256]",
- "ldp q18, q19, [x9, #288]",
- "ldp q20, q21, [x9, #320]",
- "ldp q22, q23, [x9, #352]",
- "ldp q24, q25, [x9, #384]",
- "ldp q26, q27, [x9, #416]",
- "ldp q28, q29, [x9, #448]",
- "ldp q30, q31, [x9, #480]",
- "ldr x10, [x9, #512]",
- "msr fpcr, x10",
- "ldr x10, [x9, #520]",
- "msr fpsr, x10",
"ldp x19, x20, [sp, #0]",
"ldp x21, x22, [sp, #16]",
"ldp x23, x24, [sp, #32]",
# b4-no-zeroing
diff --git a/kernel/src/arch/aarch64/entry.rs b/kernel/src/arch/aarch64/entry.rs
index 31a0cdf0..a04ba054 100644
--- a/kernel/src/arch/aarch64/entry.rs
+++ b/kernel/src/arch/aarch64/entry.rs
@@ -12,20 +12,6 @@ use super::switch::{DAIF_AT, FRAME_BYTES, RETURN_AT};
/// EL0 is, for `trampoline_entry`'s contract.
const DAIF_MASKED: u64 = 0b1111 << 6;
-/// Zero x1–x30, the registers a first entry to EL0 must not carry.
-macro_rules! zero_registers {
- () => {
- concat!(
- "mov x1, xzr\n", "mov x2, xzr\n", "mov x3, xzr\n", "mov x4, xzr\n", "mov x5, xzr\n",
- "mov x6, xzr\n", "mov x7, xzr\n", "mov x8, xzr\n", "mov x9, xzr\n", "mov x10, xzr\n",
- "mov x11, xzr\n", "mov x12, xzr\n", "mov x13, xzr\n", "mov x14, xzr\n", "mov x15, xzr\n",
- "mov x16, xzr\n", "mov x17, xzr\n", "mov x18, xzr\n", "mov x19, xzr\n", "mov x20, xzr\n",
- "mov x21, xzr\n", "mov x22, xzr\n", "mov x23, xzr\n", "mov x24, xzr\n", "mov x25, xzr\n",
- "mov x26, xzr\n", "mov x27, xzr\n", "mov x28, xzr\n", "mov x29, xzr\n", "mov x30, xzr\n",
- )
- };
-}
-
/// A thread's first entry to EL0 at `x19` on the stack `x20`, with `x0` =
/// `x21`: a process's argument is zero, a thread's is its own. `SPSR_EL1`
/// zero is EL0 with every exception unmasked, and `SP_EL1` is left at the
@@ -38,7 +24,6 @@ pub(crate) extern "C" fn process_start() {
"msr sp_el0, x20",
"msr spsr_el1, xzr",
"mov x0, x21",
- zero_registers!(),
"eret",
unlock = sym crate::sched::driver::trampoline_entry,
);
# b5-no-unmap-tlbi
diff --git a/kernel/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs
index df990ec4..b6d47d18 100644
--- a/kernel/src/arch/aarch64/paging.rs
+++ b/kernel/src/arch/aarch64/paging.rs
@@ -444,7 +444,6 @@ impl AddressSpace {
pub fn unmap(&mut self, vaddr: UserAddr) {
let va = vaddr.raw();
assert!(va & (PAGE_2M - 1) == 0, "unmap: vaddr {va:#x} not 2MB-aligned");
- let asid = self.asid.value();
let Some(directory) = self.tables.find_directory(va) else { return };
let i = index(va, 2);
let entry = directory.0[i];
@@ -457,11 +456,6 @@ impl AddressSpace {
None => entry & ADDR_2M,
};
self.tables.directory(va).set(i, 0);
- if entry & TABLE != 0 {
- tlb::asid(asid);
- } else {
- tlb::page(asid, va);
- }
crate::sched::futex::revoke_range(phys, PAGE_2M);
}
# b7-floor-stored-not-programmed
diff --git a/kernel/src/arch/aarch64/irqchip.rs b/kernel/src/arch/aarch64/irqchip.rs
index 676f0e80..6da46593 100644
--- a/kernel/src/arch/aarch64/irqchip.rs
+++ b/kernel/src/arch/aarch64/irqchip.rs
@@ -305,8 +305,7 @@ fn floor_ticks() -> u64 {
/// EL1 fire re-arms with. Returns the counter value the comparator was set
/// from, for a caller that must relate CVAL back to it without a second read.
fn arm_ticks(ticks: u64) -> u64 {
- let ticks = ticks.max(floor_ticks());
- percpu::set_armed_ticks(ticks);
+ percpu::set_armed_ticks(ticks.max(floor_ticks()));
let now = cpu::counter();
// SAFETY: the EL1 virtual timer's comparator and control; CPACR has
// nothing to say about them and `CNTKCTL_EL1` keeps EL0 out.
# m5-el0-tick-no-preempt
diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index a8c433f5..64efbbb7 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -154,7 +154,6 @@ fn irq(from_el0: bool) {
let hw = &crate::hw::HW;
hw.trace(TraceEvent { ts: hw.now(), cpu: CpuId(percpu::cpu_id()), kind: TraceKind::TimerFire });
irqchip::end(intid);
- crate::scheduler::do_preempt();
} else {
crate::preempt::set_need_resched();
percpu::note_kernel_timer_fire();
# m6-no-demand-fill
diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index a8c433f5..b24425e7 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -250,7 +250,7 @@ fn user_abort(frame: &mut Frame) {
user_fatal(frame);
}
cpu::enable_interrupts();
- let served = crate::process::handle_page_fault(frame.far, frame.esr);
+ let served = false;
cpu::disable_interrupts();
if served {
percpu::set_fault_state(CpuFaultState::Normal);
# m7-tick-never-rearmed
diff --git a/kernel/src/arch/aarch64/irqchip.rs b/kernel/src/arch/aarch64/irqchip.rs
index 676f0e80..c5a9167c 100644
--- a/kernel/src/arch/aarch64/irqchip.rs
+++ b/kernel/src/arch/aarch64/irqchip.rs
@@ -361,12 +361,7 @@ pub fn stop_timer() {
/// A timer interrupt taken: armed again for what it was last armed for, or
/// stopped if it was stopped — the one thing that deasserts it.
pub(super) fn rearm() {
- match percpu::armed_ticks() {
- 0 => stop_timer_hardware(),
- ticks => {
- arm_ticks(ticks);
- }
- }
+ stop_timer_hardware();
}
/// `timer-floor`: this CPU's timer made due with interrupts masked, then
# n3-debug-panics
diff --git a/kernel/src/arch/aarch64/tlb.rs b/kernel/src/arch/aarch64/tlb.rs
index d46f19b0..0cfe8633 100644
--- a/kernel/src/arch/aarch64/tlb.rs
+++ b/kernel/src/arch/aarch64/tlb.rs
@@ -99,11 +99,11 @@ pub fn bench() {
/// x86-64's delays an acknowledgement, and there is none here: refused.
#[cfg(feature = "test-actuators")]
pub fn debug_arm_ack_delay(_nanos: u64) -> u64 {
- toyos_abi::syscall::SyscallError::NotSupported.to_u64()
+ panic!("SYS_DEBUG: AArch64 invalidates by broadcast, so there is no acknowledgement to delay")
}
/// x86-64's delays an acknowledgement, and there is none here: refused.
#[cfg(feature = "test-actuators")]
pub fn debug_disarm_ack_delay() -> u64 {
- toyos_abi::syscall::SyscallError::NotSupported.to_u64()
+ panic!("SYS_DEBUG: AArch64 invalidates by broadcast, so there is no acknowledgement to delay")
}
# n4-double-fault-panics
diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs
index a8c433f5..2b4a0615 100644
--- a/kernel/src/arch/aarch64/trap.rs
+++ b/kernel/src/arch/aarch64/trap.rs
@@ -495,7 +495,7 @@ pub(crate) fn report_fault_stack() {}
/// refused.
#[cfg(feature = "test-actuators")]
pub(crate) fn provoke_double_fault() -> u64 {
- toyos_abi::syscall::SyscallError::NotSupported.to_u64()
+ panic!("SYS_DEBUG: AArch64 has no double fault to provoke")
}
/// `irq-storm`: this CPU floods itself with SGIs, sending each as soon as the |
|
Review of #589, final round, at b0db9fb CI Growth:
Compared with 0751537, the branch's own delta changed only in Earlier blockers
|
The review's blocker: no AArch64 test could fail on `leaf()`'s Write check. `test_rs_abuse_readonly_copyout` issued its calls with the x86 `syscall` instruction; it now goes through `toyos_abi::syscall`'s typed wrappers, with the target named by a slice over the read-only address (as `tls_dtv_race` does), so it builds for both architectures and its row in the source gate's assembly exemptions goes. - The typed-value arm was `fstat`, whose wrapper returns its `Stat` rather than taking an address. It is now `process_stats` of a child the test spawns first: the same `copy_out` path, reachable through a wrapper that takes the caller's `&mut`. - Every arm runs; the exit status carries one bit per arm that let a write through (1 read-only mmap, 2 own text, 4 straddle, 8 clock page), which the kernel's own exit record prints even once a rewritten clock page has taken logd down. The straddle arm's search for the first unwritable page reports rather than panics when no page refuses, so a kernel that grants every write still reaches the clock arm. - `tests/virtjobcase` runs it as its last job; `virt_job` builds that one binary for AArch64 (`build::build_toyos_bin`, the crate's other binaries do not all build there) and puts it on ROOT for every boot of the case. `virt_readonly_copyout` judges it. The x86 shared run stays and is declared in DRIVEN_AND_SHARED. Also: the device-memory issue now names `user_ptr`'s direct-map copies and `dump_crash_diagnostics` beside `read_user_word`; the arm64 track drops the KernelArgs clause #583 made false; the assembly issue drops its probe count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs
|
Round 7 mutations, applying at 5792355 Each applied with Target:
# w1-write-always-granted
diff --git a/kernel/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs
index df990ec4c..678319991 100644
--- a/kernel/src/arch/aarch64/paging.rs
+++ b/kernel/src/arch/aarch64/paging.rs
@@ -477,10 +477,10 @@ impl AddressSpace {
/// memory goes through this, so a syscall cannot write a page the process
/// itself may not — the clock page, a shared library's `.text`.
pub fn leaf(&self, vaddr: UserAddr, access: toyos_userbound::Access) -> Option<(u64, u64)> {
- let (at, leaf, size) = self.walk(vaddr)?;
+ let (at, _leaf, size) = self.walk(vaddr)?;
let granted = match access {
toyos_userbound::Access::Read => true,
- toyos_userbound::Access::Write => leaf & (AP_EL0 | AP_READ_ONLY) == AP_EL0,
+ toyos_userbound::Access::Write => true,
};
granted.then(|| (at.phys(), size - (vaddr.raw() & (size - 1))))
}
# w2-leaf-length-overstated
diff --git a/kernel/src/arch/aarch64/paging.rs b/kernel/src/arch/aarch64/paging.rs
index df990ec4c..c0818dba2 100644
--- a/kernel/src/arch/aarch64/paging.rs
+++ b/kernel/src/arch/aarch64/paging.rs
@@ -482,7 +482,7 @@ impl AddressSpace {
toyos_userbound::Access::Read => true,
toyos_userbound::Access::Write => leaf & (AP_EL0 | AP_READ_ONLY) == AP_EL0,
};
- granted.then(|| (at.phys(), size - (vaddr.raw() & (size - 1))))
+ granted.then(|| (at.phys(), size))
}
/// The direct-map address of `vaddr`, the leaf descriptor that maps it, and the size that leaf maps.🤖 Generated with Claude Code |
|
Review of #589, round 7, at 5792355 CI The orchestrator's guest runs at 5792355 (logs
Growth, from
Earlier blockers
Is
|
virt_job also ends its wait on the kernel's exit: record of a non-zero code for the job, so a job that dies before its TEST_END (W1 under logd's death) is judged by the code, which names the arms, not by a stall. build_toyos_bin and build_toyos_bins share their setup in TestBuild. The comment restating the exit protocol at virt_readonly_copyout is deleted. The &mut over pages nothing may write is filed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs
|
Mutation patches the review named, applied to 6475015, each shown to build (EXIT=0; X1's first form, T ( diff --git a/kernel/src/user_ptr.rs b/kernel/src/user_ptr.rs
index 2f2ea160f..3c3035aa9 100644
--- a/kernel/src/user_ptr.rs
+++ b/kernel/src/user_ptr.rs
@@ -153,7 +153,7 @@ impl<'a> SyscallContext<'a> {
/// Write a typed value into user memory.
pub fn copy_out<T: UserSafe>(&self, ptr: UserAddr, value: &T) -> Result<(), SyscallError> {
- let (kptr, _pins) = object::<T>(ptr, Access::Write)?;
+ let (kptr, _pins) = object::<T>(ptr, Access::Read)?;
if crate::actuator::copy_meets_a_remap() {
remap_race::hold(kptr.cast(), core::mem::size_of::<T>());
}X1 ( diff --git a/kernel/src/arch/x86_64/paging.rs b/kernel/src/arch/x86_64/paging.rs
index 1ad2ef7ba..89ffea37d 100644
--- a/kernel/src/arch/x86_64/paging.rs
+++ b/kernel/src/arch/x86_64/paging.rs
@@ -565,11 +565,10 @@ impl AddressSpace {
/// cannot write a page the process itself may not — the clock page, a
/// shared library's `.text`.
pub fn leaf(&self, vaddr: UserAddr, access: toyos_userbound::Access) -> Option<(u64, u64)> {
- const STORE: u64 = PAGE_USER | PAGE_WRITE;
- let (dm, rights, size) = self.walk(vaddr)?;
+ let (dm, _rights, size) = self.walk(vaddr)?;
let granted = match access {
toyos_userbound::Access::Read => true,
- toyos_userbound::Access::Write => rights & STORE == STORE,
+ toyos_userbound::Access::Write => true,
};
granted.then(|| (dm.phys(), size - (vaddr.raw() & (size - 1))))
} |
virt_unmap_touch's job is killed with code=-1 by design and is judged by its line after that, so ending the wait on any non-zero exit record failed it. The optional NOTE 5 change is reverted whole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs
|
Orchestrator: at 20ded4d — every virt_ test (12) exit 0, Fast exit 0; at 6475015 (same kernel) T red on virt_readonly_copyout and abuse_readonly_copyout, X1 red on abuse_readonly_copyout, W1 red with the job's exit code=15. Round-7 review's named changes done; landing. 🤖 Generated with Claude Code |
Stage 4 of
issues/kernel/toyos-runs-on-arm64.md— GIC, timer, MMU and exception levels — on one CPU, ahead of small-kernel stage 6 on the owner's word, with the drivers that track moves out of the kernel refused at the narrowest seam instead of ported. Stages 0–3 landed in #524.The exit, and where it stands. Stage 4's exit: a user process takes a page fault and a syscall on one CPU; the timer drives preemption; an interrupt storm ends with no lost timer tick; the longest interrupts-off window is measured against x86's; and each of the entry's three EL2 writes (
CNTHCTL_EL2,CNTVOFF_EL2,CPTR_EL2) is shown red when deleted. The first three have guest tests (below). Not met by this branch, and recorded in the track's stage 4 as owed: the interrupts-off comparison, which only metal can measure; the three EL2 deletions shown red; the TSC-named handoff (issues/kernel/the-boot-timing-handoff-is-named-for-the-tsc.md); and the instruction-cache, break-before-make and ASID-reclaim claims, which TCG cannot fail on, with the first HVF run as their exit.What changed, per decision
AArch64 kernel (
kernel/src/arch/aarch64/)paging.rs).TTBR1_EL1holds the kernel's direct map: every 4 KiB page firmware's map calls memory the kernel reads, Normal write-back, and nothing else, because a register or a hole mapped Normal is one a speculative access can reach. Registers are mapped only bymap_mmio, at 4 KiB exactly, as Device-nGnRE; a page is never retyped. Each user space is its ownTTBR0_EL1root under a 16-bit ASID fromtoyos-pcid. Live entries are replaced break-before-make with broadcastTLBI, and an executable page is cleaned and invalidated to the instruction stream before it is mapped.AddressSpace::leaf, User copies are scatter-gather: a window is its physical runs, each pinned #593's user-copy query, answers a user address's physical address and the bytes to the end of its 2 MiB block or 4 KiB page; aWriteonly where the leaf'sAP[2:1]grants EL0 read-write, which is the whole walk's answer because no table descriptor this file writes setsAPTable.toyos-bootmap::aarch64::{direct_map_end, coverage}, pure and host-tested.control_regs.rs, the entry):CPACR_EL1.FPEN,CNTKCTL_EL1.EL0VCTEN,TCR_EL1.AS. Nothing of the GIC is touched before the kernel's vectors are installed:ICC_SRE_EL1is written and read back byirqchip::init. At EL2 the entry writesICC_SRE_EL2, skipped whereID_AA64PFR0_EL1.GICnames no interface.irqchip.rs): the distributor, this CPU's redistributor, the CPU interface; only SGIs and the virtual timer's PPI are enabled. Every INTID the kernel names is oneIntidenum. The one-shot floor is enforced once, inarm_ticks, the only write of the comparator, as x86'sOneShot::ticksenforces it, so no arm and no EL1 re-arm is shorter thanMIN_ONE_SHOT.toyos-gicv3, a new pure crate with host tests: the MPIDR affinity packing (cpu::hardware_idand the MADT match both use it), theICC_SGI1R_EL1encoding with itsRSfield for Aff0 ≥ 16, and the redistributor walk with itsVLPISstride — the decisions only CPU 0 exercises in a guest.percpu.rs) throughTPIDR_EL1, every field an atomic.trap.rs): anSVCtosyscall_dispatch; a translation fault from EL0 toprocess::handle_page_fault; any other EL0 fault to a report and the end of its process; an IRQ to a tick or a kick, which preempts from EL0 and asks for a pass from EL1; anything else from EL1 to a panic. Every return to EL0 runsscheduler::exit_to_userlast, and a new thread's first does too, fromtrampoline_entrydirectly.switch.rs): x19–x30,DAIFand the thread's FP/SIMD state on the outgoing stack. The soft-float kernel never touches FP/SIMD, so it is saved only where a thread stops running. The trampolines to EL0 zero every register but the argument.SYS_DEBUGactions with no AArch64 counterpart — the double fault, the TLB acknowledgement delay — answerNotSupportedinstead of panicking the kernel.irq-stormfloods this CPU with SGIs, each sent once the last is taken (a pending SGI sent again merges with it on hardware), until the 1 ms timer has fired a thousand times through the flood.timer-floormakes the timer due with interrupts masked, callsarm_within(QUANTUM_NS)— which now returns the counter value it setCNTV_CVAL_EL0from — and reads the comparator back: it must be at leastfloor_ticks()past that same value. A counter read framed around the call, PR No QEMU test measures time, and audio is judged on metal only #562's original shape, is not a value relation: under TCG the call alone can run tens of thousands of ticks, past the floor regardless of the clamp, which is exactly how round 4 (below) found it.tlb.rs): the page-table edit that clears an entry is the one place its translation is dropped —AddressSpace::unmapandreplacebroadcastTLBI VAE1IS/ASIDE1IS+DSB ISH.arch::tlb::shootdown, whichUnmapped, the pipe window revoke and dlopen call after such an edit, does nothing on AArch64; the ASID pool's reclaim callstlb::all(VMALLE1IS) directly.Shared code: one declaration each
kernel/src/hw.rsis new and portable:KernelHw, itsKickerandMachineimpls (need_resched,idle_wait,DIAG_TICK_NS),RUNNING_CTX,report_contexts(x86's, which said more) andMIN_ONE_SHOT. Each architecture'shw.rskeeps its halt and itsHw::switch.KernelCtxand the spawn paths name the saved stack pointer and the thread pointer by role;issues/kernel/the-saved-kernel-context-names-x86-registers.mdis met and deleted.scheduler::exit_to_user; region bookkeeping isvma::Regions; the idle-stack arena issched::idle_stack.clock::counter_ticks(wastsc_ticks),percpu::kernel_timer_firesand kin (werering0_…),scheduler::leave_user_if_due(wasleave_ring3_if_due), andexit_to_user's comments speak of user mode and interrupts. The rest of the portable TSC vocabulary is filed.Stubs, owned by the small-kernel track (one line each in the track):
arch::msi_messagerefuses on AArch64, so the kernel's xHCI, NVMe, HDA, virtio-sound, virtio-console and virtio-gpu drivers each refuse their function by name;drivers::goprefuses a scanout that is not whole 2 MiB pages of its own. The GICv3 ITS moves to stage 6.Owed by name, no stage yet: the pseudo-NMI, the SBSA watchdog, the wall clock.
The guest tests
All under
Profile::VirtEl2(TCG-cpu max, EL2) butvirt_early_panicandvirt_early_fault(Profile::Virt, HVF).virt_user_mode: init reachesspawn: /system/bin/logd, which needs a demand-paged EL0 and a syscall.virt_irq_storm,virt_timer_floor: the selftests above, judged by theirPASSline throughvirt_selftest.tests/virtjobcase(wasvirtpreemptcase) runs five toybox applets and, last,test_rs_abuse_readonly_copyouton one CPU, with a job bound of four minutes; each has its own test throughvirt_job, which boots the case withTEST_KERNELand needs the job's line andexit=0.virt_jobwaits withawait_marker, so a kernel that panics ends the wait once the guest goes quiet, with the panic report in the verdict (the x86 harness'sserial::diedreading), rather than after the whole drain ceiling:virt_timer_preempts—preempt: a counting thread that never enters the kernel is preempted twice.virt_fp_isolation—fp_isolation: v0–v31,FPCRandFPSRpinned and held while a sibling that loads another state is seen to run three times; the state must read back whole.virt_first_entry—first_entry: a raw thread whose first instruction stores x1–x30; every one must be zero.virt_unmap_touch—unmap_touch: four children each write a page, print, then read it,munmapit and read it again in one assembly block (arch::read_unmap_read); each must end with exit status -1, the statuskill_process(-1)gives a fault, so a refused munmap or a panic is red.virt_debug_refused—debug_refused:SYS_DEBUG's double fault and TLB acknowledgement delay answerNotSupported.virt_readonly_copyout—test_rs_abuse_readonly_copyout, below: a syscall writes into user memory only where EL0 could store, which on AArch64 isAddressSpace::leaf'sWritecheck.userland/toybox/src/arch/, placed in the source gate as metalprobe's is; x86-64's module carries its ownread_unmap_readand names where its FP and first-entry probes are (test_rs_fpu_isolation, and the open x86 first-entry issue).test_rs_abuse_readonly_copyoutis portable (round 7, the review's blocker: no AArch64 test could fail onleaf()'sWritecheck). It issued its calls with the x86syscallinstruction; it now goes throughtoyos_abi::syscall's typed wrappers, naming the target with a slice over the read-only address astls_dtv_racedoes, and its assembly exemption insrc/sourcegate.rsis deleted. Its four arms — a read-only anonymousmmap, its own.text, the straddle (areadfrom the last writable page past.bssinto the read-only one after it) and the clock page every process maps — each ask through both copies a syscall writes with:read's bulk window, and a typedcopy_out. That typed call wasfstat, whose wrapper returns itsStatrather than taking an address; it is nowprocess_statsof a child the test spawns first, the samecopy_out. Every arm runs, and the exit status carries one bit per arm that let a write through (1 mmap, 2 text, 4 straddle, 8 clock), which the kernel's ownexit:record prints even once a rewritten clock page has takenlogddown; the straddle's search reports rather than panics when no page refuses, so a kernel that grants every write still reaches the clock arm. On x86-64 it stays a shared-boot test (abuse_readonly_copyout, Fast), proving what it proved, declared inDRIVEN_AND_SHARED.build::build_toyos_binbuilds one binary of a test crate for one architecture (the crate's other binaries do not all build for AArch64);virt_jobbuilds it once per process and puts it on ROOT for every boot of the case, since every job runs in every boot.Guest runs (the orchestrator's)
At 27f08d4 (round 3), the orchestrator measured:
virt_test EXIT=0, and the Fast tier EXIT=0;TLBIwas not the only invalidation.At 4256e5a (round 4), everything came back as wanted except b1 and b7, both green:
floor_selftest's verdict compared CVAL to a counter read taken beforearm_within, which the mutation never touches, so a slow call under TCG load hid both a missing clamp (b1) and a floored-but-unprogrammed one (b7). At 0751537 the verdict is the value relation described above; b1 and b7 are expected red by construction — b1 leavesCVAL − now = 1past whatevernowthe arm used, b7 leaves the hardware CVAL programmed off the raw, unfloored ask — both underfloor_ticks(), which QEMU's own count checks regardless of how long anything took. Changingarm_ticks's return also forcedrearm's match arm to discard it, which m7's patch is regenerated against. At 0751537 the orchestrator measured the whole guest set green and every mutation red. b260578 merged origin/main (7e15181: #583, #586, #597, #600, #555). b0db9fb merges origin/main (d1d83f6: #611, #593, #610); its one conflict wasaarch64/paging.rs, where #593 had renamed the uninhabited stub'stranslate_writabletoleaf, and this branch's realAddressSpacenow implementsleaf(above).At b0db9fb (round 6),
cargo test --test toyos-build -- <filter>in this worktree, clean but for the patch named (the Fast tier iscargo test); logs are589r6-<run>.login the orchestrator's job scratchpad:virt_virt_timer_floorvirt_fp_isolationvirt_first_entryvirt_unmap_touchvirt_virt_early_panic,virt_early_fault,virt_el2_drop(stage 3) passedvirt_timer_floorvirt_timer_preemptsvirt_user_modevirt_irq_stormvirt_debug_refusedvirt_debug_refused5792355 merges origin/main (8a66b44: #587, #603, #606, #608, #612, #615), whose kernel changes (
clock.rs,vfs.rs,fat32_adapter.rs,object/ops.rs, two selftests) touch no AArch64 file; at 5792355 the orchestrator measured: all twelvevirt_tests EXIT=0; x86-64abuse_readonly_copyoutEXIT=0; the Fast tier EXIT=0; W1 EXIT=1 with the job's kernelexit:record at code=15 (every arm); W2 EXIT=1 withTEST_ENDexit=4 and "read wrote across a writable page into the read-only one".Negative controls and the oracle
High-risk: memory management, the context switch, interrupt entry, the isolation of a new thread.
Whole-change negative control (B6).
kernel/reverted to the merge-base withorigin/main(d1d83f6 at b0db9fb), keeping this branch's tests,userland/toybox,toyos-bootmapandtoyos-gicv3:git diff --binary HEAD $(git merge-base HEAD origin/main) -- kernel/. At b0db9fb it applied, the AArch64 image builds (cargo run -- --build-only --arch aarch64) and the AArch64 test kernel checks, EXIT=0 each; tree clean after; it was not regenerated at 5792355. Expected: every stage-4virt_test red, since the kernel at the merge-base stops at stage 3.Single mutations. Each is a checked patch (below), regenerated at b0db9fb and applying unchanged at 5792355.
mutate.shapplied each, built the mutated AArch64 image withcargo run -- --build-only --arch aarch64, checked the test kernel withcargo check --target aarch64-unknown-none-softfloat --features boot-actuators,test-actuatorsinkernel/(the feature set n3 and n4 live under), EXIT=0 each at 5792355, and reversed it in the same script (git diff --quietclean after each). Expected reds:virt_timer_floor.virt_fp_isolation.zero_registers!()deleted, with its now-unused macro, which-D unused-macrosotherwise refuses):virt_first_entry.TLBIdeleted, with theasidit read — now the only invalidation an unmap gets):virt_unmap_touch.arm_ticksstores the floored span and programs the raw one):virt_timer_floor.virt_debug_refused.virt_debug_refused.virt_timer_preempts;virt_fp_isolationtoo, whose sibling runs only when the tick preempts.virt_user_modeand every job test.virt_irq_storm.leaf()grants everyWrite; the walk'sleafbinding becomes_leaf, without which-D unused-variablesrefuses the mutant, measured build EXIT=101):virt_readonly_copyout, with the job'sexit:record at code=15 — every arm.leaf()answers the leaf's whole size, not the bytes to its end):virt_readonly_copyout, code=4 — the straddle arm. W2 overstates every copy that crosses a leaf's end, so an earlier job may die first: red on the target, without the per-arm evidence.copy_outasks forAccess::Read):virt_readonly_copyoutandabuse_readonly_copyoutboth red expected. The patch built for x86-64 and AArch64, EXIT=0 each, reversed, tree clean.leaf()grants everyWrite;rightsbecomes_rightsandSTOREgoes, without which-D warningsrefuses it, measured EXIT=101):abuse_readonly_copyoutred expected. Built EXIT=0, reversed, tree clean.Host mutations run here. Deleting
direct_map_end's start-off-page refusal redsa_start_off_a_page_is_refused(EXIT=101); deleting its end-below-start refusal redsan_end_below_its_start_is_refused(EXIT=101); restored after each.Independent oracle. The Arm ARM K.a (VMSAv8-64, the generic timer, exceptions) and the GIC architecture specification IHI 0069H, exercised by QEMU's TCG model, a third-party implementation of both. It covers the GIC, timer, exception, FP-switch and first-entry claims; it cannot fail on the instruction-cache, break-before-make and ASID-reclaim claims, which the track records as owed to the first HVF run.
Gates (at 20ded4d)
cargo run -- --ci host: EXIT=0 (54 steps green);build_toyos_binshares its setup withbuild_toyos_bins(TestBuild)cargo run -- --build-only(x86-64): EXIT=0cargo run -- --build-only --arch aarch64: EXIT=0cargo test --test toyos-build -- --list: EXIT=0 (listsvirt_readonly_copyoutandabuse_readonly_copyout)aarch64-unknown-toyosandx86_64-unknown-toyoswithcargo build --bin abuse_readonly_copyout: EXIT=0 each. Guest runs are the orchestrator's.What I'm unsure of
&mutover read-only memory is filed:issues/design-debt/the-readonly-copyout-test-forms-mut-over-pages-nothing-may-write.md.munmap; a switch at thatsvc's return writesTTBR0_EL1with another ASID, which drops QEMU's TLB and makes that child fault anyway.ID_AA64MMFR0_EL1.ASIDBits,ICC_SRE_EL1under HVF on the M4) stay unmeasured until stage 6 gives HVF its RNDR.Filed, not fixed
issues/isolation/aarch64-el1-runs-without-pan.md(N1)issues/kernel/an-aarch64-crash-report-reads-through-any-user-leaf.md(N4; round 7 addsuser_ptr's direct-map copies anddump_crash_diagnostics)issues/kernel/portable-kernel-code-names-the-tsc.md(N2's remainder)issues/isolation/a-new-x86-thread-enters-ring-3-holding-kernel-register-values.mdissues/kernel/the-x86-address-space-keeps-a-page-map-nothing-fills.mdissues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.mdThe patches, applying at b0db9fb and unchanged at 5792355, and W1 and W2 at 5792355 (b6 is the command above)
🤖 Generated with Claude Code
https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs