Вся документация
С чего начать
Тесты и гейты
docs/testing.mdОткрыть на GitHubЭта страница документации пока есть только на английском. Документация собирается прямо из каталога docs/ репозитория, поэтому всегда соответствует коду. По-русски написаны руководства.
There is no test runner and no separate test binary. What checks the kernel
is, from the most often run to the least: the self-tests every boot runs,
the smoke boots CI runs on both architectures, and a gate per subsystem for
what a smoke boot cannot notice. Each of them is judged by its exit code,
never by grepping what it printed: a build that stops on a stale dependency
file says "No rule to make target", not "error", and a log that was never
written contains no PANIC: either.
The short form of this page -- which gate to run after touching what -- is
the table in CLAUDE.md. This is the long one: what
each gate does, and the failure it is there for.
Self-tests at boot
Self-tests live in src/cpp/kernel/test.cpp; each is a
Stdlib::Error TestXxx() function. They run early in boot, from
Test::Test() (called in main.cpp:Main2, and from main_arm64.cpp) and
Test::TestMultiTasking(), and a failing one returns a non-success
Stdlib::Error. The Rust side's run from the same place, through
rust_test (src/rust/kernel/src/lib.rs) -- among them the frame test
(kernel/src/frames.rs): a page mapped nowhere, written a piece at a time at
every alignment and read back on every running CPU, each through its own
slot of the temporary window, which is how every copy of a guest's memory is
made. A slot that maps the wrong page fails it (frame selftest: ...), and
so the smoke boot, on both architectures. The ring test
(kernel/src/ring.rs) takes kcore::ring::LocklessRing -- what the frame
pool and netblk pass frames and requests through -- full and empty lap after
lap, then runs one producer and one consumer through two cells, and two of
each through sixteen, each task on a CPU of its own where there are four:
nothing lost, nothing twice, each producer's values in its order, and what a
producer wrote before a push seen by whoever pops it. That last is what a
memory ordering too weak would break on arm64 alone, and the one-to-one run
is what catches it: a ring whose sequences were relaxed failed eight boots
of eight under HVF, while TCG on an x86 host may never show it. Its line
says what a push and a pop cost on one CPU and how long each run took
(ring selftest: passed ...): a number every boot prints is noticed when it
moves. And on every CPU TestSleepPrompt has a task of its own sleep a
millisecond twenty-one times while the CPU halts between interrupts: the
median sleep must be under 5 ms -- 1.0-2.4 under HVF and TCG, 10.0 when the
CPU's timer is not armed for a sleeper's deadline and the tick wakes it
(the local timer).
- To add a test, write a
TestXxx()and register it in theTest()dispatcher. - To run a single test, edit
Test()to call only that function, rebuild, boot, and watchnos.log.
Two more run at boot when there is something to run them on: TestModules
loads the modtest module embedded in the image and then feeds the loader
damaged copies of it (see Loadable modules), and
fstest=on runs the filesystem self-test on / once it is mounted
(fstest: passed or fstest: FAILED; see Filesystems).
Smoke boots
./scripts/smoke-test.sh (x86-64) and ./scripts/smoke-arm64.sh (arm64)
build in Docker, boot the kernel headless and assert the serial markers
After test → Preempt is now on → boot: complete, failing fast on
PANIC:. What each marker means is in Boot. Both take
--skip-build; SMOKE_TIMEOUT is the seconds to wait (300 by default) and
SMOKE_LOG where the serial log goes. The arm64 one runs under TCG so that
it runs anywhere, CI included; SMOKE_HVF=1 runs it under HVF on Apple
Silicon, about four times as fast.
The x86-64 smoke boot attaches virtio-blk (modern), virtio-scsi (legacy),
NVMe, virtio-net and virtio-rng, so it covers the Rust drivers and the MSI-X
path as well as the C++ underneath them. The arm64 one attaches virtio-mmio
blk, net and rng. Both boot with fstest=on.
Every refactor step must keep both green, and CI
(.github/workflows/ci.yml) runs make check, a build and a smoke boot
for x86_64 and for aarch64. A change to common code that only builds on
x86 is a broken change.
The gates
What a smoke boot cannot notice has a test of its own. Every one of them is there for a failure that looks like success -- a driver that agrees with itself, a log that ends because the network ate the rest of it, a slot that leaks once per connection -- and most were written the day after meeting one. When you meet another, write another.
All of them are in scripts/, want the kernel built first (make, or
make ARCH=aarch64), take --keep to leave their scratch directory behind,
and exit 0 only if every check passed. Most are arm64 only, because they
drive the shell over UDP and the arm64 boot is the one whose command line
carries udpshell=; the code they test is the same on either architecture.
Every one of them is also a check for undefined behaviour in the code it
drives, when the kernel it boots is built with the checks on: UBSAN=1 for
the C++ (Build) and RUSTUB=1 for the Rust
(Build), separately or together. The first
undefined operation, and the first unsafe precondition core finds
violated, is a panic naming its site, which every gate fails on. CI builds
with both and boots it on both architectures after the rest; after touching
C++ or Rust, run the gate that covers it on such a build too. A report is
nothing a plain build would show -- the first UBSan boots found a null
member access in CONTAINING_RECORD, stacks 8 off the ABI's alignment and
an arm64 boot stack that overran the page tables below it, all under gates
that passed, and the first RUSTUB=1 run found the kernel dropping a log
line too long for its buffer instead of truncating it, which had been
swallowing panic messages whole.
| Gate | Arch | Run it after touching |
|---|---|---|
wx-test.sh [--arch x86_64|aarch64] |
both | a mapping path |
panic-test.sh [--arch x86_64|aarch64] |
both | the panic path, panic=N, a reset |
parttest.py [--arch aarch64|x86_64] |
both | the block layer, the partition tables |
ext2-test.py |
arm64 | ext2 |
nanofs-test.py |
arm64 | nanofs |
disklog-test.py |
arm64 | the disk log |
netconsole-test.py |
arm64 | the trace path, a net device, netconsole |
tcp-test.py |
arm64 | TCP, the HTTP client |
sshd-test.py [--arch aarch64|x86_64] |
both | TCP's listening side, the module loader, the ffi declarations |
netblk-test.py [--arch x86_64|aarch64] |
both | the block layer's asynchronous path, the C ABI a module reaches block and net through |
netload-test.py [--arch aarch64|x86_64] |
both | the receive path, the frame pool, modules/netload, kcore::net's listener, the tick's receive poll (rxpoll) |
usb-test.py |
x86-64 | drivers/usb/ |
hv-test.py [--arch x86_64|aarch64] |
both | hv, hvarch, modules/hv -- the hypervisor |
insn-test.py |
host (CI) | the hypervisor's MMIO decoder, its decoder of a mov of a control register, and its guest page walker (hv/src/{insn,walk}.rs), against the encodings clang gives |
hv-fuzz.py [--seed N --seconds S] |
host (CI) | anything a guest reaches in the hypervisor -- its devices, local APIC and IO-APIC, the MMIO path, the Linux loader and ACPI tables, the CPUID and MSR policy, the run loop that dispatches its exits, on every CPU of a guest of several, and the guests' network: the switch, its DHCP server and NAT -- fuzzed with overflow checks on |
net-fuzz.py [--seed N --seconds S] |
host (CI) | anything the network hands the kernel -- the receive path, ARP, ICMP, UDP and TCP; the DHCP client and the DNS resolver; the HTTP client over TCP and over TLS; the UDP shell; netconsole; sshd over the ssh crate -- fuzzed with overflow checks on, each protocol against a model of it |
fs-fuzz.py [--seed N --seconds S] |
host (CI) | anything a disk hands the kernel -- the partition tables, the disk log's area, ext2 and nanofs images sound and damaged -- and the storage layers above it: the block table's bounds and claims, the VFS and its C ABI, the file ABI, the shell's storage commands, root=; fuzzed with overflow checks on, the filesystems against a model of the tree, e2fsck's judgement and power cuts |
cpp-fuzz.py [--seed N --seconds S] |
host (CI) | what firmware, a bootloader or a disk hands the kernel's C++ -- the device tree and what Board takes of it, the memory map, the GRUB environment block, a module's .ko, the x86 boot's Multiboot2 tags, microcode file, ACPI tables and command line, and the printf every line is formatted by -- and the memory management under everything: the page tables and the physical page allocator past Setup, the kernel heap; the kernel's own sources under the address and undefined-behaviour sanitizers, each against a model of what its header promises |
hv-linux-test.py --bzimage <img> [--initrd <cpio>] |
x86-64, by hand | the Linux loader, the CPUID/MSR policy, the emulated devices -- a real kernel to its shell; with an initrd, guests that stay up and the commands that reach them; --net, the guests' switch, NAT, its DHCP server and the DNS server they are given; --cpus N, guests of N CPUs, their local APICs and IPIs; --xapic, those APICs in xAPIC mode, every access of theirs by MMIO; --ioapic, an IO-APIC routing the timer, the serial port, the SCI and -- with pci=nomsi -- virtio's INTx; --acpi, a guest kernel with ACPI: the tables it is given, the PM timer and the SCI, the reset register, and hv stop's power button; --qemu-cpu MODEL, nos on a named TCG CPU -- EPYC-Rome-v1, Skylake-Client-v1,+svm,+npt,+nrip-save -- whose family and model put the guest's kernel on that CPU's paths through its MSRs |
hv-distro-test.py --iso <alpine-virt.iso> [--debian <nocloud.raw>] [--ubuntu <cloudimg.raw>] [--internet] [--cpus N [--ioapic]] |
x86-64, by hand | a distribution as it ships -- Alpine's kernel, initramfs and packages, its ISO a read-only disk: login, clock, reboot, network and the way out through NAT, and its own sshd reached from outside; Debian's cloud image, systemd provisioned by credentials, networkd by DHCP, its root written to and kept across a reboot -- both on their ACPI (--acpi-off: without), Debian shut down by hv stop's power button |
idle-wait-test.py [--smp N] |
x86-64 | a wait primitive, the scheduler's choice of the idle task |
image-test.py [--image FILE] |
x86-64 (CI) | the system image and what pours and boots it: make image, build/image/, nos-install, root=UUID= |
wx-test.sh -- W^X
One boot per probe (wxprobe=text, wxprobe=heap), each of which has to die
on the fault it asked for, at the address it printed. A kernel that survives
a probe prints SUCCEEDED (W^X broken!) and the script fails. No smoke boot
notices a mapping that stops being NX -- everything still works -- and the
heap probe is the one that fails if a mapping path ever stops setting NX on
the leaves it writes. --skip-build; WX_HVF=1 for the arm64 boots under
HVF. See Paging.
panic-test.sh -- a panic that resets the machine, when asked
Two boots that panic on purpose (wxprobe=text, early in boot), under QEMU's
-no-reboot, so that a reset is QEMU exiting by itself: with panic=1 it has
to reset within twenty seconds of the report, having said it would; without,
it has to still be there eight seconds after it, the report on its console. A
server whose only console is the network has nothing else to bring it back
from a panic -- the EX44 stayed dark until it was reset by hand from the
provider's panel after a boot that died before its network was up -- and
the system image boots nos with panic=10, after which GRUB boots the
server's own system. --skip-build; PANIC_HVF=1 for the arm64 boots under HVF.
parttest.py -- partition tables
Boots with an MBR disk and a GPT disk it writes itself -- no partitioning
tool, no privileges -- and checks what the kernel made of them: the
partitions registered and their sizes, both tables as partitions prints
them, that a partition's sector 0 is its first sector on the disk and that a
read past its end is refused, and that a filesystem mounted on a partition
keeps writers off the disk it is on. The smoke disks carry no partition
table at all, so nothing else reaches this code.
ext2-test.py -- ext2, judged by e2fsck
Works the ext2 root filesystem from the shell -- files made, grown past
their direct blocks, copied, renamed, removed, a directory removed whole --
and then lets e2fsck -fn judge the image. The boot-time fstest=on checks
that the driver reads back what it wrote; this checks the other half, that
what is left on the disk is still a filesystem a checker calls clean.
nanofs-test.py -- nanofs
nanofs is the kernel's own small checksummed filesystem, and nothing else in
the suite touches it: the smoke boots and ext2-test.py both run on ext2.
The gate is the fstest self-test over a freshly formatted disk, then files
written, synced, unmounted and read back from a second mount, so that what
is judged is what reached the disk rather than what a cache still held --
and last the image parsed by the script itself. A driver reading back its
own writes never notices a field at the wrong offset; a second
implementation of the format does.
disklog-test.py -- the kernel log on a disk
Boots with disklog=on over an area it prepares itself, then reads the area
back with scripts/disklog.py: the boot's first fifty traced lines compared
with the serial console's, line for line. It checks both halves of the
channel -- that the kernel finds the area, claims the device (so a mount and
a format of that disk are refused) and writes whole sectors with no
failures, and that the host tool parses what this boot left. Nothing else in
the suite gives disklog=on, and without the parameter no disk is so much
as read.
netconsole-test.py -- the kernel log over UDP
Boots with the log streaming to a collector the script runs, and checks what someone debugging a dead machine depends on: that the log queued before the link came up arrives once it does, that the sequence numbers are contiguous -- so that a gap can be told from a machine that stopped -- that lines produced after link-up keep coming, and that a panic's report gets out with its backtrace, which is the one time it matters most and the one time the drain task is not running to send it. On the two Hetzner machines netconsole is the only console there is. See Netconsole.
tcp-test.py -- the connecting side of TCP
Fetches from an HTTP server the script runs: a 400 KB body checked by SHA-256 (enough segments for the window to move), a connection refused, one to a black hole that has to time out, and twelve in a row to reuse ephemeral ports through TIME-WAIT. The last check is the one that catches a slot leak: after all of it the connection table has to be back where it started, because a leak of one slot per connection is invisible until the sixty-fourth.
sshd-test.py -- the listening side, and modules
End to end against OpenSSH's own client (see sshd). On arm64 it
loads sshd.ko over the UDP shell and has ssh work it: commands, a shell
with a terminal and one without, 3 MiB of output through the client's window
with rekeys in the middle, a command that prints nothing for five seconds
while the client asks every second whether the server is there (ssh -o ServerAliveInterval=1 -o ServerAliveCountMax=2 ... top 5000 -- a session
deaf while its command ran was cut off at three), a key it has to refuse,
sessions at once, a burst
of connections past the four that may be logging in, a stop and an rmmod
from inside a session; then it puts the module in /etc/rc, reboots, and
checks it came back by itself with the same host key. On x86-64 it boots an
ext2 root that already carries the module, a key and an /etc/rc, and logs
in. It is also what notices a change to the ffi declarations that a module
was not rebuilt against: a module's header carries a digest of them. Needs
ssh, ssh-keygen and host ports 2222, 8000 and 9000 free.
41 checks pass. One fails, poweroff unloads sshd before the unmount, for
reasons of its own that have nothing to do with TCP; and connections past the four logging in are refused fails now and then under load -- stragglers
of the 70-probe burst reach the server after the three quiet seconds the
test waits for, and are counted among the refusals it looks for (18 where it
wants 2). A run that fails anything else is a regression.
image-test.py -- the system image, poured and booted as on a server
What a release's nos-x86_64.squashfs has to do on a server
(Install), which nothing else here tries. The disk is an MBR
with two partitions: a "host" one holding GRUB as a server's /boot would,
whose first entry stands in for the server's own system and says so on the
serial line, and a blank one for nos; it is attached as virtio-blk. The
Hetzner boxes have NVMe, which nos reads as well, but the SeaBIOS of Ubuntu
22.04's QEMU (1.15), which CI runs, boots nothing from an NVMe disk: it says
"Booting from Hard Disk...", GRUB's core image is never read, and nothing
reaches the serial line -- the test's first boot waited out its time in CI
on every run until the disk was moved, while a Mac's newer SeaBIOS booted
it. The test mounts the image and runs the nos-install in it as
a server would -- under Linux, as root, on a block device, the partitions as
loop devices: under sudo on Linux and in CI, in the nos-builder image with
--privileged on a Mac -- with --ssh-key and --boot-once, then builds
the host's grub.cfg the way grub-mkconfig would, the machine's entry
first and what /etc/grub.d/42_nos prints after it.
The first boot must be nos, once -- the entry from the image's nos.cfg,
the root mounted by UUID read-write -- with sshd started by the image's
/etc/rc, a host key made and the given key let in, the kernel and the root's
/VERSION both the image's, no udpshell on the command line and no UDP
shell answering. The install is given a made-up machine's /proc/cpuinfo
and firmware directory (NOS_CPUINFO, NOS_UCODE_DIR) holding a file of
one microcode update for its CPU: it has to say it put that file at
/boot/ucode, GRUB has to hand it to the kernel as the module ucode, and
the kernel has to find it and say why it loads nothing under QEMU, whose
CPU is a hypervisor's. The second boot must be the host's own entry: GRUB
cleared the flag. Then nos-install update, finding the partition through the GRUB
entry, over what nos wrote: e2fsck finds the partition clean, the host key
nos made stays, the old kernel and modules go to .prev, the UDP shell it
is asked for goes into the kernel parameters and a boot is armed; the third
boot must answer with the same host key and, now, on the UDP shell. Last,
status, rollback and uninstall, and e2fsck once more. Needs
qemu-system-x86_64, ssh, ssh-keygen, ssh-keyscan, and the image
built (make image, scripts/build-image.sh); about four minutes under TCG.
netblk-test.py -- a module on the block layer's asynchronous path
Loads netblk.ko, serves an NVMe disk -- and a partition of a second one --
on a UDP port, and works it from outside with scripts/netblk.py: geometry,
data written and read back, requests it has to refuse, a load test each way,
what the kernel's own reads find on the disk afterwards, and a stop and an
rmmod that leave nothing behind. What is written at the start of the
partition has to land at the partition's start on the disk, not at the
disk's. It is the only test of submit/kick, of frames kept across calls
for the disk to DMA into, and of kcore's module-facing block and net.
x86-64 runs under KVM unless --tcg; --arch aarch64 is the one that runs
on a Mac. See netblk.
netload-test.py -- the receive path, the frame pool, a module on both
Loads the netload module and hammers its UDP target
from the host. The target answers from inside the receive path: the reply is the frame that arrived, kept past the
callback, its addresses swapped where they lie, and the replies of a batch
go to the NIC together when the batch ends. No other test touches any of
that, and all of it fails quietly -- a frame kept and never released is a
pool that runs dry an hour into a load test, and a batch never handed over
is an echo server that answers nothing and reports no error. So: every echo
is byte for byte a datagram that was sent, and none twice; a burst longer
than the reply batch still comes back; the counters agree with what was
sent; sink mode answers nothing and counts everything; more start/stop
rounds than a device has listener slots; and the frame pool ends where it
began. That last one is also what notices a frame pool that was never built
-- which is how a boot that passed every other gate once spent two hours
taking each frame from the allocator.
Then it turns the module round and checks its source, against a socket of the script's own. A paced run has to arrive whole: every datagram the size asked for, marked with its sender and a sequence number, none twice, the filler intact, the kernel's count of what it sent the same as what came; and what the script sends back has to be counted as echoes and not answered. A run with no pace has to finish and account for every datagram as sent or failed. An address nothing answers ARP for has to be refused with nothing sent, and so do bad arguments. And while a run with no pace floods, the shell is asked ten things and has to answer ten: a source that did not leave room in the transmit queue keeps it full, every answer of the shell's is released for want of room, and on a machine whose only console is the network the load test has silenced the console -- which the first version of the source did, and this check is there because of it.
Last, the module's life: rmmod with a target running has to take it down
and give the port back, a second insmod has to start on that same port,
and the frame pool has to end where it began. netload is the one module on
the receive path, so this is what tests the typed listener a module is
given -- a handler the listener owns, frames lent, the end of a batch told
(kcore::net) -- and the C ABI under it.
And the tick's receive poll, which nothing but rxpoll=on turns on. When
that stopped being true nothing showed: the switch lived in the C++ function
the tick called, went with src/cpp/net, and from then on every tick
polled. Under a flood the receive softirq ran on two CPUs in turn -- the
BSP, whose tick it is, and the one the NIC interrupts -- with an IPI at
each handover, and on the AX41 took 70% of each of two CPUs where it had
taken one, while every rate held. So at the end of all the above, booted
without the switch, net has to say the tick never polled; and a second,
short boot with rxpoll=on has to show it polling, since a switch that no
longer turns the poll on fails as quietly. The switch is in common code,
DeviceTable::poll_rx, so the arm64 boots cover both architectures.
It takes about seven minutes: 88 round trips with a three-second collection window each, by design, and the second boot.
--arch x86_64 is the short form, a minute of it: nos.iso, a root that
carries the module and an /etc/rc that loads it, starts the target and
runs the source once -- no shell at all, the x86-64 boot having none over
UDP. It checks what differs between the architectures rather than what does
not: that the module loads and binds there, that its calls across the C ABI
come back right where the ABI is the other one -- two of them return a
structure by value -- and that the target echoes and the source's run
arrives whole. Both forms take --nic igb.
idle-wait-test.py -- a wait that costs a tick
Boots with more CPUs than the kernel has busy tasks and runs blkload at
qd=1: one worker, one I/O in flight, every other CPU with nothing to run.
Asserts the read comes back in well under a tick.
It is here because of what the wait primitives used to do. A task waiting for
another CPU's interrupt -- a synchronous block read, a mutex, the TLB
shootdown's acks -- polls, and polling with Schedule() lets the scheduler
hand the CPU to the idle task, which halts it. What such a task waits for is a
counter or a store in an interrupt handler: no waiter to unblock, no IPI to
send, so nothing brings the CPU back before its own next tick. The work was
done microseconds in and the wait cost 10 ms, every time.
Every other gate is blind to it, which is the point of having this one. The
precondition is a CPU with nothing else runnable, and a smoke boot has more
polling tasks -- shell, udpsh, dhcp, netconsole, usb -- than CPUs, so
Schedule() never reaches the idle task and every latency looks right. The
same unfixed kernel reads at 45k IOPS at -smp 4 and at 98 IOPS at -smp 16.
It was found on a 12-CPU Hetzner AX41 and nowhere else, after two earlier
sightings of the same shape (a171a86, ce3087c) left the primitives
themselves unconverted.
There is nothing to tune: the bug puts p50 at exactly one tick (10223 us at
100 Hz), the fix puts it at ~22 us under KVM. Needs /dev/kvm and a host with
at least 12 CPUs, and refuses to report a pass without them -- a gate that
cannot fail is worse than no gate.
usb-test.py -- typing at the kernel
On the Dell Latitude -- UEFI, no serial port, no PS/2 -- the xHCI controller and the HID boot keyboard on it are the only way in, and no smoke boot attaches a USB controller at all. The gate boots with an emulated xHCI controller, two keyboards -- one on a root port, one behind a hub -- and a device that is neither, then types at the kernel through QEMU's monitor and reads the shell's answer off the serial console. It is built to catch the one failure that looks like success: a driver that enumerates perfectly and delivers no report leaves every boot-log check passing, and fails on "a command typed on the usb keyboard reaches the shell".
insn-test.py -- the MMIO decoder, against the assembler
The hypervisor performs a guest's MMIO access by decoding the instruction
that faulted (MMIO), and what it decodes is the
guest's own bytes. A wrong decode looks like success -- a value in the wrong
register, RIP stepped into the middle of the next instruction -- so the
decoder is checked against the assembler, on the host, where the file
compiles as it is: every form of mov Linux's MMIO accessors are -- to and
from registers of each width, REX's and the high bytes, immediates of each
size, movzx and movsx -- over sixteen addressing forms, assembled by
clang and read back by an LLVM objdump, each decoded to the access and the
length it has; the instructions that must be refused (no memory operand,
another opcode, a string move, a lock, movabs); each instruction cut short
at every length, which must be refused rather than read as a shorter one;
and two million random byte strings, which must decode without a panic and
never to a length past what was there. The guest page walker the
instruction is fetched by is checked beside it, against page tables built by
hand: 4 KiB, 2 MiB and 1 GiB pages, five levels, addresses non-canonical and
unmapped, and a fetch across a page boundary into a page mapped elsewhere.
It needs clang, llvm-objdump and cargo; CI runs it on its x86-64 leg.
The other decoder in the file, of a mov to or from a control register
(decode_mov_cr) -- what an intercept of CR4 stops a guest at, on a CPU
that does not name the register -- is checked the same way: every mov to
and from CR0, CR2, CR3, CR4 and CR8 through eight registers, REX's among
them, assembled by clang and decoded in 64-bit code and, without a REX --
which 32-bit code reads as an INC or a DEC, and is refused -- in 32-bit
code too; each cut short at every length; by hand, what no assembler gives
and a CPU takes or refuses -- prefixes it passes over, a ModRM whose mod
field it ignores with nothing after it, a lock and a repeat, a REX that is
not the last byte before the opcode; and the random bytes. And the third,
of a mov to or from a debug register (decode_mov_dr) -- what an
intercept of DR0-DR3 stops a guest at -- the same way over DR0-DR3, DR6 and
DR7, with REX.R's DR8 and up by hand, which a CPU raises #UD for before any
intercept and which crate::dr then finds is not the exit's register. Each
decoder must refuse the others' every form. A control run with REX.B left
out of the register failed 31 of its cases; one with the debug registers'
two opcodes swapped, 164.
hv-fuzz.py -- everything a guest reaches, fuzzed
A guest decides every value the hypervisor's devices are handed -- every
port it writes and how wide, every MSR, every byte of its virtio rings and
MSI-X tables, the instruction behind an MMIO fault, when it halts and with
what masked -- so a panic, an overflow or a loop that never ends anywhere in
them is one a guest can cause, on the host, under everyone else's guests.
fuzz/hv is a host program built from the hypervisor's own sources
as they are -- hv/src/devices/*, lapic, acpi, insn, walk,
linux, mmio, policy, smp and run, hvarch's VMCB layout, the
module's switch and DHCP server (modules/hv/src/{net,dhcp}.rs) and NAT
(net/src/nat.rs) -- over stand-ins for the kernel (a clock the fuzzer
moves, the locks, a vCPU's wait), for its network layer (the net devices,
their frames, ARP, hv0's end of the switch) and for the CPU, with
overflow checks on as a RUSTUB=1 kernel has them. Each target turns random bytes into what a guest does to
one device: the serial port, the 8259, the PIT, the RTC, ACPI's fixed
hardware, PCI configuration space, the local APIC through its MSRs and its
page, the IO-APIC, MSIs, a virtio disk and NIC driven through rings well
made and not -- and raced: another of the guest's CPUs topping a ring up
by plain writes while the device takes from it, as nothing on the host can
stop it doing; the NIC with its receive interrupts held apart, the clock
moved on between rounds, and held after each to owing none it should have
raised (rx_due); its transmit offloads (offload), every send -- TCP over
IPv4 to be cut, a frame whose checksum is left to the device, and noise,
each well made or not -- held frame by frame to a model of its own written
from virtio's and the RFCs' words, and on their own terms: no frame over
1514 bytes, every checksum right, the payloads in order adding up to the
send's; what it puts into the buffers its driver posts (netrx) -- a frame
into one chain, or spread over mergeable buffers with their count in its
header, a send carried whole with the header that says how to cut it, a
checksum left to finish with the header that says where, or finished first
for a driver that takes none so -- each used element held to the model's
chain, length and bytes -- the page walker over random page tables, the decoder, the
Linux loader over headers that parse, the ACPI tables for any machine, and
the DHCP server over the frames a guest's client sends, well formed and not.
place holds where guests are put on the host's cores (hv/src/place.rs)
to a model of its own (fuzz/hv/src/placemodel.rs), over hosts made up an
input -- one, two or four threads a core, caches shared by one to sixteen
cores, CPUs missing from the numbering and the extension off on some -- on
which guests are started and stopped, and the rule changed between, as the
input says: a guest refused only where no place exists, and for the reason
that comes first; each vCPU on a CPU of its own the extension is on for,
the first where cpu= said; and, its cores its own, none of them with
another guest's CPU on it, every one of them but one filled before the next
is taken, and its disks' task on a spare thread of its own cores, then on a
core no guest has, then beside a vCPU of its own. Five mutants of the rule
-- later cores not checked, one vCPU a core, the disks' task on a held core,
cpu='s core not checked, the disks' task elsewhere before a spare thread
of its own -- each failed within two inputs.
policy holds the CPUID and MSR policy to a model of its own
(fuzz/hv/src/policymodel.rs), over a host CPU made up an input -- which
vendor's, the tops of its two ranges of leaves, below the policy's and
above, and every bit of every leaf: every leaf a guest can ask answered with
no more of the host's than the model lets through, and nothing for one it
does not name or past the top the guest is told; every MSR the model names
read and written as it says -- a refused write leaving the guest's state as
it was, a taken one read back -- and every other refused both ways; and a
table, from the manuals, of what each CPUID bit promises the guest that sees
it, the MSR it will use or the bit of EFER it will set, which must then be
answered. IBPB offered with PRED_CMD refused, or AutoIBRS with its EFER bit,
is a finding at the first input. The model is written from the manuals and
not from the policy, so a leaf, a bit or an MSR the policy comes to offer is
a finding until the model is told of it too; nine mutants of the policy --
the old swallowing of every MSR, every leaf passed through, the tops not
held, a PAT of any bytes, addresses not canonical, FFXSR in EFER, an AMD
MSR on Intel, and IBPB and AutoIBRS offered with the model told -- each
failed within its first 1,600 inputs.
The guests' network has three: switch, guests on its ports and hv0
sending each other frames of every kind -- as themselves and as each other
-- held to a model of where each must go and what each must count, a
guest's bursts among them, past its share of hv0's backlog (the backlog
over the ports in use, counted afresh at each of the stack's passes) and
past its rate to every port, the clock moved on now and then for the rate
to come back, and ports whose NIC holds its receive interrupts, a frame for
which wakes nobody while the inbox is under half full -- and sends of TCP
whole and frames with their checksum left to finish among them, each handed
so to a port whose guest takes it so, and cut, or finished, for every other
and for hv0; nat,
guests' packets out into the world and the world's answers back, each
rewrite checked field by field and its sums summed from scratch; and
guestnet, the two as the module wires them, guests on the switch reaching
the world through NAT. nat floods the table past full now and then and
holds every flow a full table takes to its rule -- the one used least lately
of the guest that holds the most, the asker's own when it holds as many as
any -- from the table's own listing before the packet, a sample of them,
and a packet given no room to a guest past the 64 the table keeps shares
for; and it runs NAT's boot self-test once, which holds the cases random
input seldom makes, a tie and every slot held. And platform does it to the whole machine: a Linux guest's platform built
and loaded as hv boot builds it -- CPUs, disks, NICs, IO-APIC and all --
and run by the real run loop, LinuxGuest::run, on each of its CPUs, each
on a thread of its own and one at a time -- the one turn handed from thread
to thread directly, and the clock moved on to the next deadline when every
CPU waits, so that nothing sleeps on a real clock: sixteen times the runs a
second of the first harness, whose CPUs slept for real, and ten processes
at once some forty times theirs. Where the loop would enter the
guest, the CPU asks a script what the guest does next: a port, an MSR,
CPUID, a fault on a device's page with the instruction that made it put
where the guest's paging finds it, a mov to or from CR4 or DR0-DR3 put
there the same way -- under AMD-V, which stops a guest at each -- a halt, a window
opened, an event taken, another CPU's turn -- and between them what a
driver does over several
exits: the 8259 initialised, the APIC turned on or moved between its modes,
an IO-APIC pin routed, MSI-X turned on and its table filled, a virtio queue
set up and given requests, another CPU started by INIT and start-up IPIs.
A finding is a panic -- an index out of range, an overflow -- or a broken
invariant: an interrupt injected into a guest that cannot take one, or over
an event already on its way in; an exception's vector from the APIC; a disk
request outside the disk, not in sectors, or past the requests in flight; a
device that, its ring raced, takes twice what the ring holds in one call --
a pass that, on a host, would never end; a
frame longer than a frame handed to the switch; a DHCP answer that is no
datagram, or whose checksum is not its own; a frame the switch put
elsewhere than its destination says, or counted otherwise; a packet NAT
rewrote in any field but its own, or with a sum it did not have, a port
given to one flow while another was live on it, an answer let in that no
live flow asked for or kept out that one did, a packet counted twice or
not at all; a guest's packet for where NAT keeps the guests from -- a
metadata server, a private network, the uplink's own subnet -- sent out,
one for where they may go denied, a reject other than an administratively
prohibited to its guest quoting it, or the networks NAT lets through other
than nat allow and nat deny left them (control runs with nothing
denied, the DNS server not let through, nat allow ignored and the
reject's code wrong failed at inputs 16, 29, 1 and 16); what a guest takes
from the world for another guest's address,
and what hv0 or a guest takes from a guest not sent as that guest; a
vCPU that enters its
guest or waits with one of the kernel's locks held; a guest whose RAM would
cover its devices' pages; a mov of CR4 that by the CPU's next entry has
not come to what the script reckons -- a read whose register does not hold
the CR4 the guest wrote, a write a CPU takes that is not the guest's CR4
with MCE in the register and RIP past it, one a CPU refuses that is not a
#GP with CR4 as it was -- or a CR4 written with a bit the guest's CPUID does
not offer, PAE cleared in long mode, or a bit a CPU translates by changed
and the guest's TLB not flushed. (Control runs with the CPUID's mask left
out, PGE's flush left out, and reads answered from the register rather than
the guest's CR4 failed at the first, the first and the fourth input; the
first run of the checks found the script's own 32-bit belief outliving the
INIT that put its CPU back in long mode.) A mov of DR0-DR3 the same: a
read whose register does not hold what the guest's writes made the
register, a write that is not the guest's from then on with RIP past it,
one made at CPL 3 -- which a CPU faults before the exit -- that is not a
#GP, or one under DR7's general detect that is not a #DB with DR6.BD set
and GD clear, the instruction not run either way; and the guest's four, at
every one of its entries after, other than its writes made them. (Seven
mutants of hv/src/dr.rs -- a write into another register, a read of 0,
no CPL check, no general detect, GD left set, a write in 32-bit code
keeping its high half, the instruction not stepped past -- each failed
within 145 inputs; the first run of the checks found the script's model
outliving the long_mode its return from 32-bit code calls, which zeroes
them as a boot loader leaves a CPU.) Or a spin -- the run loop reading the clock a
million times with neither an entry nor a wait between, which on a host is
a CPU spun for as long as the guest stays so -- or a hang, ten seconds with
no input done. Each is reported with the seed and the iteration that make
it again, and the input written to out/hv-fuzz/findings/, to replay
(--replay TARGET FILE) with the panic's own backtrace.
With no arguments every target runs 50000 inputs from seed 1, the same
ones every time: the gate, which CI runs on its x86-64 leg and which takes
about half a minute. A campaign is --seed N --seconds S for as many seeds as
there are CPUs: each seed another set of inputs. Whether the whole-machine
runs go deep is HV_FUZZ_STATS=1, which says how they ended and after how
many exits -- a run that ends early reaches little, so every step that ends
one is about one in 4096 -- and what the inputs reach of each file is
clang's coverage (RUSTFLAGS="-C instrument-coverage", llvm-cov report):
on the first day the gate's inputs reached 94% of the regions of the
sources it compiles -- 93% of run.rs, 96% to 100% of each device, 99% of
the local APIC.
What it found on its first day: a virtio buffer at the top of the address
space made the disk's status write compute its address past 2^64 -- a panic
in a RUSTUB=1 kernel and a wrap to page 0 in a plain one -- and a buffer's
address is since reached only through Seg::at, which does the sum
checked; the Linux loader took a kernel's preferred address below 1 MiB,
over the zero page, the page tables and the GDT it writes there; and a vCPU
halted with a periodic APIC timer's interrupt held back by its priorities
spun its host CPU, its sleep computed from a deadline already past. Reading
the run loop for it turned up a fourth: a guest of more than 4064 MiB had
RAM over its IO-APIC's and local APIC's pages, which in xAPIC mode or with
an IO-APIC left it with devices it could not reach and nothing to say so --
guest RAM now ends below them (run::MAX_MEM_BYTES). And a fifth, not live
but one check from it: the DHCP server's is_request read a frame's
EtherType before anything had checked the frame held one -- an index out
of range on a guest's runt frame, which only the switch's own length check
kept away; it now parses the datagram first, which checks.
And on the day the switch and NAT became targets, guestnet found a guest
could send as another: the switch passed on a frame from any MAC and any
address, and NAT answers a flow at the MAC its last packet came from -- so
a guest that sent one packet of another guest's flow, with that guest's
address, took the flow, and the world's answers to it came to its port.
The same frames could tell hv0's ARP table another guest's address was
its own. The switch now checks what comes in at each port, as a cloud's
virtual switch does (ingress in modules/hv/src/net.rs): IPv4 or ARP
only, from the port's MAC, an IPv4 packet from the port's address and an
ARP message with the port's MAC and address -- or none yet, a probe's --
as its sender; the one packet with no address yet is a DHCP client's,
which the switch answers itself. The rest is dropped and counted -- hv list says how many a guest sent as another -- and the targets hold the
switch to it both ways: switch to a model of the rule, guestnet to
what the rule is for.
What it cannot see: the CPU is a stand-in, so nothing of hvarch -- its
unsafe, the VMCB and VMCS, the entry and the exit -- is fuzzed, only what
the run loop does with the exits it decodes; the interleavings are the
script's, at entries and waits, which exercises every path between the
CPUs but finds no race; the devices' backends -- the disk files, the
switch in platform -- are stand-ins that check what they are handed; and
under the switch and NAT the net layer is a stand-in too -- its devices,
frames and ARP table, hv0's receive path run on the caller's thread --
so what they do is fuzzed, one frame at a time, and the receive path's own
concurrency is not.
net-fuzz.py -- everything the network hands the kernel, fuzzed
Everything the network hands the kernel is somebody else's to choose --
every frame on the wire, every answer a server gives the HTTP client, the
DHCP client and the resolver, every byte an SSH client sends the server --
so a panic, an overflow, a lock broken or a loop that never ends anywhere on
those paths is one somebody else can cause, and on the Hetzner boxes the
network is the only console there is. fuzz/net is a host program
built from the kernel's own crates as they are -- net, netwire, tls,
fs, ssh and the sshd module's source, over kcore and ffi -- linked
with the rest of a kernel written for the purpose (fuzz/common/machine/,
which fs-fuzz shares, and the NIC in src/machine/): its C++
half as the ffi crate declares it, the locks, tasks, events, soft IRQs,
timers, the clock, the entropy pool, the log, the command table. The tasks
are threads, one of them running at a time as on a CPU of its own, handed
the turn when it blocks or -- as often as the input says -- where it lets a
lock go; a sleep ends at the tick it would end at in the kernel, and the
clock jumps to the next deadline when every task waits, so an hour of a
lease or a day of a DNS record costs what its timers cost. What the kernel
says of its locks is held to: sleeping with a spin lock held or interrupts
off, a soft IRQ handler that sleeps, a lock taken twice or in both orders, a
mutex let go by a task that does not hold it, a task that ends holding one,
an allocation or a free with interrupts off -- each a finding where it
happens. Overflow checks are on, as a RUSTUB=1 kernel has them. Each input
runs in a process of its own, forked from one booted machine: the layer's
statics are the kernel's, and one input's must not leak into the next.
The machine's NIC is the fuzzer's, and the wire goes to the world
(src/world/): a link that delays, reorders, duplicates and loses as the
input says, a LAN whose hosts answer ARP or do not, and a model of each
protocol's other end. Every frame the machine sends is checked as a
receiver would check it (check.rs): headers whole and every checksum
right, no source address the machine does not have or no host may have,
nothing to an address no packet may go to (RFC 1122 3.2.1.3), a unicast
never on the link's broadcast address (3.3.6), a multicast on its group's
own, TCP's flags in combinations that mean something. The targets:
stack: frames of every kind -- ARP, ICMP, UDP to every port with a listener, TCP, IPv6, noise -- from addresses that are and are not hosts, into the receive path;tcp: connections both ways with a peer model that keeps every byte of both streams (each byte a function of its offset, so a byte delivered that was never sent, or twice, is seen), checks every segment the machine sends against RFC 9293 -- the window, the sequence space, what may carry data -- and does what peers do: retransmits, probes a zero window, resets, goes quiet; and an attacker off the path injecting segments at every sequence number, an ICMP error at every quoted one. And now and then a second device, eth1, standing for the hypervisor's switch, from which a guest sends a SYN to a port the machine listens on -- at eth1's address or at eth0's: a listener takes connections from its own device alone, so no SYN-ACK may go out to it on either wire (the first input of the control run with the device left out of the listener's lookup got one);httpandhttps: the HTTP client fetching through chains of redirects from a server that answers well -- whose result is then known exactly -- or to break it: numbers longer than any field, chunk sizes of every length, headers that never end, a redirect anywhere, a connection reset, a pause past the idle timeout. Over TLS the server is rustls driven by hand as thetlscrate drives the client, with a certificate from the fuzzer's own CA (certs/make.sh) or one run out, for another address, or from nobody -- which the client must refuse before its request goes -- in TLS 1.2, 1.3 or both, with a close_notify or without, and now and then a byte of what it sends flipped: a body the client gives back is the answer's, from its start, and one it calls whole is whole;dns: the resolver against a server that answers with CNAME chains, compressed names, errors, nothing, and answers forged by address, port, id and flag; the cache held to the TTL, a day at most;dhcp: the client getting and keeping a lease from a server that NAKs, goes quiet, answers another transaction or another client, offers addresses no host may have, changes its terms, and a rogue that answers first. What it sends must be what its state calls for (RFC 2131 4.3); what it binds to, an ACK's terms, all of them, on the device; and no address kept past its lease or its refusal;icmp: pings both ways and ARP under them -- the world's echo requests well made and not, ARP requests and replies well made, malformed, and lying, and the shell'sping,udpsendandarp: the cache learns only what RFC 826's merge lets it, and a host that answers every ping gets every round;udpshell: the shell over UDP asked by clients near and far, in datagrams well made and not; every reply whole, in order, flagged at its end, and exactly what the command printed;netconsole: the kernel log to a collector that is there, beyond the gateway or not yet, from a machine without an address, whose NIC stalls, logging past what the ring holds and from tasks in the middle of a send: the datagrams numbered with no gap of the machine's making, each line at most once, in order, and all of it in the end but what the ring says it dropped;ssh: the sshd module on a ramfs root, and SSH clients (sshc.rs) that log in with the key it knows or a stranger's or a signature over the wrong session, run a command or a shell with a window of their choosing, rekey, answer keepalives or not, or break the protocol at one point of it -- a version line of HTTP, no cipher in common, a point of small order, a length past any, an IGNORE where the strict exchange allows none, a packet tampered with, a channel before the login, more data than the window. Every packet the server sends is opened and checked, its key exchange signed by its host key over the hash both ends made; a login is had exactly when the known key signs the right session; a command's output comes back whole; a client that breaks the protocol is told so.
At the end of every input the machine must hold nothing: no TCP slot, no
frame out of the pool, no task. A finding is any of that, a panic, a spin
(a million reads of the clock with no other task given the turn) or a hang
(twenty seconds of an input), each with the seed and iteration that make it
again and the input written to out/net-fuzz/findings/ -- --replay TARGET FILE, with --trace for every frame both ways and every line
traced.
With no arguments every target runs its own number of inputs from seed 1 --
three hundred of ssh, five thousand of icmp -- the same ones every
time: the gate, a couple of minutes, which CI runs on its arm64 leg. A
campaign is --seed N --seconds S, a seed for each CPU.
What it found on its first day, each fixed:
- the receive path delivered IPv4 it should have dropped: fragments, bad header checksums, sources of 0.0.0.0 and broadcast addresses, UDP with a wrong checksum -- and answered them, as addresses not its own;
- TCP took an out-of-window segment's window for the connection's, so a guessed segment could shut a connection's window for good; took a SYN with ACK, RST or FIN for a connection request; answered port 0 and broadcast addresses; answered nothing to a zero-window probe; kept the window of before a retransmission and sent past the peer's; and its connect called a peer that answered and closed at once a failure;
- the HTTP client's chunk sizes, status codes and redirect ports
overflowed -- a panic in a
RUSTUB=1kernel -- it called broken chunking complete, and it took a response cut inside its headers for a body made of its own status line; - the TLS client took a stream cut inside a record, and a receive that timed out, for the end of the stream: a body with no length that stalled came back whole;
- ARP waited ten seconds a try on an idle CPU; its cache learnt every sender of every request on the link, which RFC 826 does not -- a busy link pushing out the hosts the machine talks to -- and took broadcast, multicast and its own MAC, and 0.0.0.0, broadcast and its own address;
- a UDP datagram or a ping to a host whose ARP went unanswered was sent to the link's broadcast address, every host on it handed somebody else's datagram -- and netconsole's records were spent on it, lost; datagrams to 0.0.0.0 and 127/8 left the machine, and one could leave from 0.0.0.0 when DHCP let the address go mid-send;
- the DHCP client, once it had renewed, started again from DISCOVER while bound -- its lease overwritten by an offer's terms and never renewed again; it kept its address past the lease and after a refusal, put only the address of a renewal on the device, and took addresses no host may have;
- the UDP shell ran a line longer than it holds cut short;
- netconsole's drain slept 200 ms at a time holding the log's lock, with
interrupts off, whenever the device had no address -- the guard of a
match's scrutinee lives to the end of the match -- and every line any CPU traced waited behind it; and a batch whose records the ring evicted while it was on its way was sent again.
fs-fuzz.py -- everything a disk hands the kernel, fuzzed
What is on a disk is whoever wrote it's to choose -- a disk that came with
the machine, an image somebody made, a filesystem another kernel left half
written -- and a device says itself how many sectors it has and how big
one is. So a panic, an overflow, a lock broken, a loop that never ends or a
write where nothing may write, anywhere on the paths that read a disk, is
one somebody else can cause. fuzz/fs is a host program built from the
kernel's own crates -- block and fs, over kcore and ffi as they are
-- on the machine net-fuzz runs on (fuzz/common/: the C++ half, the
tasks one at a time, the lock rules, a process for each input), and disks
whose media are the fuzzer's (src/machine/disk.rs): of any geometry a
device may claim -- 520-byte sectors, 2^64 of them -- each with a volatile
write cache that a plain write goes into and a flush or a forced write
through, a power switch that pictures, at chosen requests, what the medium
would hold -- the cache's writes each put down or not -- and requests that
fail. Every request is checked where it arrives, as the block layer's
contract with a driver says: whole sectors, inside the device, never from
where the caller may not sleep, and no write to a disk nothing should be
writing -- a filesystem mounted read-only or not mounted, a partition table
being read.
The images are the fuzzer's own (src/image/), made from the formats and
not from the drivers: an ext2 as mke2fs lays one out -- every block size,
one group or many, sparse superblocks or not, 128- and 256-byte inodes --
with a tree put on it, files with holes and without, directories with
deleted entries between their live ones; a nanofs as format and the
driver leave one; MBRs and GPTs; the disk log's header. And judged the same
way: ext2::check reads an image as e2fsck -fn does -- every inode with
links in use, every block claimed once, directories parsed, link counts,
bitmaps and every count -- and sorts what it finds into corrupt (what no
crash of a correct driver may leave: a block in use and free, a block two
inodes hold, a name leading to an inode not in use) and unclean (what
e2fsck still fixes: a leak, a count, a link count, the state bit). The two
were checked against e2fsprogs over hundreds of images (FS_FUZZ_DUMP=<dir>
writes each image judged): what check calls clean, e2fsck passes; what
e2fsck faults, check calls unclean or corrupt. The targets:
part: MBRs and GPTs of every shape -- protective and not, CRCs right and wrong, entry sizes and counts past sense, slots overlapping, LBAs at the top of the numbers -- on disks of every geometry; the partitions the probe registers held to a reader of the table written fromblock's documentation (which slots, where, under what name), and the probe's writes to none; I/O and batches through every device held to its bounds and rebased onto the right sectors of its disk; claims taken and given back, held to what they are for: never two writers on a sector;disklog: prepared areas on disks and partitions, headers damaged, the device held by something else, sector sizes of every kind, lines logged before the area is known and after, from tasks and interrupt handlers; the area the documentation says is taken, its header right, its text every line in the order it came as far as the area holds it, nothing written anywhere else;ext2andnanofs: sound images mounted, read-only and not, and worked through the VFS -- opens with every flag, reads, writes and seeks at every edge of a block and of the indirect blocks, truncates both ways, creates, renames, removes, chains of directories forty deep -- each call's answer held to a model of the tree and the files open on it (targets/fsops.rs, the VFS's rules and the filesystem's limits written down), the tree read back through the VFS held to the model's, the image a clean unmount leaves clean and holding the model's tree, and mounted again, the same. A call the filesystem refused for want of room is held to having changed nothing -- a write to having left the file its size, a first part of the new data in it at most. Every power cut is at worst unclean -- which is what ext2's commit order and nanofs's copy-on-write promise -- and so is a run in which requests failed;ext2badandnanofsbad: sound images with a few fields made what they must not be -- the superblock's geometry, a group's descriptor, an inode's mode, size, links and block pointers (outside, into the metadata, into another file, at itself), a directory's entries, an indirect block, a bitmap; nanofs's with their checksums put right, as a lie told well is -- mounted if the driver takes them and worked through the VFS, held to what holds for any image: a directory that lists the same twice running, a name it lists that can be looked up, a file that reads no more than its size, a walk of it all that changes nothing leaving the kernel's heap as it found it, nothing read or written once it is unmounted;vfs: a ramfs at the root held to the model, and procfs, ext2, nanofs and ramfs mounted beside it and taken down again at paths nested, doubled, without a slash and too long; tasks of their own at work on those meanwhile; the C ABI (kernel_vfs_*) handed words that are open files, were, and never were; and the file ABI (kernel_file_*) a module keeps its configuration through, each call held to what its composition of VFS calls does to the model. A mount taken exactly when its path is free and well formed, its device nobody else's and there is room; an unmount refused while anything is open on it; a rename between mounts refused; and the disks' filesystems clean afterunmount_all. Which file an open file is (kernel_file_mountandkernel_file_key, what a guest's disk claims its image by): one for two handles on one path and two for two paths, on every filesystem the model holds (Fs::compare, so ext2's and nanofs's targets too); two mounts for files on two -- an ext2's and a nanofs's numbers are alike; and a word that is no open file told so. A control run with the node's key left out of a ramfs file's identity failed at the first input, and one with the mount left out at the 41st;shell: the storage commands the console, the UDP shell and SSH run --mount,format,cp -r,fstest,diskwriteand the rest -- with arguments of every kind, afterfstesthas passed on ramfs, ext2 and nanofs as each came; every disk no command wrote to raw clean at the end;rootfs:root=none,auto, a device, a label, a UUID, over disks and partitions carrying an ext2 labellednosor not, a nanofs, something that only looks like ext2, or nothing; what is mounted where held to whatrootfs.rsdocuments, and what it mounted for writing clean after the shutdown's unmount.
Each finding comes with the seed and iteration that make it again and the
input in out/fs-fuzz/findings/ -- --replay TARGET FILE, with --trace for
every request each disk takes and every call the target makes.
FS_FUZZ_STATS=1 adds how often each target reached each state it counts.
With no arguments every target runs its own number of inputs from seed 1,
the same every time: the gate, about two minutes, which CI runs on its x86-64
leg. A campaign is --seed N --seconds S.
What it found on its first day, each fixed:
- the device table bounded a partition's requests and not a whole disk's: a filesystem's block number past the end of its disk went to the driver, every driver trusted to refuse it; now nothing outside a device reaches its driver, the asynchronous path included;
- the GPT reader added to an LBA and a count that come off the disk
without a check -- a table on a disk that claims 2^64 sectors overflowed
them, a panic in a
RUSTUB=1kernel -- and the claims kept a disk and its partitions apart but not two partitions a table has overlap: two writers on the same sectors; - the disk log, on a device whose sectors do not divide a page -- 520 bytes,
as some disks have -- ran a full batch past its buffer: a panic at boot
with
disklog=on; and a header's boot count at its top overflowed; - ext2 took the superblock's geometry on trust: a block count past the device, an inode count that is not its groups', a group's inode table running out of the filesystem -- a crafted image overflowed the group count, a panic, and could point the driver outside its device;
- ext2's
create_dirwrote the new directory's inode before the bitmap that marks its block, andcreate_filebefore its inode's bit: a power cut between leaves an inode in use on bits the bitmap has free, for the next create to take. And a create undone after an error freed the bits under an inode already on disk in use -- a block two files come to share. The bits now go down first, and an undo writes the inode deleted before it frees anything, and leaves a leak when it cannot; - an ext2 write that ran out of room part way left the blocks it had taken past the end of the file, which e2fsck calls a bad size; one past what the blocks map went in part way, and a truncate past it made a file that could not be read to its end. A write past the end is refused whole now, and one that fails gives back what it took and leaves the file its size;
- ext2's remove was a recursion capped at 32 levels and nanofs's one without a cap, and nanofs's mount read the tree by a recursion that stopped at 32: ext2 made a tree it could not remove, and nanofs one whose deep part was gone at the next mount. All three are walks now, with no limit but the tree's;
- an ext2 directory whose load failed part way -- a block past what the
driver maps -- kept what it had read so far and read it again at every
look: every
lsof it grew the tree, for good. A load is all or nothing now;ext2badandnanofsbadwalk every image three times and hold the kernel's heap to what it was after the second; - names with a NUL in them went onto disk, which e2fsck calls illegal and nanofs cuts at the NUL; the VFS refuses them;
- a nanofs mounted read-only wrote its superblock at the unmount, and at the mount when it repaired a bitmap;
format nanofson a device too small for nanofs wrote its first two blocks over whatever was there before it failed; format and mount now refuse such a device first;- nanofs checked a file's whole checksum on every read, so reading a file a
piece at a time cost the square of its length -- a megabyte through
cat, 256 reads of all 256 blocks; now once after a mount or a write; fstest / 1underflowed the big file's patch offset -- a panic from any shell.
What it leaves, known: after a device error in the middle of a create or a rename, the directory in memory may disagree with the one on disk until the next mount, and a later create of the same name can leave that name in the directory twice. Nothing is lost or shared -- the first is the one looked up, and e2fsck renames the other -- so the checks count it unclean.
cpp-fuzz.py -- what the C++ is handed, and the memory under everything, fuzzed
The kernel's C++ reads things nobody checked before it: the device tree the
arm64 boot is handed, the Multiboot2 information GRUB hands the x86 one and
the ACPI tables its firmware leaves in memory, the firmware's memory map,
the command line, the GRUB environment block grubenv reads off /boot, a
module's .ko. Most of it is read before a headless machine has any
console at all. And under everything the
kernel does is its C++ memory management, whose error paths -- a table that
cannot be allocated half way down a walk, a heap with no page left -- no boot
takes. A read past an end, an index out of range or a wrap in any of them is
a boot that dies without a word, or a page handed out that something still
uses; and in C++ the language does not stop it where it happens. So
fuzz/cpp builds each of them for the host from the kernel's own sources,
as they are, under the address and undefined-behaviour sanitizers, over
stand-ins for the kernel around them (fuzz/cpp/common/: the tracer, the
panic path -- a panic is a finding --, the locks, which hold the kernel's
rules -- one taken twice, one let go of when not held, an allocation with one
held are findings --, the heap with each block counted and able to fail) and
a HAL of the host's own (fuzz/cpp/host/), which comes before src/cpp on
the include path and takes the place of each arch's inline one: no
privileged instruction, and the MMU's hooks -- a TLB flush, an entry made
valid -- where a target that models the MMU sees what the kernel did to its
tables. fuzz/cpp/Makefile lists what each target is built from, as the
kernel's Makefile lists what it is.
The targets, each holding what the code makes of its input to a model of what its header says it does:
fdt: device trees of QEMU virt's shape -- memory, chosen, PSCI, a GICv3 and its ITS, PCIe, the PL011 and PL031, virtio-mmio, the timer, the CPUs -- with cell counts, property lengths, strings, interrupts and windows of every kind, and damaged: nesting that ends more nodes than it began, lengths and name offsets that lie, a header that does, a blob cut short. A sound one's board -- whatBoard::Setuptook of each node, the refusals counted, the fallbacks for the rest -- is held to a reader of the tree written fromboard.h; any board to what the kernel acts on: windows the linear map reaches, interrupts it can take, bus numbers that are bus numbers;memmap: firmware maps with regions that overlap, are empty, or run past the physical address space, and the questions the page allocator asks of them -- the free-page scan,IsReserved,IsUsableRam, the RAM totals -- answered naively, page by page, in 128-bit arithmetic that cannot wrap;grubenv: blocks grub-editenv could have written, damaged, and bytes;Set,Unset,GetandForEachheld to the model's variables, room counted to the byte -- and on any block, aSetthat says yes is whatGetgives back and one that says no changes nothing;module:.kofiles as the build makes them, for x86-64 and for arm64 -- segments, dynamic symbols and relocations, the function table, the kmod header -- made to say what the loader must refuse, and damaged, handed to the loader's ownStage(all ofLoadup to the module's init, which a fuzzer does not run). A sound one's image is held to the segments and the relocations, byte for byte; its pages' permissions, its imports, its function table and the lookups a backtrace makes through it to the file. Nothing is written past the image (ASan poisons a page either side), nothing is asked to be writable and executable, and whatever the loader refused, or failed to allocate, leaves every page, mapping and heap block as it found them;pagetable: the page tables and the physical page allocator pastSetup, whose state is set up asSetupleaves it (it runs on the bootstrap linear map, which a host process has not got) over a physical memory of the host's and an MMU emulated where the kernel reaches memory through it, the TmpMap window: a slot is loaded with the page its entry maps when the kernel flushes it or makes the entry valid, the page gets the slot's contents back when it is unmapped, and an unmapped slot is poisoned for ASan. Pages allocated one at a time and in runs, mapped in every form, unmapped, protected, temp-mapped and written through, frames copied through a CPU's slot, MMIO mapped -- on machines taken to their last pages and in a window with two slots left -- are held topage_table.h's rules: every page's reference count, every translation, every page free, held, a table or the machine's and never two, pages handed out zeroed, a map that fails leaving nothing of its own, no TmpMap slot left behind;heap:Mm::AllocandMm::Free, the pools, the page runs and the three ways a driver maps pages, over a page table aspage_table.hpromises it and a VA region of the host's -- mapped VA is memory, the rest poisoned -- with pages and maps failing on the input's word: every block the size asked for, aligned, mapped and nobody else's, what was written into it there when it is freed, nothing refused while there is memory and VA to give, and every page back but the bitmaps' once everything is freed;cmdline: command lines of the parameters the kernel takes, with values of every kind -- numbers at and past their limits, addresses that are not, labels and UUIDs --, words it does not know, strays, and lines longer than the buffer, cut anywhere: every getter held to a reader ofparameters.h, and a parameter the buffer's end cuts through left out whole, never read as the shorter one it would be;multiboot: Multiboot2 infos as GRUB makes them -- memory maps of every entry size, and one longer than the map holds; the command line; both RSDP tags; the framebuffer; modules,ucodeamong other names and names the tag cuts short, of ranges empty or not; the tags this kernel skips -- and damaged: sizes that lie, a tag past the info's end, no end tag. WhatParseMultiBootInfoleaves -- the memory map region by region, the command line, the RSDP's copy, the framebuffer, the microcode module's range -- is held to the tags read in order, the map's reserved regions before its usable RAM;ucode: files of Intel's microcode updates (arch/x86_64/microcode_file.cpp), what GRUB loads from/boot/ucodeand the BSP chooses its CPU's update from, built update by update as Intel lays one out -- the original 2048-byte format, data and total sizes of their own, an extended signature table naming other CPUs and platforms, every checksum made right -- for the CPU the input makes up or for another, of revisions above and below the one it runs; then one update made unsound in a way the SDM rules out, bytes after the last, or bytes of anything written over it. Over a file the generator made,Chooseis held to a model written from the SDM (Vol. 3A, 10.11.1): the newest update for the CPU above its revision, of those before the first unsound one; over any file, a choice is an update inside the file, sound by the model's reading of one, for the CPU and newer than what it runs;acpi: the tables in a physical memory of the input's, sparse, zeros where nothing was put: an RSDP in the Multiboot tag's copy, the EBDA or the BIOS area, among decoys; an RSDT or an XSDT; and tables below and above 4 GiB, across page ends and over each other -- a MADT of every entry kind, FADTs, HPET tables and WDATs of every length and each side of the fields the kernel reads, the SSDTs and the rest a firmware lists, seconds of the kinds read among them --, now and then damaged anywhere. The TmpMap window is emulated to the slot: a mapping is a copy of the pages it maps between poisoned ones, a table's mapping is poisoned outside the table, so that a read past a table's length is a report and not a read of its neighbour, and the window refuses a mapping where the input says. A machine is held to a reader of it written fromacpi.h-- the RSDP the search finds, the first table of each kind that can be mapped, the CPUs, the IO-APIC the driver can drive, the overrides, the FADT's, HPET's and WDAT's fields, whether it boots at all; and any machine, the window cut short or not, to this: nothing mapped past the physical address space or longer than a table may be, no more of the window held at once thanacpi.hbounds, and when the parse is done nothing held but the two APICs' pages -- and not those if it failed;format:Stdlib::VsnPrintf, which every traced line, panic and command's output goes through, against the host's ownvsnprintf-- whatPRINTF_FORMAT's check rests on. Random formats of what it implements (d u x X c s,%%, the flags0and-, widths, the length modifiershh h l ll z) among literal text, over two fixed lists of twelve arguments -- ints, longs and strings side by side, some in registers and some on the stack -- each conversion matching the type at its place, and each int made by cutting a long at the call, so that a reader taking an int as a long reads the other half of its slot: what fits must be the host's output byte for byte and its length, and what does not, the host's output cut whereVsnPrintfcuts it and marked with....
The runner (fuzz/cpp/common/runner.cpp) is fuzz/common's, but for one
thing: inputs run in batches, one after another in a process forked for the
batch, the target's Reset putting the kernel's statics back between them
-- a fork of a process under ASan costs macOS fifteen milliseconds. A
finding ends its batch and is run again alone in a fresh process, whose
report is the one given; one that does not come back alone is Reset's own
bug and says so. Each finding comes with the seed and iteration that make it
again and the input in out/cpp-fuzz/findings/ -- --replay TARGET FILE,
with --trace for the kernel's trace lines. CPP_FUZZ_STATS=1 adds how
often each target reached each state it counts. With no arguments every
target runs its own number of inputs from seed 1: the gate, a minute and a half,
which CI runs on its arm64 leg. It needs clang and make on the host, and
nothing else; the programs of each host are built in a directory of their
own (out/cpp-fuzz/<system>-<machine>/), since a Mac and a container share
the tree.
What it found on its first day, each fixed:
- the device tree reader let an
END_NODEwith no node open take the depth below the root, and the next node wrote its cell counts at index -1 of the walker's stacks: a write into.bssoutside them, from the first thing the arm64 boot reads. AndBoard::Setupread everyregandinterruptsby the tree's own cell counts and not the property's length -- a count of 3 or 2^32, or a property shorter than it said, read past the blob -- and tookbootargswithout its NUL. Properties are read throughFdt::Propnow, every read bounded by its length, a third cell refused; Boardtook whatever the tree said and the kernel then acted on it: an interrupt number the arm64 interrupt table (256 INTIDs, an u8 at registration) would cut to another, a device window past the linear map, bus numbers truncated to u8. What the kernel cannot act on is refused now, counted, and said at boot; and the UART's and the RTC's interrupts fall back on their own, where one the tree placed with no interrupt was set up on INTID 0, an SGI. The blob is held to the 2 MiB the boot protocol allows;- the memory map kept a region running past 2^64 as it was, and every
question about it wrapped: a reserved region hidden from the free-page
scan, its pages handed out. A region is cut at 2^52, the physical address
space of both arches, when it is added, empty ones dropped -- one of those
counted as overlapping by
IsReservedand not by the scan; - the GRUB environment block's
ForEachread a line with no=into the next line's name, whereSetfound that line by itself:Setsaid yes andGet-- and GRUB -- read something else.SetandUnsetalso wrote blocksForEachthen called malformed, andUnsetone with no newline before the padding. The two ways of reading the block share one parser now, andSetandUnsetrefuse a block it rejects, asgrub_env.hsaid they did; - the heap's pools allocated a page with their spin lock held, interrupts
off: when mapping it failed, the page allocator shot down every CPU's TLB
and waited for them, and a CPU spinning on that pool's lock never answers.
The page is allocated with the lock let go of now, as
Pool::Freealready gave one back.
And two loud ends made plainer: with no TmpMap slot free, ZeroPage and an
unmap's walk -- MapRangeLocked's undo among them, which would otherwise
leave pages mapped behind a map that said no -- panic saying so, where one
was a bare BugOn and the other returned without a word. No walk needs more
than two slots, which pagetable checks.
And when the x86 boot's own readers were added, each fixed:
- Multiboot2: the walk read a tag's type before it knew the tag's header was inside the info -- an info with no end tag read past its end. And a firmware map longer than the memory map's 64 regions was a panic before any console a headless machine has: the map holds 128 now, what Linux's boot protocol passes, and a map longer than that is not a boot that stops -- its reserved regions go in first, and the usable RAM past what leaves room for the kernel's own regions is left out, and said;
- ACPI: every table the root listed stayed mapped in the TmpMap window for
good -- the EX44's 25, of a window of 448 slots every page allocation needs
one of -- where four are read. The four are mapped while
Parsereads them and let go of after, and so is the root, whichever way it ends; what is kept is the values, and the two APICs' pages. A table with a length short of its header, or one the window could not map, failed the whole parse -- a panic at boot -- where only the MADT is needed: a table not to be mapped is skipped now, and said. Nothing bounded a table's length -- 4 GiB failed the boot, a few hundred pages would have held the window --, and an XSDT entry past the physical address space was mapped, its bits above 51 landing in the PTE's flags: a table longer than 64 KiB, or out of the address space, is not mapped. The MADT was read past a table shorter than its own header; each IO-APIC listed was mapped over the one before, the last kept and the others' slots leaked -- on the AX41, whose MADT lists the FCH's at GSI 0 first and the GNB's past it second, the legacy IRQs, the HPET's tick among them, were set up on pins of the GNB's that none of those lines reaches (Real hardware) --, where the driver can drive the one at GSI 0; an APIC whose registers would run past its page, which put the drivers' accesses in the window's next slot, is refused; a MADT naming no IO-APIC booted on to a fault at address 0 in its driver, where it fails the parse, saying so. More than 64 interrupt source overrides failed the boot, a second for an IRQ took a slot nothing read, one to a GSI past 255 was cut to a byte by the interrupt layer: the first for an IRQ is taken, and the rest said. - the command line:
Parameters's constructor left two members unset -- zeros in the kernel only because the object's storage is static.
And when the formatter was put under the compiler's check: VsnPrintf
read every integer conversion as a 64-bit slot, whatever the argument was,
and nothing checked a call -- the convention was to cast each argument to
ulong. The ones that were not were read with whatever the other half of
their register or stack slot held: the trace level in every Trace (an
int under %u), and a few %ds and %us of ints. VsnPrintf reads C's
printf now, the length modifiers included; every printf-like function
carries PRINTF_FORMAT, the build has -Wformat=2, and 924 conversions
were rewritten to say what they print -- %lu, and %lX for the addresses
passed as ulong under %p, which prints what %p printed. Two formats
no check could see, and which the new reading would have misread, were
found by -Wformat-nonliteral and the template it cannot look through:
profile's usage, a variable, and the idle and softirq tasks' names,
handed through TAlloc<Task> -- "idle%u" over a ulong. The first is a
constant the check reads, and a Task takes a name rather than a
format. The boot log is
the same line for line on both arches, digits aside. And MemoryMap's
region hint, which memcheck read and wrote with no lock while other CPUs
mapped, is an Atomic.
What it does not reach yet: PageTable::Setup and the free list's build,
which run on the bootstrap linear map; anything that needs a second CPU;
and the block allocator under the dmesg ring (mm/block_allocator.cpp),
which every traced line takes a message from -- though not, as this page
used to say, something nothing uses. The legacy
search finds an RSDP only whole within a page -- the kernel maps the BIOS
area a page at a time --, which the reader holds it to: the Multiboot tag,
GRUB's copy, is the path every machine this kernel boots on takes.
hv-test.py -- the extension turned on and off again, and guests under it
The hypervisor is a module, and the one piece of state it
takes does not belong to it: EFER.SVME and MSR_VM_HSAVE_PA on AMD,
CR4.VMXE and VMX root operation on Intel are the CPU's, not a task's, and
they outlive an rmmod that forgets them. What is left behind then is worse
than a leak -- the page the CPU was told to save host state into has been
freed and handed to somebody else, and the code that would have turned it
off has been unmapped -- and nothing in the kernel would say so.
So the gate is a load, a turn-on for every CPU, an rmmod that is not
preceded by an hv off, and a load again; and the module answers "which
CPUs is it on for" by sending each CPU an IPI that reads the register,
rather than by reading its own bookkeeping, which is what makes the second
load able to see what the first unload left. It also checks that hv info
reports nested paging, which a guest cannot do without, that one named CPU
can be turned on without the others, and that a second insmod, a CPU that
does not exist, a word that is not a subcommand and a guest that does not
exist are each refused.
In between it runs guests. Before hv on, and bound to a CPU the extension
is off for, a guest is not run, and says which CPU. With it on everywhere,
every built-in guest runs bound to the
first CPU and then to the last, and each has to have done what it was told
-- not only "ok": the report's own lines are checked, the nested page
fault's address, error code and instruction -- and that the state a report
of it would show is the guest's at the fault: a null DS it loaded without
an exit, which under VT-x, where an EPT violation reads only what
performing an access takes, is there only if the rest was read after it --
the port and CPUID counts, the
fifteen registers each way across the hypercall, a host interrupt count
above zero for the spin, for asid that the third VM was given an ASID
one of the first two had, after a generation ended, and each read its own
page, and for mmio that its fourteen stores and loads of a device in the
MMIO window -- an immediate of each size, REX's registers and a high byte,
movzx and movsx -- were each decoded and performed, the device holding
what was stored and every register what was loaded: the MMIO path
(MMIO) on each backend, with no guest kernel to
depend on; and for ioapic that the IO-APIC sent five interrupts, three
of them levels each ended by EOI -- its serial port's pin taking two edges
and none while masked, then a level sent again at each EOI while the line
stayed up, its remote IRR set in service and clear after (The
IO-APIC); for apicv, on the same machine in
x2APIC mode, that self-IPIs at and below CR8's class were held -- none
taken, both read in IRR -- and with CR8 0 taken by priority, 0x50 and then
0x30, with 0x70 nested in 0x30's handler, ISR read showing each in service;
that three levels from the IO-APIC were each ended by the x2APIC's EOI and
sent again while the line stayed up; that nothing was left in service; and
who did it: the host, injecting all six, under AMD-V or where the CPU has no
APIC virtualization, and the CPU -- hv info's APIC virtualization --
delivering all six and taking their EOIs under VT-x, the guest stopped after
the three level-triggered ones alone (Interrupts the CPU delivers
itself);
there tpr's CR8 is the virtual APIC's VTPR and no mov of it exits; for
posted, two CPUs both running, that the first counted all thousand of the
fixed IPIs the second sent it while it spun -- and who handed them over:
where the CPU has posted interrupts (hv info) each was posted and the
notification sent for nine in ten at least, the first CPU taking them in
its guest and leaving it a tenth as many times at most, and elsewhere the
host; and for debug, through the run loop
a Linux guest's CPUs go through, that a single step and an int1 -- each a
#DB the host intercepts -- reached the guest's own handler, the first with
DR6.BS and after the stepped instruction, the second after the ICEBP: a
user process's stepi once stopped its whole VM. Under VT-x the same --
its DR6 the one the exit said, VT-x writing none at a #DB that exits, and
its handler's movs of DR6 answered by the host. And for
cr4, through the same loop, that the guest read back each CR4 it wrote --
MCE cleared, then PGE set -- while the CPU's CR4 kept MCE; that PKE, which
its CPUID never offers, PAE cleared in long mode and bit 32 were each a
#GP with no error code, CR4 as it was; and that the host performed all
eleven of its accesses. On QEMU's TCG -- known by its hypervisor leaf's
signature -- which checks a read of any control register against CR0's
intercept and so stops no read of CR4, it holds the reads to the register
instead, MCE in it, and the host to its five writes; its VMCB must ask for
the reads either way. A control run with MCE kept nowhere -- neither
hvarch setting it at entry nor set_cr4 -- failed there: the register it
read lost MCE at the first write. Under VT-x, which stops a guest at a
write of CR4 only where it changes a bit the host keeps, and answers its
reads from the read shadow, it holds the reads to the same CR4s, the CPU's
to MCE kept and VMXE, and the host to three writes performed -- MCE
cleared, PKE, bit 32 -- and two refused: PAE cleared in long mode is the
CPU's own #GP, and PGE set its own write. And for msr, through the same loop,
that SPEC_CTRL and PRED_CMD, which its CPUID does not offer, AMD's LS_CFG,
which the policy names for no CPU, and MTRRs turned on, which nothing
keeps, were each a #GP its own handler took -- in that order, with no
error code, and in the host's count of five reads, three writes and four
refusals; that MCG_CAP, MCG_STATUS and the PAT were answered; and that
CPUID told it of no speculation control and no PMU, of no basic leaf past
0x16, and of a hypervisor. Every MSR but the three segment bases comes to
the same policy on either backend, so it says the same under VT-x. A
control build whose policy did not name MCG_STATUS failed it, a fifth #GP
on both CPUs it ran on. And for dr, two VMs of one CPU each, one after
the other on the CPU the guests run on: that the second read 0 in DR0-DR3
as it started, where the first had left its own -- and where the CPU's
four were the guests', it read those -- that each read back the pattern it
wrote, and that each one's breakpoint, on a ret of its own, fired once,
with DR6.B0 and at the ret, which only its own DR0 made the CPU's before
its entry can do; then that every one of its fourteen movs of them was
the host's to perform. An execution breakpoint, not a data one: QEMU
10.2's TCG never gets past a store a data breakpoint watches in a nested
guest -- it retries the store for good and takes no interrupt, so hv run
never comes back -- and a control build that left the four the CPU's hung
the same. The world switch before this failed it on both CPUs, the second
VM reading the first's breakpoint address and pattern. Without the load
before the entry, and with a load only when the guest's DR7 enabled a
breakpoint as it was entered -- which a guest then enables without an
exit -- the first VM's breakpoint never fired, on both CPUs. Under VT-x,
which stops a guest at every mov of a debug register, the same, with
eighteen movs the host's: DR7 armed, and in the handler DR6 read and
cleared and DR7 turned off. After both rounds hv must count at least two
switches between guests a guest -- each guest's VM on CPU 0 and on CPU 3,
after another guest's -- each with an IBPB where the CPU offers one, which
QEMU's TCG does not, and hv must say so; and where hv on set
IA32_SPEC_CTRL -- under VT-x, its IBRS -- every CPU's register, read back,
must still have it. Every guest ran through the run stub's clearing of the
registers and refill of the return stack after each exit. The unload that
follows, and the load after it, are then of a hypervisor that has run
guests on those CPUs.
smp is the one guest of two CPUs, its second on another host CPU than
the first (More than one CPU), and its
verdict is the guest's own record, read out of its memory: the second CPU,
started by INIT and a start-up IPI, came up in real mode and reached long
mode, and says it is x2APIC ID 1 and CPUID's 1; its IPI reached the first;
the first's one-shot APIC timer then ran out and interrupted it, and reads
0 after; and the counts agree -- one start, one IPI each way, one timer.
Under TCG it needs QEMU 9.2 or later, and the gate refuses an older one:
before 9.2, TCG did not put a guest with paging off through the nested
table at all, and the second CPU's real mode would have run on nos's own
memory.
Which backend it exercises is the host's. Under KVM, -cpu host gives the
guest the host CPU's own extension -- AMD-V on an AMD host, Intel VT-x on an
Intel one, nested, since nos is itself a KVM guest -- and TCG's -cpu max
gives AMD-V where there is no KVM, TCG having no VMX at all. So the Intel
backend is reached only through KVM and the AMD one three ways, and between
them both are covered. Three guests test a mechanism only one vendor has:
AMD-V's software VMCB check (refused), its ASID recycling (asid) and its
shadow task-priority register (tpr). Under VT-x the CPU refuses a bad VMCS
itself and there is no ASID (VPID is off), so those two check the Intel
equivalent instead -- a non-canonical guest RIP refused at entry, and three
VMs isolated by their own EPTs -- and tpr, the same bytes on both, is
stopped at each CR8 access and answered from a shadow, where AMD-V's V_TPR
never stops it; either way the host's own CR8 has to read 0 afterwards, on
the CPU the guest ran on, which is why the guests run bound to one. The
expected output is chosen from which extension hv info names. It was the
refused guest, run before any hv on, that first caught a VMX bug the
standalone runs could not: a VMCS vmcleared at construction, where VMX may
be off, faults, so the clear had to move to the first entry.
What asid is for -- a translation a reused ASID should not have had --
can only show on a CPU that keeps translations between entries: TCG flushes
its own on every vmrun. Under TCG it checks the allocator's side, that the
third VM's ASID is one of the first two's and that a generation ended in
between; on the AX41 it checks the TLB's.
It has been shown to fail the way it is meant to, three ways. With
disable_here changed to report every CPU turned off and turn none of them
off -- the silent failure the unload round trip exists for -- six checks
fail, the second load's among them, and the module's own warnings name the
CPUs left on. With the run stub storing R8 into R9's slot, the hypercall
guest fails on both CPUs, naming R8 as 0. And with the stub's vmload of
the host's state taken out, the kernel panics at the first exit -- a page
fault at 0 in Hal::GetCurrentCpuHwId(), the guest's GS base -- and the
gate says so. And smp fails two ways more: with the start-up IPI ignored,
the second CPU never enters, and the report shows the first halted where it
waits for the second's IPI; with the APIC timer never running out, the
first halts where it waits for the timer instead.
x86-64 boots the ISO with -cpu max: the default qemu64 model reports SVM
without nested paging. It uses KVM only when the host CPU has AMD-V, since
-cpu host hands the guest the host's own extension and the guests run
under AMD-V; on an Intel host it is TCG. --arch aarch64 checks the other
half -- that on a kernel running at EL1 the module loads, says a guest
cannot run here and names the exception level, and refuses hv on and
hv run rather than attempting them.
hv-linux-test.py -- a real Linux bzImage, for a while and for good
The third and fourth of the hypervisor demos (the hypervisor
page): hv boot loads an unmodified 64-bit
Linux bzImage by the boot protocol, runs it on a vCPU for a set time, and
reports its serial console. The gate boots nos with a bzImage on its root
filesystem and an /etc/rc that turns the extension on and runs hv boot,
and looks in the report for the kernel's banner and the first lines of its
early setup -- and for what must not be there: an "unchecked MSR access
error", a refusal of an MSR the kernel took for there and read without
being ready for a fault, or an "MCA architectural violation", the panic a
refused machine-check register is. Which MSRs a kernel reads that way
depends on its configuration and on the CPU's vendor, family and model, so
after touching the CPUID and MSR policy run the boot with a kernel that has
machine checks, MTRRs and PAT (X86_MCE, MTRR, X86_PAT, as a
distribution's does), on TCG's max and with --qemu-cpu EPYC-Rome-v1 (a
Zen 2, family 0x17, whose kernel polls the machine-check banks at boot,
reads AMD's NB_CFG and probes LS_CFG) and --qemu-cpu Skylake-Client-v1,+svm,+npt,+nrip-save (an Intel CPU given AMD-V, whose
kernel reads MISC_ENABLE, the microcode revision and the platform ID) --
TCG's max is an AMD family 6, which takes few of those paths. The MSRs
the guest was refused, which it was ready for, are printed as a note.
It is a manual gate, not in CI: a bzImage is megabytes and CI cannot
build one in the time it has, so the gate is pointed at a kernel by hand.
Build a small 64-bit guest -- a tinyconfig with the 8250 serial console and
an early console on, PCI, ACPI and SMP off is enough -- and run
scripts/hv-linux-test.py --bzimage /path/to/bzImage
The initramfs --initrd takes is a static BusyBox and an /init that
mounts /proc and /sys, says nos-guest: init is up, turns the kernel's
console messages down (dmesg -n 1) and runs sh -- a kernel line printed
after the shell's prompt hides the prompt from the module. Which BusyBox
matters three ways. Its line editor has to ask the terminal where the
cursor is (FEATURE_EDITING_ASK_TERMINAL): the module types a line at a
guest only once it has heard that question, which a shell asks at its
prompt, and with a BusyBox that never asks, every hv exec waits out its
time and says the guest is not at a prompt. --net wants its httpd; and
its exec has to run a script with no #! line through the shell, as
glibc's does, for the udhcpc -s script --net writes. Ubuntu 24.04's
static BusyBox 1.36.1 does all three; Alpine's busybox-static has no
httpd and runs no such script; the official busybox image's never asks.
Under AMD-V, which on a machine with no hardware SVM is QEMU's TCG, itself
under nothing faster -- so the guest's decompressor and early boot are twice
emulated and slow, and the whole run is a couple of minutes (--secs sets
the guest's budget, --deadline the gate's patience). With --initrd it
checks the guest reaches its init and a BusyBox shell, and with --input 'id\n' --expect uid=0 that the shell runs a command typed at it. The same
guest has been run by hand on the AX41's real AMD-V, where it found what TCG
cannot (the hypervisor page).
With --initrd it then goes on to guests that stay
up, in the same boot: two hv started
side by side and one stopped mid-boot; hv exec typed at the other while it
is still booting -- answered at the prompt printed after the line went in,
not at the one before it -- and again at its prompt, where the echo must come
back whole (the cursor-position answer once made BusyBox wrap it); hv send,
hv wait, hv console; a third guest with no init and panic=1, which
panics, reboots, and must stop with its reset as the reason rather than spin
on an 8042 that is not there; hv off refusing to turn the extension off
under the running guest; and rmmod hv stopping that guest before it turns the
extension off, which the next load confirms and dmesg shows in order. Once
the shell has the console, the kernel's own lines go to the log and not to
it -- so the gate ends on a command's output (version) rather than on
rc: /etc/rc done, and reads what the vCPU tasks and the unload said with
dmesg hv:. --skip-boot and --skip-vms run one half. The same phase
restarts guests: a stopped one by hv restart, one started with restart
that resets itself until the sixth reset in a minute leaves it stopped, and
the running one, which must answer a line typed during its new boot. And
one more guest is started with an input= and restarted before its first
prompt -- the input held for that prompt, so the boot after throws it away
unread, every time -- and must answer the next line at its prompt: input a
boot throws away once left every later hv exec waiting out its time and
then saying the line was not read. The guest ran the line all the same, so
its answer is in the output either way, and the check that holds is that
hv exec said nothing of a line not read: a control run with the fix taken
out passed while it looked for the answer alone, and fails it now. Then five
guests are started against one image on nos's root, /claimed.img: a guest
whose disk writes it, and then a second writer and a reader, both refused;
the writer stopped, two readers, side by side, and a writer refused under
them -- each refusal saying whose the image is, and every refused start
taking a number all the same, which is what the checks count on. The guests
have no initrd and get no further than their panic, holding their disks the
while. Then one guest started with log, whose console nos's log takes,
prints twenty thousand lines as fast as it can (seq, the end marked by a
line it computes, flood-$((6*7)), so that the shell's echo of what was
typed is not taken for it): nos's log must take fewer than half of them --
a boot's worth and then a rate. A dmesg is a ring that holds the newest two
thousand lines, so what the flood put in it is read from its count of the
lines it pushed out, before the flood and after, the ring's slots added on
top. Of the lines left out, nos says how many once a second at most, and
each notice must name exactly the gaps in seq's numbers since the one
before, over what of the flood the ring still holds. And the guest's own
console must keep the last line. Two of these are bound to time under TCG
-- six resets in a minute, the flood within --vm-secs -- and with --cpus 2 a loaded machine fails them with or without the change under test: back
to back on 2026-10-12, a build with and one without the speculation work
failed the same three, the restarting guest never making six resets inside
a minute in its 500 s, where the one build had passed all of them an hour
before. Run it again on
a quiet machine before reading a failure of those two as the change's.
--attach is a boot of its own for hv attach: sshd and a guest started
from /etc/rc, then an ssh -tt ... hv attach 0 that types a line, waits
for its answer and types ^] -- the answer must come back, the detach be
said, no cursor query reach the terminal, and the guest run on; hv attach
from /etc/rc, where nobody can type, must refuse. And a command line of
over 300 characters sent over ssh must run whole: the dispatch once refused
anything past 255, and a distribution's hv start is longer. It needs ssh and
ssh-keygen on the host.
--disk is another: an ext4 image with a file in it, put on nos's root and
given to the guest with disk=; the guest mounts it, reads the file, writes
4 MiB and a file of its own, syncs, and reads both back after a remount; the
report must count the disk's reads, writes and flushes and no error;
nos's root, which every write of the guest's went through -- the image's
holes filled, its blocks written over -- must pass e2fsck as nos left it,
stopped rather than unmounted; and the image, taken back out of it with
debugfs, must pass e2fsck too and hold what the guest wrote. It needs a
guest kernel with PCI, legacy virtio-pci, virtio-blk and ext4 built in.
--nvme-root puts nos's root on an NVMe disk instead of virtio-blk: each
driver takes ext2's batches of blocks its own way, and NVMe's is the one the
AX41 runs.
--cpus N gives every guest of the run N CPUs, the boot's and the VMs'.
The boot must say smp: Brought up 1 node, N CPUs, and its report that the
guest started N-1 of them; in the VM phase every CPU must be online, each
must have taken its own local timer's interrupts, rescheduling IPIs must
have gone between them, and hv list must name the host CPU of each. nos
runs with two cores of two threads (-smp 4,cores=2,threads=2) in every
boot of this gate, so that where a guest's CPUs go has a topology to be
wrong on: a guest's cores being its own, a guest of two must be on one
core's two threads and the next guest on the other core. (Where the guests
are all one's own, hv cores shared puts a guest's CPUs on cores apart:
two busy loops on one core's two threads ran at three quarters of the
speed, 13 and 12 on the AX41.) In every run the VM phase must put vm 0 and
vm 1 on cores apart, refuse a third guest while the two hold both cores --
saying the free cores hold none, before the guest takes a number -- and
have hv cores name which guest holds which CPUs; the image claims, three
guests at once beside vm 0, run under hv cores shared and switch it back
after. And --disk's guest must have its disk served on its own core: the
other thread of its CPU's, or, its two CPUs filling that core, the other
core. It goes with
--disk, --nvme-root, --net and --attach, whose guests get the same
N CPUs -- they once started theirs with one whatever --cpus said -- and
there the guest's /proc/interrupts must show its virtio device on MSI-X,
a vector for the queue, with more than one CPU, and on the 8259 (XT-PIC)
with one, as a guest of one CPU always was. The guest kernel
needs Linux 6.6 or later with SMP, X86_X2APIC and X86_MPPARSE (More
than one CPU says why), and under TCG the
gate wants QEMU 9.2 or later, as hv-test.py does.
--net is the guests' network: two guests with net; each has its port's
address and MAC; a guest pings nos at 10.0.100.1 and each pings the other;
nos pings a guest out of hv0; and a page from one guest's httpd is
fetched from outside QEMU through hv forward. Then where NAT keeps the
guests from: a fetch from the test machine -- 10.0.2.2, on nos's uplink's
own subnet -- and one from 169.254.169.254, a cloud's metadata server, must
each fail at once with "No route to host", and nat allow 10.0.2.2/32 must
let the test machine through (on the stack before the deny list, the guest
had the page, and NAT mapped its connection to the metadata address). Then
the way out, through nos's NAT: the guest's /proc/net/pnp names nos's DNS server (ip='s
dns0); it fetches a page and a megabyte from a web server the test runs on
127.0.0.1 -- 10.0.2.2 to QEMU's user network, so beyond nos -- and the
megabyte's md5 must match; it pings 10.0.2.2; udhcpc must be given its
port's address, nos as its router and nos's DNS server by the switch; and
nat must say it is on from hv0 through eth0 and count the megabyte's
segments back, say where the guests are kept from, what is let through and
how many packets it denied, hv list the address the guests go out from and
the DHCP answers. With --internet, a name is also looked up through that DNS server
-- which needs the test machine to resolve names. One question at a time:
BusyBox's nslookup asks for A and AAAA at once and matches answers to
questions by their ID alone, which musl takes from the clock's nanoseconds,
and a TCG guest's clock moves in ticks -- both questions got one ID, and one
answer was thrown away as the other's duplicate (the capture showed both
answered). Last, the megabyte again into a guest kept busy by a loop: it
must arrive whole, the switch must have kicked the vCPU out of its guest
for it (kicks in hv list above 0), and the port must have dropped
nothing. Under TCG only the first two tell anything -- with the kick taken
out, kicks reads 0 and the check fails, but slirp never outruns the host's
tick, so no frame was dropped either; the drops show at line rate, on the
AX41. Nor may either guest's traffic so far -- DHCP, ARP, pings, the two
megabytes -- have gone past its share of hv0's backlog or its rate to
every port: those are for a guest that floods, which the switch target of
hv-fuzz.py makes. And one guest claims the other's address -- ip addr add of it,
arping from it, a ping from it -- and must get nothing back: the switch
stops what a guest sends as another at its port, so nos, which would answer
both, hears neither; and hv list must count the frames against the guest
that sent them and no other. (nos's ARP table is no steady witness: with
the check taken out of the switch the claim reached it, and the other
guest's own ARP put the entry right again -- once before the test could
look, and once not, when the fetch through hv forward, which the test
makes last, reached the claiming guest instead of the other's httpd.)
Then one guest is stopped with a forward to it and its flows in NAT's table:
its forward has to go with it, the other's stay, and nat show no mapping
of its address -- the next VM on its port has its MAC and address, and was
once relayed to and answered for the stopped one. Last, nos's own services
are not the guests': sshd is started on nos, and must give its banner to
the test machine through a port QEMU forwards to eth0 -- so a guest's
refusal is the listener's doing, not a server that never started -- while
a guest's nc to nos's port 22, at 10.0.100.1 on hv0 and at eth0's
address through hv0, must be refused. A guest once had the banner both
ways, and four connections that never logged in held every slot sshd has
for logging in -- on the Hetzner boxes, the only console there is. It
needs a guest kernel with networking, virtio-net, packet sockets and ip=
configuration built in, and POSIX timers: BusyBox's ping paces itself
with alarm(), which a tinyconfig leaves out, and without it the guest's
ping waits for ever after its first answer -- which reads as a network
that stopped.
--acpi says the guest kernel has ACPI, with its power button (ACPI_BUTTON)
and the input device BusyBox's acpid reads it from (INPUT_EVDEV), and
checks that it takes the tables the machine gives it (ACPI):
its boot must find the RSDP at 0xE0000, the FADT, DSDT and FACS, the PM timer
at 0x608, S5, PIC mode, the PCI host bridge and PCI's interrupts routed by its
_PRT, the power button, and the PM timer as a clocksource -- and must say
nothing of an ACPI error or warning, a firmware bug, the ELCR set behind its
back, a PM timer that failed its checks, a TSC the clocksource watchdog
marked unstable, or a PCI interrupt without a GSI. Its reboots must go
through the FADT's reset register (0x06 to port 0xcf9, where a kernel
without ACPI pulses the 8042's line). In the VM phase one more guest runs
BusyBox's acpid, with a handler that powers off -- a devtmpfs mounted
first, which the initramfs's /init does not mount and without which there
is no /dev/input for acpid to read the button from (the gate failed so,
the button unanswered, until it mounted one), the button's event device
then checked for: its SCI must be IRQ 9 on
the 8259, the PM timer one of its clocksources, and hv stop must end it as
the guest powered itself off, the report counting the press; and one more
without acpid, which hears the button and does nothing, must be stopped
when secs=5 is up, its power button went unanswered for 5 s. Without
--acpi, a guest's hv stop must say nothing in it listens to its power
button. In both, after the reload, two guests at their shells are stopped
together by hv stop all secs=3, which must press both buttons at once and
say of each what became of its press. --acpi goes with --cpus, --disk, --net and --attach, whose
guests then find their devices through the DSDT's host bridge.
--xapic has the guests' local APICs come out of reset in xAPIC mode (hv start ... xapic), and a kernel of more than one CPU then reaches them
through the page, every access a nested fault the hypervisor decodes and
performs (MMIO): the boot must say so, its report
counting the accesses, and every check of the VM phase -- the IPIs, the
timers, a guest's MSI-X with --disk and --net -- holds as it does in
x2APIC mode. A --cmdline with nox2apic is the same from the other
side: the APICs come out in x2APIC mode and the kernel takes them out of
it. It is what has a Linux 6.1 without ACPI (acpi=off) bring up more than
one CPU: its MP-table path reads the page before it looks at the mode
firmware left. The kernel
the gate was brought up with is the 6.18 tinyconfig above plus ACPI,
ACPI_BUTTON, X86_PM_TIMER, INPUT and INPUT_EVDEV.
--ioapic (with --cpus 2 or more) gives the guests an IO-APIC (hv start ... ioapic, The IO-APIC), their APICs in
xAPIC mode with it: the boot must find it -- IOAPIC[0]: ... version 32, address 0xfec00000, GSI 0-23 -- and its report count interrupts sent
through it; in the VM phase /proc/interrupts must have the timer's
ticks on its pin 2 and the serial port's on pin 4, each taken. With
--acpi the kernel routes by it, Using IOAPIC for interrupt routing, its
SMP configuration all from the MADT, and the SCI must come in as a level
on pin 9. A --cmdline without no_timer_check has Linux count the
timer's ticks through it before it trusts it; one with pci=nomsi puts a
virtio device's interrupts on its INTx line, and --disk and --net must
then find them level-triggered through the IO-APIC -- sent again at each
EOI while the device's status is unread, which a lost EOI or a remote IRR
left set would stall the disk or the network over.
hv-distro-test.py -- a distribution, as it ships
hv-linux-test's guest is a kernel built for the purpose. This one is a
distribution's, built for every machine instead: Alpine's virt ISO, its
kernel and initramfs taken out of it with xorriso and the ISO itself given
to the guest as a read-only disk (disk=...:ro), unmodified. Also manual --
the ISO is a download -- and run as
scripts/hv-distro-test.py --iso alpine-virt-3.24.2-x86_64.iso
It checks that Alpine's initramfs finds its ISO and OpenRC brings it to a
login prompt; that root logs in by hv send (a getty asks for no cursor, so
exec alone would never type at it) and it is Alpine on its virt kernel;
that its clock is the host's, from the emulated RTC, to within minutes; that
the ISO is read-only to it -- the driver says so and a write fails; that
reboot resets it, restart boots it again, and root logs in again; that
its initramfs configured eth0 from the VM's ip=, it pings nos and nos
pings it; that its resolv.conf names nos's DNS server and it fetches a page
from the test machine through NAT -- which /etc/rc lets through first,
nat allow 10.0.2.2/32, the test machine being on nos's uplink's own subnet
-- (with --internet, it also looks Alpine's
mirror up by name and apk updates from it); that apk add openssh-server
installs from the ISO; and that the
test, from outside QEMU, logs into the guest's own sshd through QEMU's
forward, nos's hv forward and the switch. Two boots and apk under TCG:
about five minutes. It needs xorriso, ssh and ssh-keygen on the host.
With --debian debian-13-nocloud-amd64.raw (the .qcow2 through qemu-img convert -O raw) it boots Debian's cloud image the same way -- its kernel
and initrd read out of the image's /boot with sfdisk and debugfs, the
image, copied sparse, the guest's writable disk -- with systemd-firstboot
given the root password, locale, keymap and timezone as credentials on the
kernel command line; and checks that root logs in with that password, that
systemctl is-system-running says running with no unit failed, that its
root is /dev/vda1, ext4, read-write, that its clock is the host's; that
systemd-networkd, given a .network for its ethernet as one more
credential, takes its port's address, nos as its router and nos's DNS server
from the switch's DHCP server, reaches nos, and fetches a page from the test
machine through NAT with curl (with --internet, resolves Debian's mirror
and fetches from it); and that nos reaches it, and
that a file written to its root is still there after reboot (waited for
with hv wait ... boot=1, since systemd's reboot hands the shell its
prompt back first); and that hv stop presses its power button and
systemd-logind has it shut down and turn itself off.
Both boot on the machine's ACPI tables, as their kernels are built to;
--acpi-off boots them with acpi=off instead, the machine as it was
before it had them -- the MP table, PCI by configuration mechanism 1, no
power button -- and then Debian's hv stop must find nothing listening.
Both modes also time the guest's clock against nos's across 40 s of idle --
its /proc/uptime against nos's uptime -- and want them within 10%: an
idle guest whose vCPU was woken only at the host's tick was once given a
timer edge for every two or three of its periods, and counted 40% of real
time on the AX41 (0.514 under TCG, which is what this check read before
the fix and reads 1.000 after). The ±5 minutes the date is allowed could
not see it.
Under TCG the guest cannot calibrate its TSC against the PIT -- an exit
costs more than the calibration loop allows -- so it stays on jiffies and
the PIT's periodic mode. --cmdline-extra "tsc_early_khz=<kHz> tsc=reliable" puts it on the TSC and high-resolution timers, which drive
the PIT in one-shot mode, as a real CPU's calibration does by itself.
--ubuntu boots Ubuntu 24.04's server cloud image (made raw) as a cloud
does: its kernel, initrd and GRUB's command line (root=LABEL=cloudimg-rootfs ro console=tty1 console=ttyS0) read out of its own /boot partition and
given unchanged, and cloud-init provisioning it from a NoCloud seed -- an ISO
labelled cidata, the VM's second, read-only disk -- with root's password
and the machine's name. It is checked as Debian is -- systemd running with
no unit failed, its root read-write and kept across a reboot, the network
and the way out, hv stop turning it off -- but its network comes up from
the ip= hv gives a networked guest, which its initramfs sets and
cloud-init keeps, rather than by DHCP.
--cpus N gives each guest N CPUs, and their local APICs -- the command
line then without the nolapic a guest of one CPU is given -- and every
one must come online; with --ioapic too, an IO-APIC, and the serial
console's interrupts -- the getty's, the shell's -- must have come through
its pin 4 (The IO-APIC): a distribution's own
kernel, not one built for the gate, routing by it.
The hardware NIC drivers
tcp-test.py and netload-test.py take --nic igb (and so does
netblk-test.py, on x86-64), which swaps virtio-net for QEMU's 82576 (QEMU
8.0 or later; CI's does not have the device, so this one is run by hand). It is the only way any of the
three hardware NIC drivers runs anywhere but on a Hetzner box -- MSI-X with
two vectors, the descriptor rings, the receive poll and its interrupt
throttle -- so run tcp-test.py --nic igb and netload-test.py --nic igb
after touching drivers/igb or the driver seam in net/src/device.rs.
r8168 and r8125 have no emulation at all. A change to either is checked
by the compiler and then on the EX44, whose only console is that NIC: a
mistake there is a machine that has to be rescued by hand. Keep their
register sequences as they are. See Real hardware.