Iron Modified EDK2 firmware
  • Shell 71.7%
  • Makefile 14.9%
  • Python 11%
  • Dockerfile 2.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Michael J. Manley b64dcf7de5 Version from release/YY.MM branches
build/version.sh with the shared rule and its test (make test-version).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvgVC2H8Wa7QukpNzTJia9
2026-10-01 16:43:54 -07:00
build Version from release/YY.MM branches 2026-10-01 16:43:54 -07:00
test Version from release/YY.MM branches 2026-10-01 16:43:54 -07:00
.gitignore Initial commit 2026-09-18 00:51:13 -07:00
CHANGELOG.md Version from release/YY.MM branches 2026-10-01 16:43:54 -07:00
CLAUDE.md Version from release/YY.MM branches 2026-10-01 16:43:54 -07:00
LICENSE.md Initial commit 2026-09-18 00:51:13 -07:00
Makefile Version from release/YY.MM branches 2026-10-01 16:43:54 -07:00
NOTICE.md Initial commit 2026-09-18 00:51:13 -07:00
README.md Version from release/YY.MM branches 2026-10-01 16:43:54 -07:00

Wise Foundry Iron firmware (iron-fw)

iron-fw builds the UEFI firmware that Wise Foundry Iron uses for virtual machines whose EFI variables live in the host variable store: libvirt's <varstore/>, which is QEMU's uefi-vars-x64 device ("host UEFI variables").

With a host variable store, the firmware keeps no variables in flash. It is a single read-only image that QEMU maps into memory, and QEMU itself holds the variables (Secure Boot keys, boot entries) in a JSON file per machine. Secure Boot keys are enforced by QEMU on the host rather than by code in the guest, so Secure Boot works without SMM and without a writable pflash device.

Alpine Linux 3.24, Iron's base OS, packages QEMU's uefi-vars module (qemu-hw-uefi-vars) but its ovmf package has no firmware built for it (QEMU_PV_VARS). iron-fw builds that firmware from upstream edk2, the same way Fedora builds its OVMF.qemuvars.fd, and generates the variable-store templates with virt-firmware.

Building

Requirements: podman (or docker) and make. The whole build runs rootless in a Debian builder container (build/Containerfile): gcc, nasm, acpica-tools, python3 and git live there, not on the host.

make            # print the targets and variables (the default goal)
make firmware   # build out/iron-fw/ (builds the builder image first)
make test       # boot it with Alpine 3.24's QEMU (see Testing)
make clean      # remove out/
make distclean  # also remove .cache/ (edk2 checkouts, virt-firmware, pip cache)

The build's version is YY.MM.PATCH (build/version.sh): YY.MM is this repository's branch when it is release/YY.MM, else (dev, any other branch, a detached HEAD, no git) the year and month it runs in (UTC); PATCH is 0 unless given (make PATCH=1); VERSION=YY.MM.PATCH overrides both. It goes into the build container as IRON_FW_VERSION and into the firmware version string.

A first build fetches edk2 and its submodules and takes a few minutes; later builds of the same edk2 release reuse the checkout in .cache/edk2/<ref>/. The edk2 build log is out/work/edk2-build.log.

What the build fetches

  • edk2: by default the newest stable release, resolved at build time with git ls-remote --tags https://github.com/tianocore/edk2 as the highest edk2-stableYYYYMM tag (release candidates such as edk2-stable202608-rc1 are ignored). The top-level git submodules are fetched with it (OpenSSL, Brotli, libfdt, MbedTLS, ...); the OpenSSL submodule is then moved to a pinned commit (see Embedded OpenSSL security patch).
  • virt-firmware: by default the newest release on PyPI, installed into a virtual environment in .cache/. It is a build tool only: it writes the JSON variable stores and is not part of the output.
  • dbx updates: Microsoft's revocation lists from microsoft/secureboot_objects, pinned by commit and sha256 in build/build.sh (see Secure Boot keys).

Pinning

Variable Default Example
EDK2_REF newest edk2-stable* tag make firmware EDK2_REF=edk2-stable202608, or a full commit id
EDK2_REPO https://github.com/tianocore/edk2 a mirror
VIRT_FIRMWARE_VERSION newest on PyPI make firmware VIRT_FIRMWARE_VERSION=26.9
EDK2_TARGET RELEASE DEBUG (logs to the QEMU debug port 0x402)
LOGO logo/logo.bmp if it exists make firmware LOGO=~/art/iron.bmp
OPENSSL_COMMIT f4dc4d58… (OpenSSL 3.5.8) a reviewed full commit id
BUILD_JOBS number of CPUs make firmware BUILD_JOBS=4

Every build records what it actually used in usr/share/iron-fw/VERSION, so a build of "latest" can be reproduced by passing those values back.

To update the dbx lists, change DBX_BASE and the two *_SHA256 values in build/build.sh to a newer commit of microsoft/secureboot_objects (PostSignedObjects/DBX/amd64/DBXUpdate.bin and PostSignedObjects/SignedByKEK2023/dbx_x64.efiauth2). Fedora follows the same files with its make-dbxupdate.sh.

