MILESTONE 2026-08-10 — TWO guests run concurrently on real hardware. A real
Zephyr RTOS v4.4.1 image runs on CPU3 (its own banner, its own heartbeats,
climbing steadily) at the same time as the FreeBSD guest on CPU0 serves ssh and
a 384 MB dd off vtbd0p3 at 4.2 MB/s under load 2.00, with zero dmesg
errors. Isolation is enforced by a second, disjoint stage-2 table and proved on
the hardware itself (AT S12E1W, ISOL 1/1); gr still returns an unpolluted
FreeBSD frame, so CPU0-side diagnostics survive a live second guest.
The route in: chimpd --zguest <elf> TFTPs the image as a third file from
U-Boot, and zstage.c copies it into the second guest's slice on CPU0 before
smp_init() and kload_enter() — before any guest on any core has run an
instruction. zboot starts it over EMAC; zstage + zunhalt retry a failed
attempt without a board reload. See docs/dual-guest.md for the full transcript,
including three real bugs the hardware found that QEMU could not (a ghost image
surviving a warm reset, a wfi that can never wake on a core with no
interrupts, and a latched zboot firing on re-arm), and one report that had to
be RETRACTED because the liveness probe — not the firmware — was wrong.
SOLVED 2026-08-10/11 — the kldload "hang" was a panic, and kldload now
works. It was never a hang: the guest sat at Press a key to reboot while
ssh went quiet and EL2 stayed healthy behind it. kldload of ANY driver ran
bus_generic_driver_added() → device_attach(pmu0) → pmu_attach() →
intr_activate_irq() → panic: Attempt to double activation of resource id: 0.
The first cause is our own DTB: /pmu lists four interrupts (the A64 wires
the PMU as SPIs 116-119, one per core) but one interrupt-affinity entry — a
leftover from trimming /cpus to expose only CPU0 to the guest, which is correct
and deliberate, except the interrupts list was never trimmed to match. So
pmu_attach() failed at every boot with ENXIO after activating IRQ 0 and never
undoing it, and device_attach() parked pmu0 in DS_NOTPRESENT explicitly to be
retried when new drivers load.
Fixed by status = "disabled" on /pmu and verified live: kldunload/kldload
of the module that used to kill the guest both return 0, uptime continuous, zero
panic lines. lima.ko still does not load, but now for an ordinary reason with
the guest unharmed — it needs drmn, which this kernel neither contains nor
ships, and gpu@1c40000 is disabled in the DTB anyway. Mali is normal work
with two named prerequisites now, not an investigation.
Four structural arguments from reading source were each refuted by one board run
before this was found, and the panic text was in the console ring the whole time —
bmc con read was reading the ring's OLD address and returning its head instead
of its tail (fixed, 9de6c1d). Full accounts:
bsdOS/hal/probe_test/ROOT-CAUSE-NOTES.md and docs/guest-dtb.md.
MILESTONE 2026-07-30 — the FreeBSD guest boots to an interactive root shell on
real hardware, and is reachable over ssh ([email protected]). Four
hypervisor bugs fixed to get there: the sync fallback racing CPU2 for the eMMC
lock (3b56041), emmc_bio not settling the controller on failure paths
(e6f1e4e), a self-latching CNTV mask that killed the guest's timebase on one
lost injection (dfc6eda), and virtio-net TX broadcasting every frame instead of
using the guest's destination MAC (f91aeff). A soak then showed that boot was
about 1-in-5 reproducible and drove four more fixes; see
docs/sessions/SESSION-STATUS-2026-07-30.md §7-§10 for the numbers, and note
that the last two (partial-sector stitch, retry budget 8) are not yet
hardware-verified.
Earlier the same day: vtimer
"permanently lost" theory retracted (banked-register misread; watchdog fired
0 times), mountroot> cracked (guest already has BZDOS-RACE retry
instrumentation, a bare CR unsticks it — matches auto_mount_root()), and a
NEW real bug found past it: single-user /bin/sh dies on a genuine GEOM EIO
(g_vfs_done():vtbd0p3[RAD(offset=801767424,length=163840)]error=5) —
partition bounds and physical media both ruled out live, points to the same
vblk-lost-kick bug class recurring at a new LBA.
Snapshot of a long multi-track session. Board = Banana Pi M64 (Allwinner A64, 4× Cortex-A53). Hypervisor runs at non-secure EL2; boots FreeBSD arm64 as an EL1 guest under stage-2.
Never make the user power-cycle/reset by hand, never propose a reset as their decision. Fix the
live HV over EMAC (sw/patch/call/w); if a binary change must land, the AGENT does the
network reload (wdt_reset + chimpd) autonomously. See memory never-suggest-reset.
- Regression found: the on-board image had
HCR_EL2.IMO=1again (gic_timer_init/vgic_initwere back inmain_dbg.c), re-creating the interrupt storm. Diagnosed live over EMAC: guest wedged inehci_reset()'s DELAY at guest PC 0x91becc;preempt_cntclimbing ~145k/s while EL2 timer only ~100/s → a non-timer IRQ storming. GICD_ISPENDR + GICC_HPPIR = INTID 106; DTB: INTID 106 = SPI 74 =usb@1c1b000EHCI, level-high. GICH list regs empty (vgic forwarded nothing) → EL2 EOI'd but never cleared the level source → re-fired forever. - Fixed LIVE (no reset):
hv.cmd('sw hcr 0x84000003')cleared IMO+FMO → storm 145k/s → 0, console 18519 → 20343 bytes. Baked intomain_dbg.cpermanently (removedvgic_init/gic_timer_init, explicit IMO/FMO clear). GICV redirect instage2_l3_uart[130/131]verified CORRECT (not the cause). - Next blocker found + fixed: after the storm, guest spun in
pci_cfgregopen/generic_bs_rpolling UART1 (serial@1c28400) IIR/MSR — three snps-dw UARTs share the one 4 KiB page at offsets 0x000/0x400/0x800. vconsole only matched UART0 (offset 0x000); UART1's IIR (page-offset 0x408) missedoff==UART_REG_IIR, returned 0 (= "int pending") → ns8250 ISR spun forever. Fix invconsole.c:reg = off & 0x3FFfolds UART0/1/2 onto one 16550 layout for IIR/LSR/USR emulation (THR capture stays UART0-only). Builds clean; landing via autonomous network reload. - Breadcrumb map note: current build uses SRAM 0x00018200 (GICT), 0x00018100 (EXC1), 0x00018300 (irq_counter); GST1 preempt counter at 0x50000b00[2]. Old handoff map (GICT@0x50000800) is stale.
✅✅✅✅✅ FreeBSD track — Watchdog & Interrupt Storm/Delay hangs fixed, guest boots past EHCI (2026-07-15 late night)
- Watchdog Disable in DTB: Traced guest hang/reset issues to FreeBSD's own watchdog driver
aw_wdog.cattaching to the hardware watchdog node and disabling the WDT we configured in EL2 as a safety mechanism. Patched/opt/bzdos/tftpboot/bananapi-min.dtsto addstatus = "disabled";towatchdog@1c20ca0, preventing the guest from disabling our safety net. - Physical Interrupt Routing (
IMO = 0): Removedgic_timer_init()anddaifclrfrommain_dbg.cto leaveHCR_EL2.IMO = 0(routing all physical interrupts directly to the guest). This lets the guest handle device interrupts (like EHCI/MMC) natively, completely resolving the interrupt storm at EL2 and the resulting hangs during EHCI initialization. - Vconsole-based Debugger Polling: Modified
el2_exc.cto calldbgmon_service()inside the Stage-2 virtual UARTvconsoletrap handler. Because the guest console driver constantly polls/writes to the virtual UART, this provides high-frequency polling opportunities for EMAC debugging commands without requiring timer ticks or EL2 interrupt preemption. - Verified on hardware: In cycle 9, the guest successfully booted past the EHCI/USB probes and attached the
uart0console driver! The EMAC network debugger remains fully active, responsive, and able to read/write guest state live during boot.
The "Cannot get physical memory regions" panic that blocked ALL boots since the 03:13 build was caused by a MISSING /memory node in bananapi-min.dtb. The node was manually added in the earlier session (see below — 18KB console captured), but a DTB rebuild at 01:30 (to disable EHCI) regenerated the .dtb from the .dts source, and the .dts did NOT have /memory. Every subsequent boot panicked at initarm+0x664 because OF_finddevice("/memory") returned -1 (FDT_ERR_NOTFOUND). The panic's stack overflow (SP into rodata, unwind_frame NULL deref) was a secondary symptom.
Fix: fdtput -c bananapi-min.dtb /memory; fdtput -t s ... device_type memory; fdtput -t x ... reg 40000000 40000000 — one command on the host, no rebuild. Also added /memory to bananapi-min.dts source so future recompilations don't lose it.
Verified on hardware: vconsole = 18,519 bytes (0x4857), exact match to the earlier session. Console shows EHCI/cpufreq/simplebus device probes. ffv = "no vector first-fault" — the panic is gone. Guest reaches the known DELAY() hang blocker (ELR=0xffff00000091bec4).
Diagnosis method: used the live EMAC debugger throughout — read breadcrumbs, guest registers, walked stage-2 page tables, compared ELF source vs DRAM (found rodata corruption was a symptom), decoded ADRP+ADD to identify the panic string, checked FDT header/structure, and finally ran fdtget on the DTB to discover the missing node. No rebuild needed for the fix.
New dbgmon commands (in the 47768-byte microkernel-dbg.bin):
sr2— dump EL2 system registers (CNTHCTL, HCR, VTCR, VTTBR, CPTR, SCTLR_EL2)sw <name> <val>— write a system register LIVE (cnthctl, hcr, sctlr1/2, vtcr, vttbr, ...)call <pa> [x0..x3]— call a hypervisor function by address (RPC), with address validation (must be in .text) + crash recovery (returns 0xDEAD if callee faults)patch <pa> <word>— write instruction + I-cache flush (hot-patch code without reset)
hvdbg.py — reusable Python library wrapping the EMAC protocol: cmd/read_words/read_bytes/dump/write_word/patch/call/wdt_reset/gict/vconsole/ffv/gr/sr2/walk_stage2/reenter_guest. Import and debug live.
chimpd.py — autonomous board supervisor: catches U-Boot, TFTP kernel+DTB, loady+bootelf ELF, monitors breadcrumbs over EMAC, auto-reset on hang.
sr2 revealed CNTHCTL_EL2 = 0x3 — set by ATF/BL31, NOT by our code. The guest already has native EL1 access to CNTPCT and CNTP_CTL/CVAL. The CNTHCTL fix from the earlier session (attempting msr cnthctl_el2, #0x3 in gic_timer_init) was a no-op (the value was already 0x3). It has been removed; gic_timer.c now has a comment documenting that ATF sets it and we don't touch it.
Guest produces 18KB console, then ELR freezes in DELAY() body (0xffff00000091bec4 / 0xffff00000091b698). Timer tick (GST1 ring) keeps advancing — the hypervisor is alive, the guest's PC is genuinely stuck. Next step: read CNTFRQ_EL0 and the DELAY calibration path. The live debugger (sr, gr, dump) can do this without a board reset.
✅✅✅ FreeBSD track — MODINIOMD tag off-by-one fixed + vconsole UART bugs fixed (2026-07-15) + vgic timer integration reverted (2026-07-15) — 18 KB of REAL console output, ready for hardware test
vgic timer integration attempted then reverted (2026-07-15, late session): hypothesis that FreeBSD picks CNTV (not CNTP) and needs it injected from the hypervisor was correct in theory, but the implementation (creating vgic.c/vgic.h to intercept GICC register access and redirect to GICV) caused a regression (NULL panic in unwind_frame before console output, worse than the baseline). Fully reverted all vgic code + integration points (removed from Makefile, main_dbg.c, gic_timer.c, stage2.c). Conclusion: the earlier three bugs (MODINIOMD tag, FAR_EL2 IPA, IIR loop) were the real culprits, not timer routing.
Preventive fix: CNTHCTL_EL2 counter access (2026-07-15, final): added early setup of CNTHCTL_EL2 = 0x3 (bits 0-1 = EL1PCEN|EL1VCEN) in gic_timer_init() to ensure the guest at EL1 can read both physical and virtual timer counters. This is necessary for FreeBSD's DELAY() busy-wait to terminate. Previously CNTHCTL_EL2 was left at reset default, which might have had EL1 counter access disabled. Clean rebuild passes. Build is ready for hardware test.
Root cause of the earlier "HOWTO returned as lastaddr" saga (see the 2026-07-14 entry below), corrected: kload.h's MODINFOMD_ENVP/HOWTO/KERNEND were 0x0007/0x0008/0x0009, off-by-one against the real sys/sys/linker.h (verified against the exact matching commit, freebsd-src releng/15.1 @ 9263fb9, which has ENVP=0x0006/HOWTO=0x0007/KERNEND=0x0008). MD_FETCH() matches by tag, so our ENVP record was silently misread as HOWTO and our HOWTO record as KERNEND. Fixed in kload.h; kload.c's howto_val reverted from the = kernend_val workaround back to a plain 0x800 (RB_VERBOSE). Verified on hardware via onebp: x21 (parse_boot_param's return value) now reads a correct, large KERNEND-based KVA instead of the leaked HOWTO value.
Two real bugs found and fixed in vconsole.c (the trap-and-emulate virtual UART0), both via reading exact FreeBSD driver source, not guessing:
- VA-vs-IPA address bug.
vconsole_handle_fault()comparedframe->far(which isFAR_EL2— the guest's faulting virtual address perexceptions.S) directly against the physicalUART0_BASE. This accidentally worked during early boot (guest MMU off, VA==IPA==PA) but broke the instantpmap_bootstrap_dmap()ran and FreeBSD started addressing UART0 via its DMAP kernel VA (e.g.0xffff007ffffff008) — the comparison then always failed,vconsole_handle_faultreturned "not mine", and since a guest-group fault's ELR is never advanced on the generic unhandled path (el2_exc.c), the guest re-faulted on the exact same instruction forever (looked identical whether-DDBG_NO_TVMwas set or not — that A/B was a red herring; the real bug was this address check). Fix: reconstruct the true IPA fromHPFAR_EL2(FIPA[47:12]) OR'd withfar's low-12-bit page offset, same pattern already used byvirtio.cin this tree. - IIR always read as 0 → false "interrupt pending" spin. Once the address bug was fixed, the guest still spun at ~4×10⁶ faults/sec cycling IIR (offset 0x08) → MSR (0x18) reads. Root-caused via the exact FreeBSD source (
sys/dev/uart/uart_dev_ns8250.cns8250_clrint(), checked out fromreleng/15.1@9263fb9, sparse-checkout extended withgit sparse-checkout add sys/dev/uart sys/dev/ic):while ((iir & IIR_NOPEND) == 0)never exits if IIR always reads 0 (masks toIIR_MLSC, reads MSR, loops forever). Fixed: vconsole now returnsIIR_NOPEND(0x01, persys/dev/ic/ns16550.h) on IIR reads instead of a synthesized 0.
Verified on hardware after both fixes: vconsole's capture ring (0x50000f00) shows total_bytes=0x4857 (18,519 real captured bytes) and fault_count=0x90b6 (37,046 — a sane, non-spinning count), both static across repeated samples (guest moved past this UART activity, not spinning on it). Dumped the ring (d 0x50000f10 512 + wrap-seam math) — genuine FreeBSD boot console text: EHCI/usbus attach, per-CPU cpufreq nodes, ofwbus/pmu/simplebus/pcm device probes. This is the first time real FreeBSD device-autoconfiguration output has ever been captured on this board.
NEW next blocker (separate from UART, not yet root-caused): guest wedges inside DELAY(). After the last captured console line (usbus3: EHCI version 1.0), the guest's elr freezes inside/around DELAY()'s internal busy-wait body (ffff00000091bec4 / ffff00000091b698, both within the DELAY symbol per the kernel disassembly) for 20+ seconds straight (4 samples, 5s apart, byte-identical elr each time) while an independent liveness counter (guest.c's GST1 ring, 0x50000b00, word[2] = tick-preemption count, driven by our own EL2 timer and unrelated to guest UART/MMU state) climbs normally throughout — i.e. the debugger's own tick mechanism is provably alive, but the guest's PC genuinely never leaves this narrow region. Ruled out (with hardware evidence, not guessing):
- Not a stale-ESR illusion:
ESR_EL2/FAR_EL2are architecturally only updated on synchronous exceptions, not IRQ/FIQ, so agrframe captured via the periodic tick'sdbgmon_service(frame)call can show staleesr/farfrom a much earlier sync fault — butelr/GPRs ARE live, correctly-saved-per-trap values, so the frozenelralone is trustworthy evidence of a real hang (a lesson worth keeping: don't decodeesr/farfrom a tick-driven capture as if it described the current instant). - Not the TVM/gtrace sysreg-trap mechanism:
gtrace's own counter (0x50002000, word[1]) is frozen at8(the handful of early-boot MMU-setup traps) — zero new TVM traps are occurring, and TVM only ever covers SCTLR/TTBR0/TTBR1/TCR/ESR/FAR/AFSR0/AFSR1/MAIR/AMAIR/CONTEXTIDR (the EL1 MMU-control registers), neverCNTPCT_EL0/CNTVCT_EL0— so a CNTHCTL_EL2-trapped-counter-read theory doesn't route through this path at all. - Leading hypothesis, not yet confirmed:
DELAY()'s internal deadline (computed from a timer frequency / calibration value) may never be satisfied — either the underlying counter genuinely isn't advancing from the guest's exact vantage point for some other reason, or (more likely, given TVM/gtrace show nothing wrong) some caller requested/computed an absurdly large delay target.srshows the guest's ownCNTP_CTL_EL0=1(ENABLE=1) already configured (separately from our own EL2 tick, which owns the physical timer IRQ viaHCR_EL2.IMO=1— worth checking whether the guest's own timer setup is starved of an interrupt it's implicitly waiting on, thoughDELAY()itself is a pure busy-poll with no interrupt dependency per the disassembly, so this may be an unrelated red herring). Next step: readCNTFRQ_EL0and the exactDELAY()/timecounter calibration path insys/arm64/arm64/machdep.cor the relevant timecounter driver from the exact-matching source (same sparse-checkout technique used above) before touching hardware again — per the DEBUG_RULES.md R2/R4 discipline, no more board cycles until a concrete, falsifiable hypothesis is in hand. - Files touched:
kload.h(tag fix, from before this session),vconsole.c(both fixes, this session)./opt/bzdos/build/freebsd-srcsparse-checkout extended withsys/dev/uart+sys/dev/icthis session (in addition to the existingsys/arm64/arm64,sys/arm64/include,sys/contrib/libfdt,sys/dev/fdt,sys/dev/ofw,sys/kern,sys/sys). - New file this session:
/opt/bzdos/microkernel/DEBUG_RULES.md— operating rules (R1–R7) for this exact class of live-hardware hypervisor debugging, derived from the specific ways this task has previously produced misdiagnoses (also mirrored into Claude's cross-session memory aschimp-debug-rules).
Board was fully gone from USB (no 1-4 device object at all, confirmed via /sys/bus/usb/devices/) for ~21h. uhubctl investigation confirmed the board's port sits directly on the host's XHCI root hub (1d6b:0002, xhci_hcd/10p — the mobo's 4 physical USB ports, not a switchable external hub), which does not support per-port power switching (ppps) — so no software-level power-cycle was possible from the host side. Since USB also supplies the board's power, this looked like a genuine VBUS/power-latch event, not just a gadget-driver race. Fix: user physically reseated the USB cable ("рестартанул") → board re-enumerated cleanly (usb 1-4: new high-speed USB device ... Product: USB download gadget, Manufacturer: bzdOS), /dev/ttyACM0 back, U-Boot => prompt responds normally. Root cause of the original power loss itself still unconfirmed (could be board-side brownout/latch, could be host-side port fault) — no recurrence yet.
- Durable-fix ideas discussed (not yet implemented): independent UART-TTL console on the board's debug header (removes dependence on the single USB-OTG channel for recovery visibility), and/or an external powered/switchable USB hub with
pppsbetween host and board (root-hub ports on this mobo don't support it) so future power-cycles can be done in software viauhubctl -a cycle. - TFTP server: none was running after the session restart (no tftpd/tftpy previously installed as a persistent service — it had been running ad-hoc in a now-gone session shell). Fixed by starting
dnsmasq --port=0 --interface=br0 --bind-interfaces --enable-tftp --tftp-root=/opt/bzdos/tftpboot --no-daemon --log-facility=- &(log at /tmp/dnsmasq-tftp.log). Re-run this if TFTP fails again after another restart andss -lun | grep :69shows nothing. - Local FreeBSD source now available at
/opt/bzdos/build/freebsd-src(see the FreeBSD section below) — DTBP question fully resolved using it, no more curling GitHub needed for that investigation. - Harness fix applied (2026-07-14) to prevent a repeat of the hang:
loady_over_acm.pygained (1)acquire_port_lock()/release_port_lock()— a cross-processflockon/tmp/chimp-acm.lock, now called automatically insideopen_tty(), so two script invocations can no longer race-open the ACM port (root suspect for the wedge: a diagnostic probe + two back-to-backdbg-boot.pyruns, one straddling a slow TFTP failure, opened/interrupt-spammed the port within seconds of each other); it also adds a 0.5s settle delay after acquiring, and is idempotent per-process (won't self-deadlock on the sb-handoff open/close/reopen pattern). (2)tftp_preflight(host,port,tftp_root,autostart=True)— checks:69is listening before anytftpboot, auto-startingdnsmasq --enable-tftpif not, instead of letting U-Boot retry into an unknown-duration internal timeout. Wired intodbg-boot.py/dbg-boot-notvm.py/fbsd-boot.py(each now callsL.tftp_preflight()right afterwait_port(), before opening the tty).repl-client.py/repl-cmd.pydon't TFTP so they only benefit from the port lock (automatic, no script change needed there).
✅✅ FreeBSD track — REAL ROOT CAUSE FOUND AND FIXED (2026-07-14 ~22:50) — "Cannot get physical memory regions" is GONE
Everything in the "FreeBSD track" section below (DTB /memory node, libfdt version skew, DTBP virtual-vs-physical) was a chase down the WRONG branch. The DTB was always fine. The real bug: MODINFOMD_HOWTO was emitted as 0 (kload.c's kload_build_modinfo(), howto_val = 0 /* normal boot */). This specific kernel build's parse_boot_param()/freebsd_parse_boot_param() (inlined, doesn't match current FreeBSD "main" source 1:1 — some vendor/version skew) treats a HOWTO value of exactly 0 as a signal that "no real preloaded modinfo was found", and silently falls through to its own internal fake_preload_metadata(NULL, 0) — a hardcoded stub with MODINFO_NAME="kernel"/MODINFO_TYPE="elf kernel"/ADDR/SIZE but no DTBP tag at all. That's why OF_init never ran (fdtp/ofw_obj->ops stayed NULL) and OF_finddevice("/memory") always returned -1 regardless of how correct our real DTB was.
- Found via: a new one-shot HVC software breakpoint tool,
onebp.c/onebp.h(now a permanent part of the debugger image, wired intoel2_exc.c's HVC dispatch ahead ofgtrace_handle_hvc, breadcrumbONEBP @ 0x50007000). Needed because HW breakpoints/watchpoints/single-step are masked (PSTATE.D=1) this early — see memorychimp-guest-debug-masked.md. Mechanism: patch one guest instruction withhvc #imm(HVC isn't gated by PSTATE.D), dump GPRs to a breadcrumb on the trap, restore the original instruction, rewind ELR by 4, resume — fires exactly once.onebp_arm(pa, imm)/onebp_handle_hvc(frame).- Walked the bug live, one checkpoint at a time: confirmed
x0at_start's "backup the module pointer" (mov x1,x0, VA_start+0x85c) was already correct (our modinfo blob's KVA,0xffff000001400000) — ruling out a loader/eret bug. Then confirmedabp->modulepwas still correct atparse_boot_param's entry. Then confirmedfdt_check_header(modulep)correctly REJECTED it (not a DTB, as expected — it's our modinfo blob). Then confirmedpreload_kmdpwas STILL correctly pointing at our real blob right after the firstpreload_initkmdp()call (i.e. the "elf kernel" search succeeded). The value only went bad after the HOWTO fetch — traced the disassembly there and found thecbnzon the HOWTO value gating a silent re-run offake_preload_metadata+preload_initkmdpthat clobberspreload_kmdp. - Caution learned the hard way: a first version of
onebp_handle_hvcalso did a raw*(volatile uint64_t*)frame->x[0]to capture "whatever x0 points at" in the same shot. That's an EL2-context dereference of a GUEST KVA with no stage-1/2 translation — wrong address space, caused an EL2-own data abort (recorded in the genericEXC1breadcrumb, harmless/self-recovering sincekind>>2 != 2advances ELR, but noisy and misleading — looked like a new guest fault until traced to the diagnostic itself). Removed; onebp now only ever readsframe->x[]/frame->elr(already-captured register values, no address translation risk).
- Walked the bug live, one checkpoint at a time: confirmed
- The fix (
kload.c,kload_build_modinfo()):howto_val = 0x800(RB_VERBOSEfromsys/sys/reboot.h— a real, harmless boothowto bit; just means more boot console chatter) instead of0. Applied tokload.c(shared bydbg/fbsd/replbuilds) — rebuiltmicrokernel-dbg.bin/microkernel-fbsd.bin/microkernel-net.binall pick it up. Makefile also updated:onebp.oadded toDBG_OBJS,FBSD_OBJS,REPL_OBJS(anything linkingel2_exc.onow needs it, since the HVC dispatch callsonebp_handle_hvcunconditionally). - Verified on hardware, post-fix:
fdtp(static var inofw_fdt.c, KVA0xffff000000eccd58) now correctly reads0xffff000001200000(our real DTB) instead of0.preload_kmdp/preload_metadata(KVA0xffff0000010d5b88/...b90) now correctly read0xffff000001400000(our real modinfo blob) instead of the fake buffer's0xffff000001134ce9. The OLD panic ("Cannot get physical memory regions",initarm+0x664) is GONE. The OLD stack-corruption symptom (SP_EL1ending up in rodata,_etext+0x70) is ALSO gone —SP_EL1at the new fault is a proper stack address (0xffff000000e878d0). - NEW (next) blocker, much further along: guest now faults at
ELR_EL1 = 0xffff000000935be4(well past the old panic point0x934da4, deeper into/afterinitarm) withESR_EL1 = 0x96000045(EC=0x25, data abort, DFSC=0x05, translation fault level 1) andFAR_EL1 = 0x1000. Latched via the existingfirstfault.c/FF1V mechanism (0x50005800, full GPR dump). Root-caused the call site from the latchedx30/LR (0xffff000000939950) andx2=0xfc0(memset length): this ispmap_bootstrap_dmap()callingmemset_early(dest=0x1000, 0, 0x1000)(caller passesw2=0x1000; memset_std's head-alignment shaves 0x40 off before thedc zvabulk loop, hence the0xfc0seen at the fault) — i.e. FreeBSD is zeroing a page at VA0x1000while building the kernel's direct map, and that VA isn't mapped yet.- Checked and RULED OUT: the DTB
/memorynode'sregproperty.fdtget -t x bananapi-min.dtb /memory reg→40000000 40000000— base0x40000000, size0x40000000(1 GiB), which is the correct/sane DRAM base for this SoC. Sofdt_foreach_mem_region's region data is fine; the bug is NOT in the DTB this time. - Traced one more layer:
pmap_bootstrap_dmapis called frominitarm(call site0xffff0000009348ec) withx0 = x21 - KERNBASE(compiled asadd x0,x21,#0x1000000000000— adding the two's-complement ofKERNBASEis cheaper than a 64-bitsub, sinceKERNBASE's low 48 bits are 0;-KERNBASE mod 2^64 == 0x1000000000000).x21 = parse_boot_param()'s return value (lastaddr= ourkernend_val, a KVA) — sox0 = kernend_val - KERNBASE = max_delta, i.e. a physical-relative SIZE/offset (≈0x1600000, the aligned kernel+DTB+modinfo extent our loader computed), not a bug by itself. The0x1000fault address must come from somewhere DEEPER inpmap_bootstrap_dmap's own loop (it iteratesphysmem_avail()regions, storing running[x21/x28/x9 +320/328/336/1360/1368/...]offsets into a static array aroundbootstack+0x160— see disassembly from0xffff0000009392c4onward) — not yet fully traced to the exact wrong value. Next step: keep walking this loop (VA0xffff000000939344bl physmem_availonward) with the samer/gvatools, or drop anotheronebpbreakpoint right at thedc zvafault site (0xffff000000935be4, though note this is insidememset_std, so a breakpoint at the caller —0xffff000000939940, theldr x0,[x21,#320]right before thememset_earlycall — would show the exact bad address landing in[x21+320]before it's ever passed down). - Confirmed via
onebp: right beforememset_early(VA0x939944, just afterldr x0,[x21,#320]at0x939940),x0 = 0x1000exactly —pmap_bootstrap_dmap's own prologue-computed value really is that small, well before the actual fault. Went one level further back to the call site itself (0xffff0000009348e8,add x0,x21,x8) to readx21/x8directly, but that shot came back internally inconsistent (x21=0x1000000000000,x8=0xffffffffffffffff— doesn't square with thex0=0x1000result already confirmed one step later). Most likely a manually-miscounted breakpoint PA on that last shot, not a new finding — didn't chase it further. Disarmedonebpagain (removed theonebp_arm()call frommain_dbg.c; module + breadcrumb stay in the tree, harmless when unarmed). Trustworthy bottom line: the argument topmap_bootstrap_dmapis genuinely in(0, 0x1000]instead of the multi-megabyte value expected — whether that'sparse_boot_param's return value itself being near-zero, or a callee-saved register (x19-x28per AAPCS) getting reused somewhere between themov x21,x0right afterparse_boot_paramand this call site, is still open. Re-verify the exactadd x0,x21,x8instruction's PA from a fresh disassembly line before trusting a register dump there again.
- Checked and RULED OUT: the DTB
- Board currently running microkernel-dbg.bin (FreeBSD guest frozen in the early panic-storm).
- All images build green: microkernel{,-stage0,-net,-repl,-fbsd,-dbg,-hdmi}.bin. Cross: aarch64-linux-gnu-.
- Reload cycle (button-free):
repl-cmd.py "reset"(REPL) or WDOG poke (wb 0x01c20cb4 1; wb 0x01c20cb8 1; w 0x01c20cb0 0x14af) from dbgmon → U-Boot (bootdelay=-1) →repl-client.py(REPL) /dbg-boot.py(FreeBSD dbg, TFTPs kernel+DTB). - Command channels: EMAC raw Ethernet 0x88B5 (repl-cmd.py / dbgmon) + reliable netcon 0x88B6. Loady over ttyACM (U-Boot). The running image drops U-Boot's ACM gadget (zombie-fix), so /dev/ttyACM0 disappears while our image runs and returns after a WDOG reset to U-Boot.
The infamous "bogus SP in rodata" (SP_EL1=0xffff000000a90070=_etext+0x70) was only a SYMPTOM. Real chain, innermost last:
SP=0xa90070 in rodata ← boot-stack overflow from a recursive exception storm
← unwind_frame(NULL) ← data abort on VA 0 (PC 0xffff000000952d04, ESR 0x96000005 = xlat fault L1)
← stack_save ← reads pcpu->pc_curthread = NULL this early (x0=0)
← kdb_backtrace
← vpanic / panic()
← initarm+0x664 (0xffff000000934da4): panic("Cannot get physical memory regions")
← fdt_foreach_mem_region (0x19ffa0) returns nonzero
← OF_finddevice("/memory") (called at 0x19ffd4) returns -1 ◀── CURRENT BLOCKER
- Confirmed: fdt_foreach_mem_region returns 0 on success, errno on failure (NOT a count). The exclusion/KERNEND path (physmem_exclude_region @0x9348a8) runs AFTER this branch, so MODINFOMD_KERNEND is NOT the cause of this panic.
- MODINFOMD_KERNEND is a VIRTUAL (KVA) end address in FreeBSD arm64 — our 0xffff000001600000 is the right kind (only matters post-panic).
- The kernel's early identity map (TTBR0) only covered ~[0x46000000, 0x47000000). Our old modinfo/DTB at 0x4a000000/0x4a100000 were unmapped → parse_boot_param's
ldr [modulep]faulted. - FIX (in kload.c / main_dbg.c / main_fbsd.c / repl.c): hand a virtual modulep (create_pagetables sizes TTBR1 from x0 when x0≥KERNBASE → maps [pa_base, phys(modulep)+~6MB]); relocate DTB→phys 0x47200000 (KVA 0xffff000001200000), modinfo→phys 0x47400000 (KVA 0xffff000001400000). Verified on HW:
gvanow maps both (DTB reads d00dfeed, modinfo reads MODINFO_NAME).
- DTB
/opt/bzdos/tftpboot/bananapi-min.dtbis a MINIMAL, non-standard DTB (root #address/#size-cells = 1/1, NOT the arm64-stock 2/2). The FreeBSD FDT parser reads cells from root and decodes 1/1 correctly; do NOT switch to 2/2 — /soc usesranges;with 1/1 and 2/2 would misparse GIC/UART/timer regs → new early fault. - Added a
/memorynode; iterated 3 formats (page-9memory@40000000; page-0 exact-namememoryfirst child +/chosen2nd). Latest blob: size 42940, totalsize 0xa7bc, md5 b000eb1a58b3a1ec8b361f78f8ea6423. Backups: bananapi-min.dtb.bak (no mem node), .page9-memnode.bak. fdtget -l /on our blob lists children: memory, chosen, cpus, display-engine, ... →/memoryIS structurally findable at root.- Loaded blob verified on HW at guest phys 0x47200000 (d00dfeed, totalsize 0xa7bc). DTBP (KVA) points at it. OF_init SUCCEEDED (the "OF_init failed"/"Cannot install FDT" panics did NOT fire).
- YET the guest still panics identically (live sr: SP_EL1=0xa90070; guest-stack vpanic caller byte-identical = initarm+0x664). So the kernel's OF layer isn't resolving /memory in the fdt it installed.
Disassembled initarm around VA 0xffff0000009347d4-0x934818 (the DTBP tag lookup + OF_install/OF_init call) and OF_finddevice/preload_search_info:
mov w1, #0x9002 ; MODINFO_METADATA(0x8000) | MODINFOMD_DTBP(0x1002)
ldr x0, [x23, #2952] ; x0 = kmdp
bl preload_search_info ; find the DTBP metadata record
cbz x0, +skip ; tag not found -> skip OF_install/OF_init entirely
ldr x22, [x0] ; x22 = *record = the DTBP VALUE itself
cbz x22, +skip ; value==0 -> also skip
bl OF_install ; OF_install("FDT", 0)
tbz w0,#0, <panic> ; OF_install failure -> panic (NOT hit — confirms tag found, x22 nonzero)
mov x0, x22; bl OF_init ; OF_init(x22 = our DTBP value)
cbnz w0, <panic> ; OF_init failure -> panic (NOT hit either)
... (converges here whether or not the tag was found; unconditionally calls fdt_foreach_mem_region later)
So OF_install/OF_init do NOT explicitly fail — meaning our DTBP tag IS found with a nonzero value, and OF_init "succeeds" per its own check. But fdt_foreach_mem_region's internal OF_finddevice("/memory") still returns -1.
Prime suspect found: a documentation/implementation mismatch in OUR OWN loader. kload.h line 173:
#define MODINFOMD_DTBP 0x1002u /* vm_paddr_t PHYSICAL pointer to the DTB */— our own header comment says DTBP should be PHYSICAL. But kload.c (~line 487) computes:
dtbp_val = kls.kernbase + (dtb_dst_pa - kls.pa_base); /* KVA, NOT physical! */— we emit a VIRTUAL (KVA) value. If the real kernel's ofw_fdt_init/ofw_fdt_finddevice (no exported symbol found in the stripped binary — likely static/inlined, could not pin down directly) internally treats the value as physical and re-translates it (e.g. DMAP-style conversion), handing an already-virtual KVA would double-translate into garbage memory — plausibly still "readable" (no crash) but not our actual DTB, which would exactly explain OF_init "succeeding" while /memory still resolves to ENXIO.
RESOLVED — DTBP physical-vs-virtual question is CLOSED, our KVA was always correct. Confirmed by reading real FreeBSD source (see checkout below, no curl needed):
sys/arm64/arm64/machdep.ctry_load_dtb():vm_offset_t dtbp = MD_FETCH(preload_kmdp, MODINFOMD_DTBP, vm_offset_t);thenOF_init((void *)dtbp).vm_offset_tis a VIRTUAL type — confirms our KVA is the right kind, not physical.kload.h's "PHYSICAL pointer" comment was simply wrong (stale/incorrect note from earlier research) — safe to ignore/fix later, not a functional bug.sys/dev/ofw/ofw_fdt.cofw_fdt_init(ofw_t, void *data): doesfdt_check_header(data)thenfdtp = data;— stores the pointer VERBATIM, zero translation. Since OF_init doesn't panic,fdt_check_headeron our DTB succeeded.ofw_fdt_finddevice(ofw_t, const char *device):offset = fdt_path_offset(fdtp, device); if (offset < 0) return (-1);— this is a bog-standard libfdt call, nothing FreeBSD-specific. SoOF_finddevice("/memory")returning -1 means plainfdt_path_offset(fdtp, "/memory")fails inside libfdt itself on our blob, despite host-sidefdtget/dtcparsing the same file fine.
libfdt version skew — ALSO RULED OUT. Built a tiny host-side test program linking the EXACT libfdt source bundled in the FreeBSD tree (sys/contrib/libfdt/{fdt.c,fdt_ro.c,fdt_wip.c,fdt_strerror.c,fdt_addresses.c}, compiled with plain gcc, see /tmp/fdt_test.c) and ran it directly against our actual file:
$ /tmp/fdt_test /opt/bzdos/tftpboot/bananapi-min.dtb
fdt_check_header: 0 (OK)
fdt_path_offset("/memory"): 140
device_type: memory
reg len=8
The exact kernel-bundled libfdt code parses our file perfectly — /memory resolves at offset 140, device_type/reg read back correctly. So: not the DTB content, not libfdt, not a version quirk, not DTBP phys-vs-virtual. Confirmed the file tested is byte-identical (md5 b000eb1a58b3a1ec8b361f78f8ea6423) to what was loaded onto the board.
This narrows the remaining mystery to ONE place: something about the GUEST's actual runtime state differs from "host file + correct libfdt", despite every static check passing. Was mid-way through re-verifying the guest's live in-memory copy via dbgmon when the board went unresponsive (see the hang note at the top of this file) — untested.
FreeBSD — NEXT STEPS (in priority order, DTBP + libfdt threads are both CLOSED — do not re-open either)
- FIRST: get the board responding again (see hang note at top of file — this blocks everything below).
- Verify the GUEST's in-memory copy of the DTB byte-for-byte, not just header+totalsize. We only checked magic (d00dfeed) + totalsize match at guest phys 0x47200000 — read the actual struct-block bytes at the
/memorynode's known host-file offset (0x8c onward, node found at libfdt offset 140 decimal = 0x8c) and diff against the host file via dbgmond 0x4720008c <len>. A partial/corruptedmemcpyinkload_build_modinfo, a cache-coherency gap (D-cache clean happens on our side, but confirm the GUEST reads via a mapping that actually sees the cleaned data and not a stale cache line), or an off-by-one inkload_dtb_totalsizeare the remaining live suspects — this is now the single most likely place the bug lives. - Fallback: instrument w0 at initarm 0x934884 (return of fdt_foreach_mem_region): 6=ENXIO (node/reg not found), 34=ERANGE (cells/len) — narrows it further if needed. Or: use the
ffv/stage-2-vector-unmap machinery to catch this SPECIFIC panic path once more precisely (it currently only proves "some early panic happened", not which one — could be extended to latch at the panic() call site directly instead of the vector, e.g. an additional stage-2 trap on the panic string's code page).
/opt/bzdos/build/freebsd-src — sparse, shallow (depth 1, blob:none filter) clone of github.com/freebsd/freebsd-src (main branch), ~32 MB. Currently checked-out dirs: sys/dev/ofw, sys/dev/fdt, sys/arm64/arm64, sys/kern, sys/sys. To pull in more of the tree without re-cloning: cd /opt/bzdos/build/freebsd-src && git sparse-checkout add <path> (e.g. sys/contrib/libfdt for the actual FDT parser). Cloned with proxy env vars unset (they were empty in this shell already — no explicit unset needed, but if a future shell has http_proxy/https_proxy set, unset them for the clone per the global proxy note).
main_dbg.ccallsstage2_unmap_guest_vector()→ unmaps the guest EL1 vector page (IPA 0x46927000) in stage-2. The guest's first fault → tries to execute its vector → stage-2 instruction abort to EL2 →firstfault_handle()(firstfault.c) latches the guest's ORIGINAL ELR_EL1/ESR_EL1/FAR_EL1 + GPRs to FF1V @ 0x50005800, remaps, resumes.ffvdecodes it.- CRUCIAL: HW breakpoints / watchpoints / single-step DO NOT work in FreeBSD early boot — the guest runs PSTATE.D=1 (debug masked) until cninit (verified: 0 bp hits). Stage-2 vector-unmap is the only way to catch early guest faults. (Saved to memory: chimp-guest-debug-masked.md.)
- CAVEAT: FF1V/ffv shows unwind_frame(NULL) for ANY early panic (all route through kdb→unwind_frame) — it does NOT distinguish the panic REASON. To get the reason, walk the guest stack above SP (FP chain {fp,lr} pairs) and resolve LRs with addr2line vs /opt/bzdos/tftpboot/kernel.
musb.c (sunxi A64 MUSB gadget). Layers solved this session (each verified on HW via breadcrumbs):
- Framing: 18-byte device descriptor was sent as 20 (32-bit-only FIFO writes padded). Fixed → word-head + byte-tail (no 16-bit halfword — A64 FIFO word+byte only).
- Hang: musb_poll AHB-stalled after bus reset. Root = a reset-time
CSR0=FLUSHFIFOwrite. Removed. word34=0x2f (musb_init complete), word35=0x3f (poll returns) confirm no hang. - DATA toggle: first EP0 IN packet after SETUP must be DATA1. Was DATA0 (SVDRXPKTRDY folded with TXPKTRDY). Fixed → SVDRXPKTRDY alone, spin for RXPKTRDY clear, then TXPKTRDY alone.
- DATAEND / SETUPEND: separated DATAEND to status stage; added COUNT0-priority SETUP decode for coalesced SETUP+SETUPEND.
- SET_ADDRESS now STICKS: word16=FADDR readback=9, word32 high=1 (device receives a SETUP at the assigned address 9). First traffic at the assigned address.
- After SET_ADDRESS sticks, the host's addressed GET_DESCRIPTOR SETUP is RECEIVED (word32 high=1) but not decoded/answered → word46=0, device count stuck at 1. Host dmesg cycles "device descriptor read/all -110", "not accepting address -62".
- USB Sonnet's latest build adds word47 (0x500000bc) = (bmRequestType<<24)|(bRequest<<16)|wLength and word48 (0x500000c0) = (COUNT0<<16)|wValue — raw capture of the addressed SETUP. NOT YET TESTED (needs REPL rebuild + disconnect→mi→3×mp, then
bc 0x500000bc 2). - Breadcrumb map (0x50000000 base, MUSB): counters 0x60 (device/reset/config/setaddr), word16/17 @0x40 (FADDR readback / timeout), word32/33 @0x80 (SETUPs@FADDR≠0 / reset-suspend), word34 @0x88 (init phase, 0x2f=done), word35/36 @0x8c (poll phase / INTRUSB), word40-43 @0xa0 (EP0-TX), word44 @0xb0 (0xA armed/0xB stuck/0xC host-read), word45 @0xb4 (SETUPEND), word46 @0xb8 (full-descriptor count), word47/48 @0xbc/0xc0 (addressed SETUP raw).
- USB test recipe: on REPL image —
c 0x42008e00(usb_gadget_disconnect) thenmithenmp 5000000×3 back-to-back (tight timing needed — host gives up in ~2s; too much network latency between mi and mp misses the window). Then read counters/words + hostdmesg | grep "1-4"/lsusb | grep 1d6b:0010.
- ktimer.c (soft timers: one-shot/periodic/cancel, ksleep), ksync.c (mutex w/ priority inheritance, counting sem, mailbox), wcet.c (period/budget/deadline monitor). Integrated into REPL.
- sched.c gained
sched_block_if(token)+sched_set_priority()(appended). REPL commands:ktt/kst/wct. - Results: KTIMER (0x50005600) pass, periodic=5, cancel ok. KSYNC (0x50005500) pass, mbox=4, sem=1. WCET (0x50005400) pass (overruns=1,budget=1,deadline=0).
All compile clean; NOT yet wired into images (integrate incrementally, build+test each):
- HW breakpoints + backtrace (hwbp.c/.h, backtrace.c/.h) — INTEGRATED in dbg/repl/fbsd (
bp/wp/bpc/bpl/bt). (Note: bp/wp don't fire in debug-masked early guest boot.) - SMP (smp.c/.h + start.S/sched.c) — linked into all images (smp.o), but
smp_init()call site NOT yet added, so secondaries stay parked. Heartbeat breadcrumb 0x50000900. Needs the el2_exc per-core timer snippet + smp_init() in a main to activate the 4 cores. - firstfault (firstfault.c/.h) — INTEGRATED in dbg (the
ffvtool above). - event-trace + profiler (trace.c/.h, profiler.c/.h) — trace ring 0x50004000 "TRC1" (16-byte entries), PROF histogram 0x50006800 "PROF". Snippets: trace_ctx_switch in sched, trace_tick/trace_trap/profiler_sample in el2_exc IRQ/trap. NOT integrated. NOTE: HUD Gantt/flame track assumed PROF@0x50004800 but profiler put it at 0x50006800 — reconcile when integrating.
- HUD Gantt/flamegraph (hud.c/.h edited) — new SCHED GANTT + PROFILE panels reading the trace ring + PROF histogram. Owns hud.c (in REPL). Only shows data once trace/profiler are integrated AND the scheduler/guest runs.
- panic-log + coredump (panic.c/.h, coredump.c/.h) — persistent panic log 0x50005000 "PANC" (survives reset, auto-dump on next boot via panic_dump_if_present); coredump over Ethernet 0x88B7 (ELF core for gdb). Snippets for el2_exc fatal path + mains. NOT integrated.
- virtio-console + virtio-blk (virtio.c/.h, virtio_console.c, virtio_blk.c) — virtio-mmio @0x0A000000 (console)/0x0A000200 (blk), RAM disk @0x7C000000, guest INTIDs 48/49. Breadcrumb 0x50005000... (collides with PANC — reassign). Needs stage2 unmap of the virtio window + el2_exc fault dispatch + guest DTB nodes. NOT integrated. (Only useful once FreeBSD boots.)
- vGIC + virtual timer (vgic.c/.h) — GICv2 virt: GICH@0x01c84000, GICV@0x01c86000, maintenance INTID25, vtimer INTID27. Self-test EL1 guest; breadcrumbs 0x50001c00/0x50001d00. Needs stage2 GICC→GICV remap + el2_exc inject. NOT integrated.
0x50000000 musb · 0x50000100 emac · 0x50000300 repl · 0x50000400 exc · 0x50000600 hwbp · 0x50000700 btr · 0x50000900 smp · 0x50000e00 dbg · 0x50000f00 vconsole · 0x50001c00/0x50001d00 vgic · 0x50002000 gtrace · 0x50002400 ffl1 · 0x50002800 sst1 · 0x50003000 hdmi · 0x50004000 trace · 0x50004800/0x50006800 prof (mismatch — HUD vs profiler) · 0x50005000 panc/virtio(collision!) · 0x50005400 wcet · 0x50005500 ksync · 0x50005600 ktimer · 0x50005800 ff1v(ffv)
Added __popcountdi2/__popcountsi2 (smp.c uses __builtin_popcountll; freestanding = no libgcc).
EL2 traps, CNTP 1ms tick + jitter, lock-free ring, slab allocator, fixed-priority scheduler, EL1 guest + preemption, netcon reliable transport, hot-reload, network REPL + remote/fast reset, HDMI 1080p + HUD dashboard, live debugger (gr/sr/gva/r/rb/d/w/wb/t/pt/ff/ss/bp/wp/bt/ffv), trap-and-emulate vconsole/gtrace.