# Incident 2026-07-12: Office1 OPNsense edge triple-faults at the BTX loader

> ## ROOT CAUSE FOUND 2026-07-12 (DOCFIX-188) -- READ THIS FIRST, THEN STOP
>
> **The guest had 2 MiB of RAM.** Not a CPU, nesting, machine-type, or console problem.
> Everything below this box is the ORIGINAL (wrong) investigation, retained for the record.
> **Do not work the "ranked next steps" -- they all chase the wrong layer.**
>
> `dmacvicar/libvirt` >= 0.9 changed `memory` from MiB (the 0.8-era meaning the modules were
> written against) to **raw libvirt units, defaulting to KiB**. With no `memory_unit`,
> `memory = 2048` rendered `<memory unit='KiB'>2048</memory>` -> QEMU **`-m size=2048k`** =
> **2 MiB**. Measured end-to-end: module input -> tofu state -> domain XML -> `virsh dominfo`
> (`Max memory: 2048 KiB`) -> the live QEMU cmdline.
>
> `boot2` is tiny and fits in 2 MiB, so it echoes `/boot.config` and *then* triple-faults
> handing off to `/boot/loader`, which does not fit. **That is why the fault was
> deterministic at exactly 262 bytes and immune to every CPU/machine/disk/console change
> tried -- none of them touched the cause.**
>
> **Fix:** `memory_unit = "MiB"` on the `libvirt_domain` in all three VM modules
> (`opnsense-edge`, `cloudinit-vm`, `node-vm` -- the same defect was latent in all of them
> and would have broken every future VR1 VM). Guarded against recurrence by
> `scripts/opentofu-validate.sh` S1. See
> `docs/changelog-20260712-libvirt-memory-unit-rootcause.md`.
>
> **Lesson for the next incident:** a bootloader that dies at a fixed byte offset, immune to
> every knob you turn, is a *resource* problem, not a CPU-feature problem. The domain XML and
> the QEMU cmdline are ground truth -- read them before theorising about nested virt.
>
> **Status:** root cause fixed in repo; live boot verification is the remaining gated step.

**Original status (SUPERSEDED):** OPEN. The Office1 OPNsense edge VM is built and *starts*, but OPNsense
triple-faults ~262 bytes into boot. Blocks the Office1 headend (router+DHCP for
`office1-local`) and therefore the Office1 NetBox VM. Written at a context limit as a
resume artifact -- a fresh session should read this + `docs/session-ledger.md` first.

## Symptom (confirmed, reproducible)
- Domain `office1-opnsense` (libvirt, `qemu:///system`) enters `running (booted)` then
  goes to `paused (unknown)`. `virsh resume` fails with *"cont: Resetting the Virtual
  Machine is required"* -> the guest **triple-faulted**.
- Serial capture (`/var/lib/libvirt/vr1/staging/office1-opnsense-serial.log`, `root:600`,
  read with `sudo cat`) contains exactly, every boot:
  ```
  /boot.config: -S115200 -h -D
  ```
  i.e. FreeBSD `boot2` echoes `/boot.config` (serial 115200, `-h` serial console, `-D`
  dual console) and then triple-faults **handing off to the BTX `/boot/loader`**. The log
  mtime updates on each boot (confirmed fresh, not stale); it is deterministically 262 bytes.

## Environment
- **Double-nested virt:** OPNsense guest -> `vcloud` host (itself a VM: virtio NIC/disk)
  -> outer hypervisor. Host CPU model reported by libvirt: **AMD `Opteron_G3`**. Nested
  KVM on (`kvm_amd/parameters/nested = 1`).
  - **CORRECTION 2026-07-12 (DOCFIX-189): the `Opteron_G3` label is a RED HERRING.** The
    real CPU is an **AMD EPYC 9965 (Zen 5, family 26)** (`/proc/cpuinfo`). libvirt's CPU
    model database does not know family 26, so it falls back to the oldest matching model
    name. The prior session built a nested-virt/ancient-CPU theory partly on this artifact.
    Read `/proc/cpuinfo`, not libvirt's model guess.
- Image: OPNsense **26.1 nano** amd64 (`scripts/opnsense-prep-image.sh 26.1`), prepped to
  `/var/lib/libvirt/vr1/office1/opnsense-26.1-nano.qcow2` (11 GiB virtual).
