universalisos/kernel/docs/AARCH64_M2_PREP_DEVICES.md

7.8 KiB

UniversalisOS AArch64 — M2-prep devices handoff (PL011 RX, vGIC LR, virtio-blk)

Status: M1 (Linux boot contract) is GREEN at Image@0x50080000, DTB@0x50e00000 (make ARCH=aarch64 PLATFORM=qemu-aarch64-virt run-linux, ad-hoc script /tmp/hermes-verify-uos-aarch64-linux-m1.sh = 10/10). ARMv7 + aarch64-classic still build+boot. A parallel writer is actively building the real-Image lane (run-linux-real, ../guests/linux-aarch64/out/Image). This doc covers the DEVICES a real kernel needs, so it does not collide with the Image/Makefile lane.

Done this increment

  • PL011 trap-emulate FR read now reflects the REAL hardware RXFE/TXFF bits (kernel/src/arch/aarch64/el2_guest.cpp, pl011_guest_read case PL011_FR) instead of forcing RX-empty. Effect: a polling Linux pl011/earlycon can consume console input the HV already has; TX-only guests are unaffected. Verified: M1 + classic aarch64 still boot, no stage-2 abort / EL2 TRAP.
  • Confirmed the carried "vgic_clear_irq undefined" note is STALE: it IS defined in vgic.cpp:90 and declared in vgic.h:27. ACK_VTIMER path is intact.

What a real Linux boot already has (no work needed)

  • Stage-2 identity, RAM=WB cacheable, PL011+GICD as holes (trap-emulate).
  • PL011 TX + control shadow (CR/IMSC/IFLS/LCRH/IBRD/FBRD) — probe-complete for Linux amba-pl011 (MIS/RIS return 0 = no pending; ICR dropped).
  • vGICD emulation (vgicd.cpp) + GICv3 vCPU iface (gicv3_vgic_init).
  • Arch timer passthrough: physical PPI27 → vgic_inject_vtimer (HW-linked LR), guest EOI deactivates. ACK_VTIMER hypercall present.
  • So a headless real Image will boot to earlycon output and then panic on a missing rootfs — that panic is the M2 milestone (kernel ran).

Remaining devices (the user's M2-prep ask)

1) PL011 RX vIRQ injection (when there is a real input source)

The guest PL011 INTID is SPI 1 = INTID 33 (QEMU_UART0_IRQ in board_qemu_virt.h). PL011 is NOT passed through (guest UART is trap-emulated), so the RX IRQ is PURELY VIRTUAL — inject with vgic_inject_virq(33), NOT vgic_inject_irq (which sets ICH_LR_HW and would wrongly link a physical IRQ). Where to inject: gate on (g_imsc has RXIM=bit4 set) AND an RX byte is available. Do NOT feed guest RX from the HV's own physical PL011 (would steal the HV console). The right source is a small per-guest RX ring filled by a dedicated input channel (later). Until that exists, headless boots need no RX. Files: el2_guest.cpp (g_imsc already shadowed; add g_rxim check + a ring), vgic.cpp (vgic_inject_virq already exists). Verify: guest with RX enabled receives injected INTID 33 exactly when a byte is queued; M1 (RX unused) stays green.

2) Full ICH_LR pending→active lifecycle (mostly present — verify/harden)

vgic_inject_irq writes LR with STATE_PENDING|HW|GROUP1|prio0|PINTID|VINTID. The GICv3 CPU iface + HW linkage mean: guest IAR1 read → ACTIVE; guest EOIR1 → deactivates physical (HW=1). vgic_clear_irq(vintid) invalidates LRs by vINTID. Hardening checklist:

  • EOI without HW linkage (purely virtual IRQs from vgic_inject_virq) just drops the LR on guest EOI — confirm ICH_HCR_EL2.EOImode==0 so a single EOI both priority-drops and deactivates. If EOImode==1 the guest must DIR1 too.
  • Confirm ICH_HCR_EL2.EN set (gicv3_vgic_init sets it) and VMCR VPMR=0xFF (set) so no priority mask drops the injected IRQ.
  • Maintenance interrupt (ICH_HCR_EL2 + EOI count) is NOT wired — fine for now since LRs are allocated lazily and there are 4-16 of them. Verify: inject vgic_inject_virq(33); guest IAR1 returns 33 then ACTIVE; EOI clears it; no LR left PENDING (dump ICH_LRn in a debug print).

3) virtio-blk (mmio, legacy) — the rootfs device (DESIGN)

Goal: let Linux mount a tiny read-only rootfs so run-linux-real reaches userspace instead of panicking. Add a NEW stage-2 hole + trap-emulator, modeled exactly on emulate_pl011/emulate_gicd in el2_guest.cpp.

