A UEFI boot agent that attaches a remote disk over NVMe/TCP and boots it.
No kernel, no initramfs, no PXE, no TFTP. It carries its own TCP/IP stack
(smoltcp, on the NIC driver's EFI_SIMPLE_NETWORK) and needs nothing above
the NIC driver from the firmware.
It runs from a USB stick or a virtual-media ISO. It loads any NIC drivers the
media carries, works out which machine it is (a stated name, else its DNS
name, else its MAC or SMBIOS serial), claims that machine's image from the
storage engine, attaches it over NVMe/TCP, publishes it as EFI_BLOCK_IO_PROTOCOL, and
chain-loads the \EFI\BOOT\BOOTX64.EFI on the attached disk. If any step
fails, or the machine's boot intent is local, it falls through to the local
disk.
media NIC drivers → smoltcp on SNP (DHCP) → names (conf | DNS name, MAC, serial) → boot intent
→ claim boothost/<name> (or default by MAC) → NVMe/TCP attach
→ publish BlockIO + device path → ConnectController
→ load \EFI\BOOT\BOOTX64.EFI from the attached ESP → StartImage
stormbootx is stage one of two. It decides which image and attaches it,
and knows nothing about what is inside. On a stormcos image the BOOTX64.EFI
it starts is stormuefi, which finds
the pallets, verifies them and starts the kernel.
USB stick / ISO stormbootx tag → claim → attach → publish → chain-load ↓
attached clone stormuefi pallets → select → verify → kernel + initramfs → Linux
| How the machine boots | stormbootx | stormuefi |
|---|---|---|
| over the network, from forge | attaches the image | boots the kernel out of it |
| from its own drive | not involved | boots the kernel out of it |
Legacy-BIOS machines are served by neither. That is the planned
stormboot4bios, which will link this repo's
crates/nvme-tcp-initiator (#10).
At boot it reads \stormboot\local.conf and \stormboot\stormboot.conf
from the volume it was loaded from (and \stormboot\state, the self-update's
record), and loads any *.efi in \stormboot\drivers\ on that volume
as a NIC driver (#26; absent on ordinary media). It writes to these:
| What | When |
|---|---|
| the attached clone on the engine | the booted OS writes to its disk, and the BlockIO handle is read-write |
the hardware clock (RTC), through UEFI SetTime |
a boot whose NTP server answers while the RTC is more than a second off (#77); UTC, time zone and daylight left as the firmware had them |
volatile EFI variables, StormBootTag, StormBootHostNqn, StormBootClock, StormBootUpdate and StormBootInstallConfig (+ its chunks) |
every boot that names the machine, attaches an image, asks NTP, has a self-update state or finds an install-config.yaml on the media (#76, #77, #83, #79); gone at the next reset |
its own boot medium: \EFI\BOOT\BOOTX64.EFI, \startup.nsh, \stormboot\stormboot.conf, \stormboot\drivers\* (with *.new/*.prev copies), \stormboot\state, and \stormboot\local.conf only to keep a name = |
a writable medium (USB stick, local ESP) with update =, when a newer signed release verifies, and the starts of its trial (#83, Self-update) |
its own boot medium: \stormboot\last.log (this start's console, at most 64 KiB) and \stormboot\trial.log (a failed trial's last start) |
every start on a writable medium, before the attach and after the fall-through (#213, The start's log); never on a read-only medium or a RAM disk |
one non-volatile EFI variable, StormBootMinSerial |
when a self-updated medium passes its trial (#83): the highest release serial this machine has run, so an older signed release can't be replayed onto it |
Every line it prints is also sent over the network once a NIC holds a lease (#162, The log over the network): nothing is written for it.
Nothing is written to the platform's network configuration any more (#56;
until 0.9 a STATIC IP4 policy was switched to DHCP in NVRAM). The NICs are
taken from the firmware's own stack for the boot (SNP opened exclusively) and
given back on the fall-through.
On a machine with a screen (GOP: a monitor, a BMC's KVM, pve's console), stormbootx clears it when it starts and draws one panel, redrawn in place (#144):
stormbootx 0.28.0 (abc1234) - server1 2026-10-14 18:41:53 UTC (NTP)
Supermicro SYS-5037MR-H8TRF serial ZM142S026113
[ OK ] firmware handoff American Megatrends firmware
[ OK ] NIC up stormnic-ixgbe.efi ac:1f:6b:8a:a7:9c 11.2 s
[ OK ] DHCP nic 0 192.168.16.100/20 MTU 1500 0.8 s
[ OK ] forge reached stormblock 20.0.0 at 192.168.31.202:9090 0.1 s
[ OK ] boot intent install (under server1) 0.0 s
[ OK ] claim a clone of server1's image, nsid 1 0.4 s
[ .. ] image loaded 48 GiB at 4096-byte blocks, nsid 1 3.1 s
[ .. ] handoff <the bootloader's latest line> 2.0 s
[#########.....................] 312 MiB read 110 MiB/s
F2: every log line - the serial console carries the full log
Under OVMF, from tests/screen-ovmf.sh and net-ovmf's shipped boot:
a network boot,
a fall-through and
F2's details.
A step is [ ] (not yet), [ .. ] (under way, yellow), [ OK ]
(green), [FAIL] (red) or [ -- ] (skipped: a local intent claims
nothing). On a fall-through the reason is under the steps, red when a step
failed, with what happens next (the console offer's countdown, the local
disk). The bar moves while the attached image's bootloader reads; how much
it will read is not known here.
Every line the boot prints, and every line the media's NIC drivers and the
attached image's bootloader print, still goes to the serial console exactly
as before, and to a second screen behind F2 (Esc or F2 again goes back).
Nothing is mixed into the panel. How: stormbootx puts its own
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL in the system table's ConOut, writes
every line to the consoles that are not a screen (a terminal on a UART, a
BMC's serial redirection), and draws the panel on the ones that are (their
handle carries GOP). It hands ConOut back, with a cleared screen, before
the network console, a local disk's bootloader, and any return to the
firmware or the EFI Shell. The layout fits 80x25 and uses the width of a
larger text mode. ui = log in stormboot.conf keeps the old scrolling
console; a machine with no screen console gets it anyway.
The consoles are read again after every connect over all handles, after the
NIC take-over and at the handoff (#219): a connect can stop a terminal and
start a new one on the same UART, and the old one's pointer writes nowhere
(forge1's serial log ended at binding the platform's own drivers first).
A change is said: console : the consoles changed under a connect: ….
As the attached image's bootloader starts, stormbootx says
handoff : watchdog 300 s until ExitBootServices; ExitBootServices marked …
and arms three things (src/handoffguard.rs):
- A line at ExitBootServices. When the bootloader (Linux's EFI stub)
calls ExitBootServices, a
BEFORE_EXIT_BOOT_SERVICEShandler writeshandoff : ExitBootServices called by the bootloader; the drivers stop their devices, the kernel is nextto the serial consoles, andExitBootServices: drivers stopping their devices; kernel nextin the panel's handoff step. Once, and nothing allocated, so the call's map key still holds. - A green line along the bottom of the screen once every driver's
ExitBootServices handler has returned (an
EXIT_BOOT_SERVICEShandler atTPL_CALLBACK, after the drivers'TPL_NOTIFYones on EDK2; it only writes to the framebuffer). - The UEFI watchdog,
handoff_watchdogseconds (default 300): a bootloader that hangs before ExitBootServices resets the machine. ExitBootServices turns it off; a bootloader that returns to stormbootx turns it off too (and with it whatever the boot manager had armed).
So a frozen screen says where the boot stopped:
| screen | stopped in |
|---|---|
the bootloader's last line (EFI stub: Loaded initrd …), no ExitBootServices line |
the bootloader, before ExitBootServices (the watchdog resets it) |
ExitBootServices: drivers stopping …, no green line |
a driver's ExitBootServices handler (a NIC driver's quiesce) |
| the green line | the kernel, after every handler returned |
Nothing after ExitBootServices can be timed by UEFI: a hang there needs a BMC watchdog the OS takes over.
src/main.rs, run():
-
A self-update on trial (#83,
src/selfupdate.rs), before anything else: counts this start, or, after two starts that never attached, puts the previous files back and chain-loads them. See Self-update. -
Banner: version and build stamp (
STORMBOOTX_BUILD: the short commit, with-dirtyif the tree had changes, set bybuild-boot-agent.sh;unstamped buildotherwise), so the console always says which binary is talking. -
Identity. A machine is known to the engine by its DNS name (#23, stormblock#199); its serial and MAC are aliases.
name = <id>(or the oldertag = <id>) instormboot.confwins over everything, and is the only name tried.- Otherwise, once the engine has been reached (step 5), the DNS name:
DHCP option 12 from the lease on the interface that reached the engine
(option 15 adds the domain for the console), else the PTR of that
interface's address, asked of the option 6 DNS server over DNS/TCP
(
src/dnsname.rs; microdns answers over TCP, and TCP is the one transport this binary carries). microdns sends no option 12: it puts the reservation's name in DNS, so on microdns networks the PTR is the path. When the reply names no DNS server, or no reply was kept for that interface,dns =instormboot.confis asked instead; each way this comes up empty is printed. The engine knows hosts by the first label, soserver3.g10.loclaimsboothost/server3. The console printsname : server3.g10.lo (from DHCP). A name that is not a valid DNS name is ignored, not guessed at. - A reservation names the NIC, not the machine (#26): a label ending
in a digit and one lowercase letter drops the letter, so
server1aandserver1bare both the machineserver1(dnsname::machine_label), and the console says so. - Otherwise SMBIOS (
src/smbios.rs, via the_SM3_or_SM_entry in the EFI configuration table): the serial of Type 1 (System), then Type 2 (Baseboard), then Type 3 (Chassis). The first one that is not a placeholder is used. - A serial shared by several machines is rejected like a placeholder
(#26): a Type 1 serial equal to the Type 3 serial on a multi-node chassis
(chassis type multi-system, blade or blade enclosure) is the enclosure's,
so Type 1 and Type 3 are skipped and Type 2 is tried.
S11075924402016, the chassis serial seven Supermicro X9 blades report, is listed outright. - Placeholders are rejected (
universal::serial_usable, host-tested against stormipmi's vectors, so both derive the sameboothost/<tag>, #7): empty or NULs,none,unknown,na,n/a,empty,invalid,default string,not applicable,not specified,serial number,system serial number,chassis serial number,base board serial number,0123456789,123456789, anything containingto be filledoro.e.m., any string made only of0,.,-and spaces, and one filler repeated (FFFFFFFF,XXXXXXXX,****, 0xFF bytes). - No usable serial is no longer a failure: once the network stack exists
(step 4) the machine's MAC identifies it, the lowest usable unicast
permanent address across every NIC (
net::machine_mac), so the answer does not depend on driver bind order. Only no serial and no MAC falls through. - The console prints the source, plus the SMBIOS model when there is one,
and a
macline after step 4.
-
No FEC step (#202; the owner, 2026-11-14: "Stormbootx calls the driver, the driver resposible for the nic getting up - speed and fec"). stormbootx knows nothing about a card but which driver binds it, chosen by PCI ID (step 4,
drivers::take_over). Each stormnic driver reads its media and speed, applies the IEEE standard FEC itself (25G/50G/100G NRZ RS, 10G/40G none, PAM4 RS-544) and reports driver, speed, FEC and link per port. Nothing is handed down to it. The FEC pin (#186, #203),fec =recovery sticks, the console's and the API'sfecand/api/v1/fecare gone;StormBootFecPinleft in NVRAM by an older stormbootx is ignored. -
NIC drivers from the media, then the network (
drivers::load_from_media,net::up, #56). Any*.efiin\stormboot\driversis started first (see The network path). Then everyEFI_SIMPLE_NETWORKhandle is opened and smoltcp starts DHCP on all of them. With no SNP at all it connects every handle and waits up to 5 s. Then it falls through, saying the NIC needs a UEFI driver. Each NIC's driver is named before its first SNP call (nic 0: driver <name>, #88). The console printstcp4 : smoltcp over SNP (nic 0 <mac>, …)andrng : firmware | rdrand | rndr | jitter. Then the self-update (#83): withupdate =on a writable medium, ask stormcentral for the current release and, if it is newer and its signature verifies, write it and restart into it (Self-update). Oneupdate :line either way. -
Where and which.
config::resolvegives the portal from the conf, falling back to compiled defaults. Unless the conf saysclaim = no, it first finds the engine (/api/v1/health) on its plain boot port, else onapi_port, and uses that port for the whole boot (#183, Ports), printed asengine : reached <addr>:<port> (the boot port)or(api_port). Then it reads the machine's boot intent (src/intent.rs):GET http://<portal>:<boot_port or api_port>/api/v1/synonyms/boothost/<name>/intent → 200 {"host":"server1","intent":"local","updated_at":…} or 404 (A stated name is the only one asked. Otherwise it goes down the names the claim tries, in the same order, and a 404 moves on to the next: the DNS name, the MAC's twelve hex digits for a machine that claims the default, then the serial. The engine resolves each through the same host table, so a DNS name it does not know yet can't hide an intent set on the host it knows by MAC.)intent what stormbootx does installclaims and boots the image, as below localfalls through to the local disk at once: no claim, no clone autoclaims and boots; with local_when_bootable = true, a bootable local disk first (below)Set a state on the engine and power-cycle the machine to get it. Any doubt reads as
auto: a 404, a non-2xx, an unreachable engine, or a body with no intent or an unknown one.autoclaims, as every doubt does (owner, 2026-09-30): until intents work on forge (stormblock#148), every boot claims and boots or installs from the network. The rule below is in the code and off by default;local_when_bootable = trueinstormboot.confturns it on.With it on,
autoboots what the machine has (#3). A local disk "can boot" when it is a whole, present, non-removable disk other than the media stormbootx was loaded from, and its GPT has an ESP carrying\EFI\BOOT\BOOTX64.EFI.esp.rsreads that, with no network and no firmware FAT, and the file is found, not read. The console printslocal : disk N (…): ESP partition P, FATn, …and falls through, orlocal : nothing to boot (…); claimingwith the reason for each disk. So a bare machine, or one the engine does not know yet, still claims and installs, and an installed one boots itself until someone setsinstall. Any bootloader counts, not only stormuefi: a disk with another OS on it boots that OS underauto. The console prints the intent and, when it defaulted, why. The contract is stormblock#148, released in stormblock v20.0.0 but not yet on forge (13.7.0 on 2026-10-02), so today every read there is a 404 and every boot isauto. Setting it isPUT …/intent {"intent":"local"}with the admin token. Resettinginstallback tolocalis the engine's job: the node's OS reportsPOST …/installed {volume}after the flow-over, and the claim reply carriesintentfor the initramfs. stormbootx never writes it.First it reads the engine's version from
GET /api/v1/health. That is the boot's first request, so it also brings the network up and tells which interface to read the DNS name from. Then it claims the machine's image, best name first:POST …/api/v1/synonyms/boothost/<stated name>/claim body {"agent":{…},"inventory":{…}} POST …/api/v1/synonyms/boothost/<DNS name>/claim body {"agent":{…},"inventory":{…},"mac":"…","serial":"…"} POST …/api/v1/synonyms/boothost/default/claim body {"agent":{…},"inventory":{…},"mac":"…","serial":"…"} POST …/api/v1/synonyms/boothost/<serial>/claim body {"agent":{…},"inventory":{…}}A claim mints a clone, so it may take a while: it waits 90 s for the reply (every other read 30 s), printing
waiting for the reply (N s)every 10 s (#157). A reply that never comes readsno reply within 90 s (the request was sent); a connection the engine closes first,the connection closed (…) with no reply after N s. Either falls through.Every claim names the boot agent (#90, stormcentral#286), so stormcentral can tell which stormbootx booted a machine, not only which release it installed:
"agent": {"name":"stormbootx","version":"0.19.0","commit":"649dd07", "media":"rustnic ixgbe@02fcc5d mlx4@398a1a4 virtio@2dcafd3 e1000e@bd67192 igb@6990341", "update_serial":5,"update":"serial:5","api_port":9180, "last_log":"stormbootx 0.42.0 …","log":"stormbootx 0.42.0 …"}commitis the build stamp (absent on an unstamped build),mediathe medium'smedia =label,update_serialthe self-update serial of the files on the medium (absent when no update wrote them), andupdatetheStormBootUpdatevalue at claim time (serial:<n>,trial:<n>:<start>,failed:<n>; absent when unset). The engine keeps it on the host's last-claim record (stormblock#177). Then the console (#213, The start's log):trial_log, the failed trial's last start, only withfailed:<n>;last_log, the previous start'slast.log;log, this start up to the claim. Each is the log's head and tail (6, 3 and 4 KiB escaped) with a[... N bytes not kept ...]line between, and the wholeagentstays within 15 KiB (the engine drops one over 16 KiB): the logs go first, neverupdate.Every claim also carries the firmware inventory (#4, #167,
src/inventory.rsandsrc/hardware.rs): everything the firmware can say with no OS and no BMC ("v":2; #20 sent only what a BMC can't, and #167, the server8 experience of a BMC that answers nothing, widened it). That is:- each NIC's MAC as firmware sees it, whether it has link, its MTU (the
lease's, #165, else the SNP's), and which driver bound it, with
media_drivertrue for a stormnic driver off the media, plus its PCI location and ID (SNP has no link speed; the stormnic drivers print it); - every mass-storage controller (PCI class 01) and its driver, if any; each is connected first, so a drive the firmware's fast boot left unconnected is still counted;
- every whole disk with a BlockIO, the boot medium marked, and how many
(
disk_count). For each: the bus (nvme,sata,ata,scsi,usb,virtio), its device path (controller, port or target), model, serial, firmware revision, WWN, size, logical and physical sector size, media (ssd,hdd,nvme), and its partition table: the GPT's entries by kind (esp,linux,windows-data, … or the type GUID) with size and name, an ESP's directories under\EFI(BOOT,ubuntu,Microsoft: the installs on it), else an MBR's four entries. FromEFI_DISK_INFO(ATA IDENTIFY, SCSI/USB INQUIRY, NVMe Identify Namespace), NVMe pass-thru Identify Controller,EFI_SCSI_IOVPD pages 0x80/0x83/0xB1, and the disk's own first blocks; all read-only; - SMBIOS: the system (Type 1: vendor, product, serial, UUID), board (Type 2), firmware (Type 0: vendor, version, date), CPU (Type 4) and memory (Type 17), and whether there is a BMC (Type 38 or 42).
It is collected once, at the first claim, before anything is attached, so the namespace this boot attaches is never in it. The console prints it too, and so the log collector gets it (#162). net-ovmf's shipped boot:
inventory : 1 NIC(s), 3 storage controller(s), 4 disk(s), no BMC; 1960 bytes with the claim system : QEMU Standard PC (Q35 + ICH9, 2009) firmware : EDK II edk2-20260812-8.fc44 (08/12/2026) disk 0 : scsi QEMU QEMU CD-ROM fw 2.5+ 5 MiB 2048 B sectors the boot medium PciRoot(0x0)/Pci(0x3,0x0)/Scsi(0x0,0x0) mbr: 2 esp 16 MiB disk 1 : scsi QEMU QEMU HARDDISK serial SCSI0167 fw 2.5+ wwn naa.5000c500a1b2c3d5 256 MiB 512 B sectors hdd PciRoot(0x0)/Pci(0x3,0x0)/Scsi(0x1,0x0) none: no partitions disk 2 : nvme QEMU NVMe Ctrl serial NVME0167 fw 10.2.2 wwn eui.0011223344556677 64 MiB 512 B sectors nvme PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,77-66-55-44-33-22-11-00) gpt: 1 esp 32 MiB "EFI system" (EFI: BOOT, ubuntu), 2 linux 30 MiB disk 3 : sata QEMU HARDDISK serial SATA0167 fw 2.5+ wwn naa.5000c500a1b2c3d4 128 MiB 512 B sectors (4096 B physical) ssd PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0) none: no partitionsThe engine keeps it beside
agentand drops it, never the claim, if it is not a JSON object or is over 16 KiB; it is cut to 15 KiB here (disks last, the rest counted indisks_omitted, partitions after 16 inpartitions_omitted):"inventory": {"v":2,"bmc":false, "nics":[{"mac":"52:54:00:12:34:56","link":true,"driver":"Virtio Network Driver", "media_driver":false,"pci":"0000:00:02.0","id":"1af4:1000","mtu":1500}], "storage":[{"pci":"0000:00:1f.2","id":"8086:2922","class":"01:06:01","driver":"…"}], "disk_count":2, "disks":[{"bus":"scsi","path":"PciRoot(0x0)/Pci(0x3,0x0)/Scsi(0x0,0x0)","model":"QEMU QEMU CD-ROM", "firmware":"2.5+","blocks":2604,"block_size":2048,"removable":true,"medium":true, "table":"mbr","partitions":[{"n":1,"kind":"esp","bytes":4194304}]}, {"bus":"sata","path":"…/Sata(0x2,0xFFFF,0x0)","model":"QEMU HARDDISK","serial":"SATA0167", "firmware":"2.5+","wwn":"naa.5000c500a1b2c3d4","media":"ssd","blocks":262144, "block_size":512,"physical_block_size":4096,"removable":false,"medium":false,"table":"none"}], "system":{"vendor":"QEMU","product":"Standard PC (Q35 + ICH9, 2009)"}, "firmware":{"vendor":"EFI Development Kit II / OVMF","version":"0.0.0","date":"02/06/2015"}, "cpu":{"model":"…","sockets":1,"cores":1,"threads":1,"max_mhz":2000}, "memory":{"mb":1024,"dimms":1}}The design for the rest of #167 (commands beside the intent, console input, the controls in stormcentral and the laptop app) is
docs/remote-control.md.A stated name is the only one tried. Otherwise a 404 moves on to the next line, and any other failure falls back to the resolved target. The DNS claim carries the MAC and serial so the engine can tie a name it has not seen to a host it already knows (stormblock#204).
The reply supplies the address, port, NQN and NSID. Both
address/portandtraddr/trsvcidspellings are accepted, with port defaulting to 4420 and NSID to 1. Any claim failure falls back to the conf's ownnqn/nsidrather than failing, unless the medium saysfallback = none. Every golden does (#36), so a machine no claim gives an image attaches nothing and boots its local disk (no network boot: no claim gave this machine an image, and the media names no fallback). The fallback they used to carry, nsid 2 on forge, was a 32 MB namespace with no ESP: every unnamed boot attached it and fell through anyway.Universal boot: one medium for every machine (#15, stormblock#200). A machine whose media states no name, and whose DNS name the engine does not know (or that has none), claims
boothost/defaultand says which machine it is with its MAC. The engine gives it a copy-on-write clone of the default release of its own, recorded as hostmac-<hex>until it is named, and the same MAC gets the same host back on every boot. The console saysbooting the default image as mac-<hex>, or, once the machine has a name the MAC is an alias of,booting <name>'s image. Not the SMBIOS serial: serials are not unique (seven Supermicro MicroCloud nodes report one chassis serial), and a serial claim would boot them all as one machine. The serial rides along in the body so the engine can resolve a machine it already knows by serial (C2NR0Q2) to that host (stormblock#202). stormbootx cannot check that itself, because every read but the claim needs a token.Only against an engine that has #200, told by the public
/api/v1/healthversion being after 19.3.0. An engine from v17 to 19.3.0 readsdefaultas one shared tag, and each machine's claim would release the clone another is running from. Against those, or when the version can't be read, it claimsboothost/<serial>as before, and the engine (v17 and later) pins an unknown serial toboothost/default. If a #200 engine has noboothost/defaultto give (404) and the machine has a serial, it tries the serial claim too; with no default the engine cannot mint a serial host, so that only finds one that already exists. A statedtag =is always claimed as that tag. The default is set through stormipmi's/api/v1/machines/default(stormcentral#29). 5a. Set the hardware clock from NTP (#77,src/clock.rs,src/sntp.rs). The X9 blades have no RTC battery, and neither their BIOS nor their BMC sets the clock. Once the claim is decided the network is up and leased, so stormbootx sends one SNTP request (RFC 4330, UDP 123) to the server in DHCP option 42 of the lease on the NIC that reached the engine, else tontp =instormboot.conf(host[:port]), else topool.ntp.org. A name is looked up with one A query over UDP to the lease's DNS server (ordns =). Two tries of a second each, for the lookup and for the request; a reply is believed only from a synchronised server (mode 4, LI ≠ 3, stratum 1..15) that echoes the request's random transmit timestamp. When NTP andGetTimediffer by more than a second,SetTimewrites UTC (Linux reads the RTC as UTC), keeping the firmware's time zone and daylight fields so EDK2 writes no NV variable. One line says what happened:clock : was 2026-01-01 00:00:05, set to 2026-10-01 21:14:03 UTC from 162.159.200.1 (step +23663638 s),clock : … UTC from …; the RTC agrees (within 1 s), orclock : NTP unreachable at … (…); left at …. It never stops the boot. A boot that falls through instead (alocalintent, a failed attach) does it on the way out, before the NICs are released.ntp = offleaves the clock alone. The outcome goes to Linux asStormBootClock(step 6a):synced:<server address>when the RTC is known to be right, elseunsynced. stormblock's initramfs reads it (stormblock#253): aftersyncedit does not step the clock again (unless the clock reads before the image was built, orrd.stormblock.ntp=always), and it records how the clock was set in/run/stormblock/clock. - each NIC's MAC as firmware sees it, whether it has link, its MTU (the
lease's, #165, else the SNP's), and which driver bound it, with
-
Attach (
src/nvme.rs, overcrates/nvme-tcp-initiator). The host NQN isnqn.2026-09.lo.storm:host-<name>, so the target knows which machine is connecting: the engine's name for the machine from the claim reply (mac-<hex>for one it booted as the default), else the DNS name, else the tag. The console prints the namespace geometry and the transfer size. 6a. Hand the name down to Linux (#76,src/handoff.rs). The initramfs claimsboothost/<name>again, and its own guess is the SMBIOS serial, which on the X9 MicroCloud blades is the chassis's: server8 booted its own image and then laid its disk from server1's (stormblock#249). So before the loader starts, stormbootx sets two volatile EFI variables under vendor GUIDab361f54-0166-44a4-a088-1ac22e98ab76, attributesBOOTSERVICE_ACCESS | RUNTIME_ACCESS(0x6, neverNON_VOLATILE), ASCII with no NUL:StormBootTag: the name claimed on: the claim reply's host when it named one, else the name claimed (server8,C2NR0Q2,mac-<hex>). With nothing claimed (the engine down, alocalintent), only a name stormbootx was given or found: the statedtag =, else the DNS name; never a serial or a MAC guess. Set as soon as the claim is decided, so a failed attach that falls through still names the machine.StormBootHostNqn: the host NQN of step 6, once the attach worked.StormBootEngine(#183):<addr>:<port>where the engine answered, so the initramfs can claim from the same port (stormblock#524). stormbootx writes no kernel line, sord.stormblock.boothost=is not its to set.StormBootClock(#77):synced:<NTP server address>orunsynced, from step 5a (or the fall-through).StormBootUpdate(#83): the boot medium's self-update,serial:<n>,trial:<n>:<start>,good:<n>orfailed:<n>(Self-update).StormBootInstallConfig(#79): the media'sinstall-config.yaml, as a header and chunks (install-config.yaml, below). Set before step 1. A value outside[A-Za-z0-9._:-](Linux ignores those) is not set. Each is printed ashandoff : …; a failure to set is printed and not fatal. Linux reads them from/sys/firmware/efi/efivars/<Name>-<guid>(four attribute bytes,06 00 00 00, then the value) ahead ofrd.stormblock.tag=and SMBIOS (stormblockdocs/boot-hooks.md, "Whose image").
-
Publish (
src/blockio.rs). BlockIO is installed with the namespace's own block size (read from FLBAS, so 4096 on a 4K namespace). A device path of one hardware vendor node (6d7a1f2e-9c34-4b8a-b1d0-5e2f7a0c9b41) goes on the same handle, thenConnectControllerrecursively. -
Chain-load. It looks for
\EFI\BOOT\BOOTX64.EFIon a filesystem whose device path starts with that vendor node, which means an ESP on the attached disk only. It is never a local disk.LoadImage+StartImage. Success never returns. -
Read the ESP itself when the firmware can't (#37,
src/esp.rs). Volumes stay 4096-byte, so a 4K image's ESP is a FAT with 4096-byte sectors (Linux mounts nothing smaller than the device's block). AMI Aptio 4 mounts one and then answersNOT_FOUNDfor a file that is there. If step 8 loads nothing, stormbootx reads the GPT (CRC-checked), finds the ESP, readsBOOTX64.EFIfrom its FAT12/16/32 at any sector size from 512 to 4096 (a boot sector with no 16-bit FAT size is FAT32 whatever its cluster count, as Linux mounts it, #67), andLoadImages the bytes from the buffer, with the device pathVendor(disk)/HD(n,GPT,…)/\EFI\BOOT\BOOTX64.EFI. The console saysboot : the firmware did not load it (…); reading the ESP here. stormuefi needs nothing more: it reads its pallets through whole-disk BlockIO and parses the GPT itself. Any other bootloader opens more files on its ESP (shim'sgrubx64.efi, systemd-boot's loader entries) through theSimpleFileSystemon itsDeviceHandle, so the bridge puts up its own (#42,src/espfs.rs). This read-onlyEFI_SIMPLE_FILE_SYSTEMoveresp.rsgoes on the ESP's partition handle, after the firmware's FAT is disconnected from it (or on a new handle with that path, if the partition driver made none). The image is loaded under that path, so it becomes the image'sDeviceHandle. The console saysesp fs : read-only filesystem on the ESP's partition handle, the firmware's FAT disconnected from it. Writes,SetInfoandDeleteanswerWRITE_PROTECTED, andblockio::withdrawtakes it back first.esp = firmware/esp = stormbootxpicks one reader alone. It is read at start-up with the rest ofstormboot.conf: nothing on the media is opened once an image is attached (#46). -
Report the bootloader's reads (#46). While the image's bootloader (stormuefi) reads through the published BlockIO, stormbootx prints a
blockio :line for a read that fails (with the NVMe/TCP error, which the firmware only sees asDEVICE_ERROR), a single read slower than 2 s, and every 64 MiB read with its MiB/s. A boot that stalls inside the bootloader then shows whether reads crawl, fail, or stopped being asked for.
The serial console is a boot's only record, and a BMC's serial-over-LAN can
die mid-install (server8, stormblock#476). So every line stormbootx prints,
and every line printed through ConOut while it runs (the media's NIC
drivers'), is also sent as a stormcast frame (#162, src/netlog.rs,
src/logwire.rs): RFC 5424 over UDP, facility local0, APP-NAME
stormbootx, the framing of glennswest/stormcast's docs/wire.md.
- To the fleet's group
239.255.42.1:5514(TTL 4) on every NIC with a lease, which mcastsyslog and stormconsole read; and, because multicast does not leave the subnet, to a unicast collector when one is named:syslog =instormboot.conf, else a top-levelsyslog:in the media'sinstall-config.yaml(host[:port], port 514 when bare; a name is looked up once at the lease's DNS server). The goldens name stormcentral's receiver,stormcentral.g8.lo:5514, sostormcentral logs --host <machine>shows the boot. Nothing named: the group alone, never an error.off: nothing is sent. - Kept from the first line (the banner) while no NIC holds a lease, at most 4000 lines or 512 KiB (the oldest go first, and the next send says how many), and sent at fixed points: once the machine's name is known, after the claim, before the attached image's bootloader starts, before a self-update resets, and on the fall-through before the NICs are given back. Never from inside a print, so the bootloader's own lines are not sent.
- HOSTNAME is the best name known when a line goes: the host the claim
booted, else the DNS name or a stated
tag, elsemac-<hex>. TIMESTAMP is the line's own time once NTP set the clock this boot (#77); before that-, and the collector keeps the arrival time. - The console says where it goes, once:
log : sent to the stormcast group 239.255.42.1:5514 and to stormcentral.g8.lo:5514 (192.168.8.x, syslog in \stormboot\stormboot.conf) as server8.
The initramfs's and the installer's lines after it are stormblock's (stormblock#477).
A node's first-boot input (stormcos#82, stormcos docs/INSTALL-CONFIG.md:
cluster name and domain, sshKey, pullSecret, apiToken, hosts[])
travels inside the boot media (owner, 2026-10-02, #79). storminstall
writes it, stormbootx hands it down, the initramfs puts it in
/state/config/install-config.yaml on a first boot, and stormpump applies it.
The slot (storminstall docs/config-slot.md):
\stormboot\install-config.yaml on the media's ESP, the STORMBOOTX FAT
volume build-boot-agent.sh makes, beside stormboot.conf. No partition of
its own: the ESP is the one volume firmware reads however the media is
presented (El Torito image on a CD, the isohybrid MBR 0xEF / GPT entry on a
USB stick, the GPT ESP of the .img), and storminstall writes it with plain
FAT file I/O. At most 256 KiB; a real one is about 1 KiB. For it to keep
working, the ESP keeps its \stormboot\ directory, stormboot.conf, and
some free space (the 4 MiB FAT12 ESP has ~4 MiB less any NIC drivers).
The hand-down (src/installconf.rs, handoff::set_install_config):
read with the rest of the media before step 1, whatever the boot does next,
into volatile EFI variables under #76's GUID, attributes 0x6. A firmware
variable may be capped at 1 KiB (EDK2's default PcdMaxVariableSize), so the
file goes in chunks:
| variable | value |
|---|---|
StormBootInstallConfig0 … StormBootInstallConfig<N-1> |
the file's bytes, 768 each, the last one shorter |
StormBootInstallConfig |
v1:<length>:<N>:<sha256, lower-case hex> |
The header is set last, and a chunk the firmware refuses takes the ones
already set with it, so a header means the whole file is there. A reader
concatenates the N chunks (each efivarfs file is four attribute bytes and
then the data) and uses the result only if its length and SHA-256 match. The
console prints install cfg : none on the media (…), or handoff : StormBootInstallConfig (<length> bytes in <N> chunk(s)) = v1:…, and never the
content: it carries the pull secret and the API token. An empty file, one over
256 KiB, or one the variable store can't hold is reported and not handed down.
A firmware's volatile variable store is a fixed size, so the store is the
real limit (#93). Fedora's OVMF takes every size up to 256 KiB (342 chunks;
tests/net-ovmf.sh hands down 64, 128, 192 and 256 KiB). Real firmware is
not measured yet. Where the store fills, the console says so and how much
fit, for example install cfg : NOT HANDED DOWN: the firmware's volatile variable store is full (OUT_OF_RESOURCES on chunk 87 of 342); 66048 of 262144 bytes fit, so this firmware takes a file of at most about 64 KiB. The node boots without its install-config.yaml.
The variables are world-readable through efivarfs once Linux is up, so the
initramfs deletes them after copying (stormblock's side). The copy is made
only when /state has none, so booting old media again never overwrites a
node's applied config.
A medium written once must not stay on that version forever (#83; the X9
blades boot from USB sticks). src/selfupdate.rs, with the format and every
decision in src/manifest.rs (core-only, host-tested). The design is the
owner's (2026-10-02); stormcentral's half, the signing and the serving, is
stormcentral#279.
-
Where.
update = http://stormcentral.g8.lo/api/v1/boothelpers/<golden>instormboot.conf, written by the golden build. stormbootx GETs<url>/current(the manifest),<url>/current.sig(an Ed25519 signature over those exact bytes, raw or hex) and<url>/files/<path>. No token.update = offpins a medium; noupdate =(media built by hand) checks nothing at boot, thoughstormbootx-stickon the running node gives such a stick its family's (#161) and the release'sstormboot.confnames it from then on; a read-only medium (an ISO on virtual media) is skipped. -
The manifest is text:
stormbootx-manifest 1 version 0.13.0 commit 1f27df3 golden golden-stormbootx-rustnic-0123456789abcdef serial 42 canary 52:54:00:12:34:56 (optional, repeatable: only these MACs) downgrade true (optional: a deliberate rollback, #96) file <sha256> <size> EFI/BOOT/BOOTX64.EFI file <sha256> <size> stormboot/stormboot.conf file <sha256> <size> stormboot/drivers/stormnic-ixgbe.efi file <sha256> <size> startup.nshPaths are only
startup.nsh,EFI/BOOT/*andstormboot/*(neverstormboot/stateorstormboot/local.conf), andBOOTX64.EFImust be there. A media golden'smedia.filesis exactly itsfilelines. -
Authentic. The signature is checked against the Ed25519 public keys compiled in (
manifest::RELEASE_KEYS, current and next) before a byte of the manifest is believed, then each fetched file's size and SHA-256. Plain HTTP is enough: whoever answers can only serve what was signed. The private key never leaves the stormcentral VM. Since v0.14.0 (#86) that is stormcentral's key,4fe28c02…0a171bce, the one it serves open athttp://stormcentral.g8.lo/api/v1/stormbootx/keys; a host test holds the bytes to that hex. A new key goes in as the next one a release before stormcentral signs with it. A test build may add one key at build time (STORMBOOTX_UPDATE_TEST_KEY, hex); no golden build sets it, and a binary that has one says so on every check. -
Never backwards. The serial must be above every serial the medium has carried, at or above
StormBootMinSerial(the machine's highest passed trial), and above any serial that failed its trial on this medium. A rollback is a new, higher serial. And theversionmust not be older than the running binary's (semver; a-pre/+buildsuffix is ignored), or a stick written from a newer build than the promotion would update itself backwards on its first boot (#96). Such a serial is declined and becomes the medium's floor (min), so the next promotion is still judged:update : current (running v0.23.0; serial 1 offers v0.14.0, which is older); not taken. A deliberate rollback to an older version saysdowngrade truein the manifest. -
A/B. Only the files that differ are fetched. Each is written as
*.new; then the current file becomes*.prevand the new one takes its name, the bootloader last; a loadable driver the release no longer carries becomes*.prevtoo.\stormboot\staterecords the trial before the swap, and the machine warm-resets into the new set:update : v0.12.0 -> v0.13.0 (golden-…, serial 6), 3 file(s), 280 KB; restarting into it. -
The trial. The new binary counts its starts (
update : serial 6 on trial, start 1 of 2; it must reach an attach) and passes once it has attached an image, or the engine answered alocalintent (update : serial 6 passed its trial (attached); it stays). After two starts without that, the third puts every changed file back from*.prev, marks the serial failed on this medium, and chain-loads the restored binary. A boot that fails for a reason that is not the release (the engine down twice) also puts it back; the cost is a stick on the previous release until the next serial. -
Linux is told:
StormBootUpdate(volatile, #76's vendor GUID) isserial:<n>,trial:<n>:<start>,good:<n>orfailed:<n>. -
local.confis the medium's own. It is read beforestormboot.conf, so its keys win, and an update never writes it, except to keep aname =ortag =that the release'sstormboot.confwould drop. -
Room. The new files are written beside the old, so the medium needs their size free (the
*.prevcopies about to be replaced count); a medium without room says so and is left alone. The 4 MiB ESP ofbuild-boot-agent.shholds a set of binary and drivers three times over. -
A failed trial says why (#213): the start that puts a set back keeps the previous start's
last.log, the failed set's last, as\stormboot\trial.log(last log : serial 8's last start (212 line(s)) kept in \stormboot\trial.log), and the restored binary's claim carries it astrial_logbesideupdate: failed:<n>. A restored binary from before #213 sends none.
A trial that never reaches the network leaves nothing anywhere else: no
lease means no syslog, and only installs get a BMC console capture (#213,
C2NR0Q2's serial 8). So every start on a writable medium keeps its own
console on it, in \stormboot\last.log (src/lastlog.rs): all of
netlog's lines so far, cut to 64 KiB (the first quarter and the end).
It is rewritten at fixed points, from the first, whenever the log grew:
after the trial count, before each media driver starts (a driver that hangs
leaves its name), after the drivers bind, after the network is up, and at
every log flush (the name, the claim, the hold, the fall-through). Not once
an image is attached (#46) until it is withdrawn, and never on a read-only
medium (last log : not kept (the boot medium is read-only)) or a RAM
disk. The previous start's file is read first, and goes with the claim as
last_log; GET /api/v1/log/last and /api/v1/log/trial serve both
files. On a node, the stick's FAT holds both files to read.
tests/update-ovmf.sh boots a writable GPT medium on a USB stick
(qemu-xhci + usb-storage) under OVMF ten times against a stub
stormcentral and a test key: stormcentral's own first promotion
(tests/fixtures/, hex signature) verifies against the release key; a
manifest signed by another key is refused; serial 5 is written, restarts,
passes its trial on the attach; serial 5 again is current; serial 6 for
another machine's MAC only is refused, then, naming this machine as a canary
too, taken, retiring the driver it no longer carries to .prev; serial 7,
larger than the ESP, is refused for room; serial 8, whose stormboot.conf
names a dead NVMe port, fails both trial starts, and the third start puts
serial 6 back, which then refuses 8. The disk is checked with mtools after
each boot, last.log and trial.log too (#213): the failed trial's second
start is in trial.log and in the restored binary's claim as trial_log.
Publishing a disk and returning to the boot manager does not boot it. A disk
that appears while a boot option is running is not in BootOrder, so the
manager moves on and drops to setup. Real EDK2's PartitionDxe also skips a
handle that has BlockIO but no device path. That is why step 7 installs one.
OVMF is lenient here and hid it. The ESP match is strict because an earlier,
looser version booted a stale Windows install off a local SAS disk.
NIC drivers from the media (#26, src/drivers.rs). Before the network
is brought up, every *.efi in \stormboot\drivers\ on the boot volume is loaded
(LoadImage by device path) and started, then every handle is connected. One
ConnectController pass runs first, so the platform's own drivers claim every
NIC they will take and a media driver only gets the ones nothing else wanted.
The console prints drivers : N of M started from \stormboot\drivers and one
line per file; a driver that fails is reported and skipped. This is for
firmware that has no UEFI driver for its NICs, like the
Supermicro X9 blades (legacy-only Intel 10G and ConnectX-3). No directory, no
change.
ARP for the next hops at the lease (#168). smoltcp sends one ARP request a second per interface, and a socket whose neighbour is unknown is silenced for a second. So the first contact with a new host within a second of another cost a whole second: the reverse-DNS lookup after the engine, the NTP exchange after it (1 s each on the Dell, the X9 blades and pve). Once a NIC holds a lease, stormbootx sends its own ARP requests for the router and the lease's DNS (option 6) and NTP (option 42) servers, and for each connection's or datagram's next hop before its first packet. They are 42-byte frames through the SNP, padded to 60, once per address per lease. smoltcp caches any ARP reply aimed at its address, so the answers fill its cache and nothing waits for them.
Where the time goes (#168). Every boot prints one line before its result with the time each step took, from the banner (net-ovmf's shipped boot, OVMF under KVM):
timing : drivers 0 ms, nics 5.15 s, lease+engine 57 ms, name 39 ms, intent 25 ms, claim 1.77 s, clock 20 ms, attach 86 ms; 7.24 s in all
The same boot with the ARP requests taken out read name 1.03 s.
media's NIC drivers and connecting every handle (a driver's Start runs in
it). nics is opening the SNPs. lease+engine waits for DHCP and reads the
engine's version, and claim covers the claim and its inventory. A boot
that falls through ends at fall-through, which includes the network
console's wait when it has one (under 300 ms on a clean local, #189), and
then clock.
stormbootx's own TCP/IP (#56, src/net.rs). The firmware's EFI_TCP4 is
not used on any machine. It is optional, off in setup on some machines, and
missing on server3 (X9) even with a NIC driver loaded. And EDK2's own stack
can't be carried instead, because since edk2-stable202405 its IPv4 drivers
refuse to start without EFI_RNG, and TcpDxe without EFI_HASH2, which old
firmware lacks. smoltcp 0.14 runs on
each NIC's SNP instead:
- Each SNP is opened
EXCLUSIVE, so a firmware MNP bound to it lets go and can't take frames meant for smoltcp. A NIC the firmware won't release is used shared, and the console says so. On the fall-through every NIC is given back (closed and reconnected), so a later boot option finds the firmware's own stack again. - DHCP is smoltcp's dhcpv4 socket, on every NIC at once, re-sent every 2 s (a 25G link takes seconds to train). It asks for options 1/3/6/12/15, and keeps the reply for the name (#23). It asks for option 42 too, for the NTP server (#77). ARP and TCP are smoltcp's.
- The MTU (#165): DHCP is asked for option 26, and once a NIC holds a
lease it runs at what option 26 offers (Ethernet's 1500 with none), never
above its SNP's
MaxPacketSize. TCP's MSS follows it, so a 9000 network on a driver that carries 9000 sends 8960-byte segments.MaxPacketSizeis the NIC driver's:EFI_SIMPLE_NETWORKhas no call to raise it, so a driver that carries 1500 (every firmware SNP seen so far, the Dell's Mellanox driver among them, and the stormnic drivers until their jumbo issues land) keeps the boot at 1500 on any network. The lease line says which:nic 0: leased 192.168.30.1/20 gw 192.168.16.1 mtu 1500 (DHCP offers 9000; the SNP of Mellanox ConnectX Driver + … carries at most 1500, …). The NVMe/TCP transfer size does not change with it: that is the controller's MDTS (#6). - UDP carries one SNTP exchange and, for a server named by name, one A query per boot (#77), on the NIC that reached the engine.
- A connection goes out on the NIC that reached a target last, else on the best-ranked NIC with a lease: link up first, then the larger MTU (the storage port is the jumbo one). Each try gets 5 s while another leased NIC is left. A reset is an answer (nothing listens there), not a wrong wire. The NIC table prints once, at bring-up.
- Waiting is never silent (#88). While no NIC holds a lease, a
connection prints
waiting for a lease (N s): nic 0 link UP, DHCP 3 out 0 in, 14 frames in; …once a second: frames in with no DHCP in is a wire that works and a server that doesn't answer; no frames in at all, a dark wire. Every SNP call is marked, and a 1 s timer event atTPL_NOTIFYprintsnic N: SNP.Receive (<driver>) has not returned after 5 s(again every 30 s) while one is stuck, since a NIC driver call that never returns is outside every deadline and can't be abandoned from one thread. A driver that hangs atTPL_NOTIFYor with interrupts off stops the timer too; thedriverline beforeStartis then the last word (src/snpwatch.rs). - Timeouts are per operation without progress: 30 s for the engine API and
the attach (
TcpSocket::connect), 5 s for the PTR query, 8 s for the console'sconnect. Nagle and delayed ACKs are off, with a 256 KiB receive window (window scaling), so a 128 KiB read is one round trip. - Time is the TSC, calibrated against
Stallat bring-up. - Randomness (
src/entropy.rs) seeds smoltcp's TCP ISN and DHCP xid, and picks each connection's local port. It uses the first source present: the firmware'sEFI_RNG_PROTOCOL(read, never installed, so the next stage never finds a weak RNG left behind), then RDSEED/RDRAND (RNDR on aarch64), then cycle-counter jitter across short stalls, hashed with SHA-256 together with the firmware time, the MAC and the SMBIOS UUID. It never refuses to boot for lack of entropy.rng = cpuorrng = jitterinstormboot.confskips the sources above it.
Under OVMF with no firmware network stack, tests/net-ovmf.sh leases from
QEMU's slirp, reads a stub engine, and attaches a stub NVMe/TCP target at
4096-byte blocks. The firmware's FAT reads a 96 MiB BOOTX64.EFI through the
published BlockIO (about 105–120 MiB/s, KVM) and starts it. Its boots
(NET_ONLY=<name> runs one):
| boot | must show |
|---|---|
shipped |
8 in flight and the NVMe stub holding at least 2 reads at once, answered out of order (#100); rng : firmware/rdrand; every claim carries agent and the firmware inventory (#4, #90), with an NVMe, a SATA and a SCSI drive's models, serials, WWNs, media and the NVMe's GPT and \EFI directories (#167); a stub SNTP server's 2031 time set in the RTC and read back by the payload (#77); a 2 KiB install-config.yaml handed down and reassembled (#79); the ISO's update = skipped as read-only (#83) |
shipped (#168) |
the timing line, with the reverse-DNS name and the clock each under a second: the DNS server is ARPed at the lease, not a second after the engine |
depth1 |
nvme_depth = 1: one read in flight (the stub never holds two), for its MiB/s beside shipped's 8 (#100) |
jitter |
firmware RNG and RDRAND/RDSEED masked: rng : jitter; told install, claims the default and hands down StormBootTag = stubhost (#76); an unsynchronised SNTP answer sets nothing (StormBootClock = unsynced) |
newhost |
no name from DHCP or PTR: claims boothost/default by MAC and boots as mac-525400123456 (#15) |
noesp |
an attach that boots nothing: blockio : withdrawn, one local disk counted, no CPU exception in BDS after (#54) |
nolease |
a NIC on a dead hub: one waiting for a lease line a second (#88) |
intent |
told local: no claim POST, nothing attached (#11); no disk that boots, so the console's 5 s stays (#189) |
localdisk, localdisk-key, localdisk-fail |
told local with a disk that boots: no wait, fall-through under 300 ms, its bootloader started; a c sent during the boot opens the console; a failed claim on that disk still waits 5 s (#189) |
ebs, watchdog |
the handoff's guard (#219), tcp4probe told by an SMBIOS OEM string: ebs calls ExitBootServices as Linux's stub does: stormbootx's line before it, tcp4probe's own line on COM1 after it, and the screen's bottom rows green; watchdog hangs before it, and handoff_watchdog = 15 resets the VM |
bridge |
esp = stormbootx: the payload starts from stormbootx's own read of the ESP and finds the read-only filesystem as its volume (#42) |
ic64 … ic256 |
install-configs of 64, 128, 192 and 256 KiB, each reassembled by the payload (#93) |
release |
no fallback namespace and the claim 404s: falls through, gives the NIC back, and the firmware's PXE on it boots the payload (#36, #68) |
virtio, virtio-modern, virtio-oldkey, virtio-verbose |
with VIRTIO_EFI: stormnic-virtio takes a transitional and a modern-only virtio-net NIC from OVMF and attaches over it; an old prefer_media_drivers is named and ignored; nic_verbose prints the driver's trace (#108, #80, #202) |
e1000e, e1000e-take |
with E1000E_EFI: QEMU's e1000e (82574L) with no option ROM, which OVMF has no driver for: stormnic-e1000e binds it and carries the whole boot (lease, claim, 96 MiB attach); with QEMU's iPXE ROM, the take-over takes it from iPXE (#70, #202) |
igb |
with IGB_EFI: QEMU's igb (82576, 8086:10c9) with no option ROM, which OVMF has no driver for: stormnic-igb binds it with no key (there is no igb family) and carries the whole boot (#69) |
mlx5-giveback, mlx5-none |
with MLX5_EFI: the real stormnic-mlx5 under the virtio family's file name declines the NIC, and the firmware gets it back and carries the boot; both drivers on one medium: virtio taken, no NIC here for stormnic-mlx5.efi (#131). QEMU has no ConnectX |
The initiator is its own crate, crates/nvme-tcp-initiator (#10): no_std
with alloc, no dependencies, over any byte stream. It was ported from
sbregistry's host initiator, and stormboot4bios (the legacy-BIOS loader) links
the same code rather than a third copy. A loader gives it a Transport
(send_all, recv_exact, and the path's MTU if it knows it) and a
Platform (open a connection to the portal, wait some milliseconds).
src/nvme.rs is stormbootx's: its smoltcp TcpSocket and boot::stall.
cargo test -p nvme-tcp-initiator runs the crate on the host against an
in-memory controller that refuses a command without PSDT=01b and an admin
command before CC.EN. It answers in several C2HData PDUs and asks for
write data by R2T.
- It speaks ICReq/ICResp, then Fabrics Connect on the admin and I/O queues,
then Property Get/Set,
CC.EN, and Identify for the controller and the namespace. Reads use C2HData. Writes use R2T/H2CData. - PSDT = 01b (SGL) on every command. A zero FLAGS byte means PRPs, which do not exist over a fabric.
CC.ENbefore any admin command. Identify on a disabled controller is a Command Sequence Error.- Synchronous, with reads pipelined (#100). One BlockIO read keeps up
to 8 commands in flight (fewer if the I/O queue is smaller), matches each
C2HData and response to its command by CCCID in whatever order the target
answers, and returns when all are done; a failed command is reported once
the others have finished, so nothing of it is left on the queue.
stormuefi reads in 1 MiB calls, so one call is eight 128 KiB commands
against stormblock. One at a time held the Dell to 46–80 MiB/s on 25 GbE:
a round trip and a target read per command. Writes stay one at a time.
nvme_depth = 1instormboot.confis the old behaviour; the attach printstransfer : 128 KiB per command, 8 in flight (…). - No header or data digests. A target that insists on them is refused.
- Transfer size comes from the controller's MDTS:
2^(12+MPSMIN) × 2^MDTS, capped at 512 KiB, or 64 KiB if MDTS is 0. It is never derived from the MTU. Against stormblock (MDTS 5) that is 128 KiB per command.
A machine whose BMC answers nothing (server8: the web login failed TLS, SOL
died seconds into a boot, the power button never took) is still reachable
while stormbootx runs. So stormbootx answers HTTP on port 9180 of every NIC
that holds a lease, from the moment its stack is up until the image's
bootloader starts (api = <port>|off in stormboot.conf; the claim's
agent says "api_port"):
| request | answer |
|---|---|
GET /api/v1/status |
phase (booting, attaching, holding, falling through, handed off), detail (the failure when holding), the machine's names and addresses, command_id (the floor), the pending and last command with its result |
GET /api/v1/inventory |
the firmware inventory ("v":2, as in the claim): drives first, NICs, controllers, SMBIOS, CPU, memory |
GET /api/v1/log |
every console line so far, the banner first (kept even with syslog = off) |
GET /api/v1/log/last |
the previous start's \stormboot\last.log (#213); 404 when the medium has none |
GET /api/v1/log/trial |
the failed trial's last start, \stormboot\trial.log (#213); 404 when none |
POST /api/v1/command |
a signed command: 202 {"accepted":<id>,…}, else 400/403/409 with {"error":…} |
Reads are open (the log already goes to the stormcast group, and the
inventory with every claim). A command counts only by its signature
(the owner's choice on #167): Ed25519, by stormcentral's key compiled in
(manifest::RELEASE_KEYS, the self-update's) or by a key on the media
(command_key = <64 hex>, which the laptop app writes into local.conf of a
stick it makes). The body (src/command.rs):
{"host":"server8","id":1792000000123,"command":"format","args":"S3Z9NB0K123456",
"issued":"2026-10-22T03:00:00Z","by":"stormcentral","sig":"<128 hex>"}and the signature is over the bytes
stormbootx-command v1\n<host>\n<id>\n<command>\n<args>\n<issued>\n
(by is not signed). host must be one of the machine's names (the
claimed host, the host the intent was answered for, the DNS name, a stated
tag, the SMBIOS serial, mac-<hex>; a MAC alias of server7 is server7,
the name the engine's record stores and a signer signs)
and id above the last one this machine took, kept in the non-volatile
StormBootCommandId (vendor GUID as below) and raised before the command
runs: each command is done once, on one machine. Use Unix milliseconds for
id, so stormcentral and the laptop app never collide.
| command | does |
|---|---|
reboot |
ResetSystem(EfiResetCold) |
poweroff |
ResetSystem(EfiResetShutdown) (ACPI S5); power-on is Wake-on-LAN or the BMC |
boot-next NNNN |
BootNext = Boot<NNNN> (it must exist; its description is said), then a reset |
install |
claim and boot, whatever the intent says (this boot) |
local |
boot the local disk, nothing claimed |
retry |
run the boot again from the engine (a hold) |
hold |
stay in stormbootx until another command |
inventory |
collect it again |
format <serial> [fast] |
zero the first and last MiB of the one drive with that serial (its MBR, GPT and backup GPT), never the boot medium, a read-only drive or a published one; full/secure are the maintenance image's (baremetalservices) |
maint |
claim the maintenance image and boot it: the usual claim with "image":"maint" (stormblock#497), down the same names, answering this host's own maint-<host> clone of boothost/maint's volume. Never the media's fallback; a reply without "image":"maint" (an engine before #497, stormboot-serve) is not attached; no boothost/maint (a 404) or either of those is a failed boot, so it holds. Before the chain-load it boots again for the maintenance image |
A command is taken at fixed points: before the intent is read (the API's,
else the engine's host record GET …/boothost/<name>/command,
stormblock#494, the same signed object), before the chain-load, and every
100 ms of a hold (the record every 5 s). One from the record is reported with
POST …/command/done {"id","result"}. Each is said on the console:
command : format APIDISK1 (id 100, from the API, 10.0.2.2, by api-test), signature good, then command : format (id 100): wiped ….
Holding. A failed network boot (no claim, a failed attach, a bootloader
that did not start) on a machine with a lease stays in stormbootx with the
API up, the panel and the log saying so, for hold = seconds (forever, or
off), then falls through as before. A machine with nothing local that
boots holds until a command (server1 sat at a UEFI shell on 2026-10-14).
hold_retry = <minutes> runs the boot again that often while holding; L on
the keyboard falls through at once. install-config.yaml's
stormbootx_hold: and stormbootx_hold_retry: are read when the media says
neither. The binary's default is off; the goldens say hold = 120. A
local intent is not a failure and never holds. tests/api-boots.sh
(sourced by tests/net-ovmf.sh) drives all of it from the host through
slirp.
Every error in run() that the hold (above) does not keep, a hold that
ends, and a local intent end in fall_through:
- If an image was attached, it takes the disk back first
(
blockio::withdraw, #54): disconnect, uninstall BlockIO and the device path, drop the NVMe/TCP connection, and printblockio : withdrawn. The disk's functions live in this image, which the firmware unloads when it returns, so a disk left installed is one the boot manager calls into freed memory (pvetest1's #UD at RIP 0x47FFFFFCA). If the firmware refuses the uninstall, stormbootx says so and resets the machine rather than return. A start that finds a disk an earlier start left behind printsblockio : N handle(s) an earlier start published are still installed. - It prints
no network boot: <reason>. - It offers a console: press c within 5 s. Silence continues, so an
unattended machine never stops at a prompt. Not on the ordinary reboot
(#189): when nothing failed (a
localintent, alocalcommand) and a local disk can boot, it does not wait (console : no wait, …), and only acpressed during the boot opens the console. Alocalwith nothing local to boot keeps the wait. - It sets the clock from NTP if the boot has not yet (step 5a, #77), so the local disk's OS gets the right time too.
- It gives every NIC back to the firmware (
net::release, #56), so a later boot option (PXE, HTTP boot) finds the firmware's own stack bound again. It says so:net : N of M NIC(s) given back to the firmware (exclusive SNP closed, reconnected), and names any NIC whose SNP would not close (#68).tests/net-ovmf.sh's fifth boot falls through with the firmware's IPv4 stack on, and the firmware's PXE on the same NIC then boots the payload over TFTP. - It counts local disks (whole, non-removable BlockIO devices; the
attached image is no longer one of them).
- If there are any, it prints
falling through to the local disk (N found). - If there are none, it says so and waits 30 s.
- If there are any, it prints
- With a local disk, it starts that disk's
\EFI\BOOT\BOOTX64.EFIitself (blockio::boot_local, #145): the first whole, non-removable disk other than the boot media whose GPT ESP carries one (the diskautoasks about, printed aslocal : disk N (…)), through the firmware's FAT, else read byesp.rs(esp =applies, as for the attached image). Returning is not enough: where the EFI Shell started stormbootx fromstartup.nsh(the X9 blades, which have no boot option for it), the shell gets control back and starts stormbootx again, and the disk never boots.local_boot = firmwareskips this step. - If nothing started, or what started came back, it returns
EFI_ABORTED, so the boot manager (or the shell) tries the next thing.
The panel is cleared when stormbootx hands over, so its last line on the
screen names the build (#180): stormbootx <version> (<commit>): handing over to the local disk's bootloader, or …: returning to what started it (nothing booted).
The console (src/shell.rs):
| Command | Does |
|---|---|
nics |
every NIC with an EFI_SIMPLE_NETWORK: MAC, MTU, link, SNP state |
state |
the address each NIC leased on stormbootx's own stack |
dhcp [secs] |
bring the stack up if it is not, and wait up to secs (default 10) for every NIC's lease |
connect IP PORT |
open a TCP connection the way the attach does (8 s) |
pci [all] |
devices on the bus, with or without a driver |
reset |
warm reset, so firmware re-reads NV config |
boot |
leave the console and continue the fall-through |
help |
the list above |
\stormboot\stormboot.conf on the volume the binary was loaded from, found
through EFI_LOADED_IMAGE_PROTOCOL. It uses key = value lines, and #
starts a comment. Each key stands alone, so a typo in one does not reset the
others.
| Key | Default (compiled) | Meaning |
|---|---|---|
portal |
192.168.31.202 |
NVMe/TCP portal, and the host the claim goes to |
port |
4420 |
NVMe/TCP port |
nqn |
nqn.2026-09.lo.g16:stormcos |
subsystem NQN if the claim fails or is off |
nsid |
2 |
namespace if the claim fails or is off |
fallback |
on | none (or no/off/false/0): no nqn/nsid is attached when no claim answers; the boot falls through to the local disk (#36). Every golden writes it; build-boot-agent.sh --no-fallback |
api_port |
9090 |
engine API port on the portal host |
boot_port |
9089 |
the engine's plain boot-only port, asked first; refused, silent or not an engine, the boot uses api_port (#183, stormblock#524). off asks api_port alone |
claim |
yes |
no / false / 0 / off skips the claim |
name (or tag) |
none (DNS name, then MAC, then SMBIOS) | states the identity; the only name claimed |
dns |
none | DNS server for the PTR of the machine's own address, when its DHCP reply names none or cannot be read (#26); build-boot-agent.sh --dns |
ntp |
none (DHCP option 42, then pool.ntp.org) |
NTP server for the hardware clock when the lease names none: host[:port], or off to leave the clock alone (#77); build-boot-agent.sh --ntp |
syslog |
none (install-config.yaml's syslog:, then the group alone) |
a unicast collector the boot's log is sent to beside the stormcast group (#162): host[:port] (port 514 when bare), or off to send nothing at all. The goldens write stormcentral.g8.lo:5514; build-boot-agent.sh --syslog |
local_when_bootable |
false |
true: auto and every doubt boot a local disk whose ESP carries BOOTX64.EFI instead of claiming (#3). Off until intents work on forge |
nvme_depth |
8 |
reads kept in flight on the attached image, 1–8 (#100); 1 is one command at a time, as before #100, for an A/B or a target that mishandles a queue. Read at start-up, with esp |
ui |
panel |
log: no boot panel (#144); the screen scrolls the log as before |
install_source |
forge |
a stormdist release medium attached (#198, Installing from a stormdist medium): forge installs from it only when the network boot gave no image and nothing local boots; local installs from it before the network; off never looks |
local_boot |
stormbootx |
firmware: the fall-through returns to whatever started stormbootx instead of starting the local disk's BOOTX64.EFI itself (#145) |
esp |
auto |
who reads the attached ESP (#37): auto (firmware, then stormbootx), firmware, or stormbootx (the bridge and its read-only filesystem, #42); build-boot-agent.sh --esp |
rng |
firmware |
the first entropy source tried (#56): firmware, cpu (skip EFI_RNG) or jitter (skip both); build-boot-agent.sh --rng |
nic_verbose |
false |
true: the stormnic drivers print their whole bring-up trace instead of one line per NIC (#80). stormbootx sets the volatile StormnicVerbose variable (GUID ce1479a2-eab9-4176-b0ad-c909ea5b8e0b, BOOTSERVICE_ACCESS, one byte 01) before it loads \stormboot\drivers; gone at the next reset, never in NVRAM. For debugging a machine without a rebuild; no golden sets it. build-boot-agent.sh --nic-verbose. From the EFI shell instead: setvar StormnicVerbose -guid ce1479a2-eab9-4176-b0ad-c909ea5b8e0b -bs =01 |
prefer_media_drivers |
none | ignored since #202 (named on the console): every stormnic driver on the media takes the NICs it matches. Before, the families that took their NICs from the firmware (#108, #131, #70) |
media |
none | the media's label, printed under the banner (media : fw); written by the golden build, build-boot-agent.sh --media |
update |
none (no self-update) | http://host[:port]/path of this medium's release on stormcentral, or off to pin the medium (#83, Self-update); written by the golden build, build-boot-agent.sh --update |
api |
9180 |
the port stormbootx's own API answers on (#167), or off |
command_key |
none | an Ed25519 public key (64 hex digits) whose signed commands are taken, beside stormcentral's (#167); the laptop app writes its own into local.conf; build-boot-agent.sh --command-key |
hold |
off |
how long a failed network boot stays up with its API (#167): seconds (120, 2m), forever or off; nothing local that boots holds until a command. The goldens write 120; build-boot-agent.sh --hold. Else install-config.yaml's stormbootx_hold: |
handoff_watchdog |
300 |
seconds the UEFI watchdog gives the attached image's bootloader to reach ExitBootServices (#219); off arms none. See The handoff's guard |
hold_retry |
0 |
minutes between boots again while holding; 0 never. Else stormbootx_hold_retry: |
stamp |
none | parsed, not used |
version, commit |
none | not read by stormbootx: the stormbootx on this medium, written by every build-boot-agent.sh run (#94) so a tool reading the FAT (storminstall, which refuses media older than v0.15.0's install-config slot) needs no EFI binary run. The version is Cargo.toml's and must be in the .efi laid down, or the build fails; the commit is the binary's STORMBOOTX_BUILD stamp, or unstamped. A self-update replaces the file with the release's, so they stay true |
\stormboot\local.conf, when present, takes the same keys and is read first,
so its values win. It belongs to the medium: a self-update never replaces it.
stormdist's full release ISO (stormdist#10) boots on its own: El Torito
UEFI/BIOS entries and a hybrid GPT, both over /boot/esp.img, the release's
own ESP (stormuefi, its kernel, the stormblock initramfs). Written to a USB
stick it is the same bytes. stormbootx finds such a medium and starts that
ESP's bootloader, with no forge and no network; the release's initramfs then
lays the disk from the medium (stormblock#442). stormbootx lays nothing
(#166: one layout writer). What it adds over the firmware booting the medium:
it checks it, it reports the medium's read rate, and it reaches machines whose
firmware would not boot the medium (a Dell 14G refuses a hybrid ISO from a
stick, #185; an X9 falls to the EFI Shell, which starts stormbootx, #60).
Before the network, every present whole disk is looked at for an ISO 9660
volume with RELEASE.toml and BOOT.toml at its root
(src/distmedia.rs). On one:
-
BOOT.sigandRELEASE.sig(64-byte Ed25519) must verify against stormcentral's release key (distmedia::DIST_KEYS, served at/api/v1/release/keys; not the self-update's boothelper key) before either file is read, andRELEASE.toml's SHA-256 must beBOOT.toml'srelease_sha256:dist : stormdist 12.74.6 (full) on PciRoot(0x0)/…/USB(0x1,0x0) (usb, 512-byte blocks): BOOT.sig and RELEASE.sig verify, RELEASE.toml matches BOOT.toml -
When it is used,
boot/esp.imgis read whole and timed, which is the medium's read rate (above 40 MiB/s a stick is on a USB 3.x port), with what the release's chunks (RELEASE.toml'sbytes) would take at that rate:dist : read boot/esp.img: 66 MiB in 1.40 s, 47 MiB/s (usb: a USB 3.x rate); the release's 3.2 GiB at this rate: ~70 s -
Its SHA-256 must be
esp_sha256.BOOTX64.EFIis read out of those checked bytes and loaded from that buffer under the device path of the firmware's own filesystem on the medium's ESP, so that filesystem is itsDeviceHandle, as on a direct firmware boot. Thetiming :line givesdist find,dist read,dist checkanddist start.
When (install_source =): by default forge first. The medium is used only
when the network boot gave no image (no NIC, no lease, no engine, no claim)
and no local disk boots, so an installed node with a release stick left
in boots its own disk. A local intent never installs. install_source = local installs from the medium before the network; off never looks. A
medium that fails a check is named and refused, and the boot goes on as if it
were not there. A diff or backup ISO (no BOOT.toml) is named and not used.
tests/dist-ovmf.sh makes a real medium with stormdist make (stormdist
v0.5.0, scripts/build-stormdist.sh) signed by a test key compiled into its
own stormbootx build (STORMBOOTX_DIST_TEST_KEY; build-golden.sh refuses
it), and boots it as a CD and as a USB stick: before the network, after a
failed network boot, an installed disk that wins, a tampered ESP, another
key, and off.
A machine can boot and install with no forge, from the owner's Mac or
Windows laptop. The laptop app (storminstall#20) runs stormblock's portable
target (stormblock-nvmeof, stormboot-serve, stormblock#474): one release
image over NVMe/TCP, with a copy-on-write overlay for each claim, and the
boothost API stormbootx already speaks (health, intent, claim, installed).
This is for machines whose BMC virtual media can't carry the full ISO, such
as the X9 blades. They boot this small medium instead. stormbootx needs
nothing different for it. The medium tells it where to claim, and the
laptop answers as forge does.
What the app makes. It starts from a media golden, unchanged:
stormbootx-rustnic for the X9s (their NICs need its drivers) or
stormbootx, as the ISO (boot/<golden>.iso, for virtual media) or the disk
image (<golden>-disk, for a USB stick). The app adds two files to the ESP
(the FAT that the ISO's 0xEF MBR entry names, or the image's one partition).
It touches nothing else:
-
\stormboot\local.conf, read before the golden'sstormboot.conf, so its keys win. A self-update never replaces it.portal = 192.168.1.50 # the laptop's IPv4 address on the node's network api_port = 9090 # stormboot-serve --http-port boot_port = off # stormboot-serve has one port; saves 15 s where a firewall drops 9089 name = server8 # the --host it serves; leave it out to claim by MAC (--host server8=MAC) syslog = 192.168.1.50:5514 # the app's log collector (#162): the boot shows live update = off # no stormcentral here; the medium keeps the app's versionportalis an address, not a name. It is also where the claim goes. The NVMe/TCP address and port come back in the claim reply (stormboot-serve --advertise, by default the address the node's request reached). Leaventpout (DHCP option 42, thenpool.ntp.org), or setntp = offon a network with no way out. -
\stormboot\install-config.yaml, the node's install-config (#79), handed down to the initramfs.
What the node does. It gets its lease and reads the laptop's health,
which is past universal boot's gate (21.2.1 from v21.4.0). Then it reads its intent:
install claims, and local falls through to its own disk. The claim mints
a fresh overlay under the node's own subsystem, which admits only
nqn.2026-09.lo.storm:host-<name>. The node attaches it, reads with 8
commands in flight, and starts the image's BOOTX64.EFI. StormBootTag
names the host the laptop answered, so the initramfs claims the same one. A
name the laptop does not serve gets a 404, and with the golden's
fallback = none the node falls through to its local disk.
tests/laptop-ovmf.sh (in the sc-build command) does exactly this under
OVMF against the real stormboot-serve. scripts/build-stormboot-serve.sh
builds it from stormblock at the pinned commit. The test adds both files to
the stormbootx golden's ISO with mtools, then boots six times: by name;
by MAC with no name on the medium; --intent local; a name the laptop
does not serve; the laptop's command record (stormblock#494, pinned at v22.3.0):
a signed hold for server7, PUT before a by-MAC boot with command_key =
in local.conf, taken before the claim and reported done, then a
poweroff PUT while it holds, read from the record and done; and a signed
maint from the record, which the laptop (one image, no maintenance image)
answers with server7's own image: not attached, it holds. Under KVM the reads ran at 69–127 MiB/s, 512 KiB per command
(the target's MDTS 8). The served image is unchanged after the boots. Not
run here: the app itself, macOS or Windows as the laptop, and metal.
A medium updates itself at boot (Self-update), but a stick whose updater is
broken never can: no stick at v0.26 or older ever updated (#148), and those
sticks still boot the node into stormcos. stormbootx-stick (#151,
stormcos#589) is a static musl Linux binary the running node runs (a boot.d
task, after the network and daily) to do what the stick's next boot would
have done:
- find the stick: a removable or USB disk whose FAT ESP carries
EFI/BOOT/BOOTX64.EFIandstormboot/(mounted read-only to look). Never a disk that is mounted (the root's, a pallet's), that something holds (dm, md), or that carries a stormblock slab (STRMSLABat its start or at a partition's). Two sticks: neither is chosen; - read
stormboot.conf(update =,version =),local.confandstate;update = offis left alone. A stick that names noupdate =(hand-built, or an exe swapped in by hand: server1's, #161) takes--update, else its family's boothelper under--boothelpers(defaulthttp://stormcentral.g8.lo/api/v1/boothelpers):stormbootx-rustnicformedia = rustnic …or, with nomedia =, a stick carrying NIC drivers;stormbootxotherwise. A stick with noversion =reads as 0.0.0, older than any release; - ask
<update>/currentandcurrent.sig, and verify the Ed25519 signature against the compiled-in release keys before the manifest is parsed; - decide with the medium's own rules: never backwards, never a serial
that failed its trial on this stick (or below
StormBootMinSerial, read from efivarfs), never during a trial, canaries only for their MACs; - apply exactly as the self-update does: files checked against the
signed size and SHA-256, written as
*.newand read back, the trial instate, then the*.prevswap with the bootloader last, put back if it fails;name =/tag =carried intolocal.conf. The stick's next boot runs the new set on trial and puts the old one back if it never attaches; - report one line per step, exit 0 unless something failed, and
/run/stormbootx-stick/status.json(outcome:updated,current,declined,off,no-stick,would-update,refused,error).
stormbootx-stick [--dry-run] [--status FILE] [--update URL] [--mac MAC]
[--boothelpers URL] [--esp DIR] [--sys DIR] [--proc DIR] [--dev DIR] [--efivars DIR] [--mnt DIR]
--update is used only when the stick names no update =. --esp updates
an ESP already mounted at DIR. The roots are for tests. The manifest, the
rules and the keys are src/manifest.rs and src/sha256.rs, included by
path: one implementation, the medium's. Built by deploy/build-golden.sh stormbootx-stick (golden stormbootx-stick: bin/stormbootx-stick, and
bin/stormbootx-stick-arm64 for an arm64 node's stick, #172: it looks for
BOOTAA64.EFI and, with no update =, takes the stormbootx-arm64
boothelper), and
tested by tests/stick.sh (a test key, signed releases from a local HTTP
stub, a directory as the ESP, and a machine of image files for the refusals).
A stick too small to take the new set beside the old is refused for room;
writing the signed raw disk image instead needs stormcentral to serve one
(#154).
A second binary, src/tcp4probe.rs. It reports which of the firmware's
network-stack protocols are present layer by layer, runs a ConnectController
pass when TCP4 is missing, and then creates and configures a TCP4 child. Since
#56 stormbootx needs only the SNP line of that report. The rest describes the
firmware, not a requirement. src/tcp4.rs and src/dhcp4.rs (the firmware
TCP4 client stormbootx used until 0.9) are compiled only into it now.
When a loader before it set them, it prints the StormBootTag,
StormBootHostNqn, StormBootClock and StormBootUpdate variables and
their attributes (handed down : …, #76, #77, #83), reassembles
StormBootInstallConfig and checks its length and SHA-256 (install cfg : …, #79), then what the RTC reads (rtc : …);
tests/net-ovmf.sh starts it as the attached image's BOOTX64.EFI and
checks them.
A stick that boots it is made with --probe (see Getting it onto a stick).
A third binary, src/espprobe.rs (#37). It connects every controller, then
for each whole disk except the one it booted from whose GPT has an ESP, it
reports whether the firmware's FAT loads \EFI\BOOT\BOOTX64.EFI, reads the
file with esp.rs, and starts it from the buffer with espboot.rs, the same
code stormbootx falls back to, under the bridge's own read-only filesystem
(espfs.rs, #42), which must be the started image's DeviceHandle. Then it
takes the filesystem back and powers off. tests/esp-ovmf.sh boots it under
OVMF against a 4096-byte virtio disk carrying a 4096-byte-sector FAT16 ESP.
The payload is tcp4probe, whose boot fs : lines do what shim or
systemd-boot would: list \EFI\BOOT, read and seek BOOTX64.EFI (its
sha256 must match), read a loader entry by a relative path, and be refused
a write. tcp4probe prints those lines on every boot, so on metal they show
what a bootloader there would find on its own volume.
Never on a workstation, and never as root. Push, then run sc-build from the
checkout. It fetches the pushed commit onto a fresh build VM (cloned for the job,
as the unprivileged stormbuild user), builds it there and destroys the VM
when the job ends. Nothing persists between jobs. The default cargo build && cargo test does not suit a no_std UEFI
crate, so name the command:
sc-build 'cargo build --release --target x86_64-unknown-uefi && mkdir -p t && \
rustc --edition 2021 --test src/intent.rs -o t/intent-test && ./t/intent-test && \
rustc --edition 2021 --test src/sha256.rs -o t/sha256-test && ./t/sha256-test && \
rustc --edition 2021 --test src/universal.rs -o t/universal-test && ./t/universal-test && \
rustc --edition 2021 --test src/dnsname.rs -o t/dnsname-test && ./t/dnsname-test && \
rustc --edition 2021 --test src/esp.rs -o t/esp-test && ./t/esp-test && \
rustc --edition 2021 --test src/sntp.rs -o t/sntp-test && ./t/sntp-test && \
rustc --edition 2021 --test src/manifest.rs -o t/manifest-test && ./t/manifest-test && \
rustc --edition 2021 --test src/installconf.rs -o t/installconf-test && ./t/installconf-test && \
rustc --edition 2021 --test src/inventory.rs -o t/inventory-test && ./t/inventory-test && \
rustc --edition 2021 --test src/pcibus.rs -o t/pcibus-test && ./t/pcibus-test && \
rustc --edition 2021 --test src/logwire.rs -o t/logwire-test && ./t/logwire-test && \
rustc --edition 2021 --test src/command.rs -o t/command-test && ./t/command-test && \
cargo test -p nvme-tcp-initiator && \
R=${CARGO_TARGET_DIR:-target}/x86_64-unknown-uefi/release && \
tests/esp-ovmf.sh $R/espprobe.efi $R/tcp4probe.efi && \
STORMNIC_DRIVERS="virtio mlx5 e1000e igb" scripts/build-nic-drivers.sh $PWD/t/nd && \
VIRTIO_EFI=t/nd/stormnic-virtio.efi MLX5_EFI=t/nd/stormnic-mlx5.efi E1000E_EFI=t/nd/stormnic-e1000e.efi IGB_EFI=t/nd/stormnic-igb.efi tests/net-ovmf.sh $R/stormbootx.efi $R/tcp4probe.efi && \
scripts/build-boot-agent.sh --iso --binary $R/stormbootx.efi --media shelltest --output $PWD/t/s.iso && \
LAYOUT=cd-last tests/shell-ovmf.sh t/s.iso old "media : shelltest" && \
LAYOUT=cd-last tests/shell-ovmf.sh t/s.iso ovmf "media : shelltest" && \
LOCAL_EFI=$R/tcp4probe.efi LIMIT=120 tests/shell-ovmf.sh t/s.iso old "from the local disk" "handing over to the local disk" "EFI\BOOT lists" && \
SECOND_CD=1 SCREEN_PNG=1 LIMIT=180 tests/shell-ovmf.sh t/s.iso old "returning to what started it" && \
SECOND_CD=1 LIMIT=180 tests/shell-ovmf.sh t/s.iso ovmf "returning to what started it" && \
tests/screen-ovmf.sh t/s.iso && \
tests/stick.sh && deploy/build-golden.sh stormbootx-stick $PWD/t/stick && \
STORMBOOTX_FW_MEDIA=1 deploy/build-golden.sh stormbootx $PWD/t/fw && \
tests/usb-layout.sh t/fw/boot/stormbootx-usb.img t/fw/media && ! tests/usb-layout.sh t/fw/boot/stormbootx.img && \
NIC=virtio tests/media-ovmf.sh t/fw/boot/stormbootx-usb.img "media : fw" "fallback : none" && \
deploy/build-golden.sh stormbootx-rustnic $PWD/t/rn && tests/usb-layout.sh t/rn/boot/stormbootx-rustnic-usb.img t/rn/media && \
NIC=virtio tests/media-ovmf.sh t/rn/boot/stormbootx-rustnic-usb.img "media : rustnic" "; stormnic-virtio.efi drives it" && \
NIC=e1000e tests/media-ovmf.sh t/rn/boot/stormbootx-rustnic-usb.img "e1000e@bd67192 igb@6990341" "stormnic-e1000e 0.2.0: " "nic 0: driver stormnic-e1000e.efi" && \
NIC=igb tests/media-ovmf.sh t/rn/boot/stormbootx-rustnic-usb.img "igb@6990341" "stormnic-igb 0.2.0: " "nic 0: driver stormnic-igb.efi" && \
deploy/build-golden.sh stormbootx-mlx5test $PWD/t/mlx5 && tests/iso-layout.sh t/mlx5/boot/stormbootx-mlx5test.iso && \
tests/usb-layout.sh t/mlx5/boot/stormbootx-mlx5test-usb.img t/mlx5/media && \
NIC=virtio tests/media-ovmf.sh t/mlx5/boot/stormbootx-mlx5test.iso "mlx5@d11b572 test" "stormnic drivers verbose" "stormnic-mlx5.efi started" "; stormnic-virtio.efi drives it" "ours : no NIC here for stormnic-mlx5.efi" && \
STORMBOOTX_FW_MEDIA=1 REPRO_MEDIA="stormbootx stormbootx-rustnic stormbootx-disk stormbootx-rustnic-disk stormbootx-mlx5test" tests/repro.sh && \
deploy/build-golden.sh stormbootx-arm64 $PWD/t/arm && \
EFI_NAME=BOOTAA64.EFI tests/iso-layout.sh t/arm/boot/stormbootx-arm64.iso && \
EFI_NAME=BOOTAA64.EFI tests/usb-layout.sh t/arm/boot/stormbootx-arm64-usb.img t/arm/media && \
deploy/build-golden.sh stormbootx-arm64-disk $PWD/t/armdisk && \
tests/update-ovmf.sh $R/tcp4probe.efi'That builds stormbootx.efi, tcp4probe.efi and espprobe.efi, and runs the
ten host test suites and the initiator crate's. Then it boots espprobe under OVMF (tests/esp-ovmf.sh),
and stormbootx itself against the stub engine and NVMe/TCP target
(tests/net-ovmf.sh, with stormnic-virtio built in the job for its
take-over boots). Last, it builds an ISO and boots its startup.nsh
from an EFI Shell (tests/shell-ovmf.sh, #60), once with the old EDK shell
and once with OVMF's own. tests/update-ovmf.sh (#83) runs last, because it
rebuilds stormbootx.efi with a test key: ten self-update boots off a
writable USB stick (Self-update).
tests/repro.sh (#53) builds stormbootx.efi twice, from two copies of
the commit at different paths with two CARGO_HOMEs, and requires the same
bytes. Two things made one commit give two digests in two goldens: lld-link
wrote the link time into the PE TimeDateStamp and a PDB's CodeView GUID
into a debug record (build.rs links with /Brepro and /DEBUG:NONE, for
every build), and panic locations carry the uefi crate's source path under
CARGO_HOME. scripts/cargo-repro.sh is cargo with --remap-path-prefix
for the checkout, the target dir and CARGO_HOME; deploy/build-golden.sh
and build-boot-agent.sh build through it, so a golden's stormbootx.efi
is the same wherever its drive mounts. A plain cargo build still carries
the path. REPRO_RAW=1 tests/repro.sh shows it.
With REPRO_MEDIA (#130) it then builds each named media golden twice,
with deploy/build-golden.sh in two clones at different paths, and requires
every file of the two trees (the .iso/.img, media/, BUILD,
SHA256SUMS) to be the same bytes; the second build runs with umask 077,
TZ=Asia/Tokyo and, through unshare -r, another uid, as another machine's
would. build-boot-agent.sh dates everything
SOURCE_DATE_EPOCH (the commit's time unless set) and derives every
identifier a tool would make up from the medium's contents: the FAT volume
ID (mkfs.fat -i, --invariant), the GPT disk and partition GUIDs
(label-id/uuid), the file times (staged, mcopy -m); xorriso takes its
dates, UUID and hybrid GPT from SOURCE_DATE_EPOCH, and writes Rock Ridge
with owner 0 (-r); TZ=UTC for both. build-nic-drivers.sh
builds the stormnic drivers with the paths remapped and /Brepro
/DEBUG:NONE. So a rebuild of a commit is its golden's bytes.
The binary has no host target, so a plain cargo test does not apply;
cargo test -p nvme-tcp-initiator runs the one host-built crate (#10).
src/sha256.rs,
src/intent.rs, src/universal.rs, src/dnsname.rs, src/esp.rs,
src/sntp.rs, src/manifest.rs, src/installconf.rs, src/inventory.rs and src/pcibus.rs are the exceptions: each uses only
core and names no crate:: item, so each compiles as its own crate with
rustc --test. esp.rs's tests build their images with mkfs.fat and
mtools, which must be on the PATH.
--edition 2021 is required, because bare rustc defaults to 2015, where
core is not in scope.
Cargo.lock is tracked, and uefi is pinned at 0.39 / uefi-raw at 0.15,
ed25519-compact at 2.6.0 (verify only, no default features, #83).
Bump them deliberately, in their own commit.
A machine whose firmware's PXE is broken (the Radxa Orion O6N's UEFI v1.2.1
leases and never sends TFTP) or that has no BMC for virtual media boots the
medium's ISO over UEFI HTTP boot. Serve the golden's
boot/<golden>.iso (stormbootx-arm64 on arm64) with Content-Type: application/vnd.efi-iso (or at least a .iso URL), and point the
firmware at it: a boot option with the URI (the firmware's HTTP boot setup
page), or a DHCP server answering the HTTPClient class with the URL.
EDK2 downloads the ISO into a RAM disk and starts its BOOTX64.EFI /
BOOTAA64.EFI, so stormboot.conf, the drivers and install-config.yaml
come with it as on any medium. A bare .efi would come with none, so the
ISO, not the binary, is what is served. stormbootx says media : a RAM disk (UEFI HTTP boot) and takes the NIC from the firmware's stack as it
always does; the self-update leaves the RAM disk alone (the served ISO is
what updates). tests/net-ovmf.sh's httpboot boot is this under OVMF,
with the URI boot option written into the variable store by
tests/bootvar.py. Its httpimg boot does the same with the FAT32 stick
image (#185) as application/vnd.efi-img (or a .img URL), for firmware that
refuses the ISO. That has run under OVMF only, not on the R6415's HTTP boot.
Packaging only; nothing here runs at boot. What ships is a golden (see
How it ships below); these scripts are what the golden build runs, and are
also usable by hand in a build. scripts/build-boot-agent.sh puts the built
.efi and a stormboot.conf onto boot media: a GPT .img to dd onto a
USB stick, or with --iso an El Torito .iso for BMC virtual media. The ISO
is isohybrid the way Debian's netinst is (an MBR 0xef partition and a GPT
entry over /esp.img), and the ESP is at mkfs.fat's own geometry (4 MiB:
FAT12, 2 KiB clusters): AMI Aptio 4 hung at POST A2 on the old pure El Torito
FAT16 ESP (#55); tests/iso-layout.sh ISO… checks both.
Dell 14G: use the stick image, not the ISO (#185). A PowerEdge R6415
(BIOS 1.12.2) refuses the hybrid ISO written raw to a USB stick and over UEFI
HTTP boot (Boot Failed, nothing from stormbootx on the console), and starts
the same binary from a GPT stick with a FAT32 ESP. Every ISO golden carries
that stick as boot/<golden>-usb.img: a protective MBR, a GPT and one 64 MiB
FAT32 ESP at LBA 2048 with the same files as the ISO (--size 64 --fat 32;
tests/usb-layout.sh IMG [TREE] checks it). dd it whole onto a stick; it
updates itself as any stick does.
Every medium carries \startup.nsh (media/startup.nsh, #60) at the root
of its ESP, and of the ISO9660 tree. A machine with no boot option for the
media (an X9 blade that has never booted the virtual CD in UEFI) drops to the
firmware's EFI Shell, which runs it. It looks through fs0..fs7 for the
volume carrying both \EFI\BOOT\BOOTX64.EFI and
\stormboot\stormboot.conf, and starts that BOOTX64.EFI. The second file
is the mark: a local disk's ESP has a BOOTX64.EFI too (stormuefi on an
installed disk, a stale Windows), never a stormboot.conf. It starts one
medium only (#180): once a stormbootx has returned, the script does not go on
to the next fsN (server7's stick returned, then a BMC's v0.10.0 CD started).
The script uses
only what the EDK shell 2.31 (EFI 1.10 mode, what AMI Aptio 4 carries) and
Shell 2.x both take. tests/shell-ovmf.sh ISO old|ovmf 'LINE' … boots an
ISO this way under OVMF, with a decoy ESP mapped before the CD. old is
EdkShellBinPkg's Shell_Full.efi from edk2-stable201811, fetched and checked
against its pinned SHA-256.
Output
defaults to tmp/images in the checkout, which in an sc-build is the job's own
drive and is deleted with it. Nothing is kept.
./scripts/build-boot-agent.sh # one stick for every machine
./scripts/build-boot-agent.sh --iso # same, as an ISO
./scripts/build-boot-agent.sh --iso --no-fallback # as the goldens: no fallback namespace (#36)
./scripts/build-boot-agent.sh --pin --portal 192.168.31.202 \
--nqn nqn.2026-09.lo.g16:stormcos --nsid 2 # one fixed namespace, no claim
./scripts/build-boot-agent.sh --probe # boots tcp4probe instead
# NIC drivers for firmware without them (#26): the Rust stormnic drivers
./scripts/build-nic-drivers.sh tmp/drivers
./scripts/build-boot-agent.sh --iso --drivers tmp/driversbuild-nic-drivers.sh builds the Rust NIC drivers, each --locked from a
pinned commit of its own repo: stormnic-ixgbe.efi (Intel 82599/X540/X552,
STORMNIC_IXGBE_REF), stormnic-mlx4.efi (ConnectX-3,
STORMNIC_MLX4_REF) and stormnic-virtio.efi (virtio-net, VMs only,
STORMNIC_VIRTIO_REF, #108) and stormnic-e1000e.efi (Intel 82574L,
82579, I217/I218/I219, STORMNIC_E1000E_REF, #70) and stormnic-igb.efi
(Intel 82575/82576/82580, I350/I354, I210/I211, STORMNIC_IGB_REF, #69),
beside a
STORMNIC-SOURCE.txt naming each commit and digest; stormnic-e1000e's
src/ids.rs must list exactly the IDs of drivers.rs's e1000e family, or
the build fails. stormnic-mlx5.efi (ConnectX-4/5/6, STORMNIC_MLX5_REF,
#131) is in the default set since #73, once it passed on C2NR0Q2 and forge1
(stormnic-mlx5#1). STORMNIC_DRIVERS (default ixgbe mlx4 virtio e1000e igb mlx5) picks which; STORMNIC_DRIVERS=virtio builds one. It checks each is a PE
boot-service driver (subsystem 11). There is no iPXE in it, on any medium or
in any golden (owner on #81: "I dont want the ipxe code. Move to ours."; #91,
#52). The iPXE intelx driver that first got the X9 blades onto the network
(#26) is in the history.
--help lists the rest: --drivers, --dns, --ntp, --media, --rng,
--esp, --nic-verbose, --update, --tree,
--engine (the engine host without pinning), --api-port, --port,
--nqn, --nsid, --size (ESP MiB, default 4), --binary, --output.
stormbootx is a UEFI application, so it has no daemon and no metrics. It
listens on one port while it runs, its own API (#167, The API, commands and
the hold): TCP 9180 on every leased NIC, api = to move or turn it
off; GET /api/v1/status is its health. Everything else it reports goes to
the console and the log. Its outbound connections:
| To | Default | Set by |
|---|---|---|
| engine, boot verbs (health, intent read, claim) | <portal>:9089, HTTP/1.1; else <portal>:9090 when that gives no health answer within 15 s of a lease (#183) |
boot_port, api_port |
| DNS server (PTR of its own address, #23) | option 6 of its DHCP lease, TCP 53 | DHCP, else dns |
| DNS server (A record of the NTP server, when it is named, #77) | the same server, UDP 53 | DHCP, else dns |
| NTP server (one SNTP exchange, #77) | DHCP option 42, else pool.ntp.org, UDP 123 |
DHCP, else ntp |
| NVMe/TCP portal (attach) | <portal>:4420, or what the claim returns |
port, or the claim reply |
| stormcentral (the self-update's manifest, signature and files, #83) | the update = URL, HTTP/1.1; its host looked up over UDP 53 |
update |
| the stormcast group (the boot's log, #162) | 239.255.42.1:5514, UDP, TTL 4, on every NIC with a lease |
syslog = off stops it |
| the engine's host record (a command, #167, stormblock#494) | the port health answered on, GET …/command, POST …/command/done |
boot_port, api_port |
| a syslog collector (the boot's log, #162) | none; the goldens name stormcentral.g8.lo:5514, UDP, its host looked up over UDP 53 |
syslog, else install-config.yaml's syslog: |
The portal defaults to 192.168.31.202 (forge). Since stormblock v17.0.0 the
engine API requires a token for everything except POST …/boothost/<tag>/claim and GET /api/v1/health. stormbootx sends none, because firmware has nowhere to
keep one. The intent read (stormblock#148) is open in the same way; setting an
intent needs the token.
How it ships: as goldens (owner, 2026-09-28, #21: everything is a
golden). Nothing is kept on the build box. A machine's virtual CD is to be
served from the golden (minismbd#7, open), not from a copied file.
deploy/build-golden.sh <golden> OUT writes one golden's tree into OUT and
does nothing else, for stormcentral to run into the volume it mounts. Our drivers only (#202): the fw goldens
(stormbootx, stormbootx-disk, no NIC drivers) are retired and build only
with STORMBOOTX_FW_MEDIA=1; the arm64 goldens carry no driver of ours yet
(no aarch64 stormnic driver is pinned) and stay as they are:
| golden | tree |
|---|---|
stormbootx (retired by #202; STORMBOOTX_FW_MEDIA=1) |
bin/stormbootx.efi, bin/tcp4probe.efi, boot/stormbootx.iso (BMC virtual media), boot/stormbootx.img (USB), boot/stormbootx-usb.img (USB, FAT32, #185), boot/tcp4probe.iso, media/, media.files, SHA256SUMS, BUILD |
stormbootx-rustnic |
bin/stormbootx.efi, boot/stormbootx-rustnic.iso (BMC virtual media), media/, media.files, SHA256SUMS, BUILD |
stormbootx-disk (retired by #202; STORMBOOTX_FW_MEDIA=1) |
boot/stormbootx-disk.img (USB stick, the stormbootx medium), SHA256SUMS, BUILD |
stormbootx-rustnic-disk |
boot/stormbootx-rustnic-disk.img (USB stick, the stormbootx-rustnic medium), SHA256SUMS, BUILD |
stormbootx-mlx5test |
a test medium, never promoted or released (#131): boot/stormbootx-mlx5test.iso, the rustnic medium plus stormnic-mlx5.efi, nic_verbose, no update =; bin/stormbootx.efi, media/, media.files, SHA256SUMS, BUILD |
stormbootx-arm64 |
bin/stormbootx.efi (aarch64), boot/stormbootx-arm64.iso (\EFI\BOOT\BOOTAA64.EFI, the fw medium for arm64 hosts), media/, media.files, SHA256SUMS, BUILD (#84; registered as a media component). The ISO is also what UEFI HTTP boot serves (#172). |
stormbootx-arm64-disk |
boot/stormbootx-arm64-disk.img (USB stick, the stormbootx-arm64 medium: 64 MiB GPT ESP, BOOTAA64.EFI), SHA256SUMS, BUILD (#172; registered as a media component) |
stormbootx-stick |
bin/stormbootx-stick (x86_64), bin/stormbootx-stick-arm64 (aarch64, #172), static musl, SHA256SUMS, BUILD |
nic-drivers |
bin/stormnic-ixgbe.efi, bin/stormnic-mlx4.efi, bin/stormnic-virtio.efi, bin/stormnic-e1000e.efi, bin/stormnic-igb.efi, bin/stormnic-mlx5.efi (#73), STORMNIC-SOURCE.txt, SHA256SUMS, BUILD (an EFI boothelper; no medium takes it as an input since #52, and it holds no iPXE since #91) |
media/ is the medium's files as a tree (EFI/BOOT/BOOTX64.EFI,
stormboot/stormboot.conf, stormboot/drivers/*, startup.nsh), and
media.files one <sha256> <size> <path> line per file: what stormcentral
signs and serves when it promotes the golden, for media to update themselves
from (#83, stormcentral#279). Each medium's stormboot.conf carries
update = http://stormcentral.g8.lo/api/v1/boothelpers/<golden>.
arm64 is its own boot helper (#84, owner 2026-10-09). The agent
compiles for aarch64-unknown-uefi: its one cycle counter is
entropy::ticks() (TSC, or CNTVCT_EL0), calibrated against Stall, and its
bootloader path is the architecture's removable-media path, so an arm64
medium carries BOOTAA64.EFI, chain-loads the attached image's
BOOTAA64.EFI and takes only a self-update manifest that names one.
stormbootx-arm64 is the firmware-drivers medium (media : fw arm64): the
stormnic drivers are x86 builds, so there is no arm64 rustnic. It is a
separate golden so an arm64 failure never holds up the x86 ones;
storminstall picks it by its manifest row's "arch": "arm64".
build-boot-agent.sh --arch arm64 lays any arm64 medium down. Until
stormcentral#604 the build template has no aarch64 std (the build adds it
with rustup), no qemu-system-aarch64 and no AAVMF, so the arm64 binary is
compiled and its medium checked (EFI_NAME=BOOTAA64.EFI tests/iso-layout.sh), but not yet booted.
The USB sticks are their own goldens (#41, owner on #35; #97), at FAT16,
which the blades boot; the FAT32 stick a Dell 14G needs is boot/<golden>-usb.img
inside each ISO golden (#185), which stormcentral does not keep yet (it takes
only boot/<comp>.iso and media/ from a media golden).
stormbootx-disk and stormbootx-rustnic-disk each hold one image, a GPT
disk with a 64 MiB FAT16 ESP at 512-byte sectors carrying the same files as
the family's ISO. dd it whole onto a stick. Its update = names the
family's boothelper (stormbootx or stormbootx-rustnic), not its own, so a
stick takes the same signed promotion as the ISO. The ESP is 64 MiB rather
than the ISO's 4 because a stick is writable and updates itself, writing the
new set beside the old. The sc-build check boots each image as a USB stick
under OVMF (tests/media-ovmf.sh IMG …). Registering them with stormcentral
is stormcentral#190.
Two media, side by side (#45, #52). stormbootx is the
firmware-drivers medium: no \stormboot\drivers at all, so every NIC is
the firmware's own UEFI driver's (the Dell R230), and the console says
drivers : none on the media; the firmware's own NIC drivers. Since #52 it
carries no iPXE (owner on #81: "I dont want the ipxe code ... we can keep the
code to use built in nic firmware"); a machine whose firmware has no driver
for its NICs (the X9 blades) boots stormbootx-rustnic, the same agent from
the same commit with the Rust drivers: stormnic-ixgbe.efi built --locked from
STORMNIC_IXGBE_REF in scripts/build-nic-drivers.sh, and no iPXE NIC
driver, so the Rust driver is what a machine booting it tests. Since #51
(stormnic-ixgbe 0dd4267) it installs EFI_SIMPLE_NETWORK_PROTOCOL on a child
handle after its bring-up and DMA check, so the network stack can bind to the
Intel 10G. Since #63 (stormnic-ixgbe 8ea722a) its Start enables memory
decode and bus mastering on AMI Aptio 4, which refuses the PCI I/O attribute
calls (stormnic-ixgbe#19). Since #65 (stormnic-ixgbe 563ea8d) a reset whose
EEMNGCTL.CFG_DONE never sets is logged and Start continues, as on server3's
82599 (stormnic-ixgbe#21). Since #101 (stormnic-ixgbe 728b328) its console is
one line per bound NIC (MAC, link speed, or link down after N ms with
LINKS/AUTOC/AUTOC2/ESDP), plus warnings, and the full bring-up trace only
when the volatile EFI variable StormnicVerbose is set (stormnic-ixgbe#22
and #26); a linked boot is about 3 s faster (stormnic-ixgbe#24). Since #192
(stormnic-ixgbe 4b4813d) its SNP carries 9000-byte frames (stormnic-ixgbe#33),
so a lease offering DHCP option 26 runs the Intel 10G at 9000 (#165). Since #34 it
also carries stormnic-mlx4.efi for the ConnectX-3, built --locked from
STORMNIC_MLX4_REF. Since #50 (stormnic-mlx4 v0.2.0) its Start brings up the
ConnectX-3, keeps it, and installs EFI_SIMPLE_NETWORK_PROTOCOL on a child
handle per Ethernet port (about 1.5 s per NIC plus up to 5 s for link), so
the network stack can bind above it. Since #64 (stormnic-mlx4 v0.2.1) it finds
the UAR's PCI I/O BAR index through GetBarAttributes, because AMI Aptio 4
numbers BARs rather than BAR registers (stormnic-mlx4#15). Since #66
(v0.2.3) it prints each port's speed, autonegotiation and module at the link
wait and on every link change, and the first own frame the adapter loops
back (stormnic-mlx4#21). Since #104 (v0.2.5) it prints each port's module
EEPROM, PTYS link modes and forced-speed support after the link wait and at
every link-down (stormnic-mlx4#17), and its console is one line per port
plus warnings, with the full trace only when StormnicVerbose is set
(stormnic-mlx4#16). Each Rust driver
is checked to be PE subsystem 11 (EFI boot-service driver) when it is built.
Since #108 it also carries stormnic-virtio.efi (stormnic-virtio 1a04808,
#75) for VMs. Every OVMF's VirtioNetDxe binds a virtio-net NIC before the
media drivers load, and a stormnic driver declines a NIC another driver holds,
so stormbootx takes it (since #202 for every stormnic driver, unasked: "we
only use our drivers going forward"). It disconnects the firmware's drivers from each virtio-net
function (leaf first, up to three passes, judged by who still holds its PCI
I/O) and connects it naming only stormnic-virtio. If that driver does not then
hold it, the firmware's driver gets it back. The console says which:
[ 0 s] 0000:00:02.0 1af4:1000: taken from Virtio PCI Driver; stormnic-virtio.efi drives it
The fw medium carries no drivers at all, so it takes nothing.
The same take-over gives a ConnectX-4/4 Lx/5/6 (15b3:1013/1015/1017/1019/101b/101d/101f,
what stormnic-mlx5 claims) held by its firmware's FlexBoot driver to
stormnic-mlx5, which every rustnic medium carries since #202 (the owner,
2026-11-14: "stormbootx cannot be reliable without a proper driver");
stormbootx-mlx5test is the same medium, verbose and with no update =. The
nic-drivers golden refuses it. The ixgbe, mlx4, igb and e1000e families take
theirs the same way. Each family's ids are vendor:device only, never the
subsystem, as Linux matches, and build-nic-drivers.sh fails when one
differs from its driver's list (docs/pci-ids.md compares them with the
kernel's). A medium from before #202 says prefer_media_drivers; it is named
(ours : prefer_media_drivers is ignored since #202) and ignored.
The driver brings the card up, speed and FEC, by its own rule (stormnic-mlx5#8). Each take is announced before the disconnect, so a Stop or Start that never
returns has named itself, and the console says what the firmware's driver left:
[ 3 s] 0000:00:02.0 1af4:1000: taking it from Virtio PCI Driver for stormnic-virtio.efi
after the disconnect: PCI I/O open by driver handle 0x3f7feb18 (attributes 0x1), PCI I/O open by Sata Controller Init Driver (attributes 0x2)
[ 3 s] 0000:00:02.0 1af4:1000: taken from Virtio PCI Driver; stormnic-virtio.efi drives it
(That is OVMF with virtio: no ConnectX has run it yet. Opens at attributes 0x1 and
0x2 are lookups and hold nothing; a BY_DRIVER open is 0x10, and an SNP on the handle would make stormnic-mlx5 decline.) A driver that declines gives the NIC back; if the firmware's driver does
not take it again the line says this NIC has no driver, and the boot goes on
over any other.
For e1000e (#70) that is an Intel 82574L or chipset GbE (the 67 8086: IDs
stormnic-e1000e binds), taken from Intel's UNDI driver or a NIC's iPXE ROM.
The driver has run only on QEMU's 82574L and the PCH parts in simulation; if
it does not bind a real part, the firmware's driver gets the NIC back.
The rustnic media builds its own drivers and takes no nic-drivers golden. Each ISO's stormboot.conf names the variant, and the console
prints it under the banner:
media : fw
media : rustnic ixgbe@02fcc5d mlx4@398a1a4 virtio@2dcafd3 e1000e@bd67192 igb@6990341
The fw medium's BUILD says drivers = none, and a --drivers given to
build-golden.sh stormbootx is ignored. Its stormboot.conf names forge
(192.168.31.202) and dns = 192.168.31.252; which image a machine boots is
its boothost on the engine, so one golden boots every machine. stormbootx is
a stormcentral component of kind media, and so is stormbootx-rustnic:
stormcentral component build <name> builds each into a drive golden whose
bytes are its boot/<name>.iso. nic-drivers is a component of kind
tree. The two -disk goldens are not registered yet (stormcentral#190).
- A UEFI driver for the NIC (
EFI_SIMPLE_NETWORK), the firmware's or one on the media (#26), and nothing above it. The firmware's MNP/IP4/DHCP4/TCP4 are not used (#56). A firmware driver is usually loaded only when the NIC is in UEFI/PXE mode in setup (Dell: Integrated NIC Enabled with PXE;GlobalSlotDriverDisableoff, or add-in cards have no UEFI driver). The Supermicro X9 blades' NICs carry legacy option ROMs only, so they bootstormbootx-rustnic, which brings its own. - Any OVMF will do for emulation, Fedora's included (
tests/net-ovmf.sh).
The older sbregistry /v1/clones/claim path is no longer here (#17): it was
compiled out from the day the boothost claim replaced it, and it spoke plain
HTTP with no credential to a node registry that is moving to TLS with auth
(stormcos#355). It is in the history before #17.
config::render/config::write_fileand thestampkey: the first self-update plan (#2), superseded by #83, which writes through its ownselfupdate.rs.- The FEC self-heal (automatic write on "all ports down") was switched off in 0.3.6. It was triggered by a single link sample. The reasoning is in its commit. The FEC pin that followed (#186, #203) was removed by #202: FEC is the NIC driver's.
v0.41.0 (#198: a stormdist release medium on a CD or USB stick installs offline, with its read rate; the rustnic media carry stormnic-igb and stormnic-mlx4 v0.2.8). v0.40.0 (#70: the rustnic media carry stormnic-e1000e, prefer_media_drivers = e1000e). v0.39.2 (#192: the rustnic media carry stormnic-ixgbe 4b4813d, jumbo frames). v0.39.1 (#189: a clean local with a disk that boots does not wait at press c). v0.39.0 (#186: fec_pin = rs, the FEC pin at boot; v0.38.1 the ConnectX scan per root bridge; v0.38.0 #185 -usb.img sticks; v0.37.0 #131 prefer_media_drivers = mlx5; v0.36.0 #183 the engine's boot port; v0.35.x #172/#180 HTTP boot, arm64 stick, one-medium startup.nsh; v0.33–0.34 #167 the API and maint; v0.29.0 #151: stormbootx-stick; v0.28.x: the boot panel #144 and
reproducible media #130; v0.27.0 #148 the self-update's lease wait; v0.26.0
#145 the fall-through starts the local disk; v0.25.0 #138's FEC-step budget).
The release freeze of 2026-10-14..17 is over. Running on hardware since 2026-09-05. A Dell PowerEdge R230 (C2NR0Q2)
booted the ISO over iDRAC virtual media, claimed boothost/C2NR0Q2 and
attached a 32 GiB 4K clone from forge over 25 GbE. The console of that first
attach, verbatim (the build before chain-loading, ea26be1):
service tag : C2NR0Q2
tcp4 : available
claim : boothost/C2NR0Q2 at 192.168.31.202:9090
interface 0 answered (MTU 1500, link up)
claimed a clone of this machine's image
portal : 192.168.31.202:4420 nsid 7
namespace : 8388608 blocks x 4096 bytes (32 GiB)
transfer : 128 KiB per command (controller MDTS 5; path MTU 1500)
blockio : published on handle 0x8301ae98
RESULT: remote image is a local disk. Firmware can boot it.
The same day, with chain-loading (dc44c71), it started stormuefi, which
booted stormcos. Today's build ends a good boot with RESULT: image attached; starting its bootloader.
On 2026-09-28 a Supermicro X9 blade (server1), whose firmware has no UEFI
driver for its NICs, loaded ipxe-intelx.efi from the media, got
tcp4 : available and reached the engine. Booted again from the golden
(golden-stormbootx-74a242a6f0e89f75, a709f9f), it named itself server1
from the PTR, claimed boothost/server1 and attached its clone over NVMe/TCP
(#26, closed). The release disk's BOOTX64.EFI then read as NOT_FOUND
on that firmware, a 4096-byte-block namespace (#33). Volumes stay 4K (owner,
2026-09-29), so stormbootx now reads the ESP itself when the firmware can't
(#37). On 2026-09-30 server1 booted release 11.56 from a 4096-byte
namespace (v0.7.0 media) once that release's ESP was FAT16 (stormcos#188),
which closed #33. On the X9 blades the firmware's own FAT loads
BOOTX64.EFI from that FAT16 4K ESP (the master's reading of server1's SOL
log on #37: every boot boot : starting \EFI\BOOT\BOOTX64.EFI from the attached image, none read by stormbootx), so esp.rs is the fallback there,
not the path. The kernel console on the X9's SOL
(COM2, ttyS1) is not stormbootx's: the release names console=ttyS1,115200
too (stormcos#220), and stormuefi ≥ 0.9.0 puts the port ACPI SPCR names last
(stormuefi#23). The same
night server3, also an X9, booted on smoltcp (v0.9.0): tcp4 : smoltcp over SNP, rng : rdrand, a lease, the claim, the attach, stormuefi and a running
stormcos (#56, #68).
Open issues:
| Issue | What |
|---|---|
| #92, #89 | self-update on metal: stormcentral serves stormbootx-rustnic serial 1 = v0.14.0, which every newer medium declines as older (#96); the promotion of v0.29.0's rustnic golden (canary server1 first) is the master's, 2026-10-17 (#92); nothing is promoted for stormbootx |
| #93 | install-config sizes on real firmware variable stores |
| #100, #99 | boot time: one NVMe/TCP command in flight (~46 MiB/s on metal); #99 (mlxfec's cap9 wait) is moot since #202 removed mlxfec |
| #71–#74 | more Rust NIC drivers, paused by the owner at P3 (each behind its repo's first issue); stormnic-e1000e is on the rustnic media since v0.40.0 (#70), stormnic-igb since #69; stormnic-mlx5 is on the rustnic media since #202 and in the default driver set since #73, passed on C2NR0Q2 and forge1 |
| #84 | arm64 media: built (stormbootx-arm64); booting it under AAVMF and registering the golden wait on stormcentral#604 |
| #138 | the R230's NIC FEC step: v0.25.0's real-time budget is released, the metal boot is not seen |
| #154 | stormbootx-stick writing a signed raw disk image when a stick has no room for a file update |
| #143 | the fall-through shows as BdsDxe: failed to start Boot0002 … Aborted |
| #110 | the claim over HTTPS against a fleet CA (stormcos#35 is closed as a design; #110 is the work) |
A slide deck of the above is in docs/presentation.md
(Marp: npx @marp-team/marp-cli docs/presentation.md).
Related: stormuefi (stage two) and stormnetboot (the earlier network-boot project, whose PXE chain this USB/NVMe-TCP path retired).