From 65ba3e3722525dabb6058be697cdd9c0039acf97 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Thu, 1 Oct 2026 06:29:24 -0300 Subject: [PATCH] qboot: enable the MTRRs, write-back by default Stock qboot leaves the MTRRs disabled, and Linux maps every range it memremaps write-back but does not know as RAM uncached-minus: a virtio-pmem region read at 8.3 MB/s where SeaBIOS, which programs them, gives 1.1 GB/s (lab runs 36818224412, 36818721809). mtrr.patch does what SeaBIOS does on the boot CPU: enabled, write-back by default, the 32-bit PCI hole from the top of low RAM to 4 GiB uncacheable. A firmware change, so a new machine fingerprint. Co-Authored-By: Claude Opus 5.5 (1M context) --- NOTICE | 3 +- qemu/Dockerfile | 6 ++- qemu/qboot/README.md | 13 +++++- qemu/qboot/mtrr.patch | 95 +++++++++++++++++++++++++++++++++++++++++++ versions.yaml | 2 +- 5 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 qemu/qboot/mtrr.patch diff --git a/NOTICE b/NOTICE index 556c19f..448a3ed 100644 --- a/NOTICE +++ b/NOTICE @@ -9,7 +9,8 @@ https://github.com/bonzini/qboot, commit 8ca302e86d685fa05b16e2b208888243da319941, under GPL-2.0 (see qemu/qboot/COPYING). write-pointer.patch modifies fw_cfg.c, include/fw_cfg.h and tables.c to implement WRITE_POINTER, DMA writes and error/bounds checks; pam.patch modifies hwsetup.c to -set q35's PAM registers in three configuration writes. The firmware built with +set q35's PAM registers in three configuration writes; mtrr.patch modifies main.c +to enable the MTRRs, write-back by default and the 32-bit PCI hole uncacheable. The firmware built with them ships in releases as qemu/qboot.bin, the patches beside it. The Apache-2.0 licence does not apply to those patches or COPYING. diff --git a/qemu/Dockerfile b/qemu/Dockerfile index 9237b08..530c56c 100644 --- a/qemu/Dockerfile +++ b/qemu/Dockerfile @@ -459,7 +459,7 @@ RUN git clone --quiet --branch "${QBOOT_VERSION}" https://github.com/bonzini/qbo git -C upstream archive "${QBOOT_COMMIT}" > upstream.tar && \ echo "${QBOOT_ARCHIVE_SHA256} upstream.tar" | sha256sum -c - -COPY qemu/qboot/write-pointer.patch qemu/qboot/pam.patch /build/ +COPY qemu/qboot/write-pointer.patch qemu/qboot/pam.patch qemu/qboot/mtrr.patch /build/ # Upstream's meson.build flags, with -Os fixed rather than taken from a build type. RUN set -eux; \ @@ -467,6 +467,7 @@ RUN set -eux; \ tar -xf upstream.tar -C src; \ patch -p1 --fuzz=0 -d src < write-pointer.patch; \ patch -p1 --fuzz=0 -d src < pam.patch; \ + patch -p1 --fuzz=0 -d src < mtrr.patch; \ cd src; \ for f in *.c *.S; do \ gcc -Os -m32 -march=i386 -mregparm=3 -fno-stack-protector \ @@ -482,12 +483,13 @@ RUN set -eux; \ # fail here - it fails as a guest that prints nothing - so the size is checked; and the patches, # because a patch that applied to nothing still leaves a tree that compiles: without the first # the firmware hangs every machine that has vmgenid, without the second it is slower and says -# nothing. +# nothing, and without the third every device memory a guest maps reads uncached. RUN set -eux; \ size=$(stat -c%s /build/qboot.bin); \ test "$size" -eq 65536 || { echo "qboot.bin is $size bytes, not 65536" >&2; exit 1; }; \ grep -q 'CMD_WRITE_PTR' src/tables.c && grep -q 'fw_cfg_write_file' src/fw_cfg.c || { echo "the WRITE_POINTER patch is not in the tree that was compiled" >&2; exit 1; }; \ grep -q '0x33333330' src/hwsetup.c || { echo "the PAM patch is not in the tree that was compiled" >&2; exit 1; }; \ + grep -q 'setup_mtrr();' src/main.c || { echo "the MTRR patch is not in the tree that was compiled" >&2; exit 1; }; \ sha256sum /build/qboot.bin FROM ${ALPINE_IMAGE} AS runtime diff --git a/qemu/qboot/README.md b/qemu/qboot/README.md index 35c0ec2..cf0dbe6 100644 --- a/qemu/qboot/README.md +++ b/qemu/qboot/README.md @@ -23,12 +23,23 @@ byte. The values are immediates: once PAM0 is ram, 0xf0000-0x100000 reads zeroes `setup_hw` has shadowed the BIOS, so a table in `.rodata` would be read back as zeroes. i440fx keeps the loop; this machine is q35 and nothing here boots the other. +## MTRRs + +Stock qboot never touches the MTRRs, so the guest boots with them disabled ("MTRRs disabled by +BIOS"). Linux then maps every range it is asked to map write-back but does not know as RAM +uncached-minus: a virtio-pmem region read at 8.3 MB/s, against 1.1 GB/s under SeaBIOS, which +programs them (lab runs 36818224412 and 36818721809, 2026-10-01). `mtrr.patch` does what SeaBIOS +does, on the boot CPU only - Linux gives its own MTRR state to the CPUs it starts: enabled, +write-back by default, and the 32-bit PCI hole, from the top of low RAM to 4 GiB, uncacheable in +naturally aligned power-of-two ranges. Fixed-range MTRRs stay off. If the hole does not fit the +variable ranges the CPU has, they are left disabled rather than caching MMIO. + ## Build `task qemu:build` builds it, in the `qboot` stage of `qemu/Dockerfile`, and it lands beside the SeaBIOS blobs as `_output/qemu/qboot.bin`, where a release, `task qemu:fetch` and CI all find it. The stage clones the pinned commit, verifies the SHA-256 of the tar `git archive` -writes for it, applies `write-pointer.patch` and then `pam.patch` with no fuzz, and compiles with upstream's +writes for it, applies `write-pointer.patch`, `pam.patch` and `mtrr.patch` with no fuzz, and compiles with upstream's meson.build flags plus `-Os`. It asserts on what came out: 65536 bytes, the ROM window it is linked for, and the patch's code in the tree that was compiled. diff --git a/qemu/qboot/mtrr.patch b/qemu/qboot/mtrr.patch new file mode 100644 index 0000000..8a2d6c2 --- /dev/null +++ b/qemu/qboot/mtrr.patch @@ -0,0 +1,95 @@ +diff --git a/main.c b/main.c +index afa2200..1a156f6 100644 +--- a/main.c ++++ b/main.c +@@ -77,6 +77,82 @@ static void extract_e820(void) + e820_seg = ((uintptr_t) e820) >> 4; + } + ++/* ++ * MTRRs: enabled, write-back by default, and the 32-bit PCI hole - from the top of low ++ * RAM to 4 GiB - uncacheable, as SeaBIOS sets them. Left disabled, Linux maps every ++ * range it memremaps write-back but does not know as RAM - a virtio-pmem region, for ++ * one - uncached-minus, and reads it at 8 MB/s. Only this CPU: Linux gives its own ++ * state to the APs it starts. Fixed-range MTRRs stay off, so below 1 MiB is the ++ * default type too. ++ */ ++#define MSR_MTRRcap 0xfe ++#define MSR_MTRRdefType 0x2ff ++#define MTRR_PHYSBASE(n) (0x200 + 2 * (n)) ++#define MTRR_PHYSMASK(n) (0x201 + 2 * (n)) ++#define MTRR_TYPE_UC 0 ++#define MTRR_TYPE_WB 6 ++#define MTRR_DEF_ENABLE 0x800 ++#define MTRR_MASK_VALID 0x800 ++ ++static inline void cpuid(uint32_t leaf, uint32_t *a, uint32_t *b, uint32_t *c, uint32_t *d) ++{ ++ asm volatile("cpuid" : "=a"(*a), "=b"(*b), "=c"(*c), "=d"(*d) : "0"(leaf), "2"(0)); ++} ++ ++static inline uint64_t rdmsr(uint32_t msr) ++{ ++ uint32_t lo, hi; ++ asm volatile("rdmsr" : "=a"(lo), "=d"(hi) : "c"(msr)); ++ return ((uint64_t)hi << 32) | lo; ++} ++ ++static inline void wrmsr(uint32_t msr, uint64_t val) ++{ ++ asm volatile("wrmsr" : : "c"(msr), "a"((uint32_t)val), "d"((uint32_t)(val >> 32))); ++} ++ ++static void setup_mtrr(void) ++{ ++ uint32_t a, b, c, d; ++ uint64_t phys_mask, base = lowmem, top = 1ull << 32; ++ int vcnt, n = 0; ++ ++ cpuid(1, &a, &b, &c, &d); ++ if (!(d & (1 << 12)) || !(d & (1 << 5))) /* MTRR, MSR */ ++ return; ++ vcnt = rdmsr(MSR_MTRRcap) & 0xff; ++ cpuid(0x80000000, &a, &b, &c, &d); ++ phys_mask = (1ull << 36) - 1; ++ if (a >= 0x80000008) { ++ cpuid(0x80000008, &a, &b, &c, &d); ++ phys_mask = (1ull << (a & 0xff)) - 1; ++ } ++ ++ wrmsr(MSR_MTRRdefType, 0); ++ for (n = 0; n < vcnt; n++) { ++ wrmsr(MTRR_PHYSBASE(n), 0); ++ wrmsr(MTRR_PHYSMASK(n), 0); ++ } ++ /* [lowmem, 4G) in naturally aligned powers of two. */ ++ for (n = 0; base < top && n < vcnt; n++) { ++ uint64_t size = top - base; ++ while (size & (size - 1)) ++ size &= size - 1; ++ while (base & (size - 1)) ++ size >>= 1; ++ wrmsr(MTRR_PHYSBASE(n), base | MTRR_TYPE_UC); ++ wrmsr(MTRR_PHYSMASK(n), (~(size - 1) & phys_mask) | MTRR_MASK_VALID); ++ base += size; ++ } ++ /* Not all of the hole fits: leave them off rather than cache MMIO. */ ++ if (base < top) { ++ for (n = 0; n < vcnt; n++) ++ wrmsr(MTRR_PHYSMASK(n), 0); ++ return; ++ } ++ wrmsr(MSR_MTRRdefType, MTRR_DEF_ENABLE | MTRR_TYPE_WB); ++} ++ + int __attribute__ ((section (".text.startup"))) main(void) + { + bool have_pci; +@@ -98,6 +174,7 @@ int __attribute__ ((section (".text.startup"))) main(void) + fw_cfg_setup(); + extract_acpi(); + extract_e820(); ++ setup_mtrr(); + setup_mptable(); + extract_smbios(); + boot_from_fwcfg(); diff --git a/versions.yaml b/versions.yaml index a099df4..cf9c363 100644 --- a/versions.yaml +++ b/versions.yaml @@ -152,7 +152,7 @@ the machine's BIOS, built in qemu/Dockerfile's qboot stage. archive is the sha256 of what `git archive` writes for the commit with debian's git, which nothing but a build can compute, so a bump is by hand: the commit, a build, the archive's sum from its failure, - write-pointer.patch and pam.patch rebased onto it, NOTICE and qemu/qboot/README.md restated, and + write-pointer.patch, pam.patch and mtrr.patch rebased onto it, NOTICE and qemu/qboot/README.md restated, and `task boot:firmware` again. - name: actionlint