Embedded OpenSSL security patch

The tested edk2-stable202608 baseline references OpenSSL 3.5.7. The builder checks out OpenSSL 3.5.8 commit f4dc4d58b48d346a8270183f89acf826d459b0ca in that submodule before compiling, independently of the EDK2 gitlink. OPENSSL_COMMIT accepts a reviewed full commit id override; it is passed through the Makefile (make firmware OPENSSL_COMMIT=...). The firmware VERSION manifest records the actual source commit and version. Host APK updates do not update this embedded library. Firmware compilation and both variable-store boot smoke tests must pass after changing this pin.

Outputs

out/iron-fw/ is a root-filesystem overlay: Iron's image build copies it verbatim onto the rootfs.

Path Contents
usr/share/iron-fw/OVMF.qemuvars.fd The firmware, 4 MiB: the whole FV/OVMF.fd of OvmfPkg/OvmfPkgX64.dsc, X64, RELEASE
usr/share/iron-fw/vars.blank.json Empty variable store (virt-fw-vars --output-json)
usr/share/iron-fw/vars.secboot.json Secure Boot enabled, Microsoft keys enrolled, current dbx
usr/share/iron-fw/VERSION Build manifest: edk2 tag and commit, OpenSSL submodule commit and version, virt-firmware version, dbx sources, logo, build date, sha256 of every file
usr/share/qemu/firmware/90-iron-ovmf-qemuvars-x64-sb-enrolled.json QEMU firmware descriptor: SB enabled, keys enrolled (vars.secboot.json)
usr/share/qemu/firmware/91-iron-ovmf-qemuvars-x64-sb.json QEMU firmware descriptor: SB supported, not enrolled (vars.blank.json)

Build options

The firmware is built with the options of Fedora's [build.ovmf.qemu.vars] (edk2-build.fedora): -D options from ovmf.common (NETWORK_HTTP_BOOT_ENABLE, NETWORK_IP6_ENABLE, NETWORK_TLS_ENABLE, NETWORK_ISCSI_ENABLE, NETWORK_ALLOW_HTTP_CONNECTIONS, TPM2_ENABLE, TPM2_CONFIG_ENABLE, TPM1_ENABLE=FALSE, CAVIUM_ERRATUM_27456), ovmf.4m (FD_SIZE_4MB, DEBUG_TO_MEM) and ovmf.qemu.vars (QEMU_PV_VARS, SECURE_BOOT_ENABLE, BUILD_SHELL=FALSE), and the PCDs of nx.strict (strict NX memory protection) and la57 (5-level paging). PcdFirmwareVersionString is iron-fw-<version>-<edk2 ref> and PcdFirmwareReleaseDateString the date of the edk2 commit, as Fedora's edk2-build.py sets them.