- Module: `opentofu/modules/opnsense-edge`, instantiated as `module "office1_opnsense"` in
  `opentofu/main.tf`. LAN=`office1-local` (`vtnet0`), WAN=`office1-wan` (`vtnet1`, NAT
  `172.30.1.0/24`). Config ISO (real-ISP-router config, DOCFIX-185) at
  `/var/lib/libvirt/vr1/staging/office1-opnsense-config.iso`.

## What was tried -- ALL applied and verified in the domain XML, NONE resolved it
1. **Serial console added** (module gap -- nano is serial-only). Got the boot *to* the
   loader stage (from a no-console early fault to the 262-byte `/boot.config` point).
2. **`machine` q35 -> i440fx** (`pc-i440fx-noble`, verified). No change. (Note: machine +
   cpu are create-time; the provider does an in-place "change" that does NOT apply --
   must recreate the domain: `virsh destroy && virsh undefine`, then `tofu apply`.)
3. **Disk: COW overlay -> direct per-VM copy** of the nano (verified 11 GiB / 2.14 GiB
   allocated, no backing). No change.
4. **CPU `host-passthrough`** (verified). No change.
5. **Disable AMD `svm`** (`<feature policy='disable' name='svm'/>` in XML -- the documented
   AMD nested-virt fix, forum: `-cpu host,-svm`). **Still 262 bytes.** NOTE: the provider's
   CPU-feature key is `features` (plural); `feature` validates but is silently dropped.

## Ranked next steps for a fresh session
1. **Full forum CPU flag set**, not just `-svm`: add `+kvm_pv_eoi,+kvm_pv_unhalt` (and try
   without `svm` masking too). Likely via `libvirt_domain.qemu_commandline` since the
   provider's CPU `features` may not fully translate under `host-passthrough`. Confirm the
   masking actually reaches the guest CPUID.
2. **Video device (`-D` dual console).** `/boot.config` requests dual console but the domain
   has **no video/graphics device** -- `boot2`'s VGA init may fault. Add a `graphics` +
   `video` (VGA/std) device and retry. (Cheap, plausible, not yet tried.)
3. **Memory 2 GB -> 4 GB** (below OPNsense's 3 GB min; guides use 4096). Recreate to apply.
   - **CORRECTION (DOCFIX-189): this step was RIGHT FOR THE WRONG REASON, and its premise is
     unsourced.** The memory *was* the problem -- but it was 2 MiB, not "2 GB but a bit
     small", and no measurement was taken to notice that. The "3 GB min" figure is
     UNVERIFIED (no source given); do not propagate it. If 2 GiB proves insufficient after
     the real fix, check OPNsense 26.1's documented minimum before picking a number.
4. **UEFI boot (OVMF)** instead of legacy BIOS/BTX -- sidesteps BTX entirely, but the nano
   is a BIOS/MBR image, so this needs care (or a different image build).
5. **Outer-hypervisor CPU:** under double-nesting, vcloud's exposed CPU (`Opteron_G3`) may
   not provide what FreeBSD's BTX needs. Consider testing a minimal FreeBSD/OPNsense boot
   directly on vcloud to isolate whether it's nesting-depth-specific.

## Reproduce / operate
- Recreate + boot: `cd opentofu && source ~/vr1-stage1.env && virsh -c qemu:///system
  destroy office1-opnsense; virsh -c qemu:///system undefine office1-opnsense; tofu apply`.
- Watch: `virsh domstate --reason office1-opnsense`; serial via `sudo cat` the log above.
- Provider CPU/serial schema is attribute-style + nested under `devices`/`cpu`; introspect
  with `tofu providers schema -json` (see how `serials`/`features` were found this session).
- As-executed log: `~/as-executed/2026-07-12-dc-dc-phase1-office1.log`.
- Creds (jumphost-only, 0600, `~/vr1-office1-creds/`): SSH key + OPNsense root pw/hash.
  **Operator: revoke the pasted NetBox token + these at close.**

## Cross-refs
- `docs/changelog-20260712-opnsense-edge-boot-fixes.md` (DOCFIX-187, the module changes).
- `docs/changelog-20260712-office1-opnsense-edge-build.md` (DOCFIX-186, the build + the
  apparmor/config-iso-staging findings).
- `docs/changelog-20260712-opnsense-edge-real-isp-router.md` (DOCFIX-185, config posture).