MMIO window (pick an unused QEMU virt slot, e.g. 0x0a000000, 4 KiB):

  • Add QEMU_VIRTIO_BLK_BASE/SIZE/IRQ to board_qemu_virt.h (IRQ = some free SPI, e.g. INTID 48 = SPI 16 — must match what guest.dtb will advertise).
  • stage2.cpp s2_fill_l2: add a third hole (leave_mmio_holes) for that 2 MiB window so accesses trap to EL2 (like PL011/GICD).
  • el2_guest.cpp: emulate_virtio_blk(frame, far, iss) mirroring emulate_pl011 (decode off/sas/srt/wnr/sf, dispatch on the legacy virtio-mmio register map), wired into the EC_DATA_ABORT_LEL chain before the vSError fallback.

Legacy virtio-mmio register map (the only one Linux's virtio_mmio.c probes when the DT node says "virtio,mmio" with no magic-version note — use version 1): 0x000 MagicValue R = 0x74726976 ("virt") 0x004 Version R = 1 (legacy) 0x008 DeviceID R = 2 (block) 0x00c VendorID R = 0x554f5350 ("UOSP") 0x010 DeviceFeatures R = (1<<5) read-only | (1<<8) blk_size | (1<<2) flush? no keep minimal: bit5(VIRTIO_BLK_F_RO) 0x014 DeviceFeaturesSel W (0/1) — only sel0 supported 0x020 DriverFeatures W ; 0x024 DriverFeaturesSel W 0x030 QueueSel W ; 0x034 QueueNumMax R (=64) ; 0x038 QueueNum W 0x040 QueueReady (v1) / or legacy 0x044 QueueAlign, 0x048 QueuePFN W *** For Version=1 LEGACY: use QueuePFN (physical page frame of the virtqueue), QueueAlign (=4096), QueueNotify 0x050. This is the path Linux 5.x still negotiates when it offers legacy. *** 0x060 InterruptStatus R (bit0 used-buffer) ; 0x064 InterruptACK W 0x070 Status R/W (ACKNOWLEDGE=1, DRIVER=2, DRIVER_OK=4, FEATURES_OK=8) 0x100+ Device config space: virtio_blk_config { u64 capacity; u32 blk_size; ... }

Virtqueue (legacy, QueuePFN points at a single contiguous region in guest RAM):

  • Parse desc[QueueNum] (16 bytes each), avail ring, used ring at the legacy offsets (avail after desc table, used on next 4K page). All guest addresses are IPA==PA (stage-2 identity) — HV reads them directly.
  • On QueueNotify(0): walk avail → desc chain; a block request is virtio_blk_outhdr { u32 type(0=READ/1=WRITE/5=FLUSH), u32 ioprio, u64 sector } followed by data descs (READ: device->guest) and a 1-byte status desc (VIRTIO_BLK_S_OK=0). Copy from the in-RAM disk image (sector*512) into the guest data buffers, set status, append to used ring, set InterruptStatus bit0, inject vgic_inject_virq(QEMU_VIRTIO_BLK_IRQ).
  • Backing image: embed a tiny (e.g. 64 KiB) read-only disk via .incbin like guest.dtb is embedded (_binary_disk_img_start/end), or load it with -device loader at a fixed PA the HV reads. Start with a known pattern (e.g. first sector = a signature) and verify Linux reads sector 0 == that.

Resume steps / success criteria:

  1. Add constants + stage-2 hole; boot M1 → still green (hole is inert until a DT node references it).
  2. Implement register file (Magic/Version/DeviceID/Status/Queue*) only; guest with a virtio,mmio DT node probes, sees Magic+DeviceID=2, reads config capacity. Verify via a guest that prints the probed capacity over PL011.
  3. Add virtqueue READ path + in-RAM image; guest reads sector 0 == signature.
  4. Wire InterruptStatus + vgic_inject_virq(IRQ); guest's IRQ handler fires.
  5. M2: hand the real kernel a rootfs image; Linux mounts / → userspace.

Files touched: board_qemu_virt.h (constants), stage2.cpp (hole), el2_guest.cpp (emulate_virtio_blk + chain), Makefile (embed/loader for the image), guest_payload/guest.dts (add virtio,mmio node + interrupt = IRQ). Pitfalls: legacy vs v2 register split is THE classic virtio bug — pick legacy (v1) and stick to it; virtqueue physical addresses must be guest IPA (==PA); used-ring idx must be wrapped; InterruptStatus must be cleared by InterruptACK.

Verification workflow (unchanged)

ad-hoc only, never suite green. make run-linux (M1) must stay green after every change; make ARCH=armv7 PLATFORM=qemu-arm-virt must still build+boot. Stale-object footgun: any board-header or LINUX_BOOT flip needs make clean (run-linux already does this).