One difference: Fedora builds this image as DEBUG (its build config has no tgts, so edk2-build.py's default applies), which is why its descriptors carry verbose-dynamic. iron-fw builds RELEASE by default, which prints no debug messages, and its descriptors leave verbose-dynamic out. EDK2_TARGET=DEBUG builds the Fedora variant, descriptors included.

Secure Boot keys

vars.secboot.json is made like Fedora's, except for the platform key:

virt-fw-vars --output-json vars.secboot.json \
    --set-dbx DBXUpdate-2011.x64.bin --add-dbx DBXUpdate-2023.x64.bin \
    --enroll-microsoft --secure-boot

Fedora uses --enroll-redhat, which enrolls Red Hat's certificate as PK. iron-fw is not a Red Hat product, so it uses --enroll-microsoft, the only enrollment in virt-firmware that uses no vendor key besides Microsoft's:

  • PK: Microsoft's 2023 OEM platform key (Windows OEM Devices PK), with the Microsoft owner GUID, as Hyper-V and other Microsoft-keyed platforms use.
  • KEK: Microsoft Corporation KEK CA 2011 and KEK 2K CA 2023.
  • db: Microsoft Windows Production PCA 2011 and Windows UEFI CA 2023, Microsoft Corporation UEFI CA 2011 and UEFI CA 2023 (third party: shim, and so most Linux distributions), and Microsoft Option ROM UEFI CA 2023.
  • dbx: Microsoft's revocation lists: the last update signed with KEK CA 2011 (PostSignedObjects/DBX/amd64/DBXUpdate.bin) sets dbx, and the current update signed with KEK 2K CA 2023 (PostSignedObjects/SignedByKEK2023/dbx_x64.efiauth2) is appended. These are byte for byte the files Fedora ships as DBXUpdate-20260630.2011.x64.bin and DBXUpdate-20260901.2023.x64.bin.

Because PK is Microsoft's, only Microsoft can sign KEK updates for these machines; db and dbx updates signed with the Microsoft KEK apply as usual. To enroll a different PK (for example one of your own), replace --enroll-microsoft in build/build.sh with --enroll-cert <cert> or --enroll-generate <name>; virt-fw-vars --help lists the options.

How Iron uses it

Iron's image build (iron/build/mkrootfs.sh) copies out/iron-fw/ onto its root filesystem; iron/Makefile mounts ../iron-fw/out/iron-fw (or IRON_FW_DIR) read-only for it. Any image other than a dev one (IRON_VARIANT=dev) refuses to build without the firmware; a dev image warns and keeps every UEFI machine on the flash store. The files land next to Alpine's qemu-hw-uefi-vars package (QEMU's uefi-vars module, /usr/lib/qemu/hw-uefi-vars.so). Iron's agent names the files directly in the libvirt domains it writes (internal/vm/domain.go): a machine with the host variable store gets

<os firmware='efi'>
  <loader type='rom' format='raw'>/usr/share/iron-fw/OVMF.qemuvars.fd</loader>
  <varstore template='/usr/share/iron-fw/vars.secboot.json'/>  <!-- or vars.blank.json -->
</os>

and libvirt copies the template to the machine's own JSON store on first start. The descriptors in usr/share/qemu/firmware/ let libvirt pick the firmware by feature (host-uefi-vars, secure-boot, enrolled-keys) for domains that do not name a loader. Iron reports the host variable store as available only when the firmware, both templates and the QEMU module are all present.

OVMF shows a logo, centred on the screen, while it starts. It comes from the edk2 tree:

  • The image is MdeModulePkg/Logo/Logo.bmp (upstream: 193 x 58, 8 bits per pixel with a palette).
  • MdeModulePkg/Logo/Logo.idf declares it (#image IMG_LOGO Logo.bmp), and MdeModulePkg/Logo/LogoDxe.inf builds it, with Logo.c, into the LogoDxe driver as an HII image resource. Logo.c publishes it through the EDKII_PLATFORM_LOGO_PROTOCOL with EdkiiPlatformLogoDisplayAttributeCenter and offset 0,0.
  • OvmfPkg/OvmfPkgX64.dsc lists MdeModulePkg/Logo/LogoDxe.inf as a component and OvmfPkg/OvmfPkgX64.fdf places it in the DXE firmware volume (INF MdeModulePkg/Logo/LogoDxe.inf).
  • At boot, OVMF's PlatformBootManagerAfterConsole (OvmfPkg/Library/PlatformBootManagerLib/BdsPlatform.c) calls BootLogoEnableLogo() (MdeModulePkg/Library/BootLogoLib), which draws the image in the middle of the current graphics mode: x = (screen width - logo width) / 2, y = (screen height - logo height) / 2. It is not scaled; an image larger than the screen is not drawn. The same image is published to the operating system in the ACPI BGRT table, so a Linux splash (Plymouth themes that use the firmware logo) or Windows shows it too. The screen around it is black.

To use your own logo, put it at logo/logo.bmp (or pass LOGO=path) and run make firmware. The build validates it with build/bmpcheck.py, copies it over MdeModulePkg/Logo/Logo.bmp in its edk2 checkout (restoring the stock file first on every build) and records its size and sha256 in VERSION. The file must suit edk2's BMP converter (BaseTools/Source/Python/AutoGen/GenC.py, BmpImageDecoder):

  • Windows BMP with a 40-byte BITMAPINFOHEADER (no V4/V5 headers), bottom-up.
  • Uncompressed (BI_RGB), 1, 4 or 8 bits per pixel with a palette, or 24 bits per pixel. No 32-bit, no alpha channel, no RLE.
  • Width and height at most 65535, and nothing after the pixel data.
  • Keep it reasonably small. The DXE firmware volume that holds it is 0xE80000 bytes uncompressed (about 7 MiB free in the first iron-fw build) and is LZMA-compressed into the main volume (FVMAIN_SIZE, 0x348000 bytes, about 1.5 MiB free). A picture that does not fit makes the build fail; the build log (out/work/edk2-build.log) reports the DXEFV and FVMAIN_COMPACT fill levels. Something up to a few hundred pixels wide fits easily, and a small logo looks the same on any resolution.
  • Transparent areas are not supported; use a black background to blend with the screen.

With ImageMagick, a suitable file is

magick iron.png -background black -alpha remove -type TrueColor BMP3:logo/logo.bmp
# or, 8 bits per pixel:  magick iron.png -background black -alpha remove -colors 256 -type Palette BMP3:logo/logo.bmp
python3 build/bmpcheck.py logo/logo.bmp

Testing

make test boots the firmware built by make firmware in an alpine:3.24 container with Alpine's QEMU and its uefi-vars-x64 device, under TCG and without disks, once with each template (30 seconds each; SMOKE_SECONDS changes that for test/smoke.sh run by hand, but make test does not pass it into the container). It passes when the serial console shows the firmware at work and QEMU has written boot entries back to the machine's JSON store without losing any template variable. The logs and stores are kept in out/test/.

License

iron-fw's own files (build scripts, descriptors, documentation) are licensed under the Apache License, Version 2.0 (LICENSE.md). The firmware it builds is edk2 with its bundled components under their own licenses; see NOTICE.md.