Roadmap
AXL Roadmap
Unified phase tracker for the AXL library and SDK. Phases from AXL-Design.md and AXL-SDK-Design.md are combined here in execution order.
Legend: [x] done, [ ] pending, [-] in progress
Library Phases (AXL-Design.md)
Phase R: Rename UdkLib to AXL — DONE
[x] Global symbol rename (UDK_ -> AXL_, Udk -> Axl)
[x] Directory and header renames
[x] Update all consumer repos (uefi-devkit, axl-webfs, softbmc, ipmitool)
Phase S1: axl_mem — DONE
[x] axl_malloc / axl_free / axl_realloc / axl_calloc
[x] axl_strdup / axl_memdup
[x] Debug features: fill patterns, fence checks, leak dump, stats
Phase S2: axl_strbuf — DONE
[x] AxlStrBuf string builder
[x] UTF-8 / UCS-2 conversion
[x] Base64 encode/decode
[x] axl_strlcpy / axl_strlcat
[x] axl_asprintf
Phase S3: axl_io — DONE
[x] AxlStream abstraction (console, file, buffer)
[x] axl_printf / axl_fprintf / axl_print / axl_printerr
[x] axl_fopen / axl_fread / axl_fwrite / axl_readline
[x] axl_file_get_contents / axl_file_set_contents
Phase S4: AXL_APP — DONE
[x] Entry point macro: int main(argc, argv)
[x] Shell argument conversion (UCS-2 to UTF-8)
[x] axl.h umbrella header (self-contained, no EDK2 leaks)
Phases M1-M6: Module Migration — DONE
[x] M1: AxlLog — GLib-style logging API
[x] M2: AxlData — hash, array, string, JSON
[x] M3: AxlUtil — file, path, args, hexdump, time, smbios
[x] M4: AxlLoop — event loop, timers
[x] M5: AxlTask — arena allocator, worker pool
[x] M6: AxlNet — TCP, HTTP server/client, URL parsing
Phase C1: Style Guide Compliance — DONE
[x] STATIC -> static, TRUE -> true, FALSE -> false, BOOLEAN -> bool
[x] Spaces around operators, ///< doc comments
[x] All axl-*.c files pass style audit
Phase C2: Dogfooding — DONE
[x] Replace AllocatePool/FreePool with axl_malloc/axl_free in migrated modules
[x] Replace AsciiStrLen/AsciiStrCmp with axl_strlen/axl_strcmp
[x] Replace CopyMem/ZeroMem with axl_memcpy/axl_memset
[x] Replace AsciiSPrint with axl_snprintf
[x] AXL_LOG_DOMAIN in all modules
[x] Consumer projects updated (uefi-devkit, axl-webfs)
Phase C3: Test Modernization — DONE
[x] Shared test header (axl-test.h)
[x] All 9 test files converted to AXL_APP entry points
[x] Wide-string test output replaced with axl_printf
[x] Test .inf files updated (_AxlEntry, AxlAppLib)
[ ] Consumer build verification in test-axl.sh (axl-webfs, uefi-devkit)
Phase C4: Style Compliance Pass 2 — DONE
[x] Add Doxygen @brief/@return to all existing functions
[x] Multi-line params with ///< on all function definitions
[x] Replace remaining UEFI types with standard C in non-backend code
[x] Remove unnecessary #include <Uefi.h> from abstracted files
Phase C5: GLib API Alignment — DONE
Audit all public APIs against GLib naming and align before the API stabilizes. AXL is “GLib for UEFI” — the API should feel familiar to anyone who knows GLib.
[x] Audit every public function name against GLib equivalents
[x] Audit argument order (GLib puts the “object” first)
[x] Audit return conventions (GLib returns the modified list head from list ops)
[x] Rename AxlStrBuf → AxlString / axl_string_* (matches GString)
[x] Rename AxlHash → AxlHashTable / axl_hash_table_* (matches GHashTable)
[x] Swap axl_strjoin arg order to (separator, arr) matching g_strjoinv
[x] axl_string_new(init) takes optional init string matching g_string_new
[x] axl_string_append_c (was putc), axl_string_append_len (was append_n)
[x] Add GLib string search/test functions (strstr_len, strrstr, has_prefix, has_suffix, etc.)
[x] Add str_equal, strncasecmp, strv_contains, strv_equal
[x] AxlHashTable: generic keys, insert/replace/lookup/contains/steal/foreach_remove/iterator
[x] AxlHashTable: drop _w API, add AxlDestroyNotify, AxlHashFunc, AxlEqualFunc
[x] AxlString: rename printf→append_printf, add prepend/insert/erase/truncate/overwrite
[x] AxlArray: add remove_index, remove_index_fast, remove_range, set_size, sort_with_data
[x] AxlList: add insert_before/after, remove_all/link, sort_with_data, copy_deep
[x] AxlSList: add insert_before, remove_all/link, sort_with_data, copy_deep
[x] AxlQueue: add find, find_custom, remove, remove_all
[x] New shared types: AxlCompareDataFunc, AxlCopyFunc
[x] Update all consumers (tests, examples, axl-webfs)
Resolved divergences:
AXL (before) |
AXL (after) |
GLib equivalent |
Status |
|---|---|---|---|
|
|
|
DONE |
|
|
|
DONE |
|
|
|
DONE |
string-only keys + |
generic keys via |
|
DONE |
(missing) |
|
|
DONE |
(missing) |
|
|
DONE |
|
|
|
DONE |
|
|
|
DONE |
|
|
|
DONE |
(missing) |
|
|
DONE |
(missing) |
|
|
DONE |
(missing) |
|
|
DONE |
(missing) |
|
|
DONE |
(missing) |
|
|
DONE |
Intentional divergences (keeping):
AXL |
GLib |
Why |
|---|---|---|
|
GIOChannel |
POSIX names are universal |
|
|
Shorter, UEFI has one loop |
|
|
Single-char delimiter sufficient |
|
|
Struct has size+is_dir metadata |
|
|
More correct on 64-bit |
|
abort-on-OOM |
UEFI must handle OOM |
|
|
OK (matches) |
|
|
Evaluate: return name only vs struct? |
SDK Phases (AXL-SDK-Design.md)
SDK Phase 1: Core — DONE
[x] install.sh: build library, package SDK
[x] axl-crt0.c: UEFI entry point stub
[x] axl-cc: command-line build wrapper
[x] axl.cmake: CMake integration
[x] hello.c example verified in QEMU
[x] X64 architecture support
SDK Phase 2: Polish — DONE
[x] AARCH64 cross-build support (build + tests + SDK + axl-cc aa64)
[x] Test CMake build end-to-end (verified in QEMU)
[x] Verify net module works (SDK includes AxlNetLib + all protocol GUIDs via sdk-ref target)
[x] Better error messages in axl-cc (source validation, tool checks, install hints)
[x]
--verboseflag for axl-cc[x]
axl-cc --version/axl-cc --help
SDK Phase 3: Distribution — DONE
[x] GitHub Actions:
.github/workflows/release.ymlbuilds SDK on tag push for x64 / aa64, both with and without TLS.[x] Versioned releases:
axl-sdk-<version>-<arch>[-tls]-linux.tar.gzuploaded to GitHub Releases. Version comes fromVERSIONfile at repo root (currently0.1.0).[x] Version stamp in
axl-cc --versionoutput (shown by the generated wrapper inscripts/install.sh).[x] Release notes: auto-generated body in
release.yml— download table, prerequisites, quick start, docs links.
SDK Phase 4: Advanced Features — DONE
[x] Multi-file projects in axl-cc (works, tested)
[x]
--netnot needed — network GUIDs are already in libaxl.a[x]
axl-cc --type driver|runtimefor DXE/runtime driver targets[x]
axl-cc --entry <name>for custom driver entry points[x]
axl-cc --debug— debug build (-Og, DWARF symbols, leak tracking, .map file)[x]
axl-cc --release— release build (-Os, -DNDEBUG) [default][x]
axl-cc --run— build and launch in QEMU via run-qemu.sh
SDK Phase 5: Backend Abstraction — DONE
Created axl-backend.h internal abstraction layer, migrated all
library modules to use it. Originally supported EDK2 and gnu-efi
backends (Phases 5a-5e); both were removed in Phase N7 in favor
of the native backend.
[x] Backend API: memory, console, time, file I/O, wide-string, events
[x] All library modules migrated to backend API
[x] Portable replacements for EDK2 intrinsics (hex parser, CAS, CpuPause)
Native UEFI Backend (AXL-Native-Backend-Design.md)
AXL provides its own UEFI type definitions, CRT0, and build toolchain. No external dependencies (no EDK2, no gnu-efi). Supports applications, boot service drivers, and runtime drivers.
Project Restructure — DONE
[x] Standard C project layout:
include/+src/at root[x] Tests moved to
test/unit/+test/integration/
Phase N1: UEFI type headers — DONE
[x] Create
include/uefi/with 7 self-contained headers (1540 lines)[x] Types, status codes, system tables, protocols, GUIDs from UEFI 2.10
[x] Compiles clean on x86_64 and AARCH64 (-Wall -Wextra -Wpedantic)
Phase N2-N4: Native backend + CRT0 + tests — DONE
[x] Create
src/backend/native/axl-backend-native.c(37 backend functions)[x] Create
src/crt0/axl-crt0-native.c(app entry point)[x] Add
AXL_BACKEND_NATIVEcase toaxl-backend.h[x]
Makefile— gcc + ld + objcopy, builds libaxl.a + all test EFIs[x] 411/411 tests pass on native backend
[x] Remove EDK2 header dependencies from unit tests
[x] Delete compat shim layer (12 files, -377 lines)
[x] Backend directory restructure (gnuefi/, native/ subdirs)
[ ] Type remaining BS slots for drivers (InstallProtocolInterface, etc.)
[ ] Add EFI_DRIVER_BINDING_PROTOCOL to protocols header
[ ] Shell argument parsing (EFI_SHELL_PARAMETERS_PROTOCOL)
Phase N5: SDK integration — DONE
[x] Update
install.shto support native backend[x] Generate
axl-ccthat uses native backend[x]
axl-cc --type driver|runtimesupport[x] CMake integration for SDK consumers
[x] Test:
axl-cc hello.c -o hello.efiwith zero external deps
Phase N6: UEFI header generation from spec HTML — DONE
[x] Manifest-driven generator (
scripts/generate-uefi-headers.py)[x] Manifest (
scripts/uefi-manifest.json5): 275 definitions, dependency-ordered[x] Spec downloader (
scripts/download-uefi-specs.py): UEFI 2.11, PI 1.8, ACPI 6.5, Shell 2.2[x] Extracts from
<pre>blocks (struct, enum, funcptr, define, typedef) and Table 2-4[x] Unknown struct member types auto-replaced with
void *[x] 327 GUIDs with EDK2-style aliases
[x]
--checkflag validates source code against manifest (integrated into build.sh)[x] Shell protocols hand-written (PDF-only spec)
[ ] Add more protocols to manifest as needed (PCI, USB, HII, etc.)
Phase N7: Remove EDK2/gnu-efi backends — DONE
[x] Remove EDK2 backend (AxlPkg/, .inf files, axl-backend-edk2.c, deps.conf)
[x] Remove gnu-efi backend (Makefile.gnuefi, axl-backend-gnuefi.c, compat headers)
[x] Simplify axl-backend.h, axl.h, source files — remove all backend conditionals
[x] Simplify scripts (build.sh, install.sh, test-axl.sh, test-all.sh)
[x] Rename Makefile.native to Makefile
[x] Native backend is the only backend
Networking Phases (Future)
These phases add features to the existing AxlNet module.
Phase 8: HTTP Server Features — DONE
[x] Middleware pipeline (
axl_http_server_add_middleware, runs in order)[x] Static file serving (
axl_http_server_add_static)[x] Route-based dispatch with prefix matching
[x] WebSocket (RFC 6455) — upgrade handshake, frame parser/builder, per-connection state, ping/pong/close, broadcast
[x] Authentication —
axl_http_server_use_auth+ per-route auth flags enforcement (401/403)[x] Response caching — AxlHashTable-based with TTL, cache check/store in dispatch, invalidation
[x] Upload streaming — route registration with
AxlUploadHandler
Hardening pass complete (April 2026 code review):
[x] Cache key heap corruption —
axl_http_dispatch.cnow strdup’sdup_keybefore inserting into the cache table.[x] NULL upload handler dereference — dispatch guards on
route->handler != NULL.[x] WebSocket broadcast nested event loop —
axl_http_server_ws_broadcastuses asyncsend_startons->loop, not a temporary sync loop.[x] TLS + WebSocket data path —
on_conn_dataTLS block now branches onis_websocket || is_upload_streamand decrypts into the chunk buffer.[x] WebSocket ping/close bypassing TLS —
process_websocket_datasends pong/close viaaxl_tls_writewhentls_ctx != NULL.[x] DISCONNECT firing after socket close —
reset_connectionfiresAXL_WS_DISCONNECTwhile the transport is still open.
Cache policy — all three addressed in commit ece9317:
[x]
cache_maxenforced via FIFO eviction on the hash table.[x]
axl_http_server_set_route_ttl(path, ttl)stores per-route TTLs in aroute_ttlshash on the server and consults it at cache-store time.[x]
axl_http_server_cache_invalidate(prefix)removes matching entries viaaxl_hash_table_foreach_removewith a prefix predicate.
18 integration tests added at the same time.
Phase 9: TLS Support — DONE
[x] TLS support via mbedTLS 3.6.3 (optional: AXL_TLS=1)
[x] axl_tls_generate_self_signed (ECDSA P-256)
[x] HTTP server HTTPS (axl_http_server_use_tls)
[x] HTTP client HTTPS (auto-detect https:// URLs)
Phase 10: SoftBMC Migration
[ ] Migrate SoftBMC to AXL networking stack
[ ] Use AxlAsync (Phase A2) for firmware update endpoint
[ ] Use AxlBufPool (Phase A1) for VNC tile buffers
BMC Access Phases (Future)
Phase B1: AxlIpmi — DONE
Local BMC access via IPMI. Four transports, auto-selected: EDKII
IPMI_PROTOCOL → Dell EFI_IPMI_TRANSPORT → SMBIOS Type 38
(KCS or SSIF) → x86 default KCS 0x0CA2/0x0CA3.
[x]
axl_ipmi_session_new()+_free()+ AUTOPTR cleanup[x]
axl_ipmi_raw(netfn, cmd, req, resp)lowest-level entry[x]
axl_ipmi_get_device_id()— BMC info[x]
axl_ipmi_get_sensor_reading()[x]
axl_ipmi_sdr_info()/axl_ipmi_sdr_get()— SDR iteration[x]
axl_ipmi_sel_info()/axl_ipmi_sel_get_entry()— SEL iteration[x]
axl_ipmi_get_chassis_status()/axl_ipmi_chassis_control()[x]
axl_ipmi_fru_info()/axl_ipmi_fru_read()[x] Formatting helpers:
axl_ipmi_completion_code_string(),_sensor_type_string(),_entity_id_string()[x] KCS transport (
src/ipmi/axl-ipmi-kcs.c)[x] SSIF transport (
src/ipmi/axl-ipmi-ssif.c) — multi-part framing + 60 ms inter-command delay[x] EDKII vendor protocol (
src/ipmi/axl-ipmi-edkii.c)[x] Dell vendor protocol (
src/ipmi/axl-ipmi-dell.c) — CC synthesis[x] Auto-detection via SMBIOS Type 38
[x] Backend hooks:
axl_backend_io_read8/write8(SMBus access was originally a backend hook; Phase B1a promoted it into its ownAxlSmbusPlatform Access Module — see below.)[x]
axl_ipmi_session_new_with_callback()for unit tests + pluggable transports[x] 43 unit tests (mock-callback transport, every typed wrapper + negative paths)
[x]
tools/ipmi.efi— stripped-down ipmitool-equivalent (info / chassis / sel / sdr / sensor / fru / raw)
Consumer projects: uefi-ipmitool (stays EDK2-based for now;
sunset once tools/ipmi reaches feature parity), SoftBMC EC module.
Phase B1a: AxlSmbus module split — DONE
Extracted the SMBus / I2C block transfer primitives from the backend
layer into a first-class Platform Access Module. Motivation: a second
SMBus consumer is imminent (AxlSpd, Phase B3 below), so the
anonymous axl_backend_smbus_* pair graduates to a proper module
with session handle, transport vtable, and its own tests before
AxlSpd lands on top.
[x]
include/axl/axl-smbus.h— public API (opaqueAxlSmbussession,axl_smbus_read_block/_write_block, transport enum + string,AXL_SMBUS_BLOCK_MAX)[x]
src/smbus/module:axl-smbus.c(dispatcher + auto-detect),axl-smbus-hc.c(EFI_SMBUS_HC_PROTOCOL pass-through),axl-smbus-i2c.c(EFI_I2C_MASTER_PROTOCOL framing — the B1 code path),axl-smbus-format.c,axl-smbus-internal.h[x]
test/unit/axl-test-smbus.c— capturing-mock I2C Master + SMBus HC protocols viagBS->InstallProtocolInterface. 40 test_check calls across 9 test functions, including direct regression coverage for B1’s byte-count prefix (writes) and count-byte strip (reads).[x] AxlIpmi SSIF migrated:
SsifCtxholds anAxlSmbus *and all calls go through the public module;axl_backend_smbus_*removed fromsrc/backend/axl-backend.handsrc/backend/native/axl-backend-native.c.[x] Ratchet bumped 1216 → 1255 on both X64 and AArch64 (the churn-collapse cleanup in a later test-hygiene pass dropped one install-success pass line).
Phase B1b: SSIF end-to-end QEMU regression — DONE
Closed the B1 regression-coverage gap end-to-end. B1 (I2C Master
fallback framing) now has both unit-level regression (Phase B1a’s
AxlTestSmbus via capturing mock) and integration-level regression
exercising real SMBus wire traffic through QEMU’s ICH9 controller.
[x]
sdk/examples/smbus-hc-shim.c— DXE driver that finds the ICH9 SMBus PCI function (8086:2930), enables HOSTC + I/O decode via raw CF8/CFC config-space I/O, and publishesEFI_I2C_MASTER_PROTOCOLbacked by the ICH9pm_smbusregister model. Intentionally publishes only I2C Master (not SMBus HC) so AxlSmbus’s I2C fallback — which is where the B1 bug lived and what real Dell/Grace firmware exposes — is the code path exercised.[x]
test/integration/common-test.shgainstest_add_ipmi_bmc_sim_ssif(sibling to the KCS helper).[x]
test/integration/test-ipmi-ssif-qemu.shloads the shim, then runsAxlTestIpmi hwagainst QEMU’ssmbus-ipmidevice. 66 passes (61 unit + 5 hardware path) in ~7 s.[x] Regression proof: reverting B1’s byte-count prefix in
src/smbus/axl-smbus-i2c.cfails the SSIF script withFAIL: real_hw: Get Device ID succeeds against live BMC(2 failures, 61 passes). CI would catch a future regression.[x] The “SSIF (B1) is not covered here” note removed from
test-ipmi-qemu.sh; the two scripts now cross-reference.
Phase B3: Platform Access — follow-on modules
R+1 (downstream-consumer-driven; the motivating scenario is a hardware-diagnostic CLI port from Linux to UEFI):
[x] AxlAcpi — ACPI table discovery + typed readers. RSDP → RSDT/XSDT walk, signature lookup with cursor iteration matching
axl_smbios_find_next, public checksum verifier, typed readers for MCFG (PCIe ECAM segments), MADT (IOAPIC on x86 + GIC regions on aa64), and FACP/FADT (SMI cmd port, PM1 blocks, DSDT pointer). No AML interpretation — that’s ACPICA-sized and out of scope. Header:axl/axl-acpi.h. Source:src/acpi/axl-acpi.c.[x]
axl_io_port_*— promoteaxl_backend_io_*to public with 16/32-bit variants. Build-gated to x86 (compile error on AArch64, not runtime no-op). Header:axl/axl-port.h.[x]
axl_nvstore_*extensions — namespace registration so vendor GUIDs (Dell/HPE/Lenovo OEM variables) plug in by name without exposing GUIDs at access sites;_delete,_iter,_get_attrs. Built-in namespaces “global” and “app” remain pre-registered. Behaviour change: unregistered namespace is now an error (was: silent fallback to global).[x] AxlBoot — typed boot-option management.
AxlBootOptionstruct (description / device-path text / opt_data) with_option_get/_set/_delete/_free,_order_get/_set,_next_get/_set/_clear,_current_get.EFI_LOAD_OPTIONcodec stays internal — raw wire bytes never cross the public API. Header:axl/axl-boot.h. Source:src/util/axl-boot.c.
R+2:
[x] AxlPci — ECAM-based config-space access via MCFG. Lazy on first call; typed
AxlPciAddrstruct (16-bit segment, 8/5/3-bit bus/dev/func). 8/16/32-bit read+write at any register offset 0..4095.axl_pci_nextcursor enumerates every responding function, skips empty slots, honours the multi-function header bit._find_by_vid_didand_find_by_class(24-bit class triplet) for direct lookups. Cursor-style legacy cap walk (status bit 4 + cap pointer at 0x34) and PCIe extended cap walk (offsets 0x100+, 12-bit next pointers). VPD reader walks the keyword-tagged RO/RW resources (PCI 3.0 §6.4) with the F-bit handshake. Header:axl/axl-pci.h. Source:src/pci/axl-pci.c.[x] AxlImage — opaque
AxlImage *handle for executable image lifecycle. Thin wrapper overaxl_driver_load/_unloadadding distinct_startsemantics that capture the image’s EFI_STATUS exit code (axl_driver_startdrops it because drivers don’t exit cleanly). Backend-neutral name; future Linux/coreboot backends map toposix_spawn/payload loaders. Header:axl/axl-image.h. Source:src/util/axl-image.c.
R+3:
[x] AxlMemPhys — physical-memory access.
_map/_unmapheld mappings (Linux backend wouldmmap("/dev/mem")here; identity-mapped no-ops on UEFI), one-shot_read{8,16,32,64}/_write{8,16,32,64}helpers, and_searchfor byte-pattern scans inside a mapped region. Header:axl/axl-mem-phys.h. Source:src/util/axl-mem-phys.c.[x] AxlWatchdog —
_disarm/_set(seconds)/_pet. WrapsgBS->SetWatchdogTimer. Without this, the firmware’s 5-minute default watchdog kills any long-running diagnostic._petre-arms to the value last passed to_set. Header:axl/axl-watchdog.h.[x]
axl_reset_*— already shipped pre-R+3 inaxl/axl-sys.hasaxl_reset(int type)withAXL_RESET_COLD/_WARM/_SHUTDOWNconstants. Maps togRT->ResetSystem.[x] AxlRng —
axl_rng_bytes(out, len)wrapsEFI_RNG_PROTOCOL(UEFI 2.11 §37.5). Returns -1 if the protocol isn’t published; consumers that need a deterministic fallback layer their own. Header:axl/axl-rng.h.
R+4 — DONE (2026-05-01):
[x] AxlSpd — DDR4/DDR5 SPD reader on
AxlSmbus. Cursor iteration over 0x50..0x57; key byte at SPD offset 2 selects the codec (0x0C=DDR4, 0x12=DDR5). DDR4 uses the EE1004 SPA pseudo-slave (0x36/0x37) for the upper 256-byte manufacturing block; DDR5 pages via SPD5118 MR11 across eight 128-byte windows.AxlSpdInfocarries module/DRAM JEP-106 codes (raw, parity preserved), part number, serial, manufacture year/week, capacity, JEDEC speed grade, ECC/registered flags. Pure-decoder entry pointaxl_spd_decode(buf, len, *out)runs the same codec on a captured buffer with no SMBus involvement — used by the cross-arch unit tests and by consumers doing offline analysis. DDR3 deliberately deferred. Header:axl/axl-spd.h. Source:src/spd/.[x]
tools/memspd.c— verbslist / show <slot> / decode <slot>. Vendor lookup is data-driven viashare/jedec.json(15-vendor stub; auto-discovered next to the .efi or via--jedec-file). Missing sidecar → manufacturer fields print as raw 16-bit hex codes.[x]
scripts/qemu-patches/0001-smbus-eeprom-add-memdev-link.patch— addsmemdev=<link<memory-backend>>to QEMU’s smbus-eeprom device sotest/integration/test-spd-qemu.shcan attach a canned SPD blob at SMBus 0x50 and exercise the full wire path throughSmbusHcShim.efi. Stock QEMU 10.x rejectsmemdev=on smbus-eeprom; the patch is small (~40 lines), idiomatic (mirrorspc-dimm’smemdev=link), and a candidate for upstreaming.[x] AxlSmbus byte ops —
axl_smbus_read_byte/axl_smbus_write_byte(SMBus spec §5.5.4 / §5.5.5). Required for SPD EEPROMs (24Cxx-style, no SMBus block framing) and for SPD5118 hub register access. Implemented in both HC (EfiSmbusReadByte/EfiSmbusWriteByte) and I2C-Master transports; SmbusHcShim extended to translate the new shapes to ICH9 SMBus Read Byte / Write Byte protocol commands.[x] AxlPlatform tests — 21 new SPD tests (DDR4 + DDR5 decode + bogus-input rejection + probe enumeration). Cross-arch balanced; ratchet 1842 → 1863 on both x86 and AArch64.
R+5 — PCI tooling DONE; USB tooling pending:
[x]
tools/lspci.c— Linux-style PCI lister. Shipped 2026-05-02 in commit 485c517 (initial flat list + caps + hex + filters), extended in commit 316c72c with-ttree view and JSON5-backed vendor/device name decoding. BAR decoding still deferred.[x]
axl_pci_cap_id_str+axl_pci_ext_cap_id_str— small lookup tables inaxl-pci.{h,c}mapping legacy and PCIe extended capability IDs to human strings. Shipped 2026-05-02 in commit bf0273d.[x]
axl_pci_bridge_info+axl_pci_tree_for_each— PCI topology API. Shipped 2026-05-02 in commit 2d91898. Test runner now injects a pcie-root-port + virtio-rng so bridge code isn’t shipped without coverage (commit 153992f).[x] PCI vendor/device/subsystem name database — initial cut shipped 2026-05-02 in commit eb38bac (
axl_pci_ids_load/axl_pci_vendor_name/axl_pci_device_name+ curatedshare/pci-ids.json5). Extended 2026-05-02 per consumer feedback into a 5-commit chain that landed: handle API + length contracts + load -1/-2 split (commit 2d30e91); subsystem lookup + iter API + Python S-line extractor (commit 938ebcb); class-string format flags viaAxlPciClassFmtenum (commit 13fa258); composed-name helperaxl_pci_format_namewith handle parity (commit ba37a8b); optional class-name overlay sidecarpci-class.json5(commit 0d97935). Same JSON5 sidecar pattern memspd uses forjedec.json5. Test runner stages the JSON5 next to test EFIs so the loaders are exercised end-to-end. Shared by lspci and downstream consumers.[x] AxlUsb — USB device enumeration via
EFI_USB_IO_PROTOCOLhandles. Shipped May 2026 in six phases: - Phase A (commitda71a2e): enumeration + vid/pid viaaxl_usb_nextcursor +axl_usb_get_vid_pid. - Phase B (commitf8e4557): interface class triplet decode (axl_usb_get_class+axl_usb_class_string_fmt) with compiled-in USB-IF Defined Class Codes tables. - Phase C (commitd39b7c2): string descriptor reads —axl_usb_get_manufacturer/_product/_serialoverUsbGetStringDescriptor+ UCS-2 → UTF-8. - Phase D (commit8a04454): vendor/device-name JSON5 sidecar (axl_usb_ids_*mirroring AxlPciIds), built on the shared AxlSidecar scaffold fromf875b36. - Phase F (commit75dcd43): faithful USB hub-port chain viaaxl_usb_tree_for_each— depth derived from the EFI device path’s USB messaging-node count, parents- before-children sort guarantee from the existing dev-key order, no second sort pass.[x]
tools/lsusb.c— Linux-style USB lister built on AxlUsb (Phase E, commit249c3c4). Default short form,-s BBB[:DDD]and-d V[:P]filters,-nnumeric,-v/-vvverbose with class triplet + string descriptors,-ttree view (real USB hub-port topology viaaxl_usb_tree_for_each).--ids-fileoverrides sidecar autodiscovery. Same--debug/-vconvention divergence as lspci.[x] AxlSidecar (commit
f875b36) — shared JSON5 sidecar scaffold (<axl/axl-sidecar.h>) consumed by AxlPciIds, AxlPciClassDb, AxlSpdIds, AxlUsbIds.axl_sidecar_open_file/_open_buffer/_check_schemaplus an internal singleton-with-atexit + foreach trampoline.AxlSidecarStatusenum (AXL_SIDECAR_OK/_FILE_MISSING/_PARSE_ERROR) replaces the prior0/-1/-2magic numbers across every load API.tools/memspd.cmigrated off its inline JEDEC loader ontoaxl_spd_ids_load.[x]
scripts/{pci,usb}-ids-to-json5.py(commit872bbf6) — bulk converters for the canonical pci.ids / usb.ids text databases. Line-level parser hoisted into shared_ids_parser.py(~200 lines) since pci.ids and usb.ids use the same tab-indented hierarchy.share/jedec.json5stays hand-curated (no canonical text database upstream). Both installed to/usr/share/axl/scripts/in the .deb/.rpm (commit85973b4); both--self-tests pass from the installed location.
Post-v0.11.0 follow-ups — vendor-neutral typed wrappers
Round-2 vendor-neutral additions surfaced from a downstream- consumer session, all retiring re-implemented patterns in consumer code with public typed wrappers:
[x]
axl_ipmi_chassis_identify(commit1264e39) — typed wrapper around IPMI Chassis 0x04 (front-panel ID LED). Replaces the rawaxl_ipmi_raw(s, 0x00, 0x04, ...)consumers were writing.[x]
axl_pci_get_header_type+axl_pci_get_subsystem(commitffc9177) — typed readers for PCI config offset 0x0E (header type + multi-function bit) and 0x2C/0x2E (SVID/SDID with Type-0 check baked in). NewAxlPciHeaderTypeenum.[x]
axl_nvstore_get_alloc(commit063f391) — read-with- malloc variant ofaxl_nvstore_getfor variable-length NV values. Probe-then-grow uses the probe rc to distinguish empty (success, 1-byte NUL allocation) from missing (-1).[x]
axl_smbios_get_oem_string(commit063f391) — convenience reader for Type 11 OEM Strings by 1-based global index across all Type 11 records.[x]
AXL_ARG_CHOICEtyped positional / flag (commit063f391) — string restricted to a caller-supplied set, with framework-side rejection +<a|b|c>value-hint help. Field appended toAxlArgDescso existing designated- initializer literals keep working.[x]
assert_in_section LABEL SECTION_MARKER PATTERN(initial commit2adc170; signature simplified in round-3 commit) — section-aware assertion helper inscripts/axl-common.shfor nsh-driven QEMU log assertions. Reads the log path from$LOG(falls back to$TEST_CLEAN_LOG) so callers don’t repeat the path.[x]
axl_stream_set_stdout_tee+axl_stream_set_stderr_tee(round-3 commit) — log-tee primitive. Newteefield on the internalAxlStreamstruct;axl_writeforwards bytes tos->teeafter the primary write. NULL clears, multiple calls replace (no chain). Replaces the ~50-line tee- callback + atexit-cleanup pattern consumers wrote per tool with a-o:<file>log option.[x]
run-qemu.sh --qemu-arg STRING(round-3 commit) — repeatable literal-token passthrough to qemu’s command line. Each STRING is shell-word-split; values with embedded spaces aren’t supported.[x]
run-qemu.sh --ipmi/--ipmi-extern SOCK/--ipmi-prop K=V(round-3 commit) — IPMI BMC simulator shortcuts at the canonical KCS port 0xca2 (matches AxlIpmi’s KCS default andtest/integration/test-ipmi-qemu.sh).--ipmi-externpaired withipmi-simis the path to verifying full BMC behavior including Chassis Identify and OEM commands on QEMU. aa64 warns and skips — no QEMU IPMI device support there.[x]
QEMU_DRYRUN=1env onrun-qemu.sh(round-3 commit) — prints CMD tokens one perQEMU_DRYRUN: <token>line and exits 0 without launching qemu. Backs the newtest-run-qemu-flags.shflag-shape regression tests.[x]
axl_smbios_get_oem_stringtruncation contract amended (round-3 commit) — gains a*requiredout param; too-small buffers now return -1 with*requiredpopulated (was: silent truncation with rc=0). Lets callers size a follow-up allocation exactly.[x]
AxlArgDesc.choices_case_insensitiveflag onAXL_ARG_CHOICE(round-4 commit) — additive per-descriptor bool that switches CHOICE validation to ASCII case-folded comparison. Default false preserves the byte-equal contract. Helps consumers migrating from CLIs that accept mixed-case variants (e.g.dd_cfg/DD_CFGboth valid). Help-line renders<a|b|c> (case-insensitive)so users know the relaxed match is in effect.[x]
axl_console_read_key+axl_console_flush_input(round-5 commit) — interactive single-keystroke read with bounded timeout in new<axl/axl-console.h>. Wraps the backend ConIn primitives + a freshly-created timer event, closed unconditionally on return. Three timeout modes (0 non-blocking /UINT64_MAXforever / millisecond bound). Unblocks any interactive UEFI tool — y/n prompts, “press any key”, arrow-key menus.[x]
axl_image_verify_signature+axl_image_signature_info_free(round-5 commit; CN extraction in round-5 follow-up) — PE Authenticode signature inspection without launching the image, in new<axl/axl-image-verify.h>. Two-axis check: presence (pure PE Certificate-Table parse, no firmware dependency) + validity (firmware dry-run viaLoadImage(SourceBuffer)+ immediateUnloadImagewhenconsult_db=true). Caller controls the security-protocol- callback side-effect cost.subject_cn/issuer_cnpopulate from the first cert in the PKCS#7 SignedData bundle via an in-tree DER walker (PrintableString / UTF8String). Best-effort diagnostic-only — not a security-decision input.[x] Vendor-neutralization sweep (commit
8d06e8f) —axl_smbios_slot_usage_str(0x05)now returns spec-canonical"Unavailable"(was"CPU NOT INSTALLED"); chassis 0x23 “Mongoose Mini PC” comments → “Mini PC” per SMBIOS 3.7;AxlIpmiCapabilities.dell_idrac_interfaceremoved (reverse- engineered GUID, no public spec). New “Spec-decoder strings are spec-canonical” rule indocs/AXL-Coding-Style.md.
Phase B2: Redfish Support — DONE (as tool, not library)
Decided against a library-level axl_redfish_* module — the existing
HTTP client + JSON APIs cover everything Redfish needs. Instead built
rfbrowse.efi as a standalone tool.
[x]
rfbrowse.efi— Redfish REST API browser (tools/rfbrowse.c)[x] Session auth (POST → X-Auth-Token) and Basic auth
[x] URI shortcuts (systems, thermal, power, chassis, etc.)
[x] Collection member listing (–members, –expand)
[x] Colored JSON pretty-print and raw mode
[x] Python mock server + 12 integration tests (test-redfish.sh)
No library module needed — rfbrowse uses axl_http_client + axl_json directly. If a library API is needed later (ipmitool, SoftBMC), extract the ~50 lines of session management then.
Async Work Phases (Future)
Phase A1: AxlBufPool — preallocated buffer pool — DONE
[x]
axl_buf_pool_new(count, buf_size)— allocate pool of fixed-size buffers[x]
axl_buf_pool_get(pool)— grab a free buffer (NULL if exhausted)[x]
axl_buf_pool_put(pool, buf)— return buffer to pool[x]
axl_buf_pool_available(pool)— number of free buffers[x]
axl_buf_pool_buf_size(pool)— query buffer size[x]
axl_buf_pool_free(pool)— release pool and all buffers[x] Zero-copy design: LIFO free-stack, no memcpy on get/put
[x] 18 unit tests (basic, exhaustion, distinct, LIFO order, NULL safety)
Phase A2: AxlAsync — AP-offloaded async work queue — DONE
Bridges AxlTask (AP core dispatch) with AxlLoop (main loop events). Enables offloading CPU-heavy work to Application Processors while the BSP continues servicing the main loop (network, timers, UI).
[x]
axl_async_init(max_pending)— initialize with configurable queue depth[x]
axl_async_submit(loop, work_fn, data, arena, done_cb)— dispatch work_fn to an AP core, fire done_cb on the BSP when complete[x] Idle source polls
axl_task_pool_poll(), auto-removed when idle[x] Automatic single-core fallback: runs work_fn + done_cb synchronously
[x] Cancellation:
axl_async_cancel(handle)— best-effort (suppresses done_cb)[x]
axl_async_pending()— query in-flight job count[x]
axl_async_shutdown()— drain and free[x] 13 unit tests (init, submit, loop integration, cancel, pending, NULL safety)
File transfer example (firmware update):
AxlBufPool *pool = axl_buf_pool_new(4, 64 * 1024); // 4 x 64KB
// HTTP handler receives chunks on the BSP:
void on_chunk_received(void *chunk_data, size_t len, void *ctx) {
void *buf = axl_buf_pool_get(pool); // grab free buffer
axl_memcpy(buf, chunk_data, len); // copy into pool buffer
TransferCtx *tc = make_ctx(buf, len, offset);
axl_async_submit(loop, verify_and_stage, // runs on AP
tc, on_chunk_done, tc); // callback on BSP
// returns immediately — BSP keeps accepting connections
}
// Runs on AP core (no Boot Services access):
void verify_and_stage(void *arg) {
TransferCtx *tc = arg;
tc->crc = crc32(tc->buf, tc->len); // CPU-heavy work on AP
tc->status = validate_chunk(tc);
}
// Fires on BSP main loop after AP completes:
void on_chunk_done(void *arg) {
TransferCtx *tc = arg;
if (tc->status == OK) {
flash_write(tc->offset, tc->buf, tc->len); // BSP: Boot Services OK
}
axl_buf_pool_put(pool, tc->buf); // return buffer to pool
free(tc);
}
Double-buffer ping-pong pattern:
BSP receives data into buffer A, submits A to AP for processing
BSP starts receiving into buffer B while AP works on A
AP finishes A → done_cb fires → BSP submits B, starts receiving into A
Maximizes throughput: network I/O and computation overlap
AP constraints in UEFI:
APs cannot call Boot Services (only BSP can)
APs can do: memcpy, checksum, CRC, crypto, decompression, parsing
BSP handles: network I/O, file I/O, flash writes, protocol calls
ARM: cache flush needed for shared buffers (x86 is coherent)
Consumer projects:
SoftBMC: firmware update endpoint, bulk SMBIOS collection
axl-webfs: WebDAV PUT (large file writes to UEFI filesystem)
uefi-devkit: image deployment
Phase A3: AxlDefer — deferred work queue — DONE
BSP-only work queue drained by the main loop on each tick. Allows code in constrained contexts (protocol notifications, nested callbacks, interrupt-like handlers) to schedule work for “later this tick” without blocking or re-entering the loop.
[x]
axl_defer(loop, fn, data)— enqueue work (function + context pointer)[x]
axl_defer_cancel(loop, handle)— remove pending work before it fires[x] FIFO ordering: work fires in submission order
[x] Loop integration: queue drained at the start of each loop iteration, before timer/event sources are checked
[x] Fixed-capacity ring buffer (no malloc in the hot path)
[x] 8 unit tests (basic, cancel, FIFO order, re-entrant, null safety)
Example: protocol notification handler
// This runs in a LocateProtocol notify context — can't do complex
// work here, but can schedule it for the next loop tick:
void on_protocol_installed(void *ctx) {
axl_defer(loop, initialize_new_protocol, ctx);
}
// Runs safely on the next main loop iteration:
void initialize_new_protocol(void *ctx) {
locate_and_configure(ctx); // Boot Services OK here
start_polling_timer(ctx);
}
Phase A4: AxlPubsub — publish/subscribe event bus — DONE
Decouples event producers from consumers. Modules publish on named topics, other modules subscribe with callbacks. The main loop dispatches subscriber callbacks (via AxlDefer) so handlers run in a safe context.
Renamed from axl_signal_* to axl_pubsub_* pre-1.0 to free the
axl_signal_* namespace for the POSIX-style interrupt API (see
Phase A7).
[x]
axl_pubsub_register(loop, name)— register a named topic[x]
axl_pubsub_subscribe(loop, name, callback, data)— subscribe[x]
axl_pubsub_unsubscribe(loop, handle)— unsubscribe by handle[x]
axl_pubsub_publish(loop, name, event_data)— publish (deferred delivery)[x] Multiple subscribers per topic (linked list, order-independent)
[x] Payload: opaque
void *passed to all subscribers[x] Auto-create topics on first subscribe or publish
[x] 13 unit tests (basic, multi-sub, unsubscribe, unknown, auto-create, user_data)
[ ] Optional: typed variants with compile-time checked payloads
Example: network state change
// Network module publishes when IP changes:
axl_pubsub_publish(loop, "ip-address-changed", &new_ip);
// Splash screen subscribes:
axl_pubsub_subscribe(loop, "ip-address-changed", splash_update_ip, NULL);
// REST API subscribes independently:
axl_pubsub_subscribe(loop, "ip-address-changed", api_update_endpoint, NULL);
// Both handlers fire on the next loop tick — neither knows
// about the other. Adding a third subscriber (e.g., mDNS
// announcer) requires zero changes to the network module.
Consumer projects:
SoftBMC: decouple modules (network → splash, EC → sensors → REST API)
axl-webfs: filesystem mount/unmount notifications
Any multi-module UEFI application built on AXL
Phase A5: AxlEvent foundation + sync-primitive reorg — DONE
Promoted AxlEvent to a first-class struct wrapping EFI_EVENT with
signalled/reset state. Relocated the sync primitives (Cancellable,
Wait) out of src/util/ into a dedicated src/event/ module.
AxlCompletion collapsed into AxlEvent (structurally identical;
UEFI-native name wins over the Linux-kernel-struct-completion echo).
Framing (chosen April 2026, executed April 2026): D + III.
Directory named src/event/ after its foundational type; the word
overload between “event loop” and “AxlEvent” embraced explicitly in
docs (“an AxlEvent is a one-shot latch backed by a UEFI event; the
event loop dispatches them”).
[x] New
src/event/module.axl-event.{c,internal.h}new;axl-cancellable.{c,internal.h}andaxl-wait.{c,internal.h}moved fromsrc/util/. Newsrc/event/README.mdis now the prose home for the three primitives.[x] Promoted
AxlEventto a proper struct ininclude/axl/axl-event.h. Public API:axl_event_new/free/ signal/reset/is_set/handle/wait/wait_timeout. The rawvoid *typedef inaxl-loop.hremoved; raw handle type renamed toAxlEventHandleand promoted to the public header (was internal insrc/backend/axl-backend.h).[x] Collapsed AxlCompletion into AxlEvent.
axl-completion.hdeleted;AxlCancellablekept as a typed contract wrapper composingAxlEvent *with the magic-number UAF guard.[x] Sphinx update.
docs/sphinx/modules/async.rstrenamed toevent.rst;axl-event.hdoxygenfile block added;index.rsttoctree updated.[x] Side cleanup.
src/task/axl-arena.cmoved tosrc/mem/. Arenas are allocators, not task/offload primitives.[x] CHANGELOG entry for the breaking migration (
AxlCompletion→AxlEvent, raw-handle API removal,<axl/axl-completion.h>→<axl/axl-event.h>).[x] Updated
CLAUDE.mdmodule table + Project Layout tree.[x] Plan deviation (documented). Original plan had
axl_loop_add_eventtakeAxlEvent *. That would have forcedsrc/net/to wrap every firmware-owned completion token in an AxlEvent struct — semantically wrong and adapter overhead on every async op. Corrected to keep the entry taking anAxlEventHandleand exposingaxl_event_handle(e)as the extractor for AXL-managed events.[x] Verify: X64 + AARCH64 tests 1302/1302 passing (up from 1295 — new AxlEvent surface tests); event-demo + cancellable-demo clean in QEMU, no leaks.
Phase A6: Concurrency Model documentation — DONE
Landed after A5. Single authoritative doc telling users which synchronization primitive to reach for.
[x] New
docs/AXL-Concurrency.md. Four-axis taxonomy table (dispatch / coordination / notification / offload) with a loop-integration column, decision guide (“I need to… → use…”), the word-overload disclaimer (“event” = loop + source + AxlEvent type), and comparison with adjacent ecosystems (GLibGMainLoop+GCancellable, Python asyncio, libuv, Linux kernelstruct completion, C++std::latch).[x] Cross-linked from
docs/AXL-Design.md(after §API Overview),docs/AXL-SDK-Design.md(§Async-op cancellation), andsrc/event/README.md(after §When to use what).[x] Sphinx guide page:
docs/sphinx/guides/concurrency.rst.. include::of the markdown file; added to the Guides toctree inindex.rstbetween Design and Coding Style.[x] Explicitly documented the “why not” positions (GIL, stackful coroutines, protothread macros, macro-async/await) so future contributors don’t re-litigate them.
Phase A7: AXL runtime — lifecycle services (signals, yield, atexit, default loop) — DONE
Status: landed April 2026 as seven commits on main
(3789aea…4368256). See docs/AXL-Lifecycle.md
(status: implemented) and src/runtime/.
Since AXL controls every public API, we approximate Linux-style
signal responsiveness cooperatively — axl_yield() in tight app
loops, a centralized break handler invoked on Ctrl-C, and a POSIX-
flavored axl_signal_install / axl_atexit / axl_exit surface
for ergonomics. Full preemption is not reachable under UEFI BSP;
CPU-bound code that ignores AXL APIs remains uninterruptible –
documented as such.
What landed:
[x] CRT0 (
src/crt0/axl-crt0-native.c) calls_axl_init(ImageHandle, SystemTable)->main->_axl_cleanup._axl_init/_axl_cleanupnow live insrc/runtime/axl-runtime.c.[x]
axl_loop_default()— singleton, lazy-created on first call, freed during_axl_cleanup.[x] Shell break-flag + break-event detection in
axl_loop_next_eventandaxl_yieldcalls_axl_signal_on_break, which setsg_axl_interruptedand invokes the user handler exactly once.[x]
axl_signal_install/axl_signal_default/axl_interrupted/axl_exitpublic API atinclude/axl/axl-signal.h(namespace freed pre-landing by renaming pub/sub toaxl_pubsub_*in PR #1).[x]
axl_yield()public API. Non-blocking default-loop dispatch; pollsaxl_backend_shell_break_flagdirectly when no default loop exists so yield-only apps still observe Ctrl-C. Default-policy auto-exit viaaxl_exit(1)when no user handler is installed.[x]
axl_atexit(fn, data)/axl_atexit_remove(handle)–include/axl/axl-atexit.h, LIFO drain during_axl_cleanupbefore the registry sweep.[x] Tier-1 firmware-resource registry –
src/runtime/axl-registry.c. AxlArray-backed + monotonic seq for true LIFO sweep; always on (design §9).[x]
axl_exit(rc)as the blessed exit path. NORETURN. Runs atexit + sweep, thenaxl_backend_boot_exit(rc)->gBS->Exit. Both return-from-main and explicitaxl_exitconverge on_axl_cleanup; output is byte-identical.[x]
AxlArenaregistered as tier-1. Arena sub-allocations bypass individual tracking by design; the arena itself carries the registry entry.[x] Caller attribution via macro shims on
axl_event_new,axl_loop_new,axl_cancellable_new,axl_arena_new. Sweep warnings name the user’s call site (or library call site for library-internal allocations — which correctly freed never reach the sweep anyway).[x]
axl_loop_iterate_until(nested-wait primitive, design §5.6) — lets callers inside a loop callback wait on an event without freezing the outer loop’s other sources.[x]
runtime-demo.c— 8 subcommand scenarios covering every facet, validated on X64 + AARCH64.[x]
test/unit/axl-test-runtime.c(AxlTestRuntime) — 16test_checkcalls covering atexit, registry, yield, interrupted, signal-install.[x] Cooperative-concurrency caveat documented in docs/AXL-Lifecycle.md §11 and docs/AXL-Concurrency.md.
Deferred to a future phase (both captured in docs/AXL-Lifecycle.md §10):
[ ] Release-mode heap auto-sweep.
mAllocListexists only underAXL_MEM_DEBUGtoday; making release-build sweeps possible costs ~16 bytes per allocation. Implement when a long-running app like SoftBMC or persistent-service axl-webfs needs the firmware-pool safety net. Short-lived tool apps don’t benefit — firmware reboot reclaims pool memory.[ ] Watchdog opt-in (
axl_watchdog_enable(seconds)) – library-livelock guard, not a signal mechanism. No concrete caller has asked for it yet.
Design decisions locked in (see design doc §7, §9):
No
longjmpfrom break notify — async-signal-unsafety.No UEFI watchdog repurpose — reset-only on every platform.
No NMI or platform-specific preemption hooks.
CPU-bound no-AXL code is documented as uninterruptible, not papered over.
Phase D1: AxlRadixTree — compact prefix tree — DONE
Radix tree (compact prefix tree) with edge splitting, longest-prefix lookup, and foreach iteration. Used internally to replace the HTTP server’s fixed 32-route array with O(k) route matching.
[x]
axl_radix_tree_new/axl_radix_tree_new_full— create tree[x]
axl_radix_tree_insert— insert with automatic edge splitting[x]
axl_radix_tree_lookup— exact key match[x]
axl_radix_tree_lookup_prefix— longest-prefix match (key feature)[x]
axl_radix_tree_remove— remove with node collapse[x]
axl_radix_tree_foreach— depth-first iteration with key reconstruction[x] HTTP server route table refactored to use radix tree
[x] 50 unit tests (insert, lookup, prefix, edge split, foreach, value_free, HTTP keys)
Phase D2: AxlRingBuf — layered ring buffer — DONE
Byte-oriented ring buffer (kfifo-inspired) with three API layers:
[x] Layer 1 (Bytes): push, pop, peek, discard, zero-copy scatter/gather regions
[x] Layer 2 (Messages): push_msg, pop_msg, peek_msg (variable-size, length-prefixed)
[x] Layer 3 (Elements): push_elem, pop_elem, peek_elem, peek/set_nth_elem (fixed-size)
[x] Power-of-2 sizing with monotonically increasing uint32_t indices
[x] Reject-on-full (default) and overwrite-on-full modes
[x] Struct exposed for embedding (init/deinit, no heap required)
[x] Custom buf_free callback for pluggable deallocators
[x] Refactored AxlDefer to use embedded AxlRingBuf (element API)
[x] Refactored AxlLogRing to use embedded AxlRingBuf (element API + axl_backend_free)
[x] 56 unit tests (bytes, wrap, overwrite, peek, regions, elements, messages, init, user buffer)
Graphics Phases (Future)
Phase G1: Graphics Output Protocol support
[x] GOP types in uefi-manifest.json5 (extracted from spec)
[x] AxlGfx module: basic framebuffer ops (fill, blit, capture)
[x] gfx-demo.c example
[x] Bitmap font renderer (8x16 VGA font, scalable)
[x] Text drawing API (axl_gfx_draw_text)
Phase G2: AGL (AximCode Graphics Library) — not started, separate project
GTK-like widget toolkit built on AxlGfx. Would be a separate repo. Blocked on a consumer need (SoftBMC local UI is the first candidate, but SoftBMC hasn’t migrated to AXL yet — see Phase 10).
[ ] Basic widgets: label, button, panel, list
[ ] Layout engine (vertical/horizontal box)
[ ] Input handling (keyboard + pointer via UEFI protocols)
[ ] Theming / color scheme support
Shell Integration Phases (Future)
Phase S5: Environment and working directory — DONE
[x]
axl_getenv(name)/axl_setenv(name, value, overwrite)/axl_unsetenv(name)[x]
axl_get_current_dir()/axl_chdir(path)[x] Type
GetEnv,SetEnv,GetCurDir,SetCurDir,Executein EFI_SHELL_PROTOCOL[x] Backend:
axl_backend_shell_getenv/setenv/getcwd/chdir/executein all 3 backends[x] 10 unit tests (set, get, overwrite, unset, missing, cwd, chdir)
Phase S6: System operations — DONE
[x]
axl_reset(type)— system reset (AXL_RESET_COLD/WARM/SHUTDOWN)[x]
axl_map_refresh()— rescan device mappings via Shell “map -r”[x]
axl_driver_load/start/connect/disconnect/unload— driver lifecycle
Phase S7: Socket abstraction layer — DONE
GLib-style socket layer wrapping existing AxlTcp/AxlUdp. Unifies inconsistent address handling (hostname strings, AxlIPv4Address, raw bytes) behind a clean API.
[x] AxlInetAddress — IPv4 address with parsing, formatting, comparison
[x] AxlSocketAddress — address + port pair, interop with AxlIPv4Address
[x] AxlSocket — unified stream/datagram socket, delegates to TCP/UDP
[x] AxlSocketClient — high-level DNS + connect helper
[x] Async variants: connect_async, accept_async, send_start, receive_start
[x] Tests: 12 address tests (no network) + 6 socket tests (network)
Configuration Framework (Future)
Phase CF1: AxlConfig — unified options system — DONE
Typed configuration framework with descriptors, auto-apply, and command-line parsing. Used by HTTP client, HTTP server, and available to consumer apps.
static const AxlConfigDesc descs[] = {
{ "timeout.ms", AXL_CFG_UINT, "10000", 't', "Per-operation timeout", 0, 0 },
{ "keep.alive", AXL_CFG_BOOL, "true", 'k', "Reuse connections", 0, 0 },
{ "port", AXL_CFG_UINT, "8080", 'p', "Listen port", 0, 0 },
{ 0 }
};
AxlConfig *cfg = axl_config_new(descs, NULL, NULL);
axl_config_set(cfg, "timeout.ms", "30000"); // programmatic
axl_config_parse_args(cfg, argc, argv); // command-line
size_t timeout = axl_config_get_uint(cfg, "timeout.ms");
[x]
AxlConfigtype with typed get/set (get_uint,get_bool,get_string)[x] Option descriptors with type, default, description
[x]
axl_config_parse_args— populate from argc/argv[x]
--helpgeneration from descriptors (viaaxl_config_usage)[x] Type validation on set
[x] Embed in HTTP client and HTTP server (
http_client_descs,http_server_descs)[x] Option cascade: command-line overrides config overrides defaults
[x] Unify
axl_args_*andaxl_config_*into a single API —axl_args_*removed; every tool, test, and example now usesAxlConfigDescfor both config and CLI.
Tools
UEFI command-line utilities built on AXL, plus host-side developer tools.
UEFI Tools (tools/)
[x] hexdump.efi — hex/ASCII file viewer
[x] fetch.efi — HTTP client (curl-like)
[x] find.efi — recursive file finder
[x] grep.efi — pattern search
[x] sysinfo.efi — system inventory (firmware, SMBIOS, memory)
[x] netinfo.efi — network diagnostics and ping
[x] mkrd.efi — RAM disk management
[x] rfbrowse.efi — Redfish REST API browser
[x] lspci.efi — Linux-style PCI lister (Phase B3 R+5; tree view, JSON5-backed vendor/device names, –bridges in run-qemu.sh)
[x] lsusb.efi — Linux-style USB lister (Phase E of AxlUsb, commit
249c3c4; tree view viaaxl_usb_tree_for_each, JSON5-backed vendor/device names, qemu-xhci + usb-mouse + usb-hub + usb-tablet topology in common-test.sh)[x] memspd.efi — DDR4/DDR5 SPD reader on AxlSpd (Phase B3, JEDEC ids JSON5 sidecar via
axl_spd_ids_*)[x] cat.efi — concatenate files to stdout
Host Tools (scripts/)
[x] rsod-decode.py — UEFI crash dump (RSOD) decoder with MAP file support
[x] run-qemu.sh
--interactive+--mount— interactive UEFI shell with virtiofs host-fs mount (v0.2.8)[x] axl-sdk-host-tools tarball + .deb — packaged for downstream consumers without source clone (v0.2.9)
[ ] Fold
qemu_launchintorun-qemu.shas a daemon-lifecycle mode. uefi-devkit’scommon.shcurrently providesqemu_launch {start|stop|status|logs}— a long-running QEMU lifecycle manager used by softbmc, videoterm, uefi-ipmitool for BMC/server-style components. run-qemu.sh’s--backgroundmode is bare-bones (just emits PID/SOCKET to stdout). Plan: addrun-qemu.sh --start --name=foo,--stop --name=foo,--status --name=foo,--logs --name=foowith the same file-layout conventionqemu_launchuses (build/qemu/.{pid,sock,log,qcow2,vars.fd}). Then convert uefi-devkit’s qemu_launchto a thin wrapper around the host-toolsrun-qemu.sh, retiring the duplicate QEMU-invocation code path.Migration: v0.3.x once consumers (softbmc, videoterm, uefi-ipmitool) have been updated to reference the consolidated invocation. axl-common.sh stays put as the shared discovery library — no churn for downstream.
Hardware Fixture Capture & Replay (Future)
Vendor-neutral capture-and-replay of UEFI platform identity
(SMBIOS, ACPI, PCI/USB/video manifests, SPD, TPM, NVRAM/Secure
Boot, ESRT, CPU, network details, Redfish, IPMI) so axl-sdk tools
can be exercised against real-world platforms under QEMU without
lab access. New dedicated UEFI capture tool (tools/mkfixture.c
→ mkfixture.efi, mirroring the mkrd.efi naming pattern) writes
a fixture directory; axl-emulate <fixture-dir> (Python wrapper
around run-qemu.sh) replays it. Keeping replay in a separate
tool stops run-qemu.sh from ballooning as HF4–HF8 layer in
USB shims, EDID injection, CPU mapping, TPM seeding, Secure Boot
vars, Redfish mock, IPMI sim, etc.
Reuses the existing
scripts/qemu-patches/ infrastructure
(originally added for AxlSpd’s wire-path test) for command-line
device injection, plus host-side daemons (swtpm, DMTF Redfish
Mockup-Server, OpenIPMI ipmi_sim) wired by lifecycle code modeled
on the existing virtiofsd handling.
Full design: AXL-Hardware-Fixture-Design.md.
Phase HF1: run-qemu.sh low-level flags
[ ]
--smbios-file FILE→-smbios file=FILE[ ]
--acpi-table FILE(repeatable) →-acpitable file=FILE[ ]
--spd ADDR:FILE(repeatable) →memory-backend-file+smbus-eeprom,memdev=(depends on existing patched QEMU; probe and error clearly when absent; aa64 warn-and-skip)[ ]
--tpm/--tpm-state DIR/--tpm-model tpm-tis|tpm-crb|tpm-tis-device→ spawn swtpm (raw state passthrough; captured-fixture seeding deferred to HF5) and wire-tpmdev emulator+ tpm device. Arch-aware default model (tpm-tis on x64, tpm-tis-device on aa64; tpm-crb is x86-only). swtpm absent on PATH ⇒ hard error with install hint.[ ] NOT a new flag: IPMI is already covered by the existing
--ipmi/--ipmi-extern/--ipmi-propin run-qemu.sh; no--ipmi-simalias added.[ ] Hand-craft first fixture from the Proxmox dev VM (
dmidecode --dump-bin,acpidump -b) to validate replay path before writing the capture tool.
Phase HF2: mkfixture.efi (manifest-grade UEFI walks)
Phase HF2 ships in slices — HF2.1 covers the smallest viable fixture (SMBIOS + ACPI + manifest); HF2.2+ adds the per-protocol JSON manifests; HF2.3 adds alternative write targets.
HF2.1 — DONE (commit 46a326c):
[x] New tool
tools/mkfixture.c→mkfixture.efi(separate fromsysinfo— capture is a binary-blob writer, not an inventory display, and conflating them muddies both; cross-tool sharing happens at the library layer)[x] Dump SMBIOS3 raw bytes via EFI Config Table
[x] Walk ACPI RSDT/XSDT, write each table as
acpi/<sig>.dat[x] Write
manifest.json(vendor, model, BIOS rev/date, capture-tool version, fixture format)
HF2.2 — DONE:
[x] CPU capture: direct CPUID (x86) / MIDR_EL1 (aa64), write
cpu.json(vendor, family/model/stepping, brand string, feature words). Future axl-emulate--cpu-from-fixturemaps to a QEMU-cpu MODELchoice.[x] ESRT capture: read EFI Config Table
EFI_SYSTEM_RESOURCE_TABLE_GUID, writeesrt.json(per-component FwClass GUID + version + last-attempt status)[x] New public API
axl_efi_find_config_table(axl-runtime.h) so tools can do one-shot config-table lookups without duplicating the EFI Configuration Table walk
HF2.3 — TODO (manifest expansion):
[ ] Enumerate PCI via
EFI_PCI_IO_PROTOCOL, writepci.json(VID/DID/class/subsys/BARs) — manifest only, not replayed[ ] Walk USB via
EFI_USB_IO_PROTOCOL+EFI_USB2_HC_PROTOCOL, writeusb.json(topology, VID/PID, class/subclass/protocol, strings) and per-device descriptor blobs inusb/*.bin[ ] Walk GOP/EDID via
EFI_GRAPHICS_OUTPUT_PROTOCOL+EFI_EDID_DISCOVERED_PROTOCOL, writevideo.json(mode list, current mode, FB base, pixel format) andedid/*.binper display; GPU option ROM (gpu-rom/*.bin) on-demand only[ ] Network details: per-NIC MAC + link state via
EFI_SIMPLE_NETWORK_PROTOCOL, SR-IOV VF count from PCIe extended config, writenet.json[ ] NVMe capture: per-controller Identify Controller / Identify Namespace via
EFI_NVM_EXPRESS_PASS_THRU_PROTOCOL, writenvme/<bdf>.json(manifest only — replay is HF9 patch candidate)
HF2.4 — TODO (alternative write targets):
[ ] Support write targets: local FS (
fs0:\fixtures\...) is the default and shipped in HF2.1; add virtiofs--mountand HTTP POST
Phase HF3: scripts/axl-emulate (replay wrapper)
New Python tool, ships in host-tools tarball. Wraps run-qemu.sh
rather than extending it — keeps run-qemu.sh focused on QEMU
launching primitives, gives fixture-replay logic its own home so
HF4–HF8 doesn’t balloon run-qemu.sh further.
axl-emulate <fixture-dir> [efi-file] [args...]
[--keep-acpi NAME] [--drop-acpi NAME] [--strict-acpi]
[--arch X64|AARCH64]
[-- run-qemu-args...]
[ ]
scripts/axl-emulate(Python, no extension;#!/usr/bin/env python3) — auto-discover and translatesmbios.bin,acpi/*.dat,spd/*.bin,tpm/(presence triggers--tpm-state) into the corresponding run-qemu.sh primitives[ ] Default-drop ACPI tables that describe captured-platform topology incompatible with QEMU (
MCFG,MPST,PMTT,HMAT,SLIT,SRAT,SPCR,DBG2); detection via 4-byte signature read from each.dat[ ]
--keep-acpi NAME/--drop-acpi NAME/--strict-acpioverrides for the denylist[ ] Print
manifest.jsonsummary at startup if present so user sees which machine the guest is impersonating[ ] Warn (not block) when fixture metadata disagrees with
--arch[ ] Pass-through
--separator: anything after lands as additional run-qemu.sh args (compose with--background,-i,--mount,--gdb, etc.)[ ] Future-phase wiring (NOT in HF3 scope) —
--usb-shim(HF4),--edid/--gpu-rom(HF4),--cpu-from-fixture/--mac(HF4), TPM event-log seeding (HF5), Secure Boot / boot-vars injection (HF6), Redfish mock spawn (HF7), IPMI sim spawn (HF8) all land in axl-emulate as their phases ship.
Phase HF4: SPD capture
[ ] Add SPD walk to
mkfixture(NOT extendingmemspd, which stays an inspection tool — same separation argument as sysinfo-vs-mkfixture). Dump every populated SMBus EEPROM at 0x50–0x57 tospd/0xNN.bin[ ] Validate: capture on a real box, replay via Phase HF3
--fixturepath, AxlSpd output should match bit-for-bit[ ] Document SmbusHcShim.efi requirement on QEMU + native ICH/PCH driver expectation on real Intel platforms
Phase HF5: TPM capture & replay
[ ] Capture: PCR values (SHA-1 + SHA-256 banks), capabilities, manufacturer ID, firmware version via
EFI_TCG2_PROTOCOL; writetpm.json[ ] Capture: full TCG event log via
EFI_TCG2_PROTOCOL.GetEventLog(); writetpm/event-log.bin[ ] Replay: spawn
swtpmwith seeded state directory, wire-tpmdev emulator+tpm-tis(default) ortpm-crbper fixture; lifecycle modeled on virtiofsd[ ] Document caveat: seeded PCRs reflect source platform; replay guest’s OVMF measurements diverge by design
Phase HF6: UEFI Variable injection (Secure Boot + boot order)
[ ] Capture: walk
EFI_GLOBAL_VARIABLEGUID forBoot####,BootOrder,BootCurrent,BootNext,Timeout; writevars/global/<name>.bin[ ] Capture: PK / KEK / db / dbx via Variable Services; write
vars/secureboot/{PK,KEK,db,dbx}.bin(rawEFI_SIGNATURE_LISTbytes)[ ] Replay:
--secureboot DIRand--boot-vars DIRinject into OVMFvars.fdcopy before QEMU launch (likely viavirt-fw-varsfrom libvirt)[ ] Detect non-secboot OVMF (default
OVMF_CODE.fdignores Secure Boot vars) and warn clearly when--securebootis given against it
Phase HF7: Redfish capture & replay
[ ] Capture: walk service root via axl HTTP client, save JSON tree mirroring
/redfish/v1/...[ ] Replay:
--redfish-mock DIRspawns DMTFRedfish-Mockup-Server, adds--hostfwd <port>:443(lifecycle modeled on virtiofsd)[ ] Reference / validation: OpenBMC firmware running in a sibling QEMU instance — real bmcweb stack, no lab hardware
[ ]
--openbmc-qemu PATHflag for direct OpenBMC sibling launch
Phase HF8: IPMI capture & replay
[ ] Capture: in-band KCS sweep (Get-* commands), write raw response bytes to
ipmi/<cmd>.bin[ ] Replay:
--ipmi-extern PATHwires QEMU’sipmi-bmc-externto OpenIPMI’sipmi_simseeded with captured replies[ ] Standard commands only — no vendor OEM in the initial pass
Phase HF9: Additional QEMU device-injection patches
Add as future fixture artifacts demand. The existing
0001-smbus-eeprom-add-memdev-link.patch is the canonical example
of the pattern. Likely candidates:
[ ] SMBIOS handle preservation for OEM Type-N injection
[ ] Non-EEPROM SMBus sensors (LM75-style temp, fan controllers)
[ ] IPMI FRU storage seeding for
ipmi-bmc-sim[ ]
usb-stubdevice for non-class-compliant USB descriptor replay (enables “device appears in bus walk” tests)[ ] ESRT publication: a QEMU pseudo-device that publishes a captured
esrt.jsonas the EFI_SYSTEM_RESOURCE_TABLE config table at boot[ ] NVMe Identify Controller replay: extend
-device nvme(or add a sidecar device) to source Identify Controller / Identify Namespace responses from a captured blob
Phase HF10: Sanitization & public fixtures
[ ]
--sanitizeflag (or post-processor): zero serials/asset tags, replace MACs with locally-administered ranges, review OEM strings for proprietary data[ ] Decide: sibling repo
aximcode/axl-fixturesvs. local-only[ ] Contributor sanitization-review process (if going public)
Documentation Phases (Future)
Phase D1: API reference
[x] Sphinx+Breathe auto-generates API docs from header comments
[x]
docs/AXL-API-Reference.mdremoved (redundant with generated docs)[x] Add examples to each module section (in src/*/README.md, included by Sphinx)
Phase D2: Generated documentation — DONE
[x] Doxyfile + Sphinx + Breathe for HTML/man generation
[x] 17 module pages with prose overviews, code examples, UEFI glossary
[x] CI integration: auto-deploy to axl.aximcode.com on push (Cloudflare Pages)
[x] Man pages generated for all modules
[x] Landing page: version, license, header, source metadata
[x] Guides section: Getting Started, Design, Coding Style, SDK, Porting, Roadmap
[x] Shared Types reference page (all callback types indexed)
[x] Design docs (AXL-Design.md, etc.) integrated into Sphinx sidebar
Platform Abstraction (Future — coreboot support)
Separate UEFI-specific code from platform-agnostic code so AXL can target coreboot (and potentially other firmware environments) in addition to UEFI.
Current state: 29 of 47 source files are already platform-agnostic.
18 files make direct UEFI calls (87 call sites) outside the backend
abstraction layer. The backend header (src/backend/axl-backend.h)
defines the abstraction API but not all modules use it consistently.
Phase P1: Audit and classify modules
Categorize every source file:
Core (platform-agnostic): mem, format, data, str, string, json, cache, list, slist, queue, hash-table, args, config, hexdump, log-ring, defer, signal, arena, buf-pool, url, http-middleware
Backend-abstracted (uses backend API, not UEFI directly): io, log, loop, time, task-pool, async, io-buf, io-file, log-file
UEFI-coupled (calls gBS/gRT/gST/protocols directly): tcp, udp, net-util, http-server, http-client, http-core, gfx, mem (pages), driver, nvstore, service, smbios, sys, app, tls, mbedtls-platform
[ ] Document the classification in a table
[ ] Identify which UEFI calls in coupled modules should become backend functions vs. staying in a UEFI platform module
Phase P2: Expand backend abstraction
Move direct UEFI calls behind new backend functions:
[ ]
axl_backend_locate_protocol(guid, interface)— wraps gBS->LocateProtocol, LocateHandleBuffer, HandleProtocol[ ]
axl_backend_alloc_pages(count, phys_addr)— wraps gBS->AllocatePages/FreePages[ ]
axl_backend_create_event(type, callback, ctx, event)— wraps gBS->CreateEvent/CloseEvent/CheckEvent/SignalEvent[ ]
axl_backend_install_protocol(handle, guid, interface)— wraps gBS->InstallProtocolInterface/UninstallProtocolInterface[ ]
axl_backend_get_variable / set_variable— wraps gRT->GetVariable/SetVariable[ ]
axl_backend_exit(status)— wraps gBS->Exit
Networking is the largest task: TCP, UDP, and HTTP use UEFI protocol calls extensively (service binding, completion tokens, Poll, Configure). Options: a) Abstract each protocol behind a backend socket API b) Keep networking as a UEFI-only module, provide a separate coreboot networking module later (Linux socket API) c) Define a portable socket API in the backend, implement for UEFI (TCP4/UDP4) and coreboot (Linux sockets) separately
Option (c) is cleanest but most work. Option (b) is pragmatic.
Phase P3: Split source tree
Reorganize into platform-agnostic and platform-specific directories:
src/
core/ ← platform-agnostic (mem, str, data, format, log, etc.)
platform/
uefi/ ← UEFI backend + UEFI-specific modules
coreboot/ ← future: coreboot backend
net/ ← networking (may stay UEFI-specific initially)
[ ] Move core modules to src/core/
[ ] Move UEFI-specific code to src/platform/uefi/
[ ] Update Makefile, install.sh, and header paths
[ ] Verify all builds and tests pass
Phase P4: coreboot backend stub
[ ] Create
src/platform/coreboot/axl-backend-coreboot.c[ ] Implement core backend functions (console, memory, time)
[ ] Build
libaxl-core.a(platform-agnostic subset)[ ] Test core modules on coreboot (or Linux as a proxy)
Dependencies: This is a large architectural change. Should be done after the API stabilizes (post-1.0) to avoid churn during the refactor. The backend abstraction layer was designed for this split from the beginning.
Estimated effort: P1 (1 day), P2 (3-5 days), P3 (2-3 days), P4 (3-5 days). Total: ~2-3 weeks.
C++ Bindings — axlmm (Future)
Sibling C++ wrapper library over the C public surface, modeled on
GLib’s glibmm. The C library remains canonical; axlmm is a
thin, optional layer for consumers who prefer C++ ergonomics
(RAII, exceptions, range-based for, std::string_view) without
requiring the C surface to grow C++-aware.
Why a sibling library, not a rewrite:
Most UEFI tooling is plain C; the C library carries no C++ toolchain, runtime, or ABI cost for those consumers.
C++ consumers (UEFI test harnesses, vendor diagnostic tools that already use C++, future axl-sdk-built apps with richer object models) get an idiomatic surface without losing access to the C primitives.
GLib/glibmm precedent — independently versioned, additive, never blocks the C library’s release cadence.
Scope (in):
RAII wrappers for handle-bearing types: AxlStream, AxlEvent, AxlCancellable, AxlHashTable, AxlArray, AxlList, AxlSlist, AxlQueue, AxlStrBuf, AxlHttpServer, AxlHttpClient, AxlPciIds, AxlUsbIds, AxlSpdIds, AxlSidecar, AxlArena, AxlBufPool, AxlRingBuf, AxlRadixTree, AxlCache.
std::string_view/std::span<const std::byte>friendly overloads where the C API takesconst char *, size_torconst void *, size_t.Exception-throwing variants (
axlmm::Exceptionderived fromstd::runtime_error) of the error-returning C functions, gated per call site (the underlying C function stays available for consumers that prefer error codes).Range-based-for adapters for the
_foreach/_iter/_next-cursor APIs (AxlPci cursor, AxlUsb cursor, AxlHashTable iter, AxlArray iter, AxlSidecar foreach).Type-safe wrappers for the variadic format APIs (
axl_printffamily) using parameter packs.
Scope (out — non-goals):
No STL allocator integration. AxlMem stays the allocation root;
axlmmcontainers wrap AxlArray/AxlHashTable rather than re-implementingstd::vector/std::unordered_mapover AxlMem.No rewrite of any C internals.
axlmmis consumer-only headersa small linkable shim where exception translation needs a hidden symbol.
No template-heavy headers — UEFI binary size matters. Templates used sparingly (handles, span overloads); no header-only generic-algorithms library.
No runtime polymorphism for handle types — the C API’s opaque pointer model maps to a single
classper type, not an inheritance hierarchy.No coroutine /
std::futureintegration in the first cut. The AXL async model is callback-based; bridging it to coroutines is a separate larger design (revisit if a consumer asks).
Toolchain:
New
axl-c++driver (oraxl-cc --lang=cpp) — same flag surface asaxl-cc, invokesg++for compilation, otherwise identical link flow. Build-gated byAXL_CPP=1at SDK install time so the C-only install stays minimal.Headers under
include/axlmm/*.hpp. Umbrella<axlmm.hpp>mirroring<axl.h>. Namespaceaxlmm.Shipped as a separate package:
axl-sdk-cpp.deb/axl-sdk-cpp.rpmdepending onaxl-sdk. The baseaxl-sdkpackage never grows a libstdc++ runtime dependency.Unit tests under
test/unit/axlmm-test-*.cpp, run by the sametest-axl.shratchet (separate count tier so the C-side ratchet isn’t perturbed).
Phase CPP1: Foundation + handle-bearing wrappers
[ ]
axl-c++toolchain driver (oraxl-cc --lang=cpp)[ ]
<axlmm/handle.hpp>— common RAII handle template (move-only, configurable deleter)[ ]
<axlmm/exception.hpp>—axlmm::Exceptionbase + per-status derived types (CancelledError,NoMemoryError, etc.)[ ] First wrapper pass: AxlStream, AxlEvent, AxlCancellable, AxlStrBuf, AxlArena
[ ] Build-gate
AXL_CPP=1inscripts/install.sh; new package target in release flow[ ]
sdk/examples/hello.cppbuilds viaaxl-c++and prints to a wrapped AxlStream
Phase CPP2: Containers + iteration
[ ] AxlArray, AxlList, AxlSlist, AxlQueue, AxlHashTable wrappers with
begin()/end()and range-for support[ ] AxlSidecar foreach adapter
[ ] AxlPci / AxlUsb cursor adapters (range-for over enumeration)
[ ]
std::string_view+std::spanoverloads across the Phase CPP1 surface
Phase CPP3: Networking + format
[ ] AxlHttpServer / AxlHttpClient wrappers (route registration via lambdas with capture-friendly storage)
[ ] AxlTcp / AxlUdp socket wrappers
[ ] Type-safe
axlmm::format/axlmm::printparameter-pack wrappers overaxl_printffamily
Phase CPP4: Polish
[ ] Sphinx documentation generation for
<axlmm/*.hpp>(Doxygen already supports C++; needs Breathe pages alongside the C module pages)[ ] Cross-arch CI coverage (X64 + AARCH64 axlmm test build)
[ ] Migration recipe doc: “porting a C consumer to axlmm” with before/after examples
Open questions (defer until CPP1 lands):
Coroutine bridge for async APIs —
co_await-able wrappers around AxlCancellable-aware async ops. Probably worth it eventually but a real consumer should drive the design.C++20
std::expectedvs. exceptions for the error-returning variants — exceptions are the GLib precedent;expectedis closer to the underlying C semantics. Likely both, with the caller picking per call site.libstdc++ in a UEFI environment — exception unwinding, RTTI, and
std::stringallocation all have caveats in firmware. CPP1 must validate the toolchain end-to-end before CPP2 commits to STL types in the public headers.
AxlXml — generic XML reader + writer (LANDED 2026-05-10)
X1 + X2 shipped. Writer in src/data/axl-xml-writer.c, reader in
src/data/axl-xml-parse.c, public surface
<axl/axl-xml.h>. 53 unit tests. The pre-landing draft below is
kept for the design history; the shipped API differs in detail
(value-typed AxlXmlWriter per JSON’s pattern, pull-token reader
AxlXmlReader returning AxlXmlToken rather than callback-SAX,
AXL_XML_WRITER_PRETTY flag rather than separate pretty-print
setter). Remaining work:
X3: WebDAV PROPFIND emit migration — replace the hand-rolled XML emit in
src/net/axl-http-webdav.c::emit_entrywithaxl_xml_writer_*calls. ~50 lines deleted + 7-line escaper removed. Existing integration tests should be unchanged.WebDAV W6 (class-2 verbs) — PROPPATCH / LOCK / UNLOCK request bodies become implementable with the reader. Gated on a consumer asking.
Dell delldiagslinux port —
stout::XmlSinkmigrates toAxlXmlWriter;pugi::xml_documentconsumers insystemconfig/*and every module’sconfiguration.cppmigrate to the pull-token reader. Pre-1.0 axl-sdk Linux port is the trigger.
Original design notes (kept for history)
Surfaced 2026-05-10 during the WebDAV W2 landing. WebDAV’s
PROPFIND emit currently hand-rolls XML via axl_string_append
calls in src/net/axl-http-webdav.c::emit_entry (~50 lines) +
a 7-line escaper for <>&"'. Two design pressures converging:
WebDAV growth — PROPPATCH and LOCK request bodies are real XML documents that need parsing (out of scope for v1 per
sdk-prompts/2026-05-10-webdav-server.md, but next on the WebDAV roadmap when a consumer asks). The hand-rolled emit also picks up a second site (PROPPATCH response, LOCK response) — the escaper and structural correctness start fragmenting.Second consumer — at least one other AximCode use case has surfaced (user noted 2026-05-10, not yet documented in the SDK; will pull the requirements in when the consumer-side prompt lands).
Phase X1: AxlXmlWriter — minimal streaming writer
AxlXmlWriter *w = axl_xml_writer_new();
axl_xml_writer_decl(w, "1.0", "utf-8");
axl_xml_writer_start(w, "D:multistatus");
axl_xml_writer_attr(w, "xmlns:D", "DAV:");
axl_xml_writer_start(w, "D:response");
axl_xml_writer_start(w, "D:href");
axl_xml_writer_text(w, "/dav/preset-stat"); // auto-escaped
axl_xml_writer_end(w);
/* ...propstat... */
axl_xml_writer_end(w);
axl_xml_writer_end(w);
char *xml = axl_xml_writer_steal(w); // caller owns
axl_xml_writer_free(w);
Scope:
Element start / end with tag-balance enforcement (writer refuses to emit
</D:foo>when the open stack top is<D:bar>).Attributes with auto-escaping of
"and&.Text content with auto-escaping of
<,>,&.Backed by
AxlStringfor buffer growth.Optional pretty-print (newlines + indent — useful for PROPFIND responses; off by default for compactness).
Namespace-prefix handling stays caller-managed: writer treats
D:multistatusas an opaque qname; namespace declarations are normal attributes viaaxl_xml_writer_attr. No xmlns scope tracking.
Phase X2: AxlXmlReader — minimal SAX/event reader
typedef int (*AxlXmlEventCb)(void *user, const AxlXmlEvent *ev);
axl_xml_reader_parse(buf, len, on_event, user);
Events: START_ELEMENT(qname, attrs), END_ELEMENT(qname),
TEXT(content, len). Caller owns state-machine assembly into
whatever shape they need (DOM, application records).
Scope:
UTF-8 input; reject UTF-16 / EBCDIC declarations.
Decode the five named entities (
&<>"') and decimal/hex character references (&#NNN;,&#xHHHH;). Reject unknown named entities (no DTD support).Skip XML decl, comments, processing instructions, CDATA sections (treat CDATA content as TEXT events).
Reject DOCTYPE — no entity expansion = no billion-laughs vector.
Strict well-formedness: tag balance, single root.
Stop calling consumer callbacks on first error; return
AxlStatusso caller can distinguish parse error from callback abort.
Phase X3: WebDAV migration
Once X1 ships, replace emit_entry + xml_escape_append in
src/net/axl-http-webdav.c with AxlXmlWriter calls. Net LOC
roughly the same, structural correctness by construction.
Once X2 ships, add PROPFIND request-body parsing (defer for
WebDAV v1 today): <allprop/> vs <prop>...</prop> vs
<propname/>. Then PROPPATCH and LOCK request bodies become
implementable.
Out of scope (intentional)
DTD validation, schema validation (XSD/RelaxNG), XPath, XSLT — none of these have an axl-sdk consumer driving them.
DOM API on top of the SAX reader. Add iff a consumer wants it; meanwhile callers build their own state machines on the events.
XML signatures / encryption (XMLDSig / XMLEnc) — out of scope unless an EDK2 capsule manifest consumer asks.
Tests
Unit:
Writer: round-trip via reader (build a doc, serialize, reparse, compare event sequence).
Writer: tag-balance refusal (start
<a>, attemptend("b")).Writer: auto-escape on
<>&"'in text and attr values.Reader: well-formed input → expected event sequence.
Reader: malformed input (mismatched tags, unclosed element, DOCTYPE, unknown entity) → AXL_ERR.
Reader: entity decoding (
&,A,A).
Integration:
WebDAV PROPFIND emit migrated; existing assertions unchanged.
(Once X2 + PROPPATCH lands) cadaver / davfs2 round-trip PROPFIND with non-allprop bodies.
EFI Encapsulation — public-API hygiene (Future)
Three-phase plan to eliminate EFI_* / gBS / gRT / EFIAPI /
#include <uefi/...> from all axl-sdk consumer code, including
spec-protocol publishers. Full design in
AXL-EFI-Encapsulation-Plan.md;
audit baseline captured there.
Phase A (v0.18.0, ~1 day) — uefi-devkit crashhandler’s
report.cmigrates toaxl_nvstore_*+axl_fs_*+ new helpersaxl_app_boot_open,axl_image_get_base. No new SDK abstractions invented; existing primitives nobody reached for.Phase B (v0.18.0 or v0.18.1, ~1.5–2.5 days) — new
<axl/axl-cpu.h>with typedAxlCpuExceptioncontext; thunks internally overEFI_CPU_ARCH_PROTOCOL’sRegisterInterruptHandler. Crashhandler exception path drops allEFI_*references.Phase C (v0.19.0, ~1.5 weeks) — new
<axl/axl-fs-provider.h>with consumer-supplied vtable + SDK-emittedEFI_FILE_PROTOCOL/EFI_SIMPLE_FILE_SYSTEM_PROTOCOLthunks + UCS-2 ↔ UTF-8 boundary marshalling. axl-webfs’ssrc/mount/rewrites against the new abstraction; ~148 EFI hits → 0. Second FS-provider consumer (HTTP-mirror, compressed bundle, mock-fs for tests) becomes a ~50-LOC project.
After all three: consumers #include <axl.h> and write zero
EFI_* identifiers in any source file, even for drivers,
protocols, and spec-interface publishing. <uefi/...> becomes
transitive-only.
API Hygiene — Return Value Conventions (Future)
The public API surface mixes return-value shapes that pre-date a
unified convention. A 2026-05-04 audit of include/axl/*.h (~705
non-void public functions) categorized them and the result is on
file in this section. The motivation was a discussion about whether
to introduce typed Axl*Status enums for return values; the answer
turned out to be “narrower than that, but also wider than that”
once the data was in.
Background
AxlSidecar precedent (v0.11.0) —
AxlSidecarStatus(OK / FILE_MISSING / PARSE_ERROR) replaced 0/-1/-2 magic in 15 sidecar loaders. Set the project pattern: typed status enum only when 3+ outcomes are genuinely distinguishable and consumers branch on them.AxlStatus introduction (post-v0.11.2) — promoted the pre-existing
AXL_OK / AXL_ERR / AXL_CANCELLED#definetriple to a typed enum and addedAXL_TIMEOUT (-3)to disambiguate deadline-elapsed from invalid-arg in the wait/event family. Adopted by the wait/event/Tier-4-net cluster (~12 functions).
Phase H1: named-constants hygiene + targeted predicate flip
Reframed 2026-05-04 after an aborted “bool sweep” attempt
(commits cbc26e3..5132f76, reverted in 2bd2942). The original
2026-05-04 audit found ~190 int-returning functions whose
docstrings only mentioned 0 and -1 and labeled them
“bool-in-disguise.” That framing was wrong — it conflated
“binary outcome” with “predicate.”
Per AXL-Coding-Style.md §”Return Value Conventions”, the table draws a sharper line:
Pattern |
Type |
Examples |
|---|---|---|
Predicates (yes/no question) |
|
|
Operations (do something, can fail) |
|
|
The flipped-and-reverted batches were operations, not predicates:
file/dir/volume ops, ring-buf push/pop, TLS connect/write/read,
mem-phys read/write/map, driver load/start/unload, string
builders. Those belong in the int row. The diagnostic was every
batch produced boundary-translation sites of the form
rc = axl_foo(...) ? 0 : -1; — the call sites telling you the
callee wanted to remain int. Compounding signal: 3 of 8 batches
shipped sign-flip bugs caught only by independent code review
(HTTPS client, mkrd, axl_string_len).
Reframed plan, two distinct passes:
H1a: Named-constants hygiene — DONE (2026-05-04)
Shipped across 16 module commits in two passes after the bool-
sweep revert. ~280 single-failure-mode operations across 35 public
headers now return AXL_OK/AXL_ERR named constants. Signatures
unchanged — semantically a no-op since AXL_OK==0 and AXL_ERR==-1
by AxlStatus enum contract — but call sites read as status checks
instead of magic-int comparisons.
First pass — 9 modules from the original audit (b53ab94..8f96c67):
# |
Header |
Ops |
Commit |
|---|---|---|---|
1 |
axl-fs.h |
9 |
b53ab94 |
2 |
axl-ring-buf.h |
10 + 1 helper |
22187dc |
3 |
axl-tls.h |
5 (multi-shape kept literal) |
fc6a366 |
4 |
axl-mem-phys.h |
10 |
0938fa8 |
5 |
axl-stream.h |
5 (count returners kept literal) |
beffdfd |
6 |
axl-sys.h |
9 (DP iterator kept literal) |
291af6a |
7 |
axl-driver.h |
12 + statics |
13a41cd |
8 |
axl-string.h |
11 builders (axl_string_len kept literal) |
db3c66f |
9 |
axl-http-server.h |
17 + 3 callback typedefs |
8f96c67 |
Re-audit pass — 26 additional headers (1b4f0fa..3740a12):
The post-H1a re-audit revealed 37 untouched headers with int 0/-1 docstrings beyond the original audit set. Cluster commits:
# |
Cluster |
Headers |
Ops |
Commit |
|---|---|---|---|---|
10 |
smbios |
1 |
19 |
1b4f0fa |
11 |
pci |
1 |
13 |
35bb8a9 |
12 |
hardware |
acpi+smbus+ipmi+usb+spd |
18 |
dc59472 |
13 |
data-structures |
array+cache+queue+radix-tree |
11 |
aad7df3 |
14 |
util-storage |
nvstore+config+env+path+boot |
22 |
95d4120 |
15 |
util-misc |
gfx+watchdog+rng+console+str+mem+digest+image+image-verify+diag+log |
30 |
b4993b1 |
16 |
networking |
tcp+udp+socket+socket-client+net+http-client+http-core+url |
~50 |
3740a12 |
Two headers explicitly EXCLUDED as non-single-failure shape:
axl-loop.h— multi-shape (event loop returns 0/1/-1 for different states; some funcs propagate callback rcs).axl-subcommand.h— pass-through (returns whatever the subcommand returned).
Plus axl-tls.h’s axl_tls_handshake and axl_tls_read keep their
0/1/-1 multi-shape docstrings literal.
A regression caught in the re-audit pass: the hardware cluster
(commit dc59472) wrongly converted axl_usb_get_string’s -1
returns to AXL_ERR. That function is a count returner where -1
is the error sentinel for a positive-int-returning function, NOT
a status code. Reverted in the util-misc cluster commit (b4993b1).
The h1a-convert.py tooling was upgraded to use header-driven scope
filtering (extracts ops from @return AXL_OK docstrings + contract-
sharing chains via Like X / as X / @ref X) so the script no
longer touches count returners or comparison functions.
Tests 2555/2555 both arches at every commit; HTTP integration 62/62 verified at the http-server commit.
Methodology lessons captured for future H1 work:
Independent code review caught real issues in 2 of 9 modules: 6 missed call sites in axl-fs (regex blind spot for callers using
int rc = axl_foo(...)thenif (rc != 0)), 1 missed rc-indirection in axl-ring-buf, 1 wrapper-chain inconsistency in axl-tls (client_send wrapping axl_tls_write). Per-modulegrepfor both direct-call comparisons AND intermediate-rc comparisons is now the standard pre-commit check.Some headers needed
#include <axl/axl-macros.h>added: axl-mem-phys.h, axl-string.h. The macros include is required whenever the public docstrings reference AXL_OK/AXL_ERR so consumers comparing against the constants get them defined.Multi-shape functions stayed literal: axl_tls_handshake, axl_tls_read (return 0/1/-1 for “more data needed”), axl_dir_walk + dir_walk_recursive (callback-rc pass-through), axl_device_path_for_each (multi-shape iterator), axl_stream_for_each_line. Recognizing these by their contract — anything returning more than success+failure — is the discriminator.
Count returners stayed literal: ring_buf push/pop/peek (uint32_t bytes), axl_string_len (size_t), axl_pread/pwrite (axl_ssize_t), axl_device_path_size (size_t), ring_buf get_length/readable/writable/capacity. The
0return means “0 bytes,” not “OK.”
H1b: Targeted predicate flip — NO-OP (2026-05-04)
A post-H1a sweep for int-returning predicates found zero
candidates needing flip. Every genuine predicate in the SDK
already uses bool:
axl_*_is_*/axl_*_has_*/axl_*_contains— none exist asint-returners.Existence/availability/state predicates already bool:
axl_acpi_checksum_ok,axl_gfx_available,axl_loop_is_running,axl_net_is_available,axl_queue_is_empty,axl_ring_buf_is_empty,axl_ring_buf_is_full,axl_task_pool_done,axl_tls_available.Iteration predicates already bool:
axl_dir_read,axl_log_read,axl_json_array_iter_next,axl_stream_read_line, etc.
Two int-returning functions have docstrings with 1 if X, 0 if Y
shape (axl_hash_table_insert / axl_hash_table_replace), but they
return THREE distinct values (1 = new entry, 0 = existing
replaced, -1 = error). That’s multi-shape, not a yes/no predicate
— would be a Phase H3 candidate for a per-module typed enum if
3+ branchable outcomes earn their keep.
H1b is closed. The convention rule (predicates → bool) was already in force; the bool sweep had it backward.
What we explicitly walked away from
No bulk
int → boolsweep. Tried it; reverted it. Operations stayintAXL_OK/AXL_ERR per the style guide.No new
Axl*Statusenums unless 3+ outcomes earn their keep (Phase H3 rule, unchanged).
Phase H2: AxlStatus expansion — opportunistic
The async TCP cluster documents AXL_CANCELLED in its callback
signature (void (*AxlTcpCb)(AxlTcp *, int status, void *) —
int status should be AxlStatus status). Migration was deferred
from the initial AxlStatus commit because the callback type change
ripples through every consumer’s TCP callback. Worth doing the next
time a TCP-touching change is in flight anyway.
[ ]
AxlTcpCbint status→AxlStatus status(and the analogous HTTP-client callback). Updates axl-webfs callsites.[ ] Other multi-outcome candidates surfaced by future audits.
Phase H3: Per-module status enums — case-by-case
Don’t create them prophylactically. Future test before adding a new
Axl*Status: “do consumers need to write three or more distinct
branches based on the rc?” If yes, typed enum (sidecar/wait
precedent). If no, bool (or int/AxlStatus if it fits the
existing conventions).
What we’re explicitly NOT doing
Not promoting
axl_args_runto AxlStatus. It’s POSIX-exit- code shaped (returns frommain()straight into the process exit code, where AxlStatus’s negative values would round to 254/255). Documented inline inaxl-args.h.Not changing comparison-style functions.
axl_strcmp,axl_memcmp, etc. return libc-style sign — that’s information, not a status code.Not changing count-returning functions.
int axl_args_get_pos_count,axl_snprintf,axl_args_get_multi_count, etc. return values, not statuses.Not adding numeric-value mapping macros.
AXL_CANCELLEDis(-2)and stays(-2); the enum members ARE the contract, the numeric values ARE the contract, both work, no glue needed.
Repo Merge (Complete)
[x] Rename axl-sdk -> axl-sdk-old on GitHub
[x] Rename libaxl -> axl-sdk on GitHub
[x] Copy SDK files into merged repo
[x] Rework install.sh for local library
[x] Update docs and consumer projects
Known Gaps and Issues
Items that are not part of any phase but should be tracked. Discovered during code review and refactor work, not during original planning.
Testing / tooling gaps
[x] OOM injection testing.
axl_mem_fail_next_alloc(N)ininclude/axl/axl-mem.h(implemented insrc/mem/axl-mem.c) arms the Nth next allocation to return NULL without touching the backend. 13 allocator-primitive tests intest/unit/axl-test-mem.cand 13 container tests intest/unit/axl-test-data.cexercise the silent-OOM paths that were otherwise unreachable. Hook is gated onAXL_MEM_DEBUG; no-op in release.[x] Static analysis in CI.
clang-tidyruns as a third job in.github/workflows/ci.yml. The policy lives in.clang-tidyat the repo root (bugprone-*+clang-analyzer-*minus five documented noisy checks,WarningsAsErrors: '*'). 11 pre-existing findings fixed at the same time, including two real null-derefs insrc/data/axl-list.ccaught byclang-analyzer-core.NullDereference.[~] Fuzz harness — scaffold landed, more targets pending.
test/fuzz/now holds a standalone host-side libFuzzer build (clang -fsanitize=fuzzer,address) with two harnesses wired up:url_fuzzforaxl_url_parseandjson_fuzzforaxl_json_parse. Both reuse a sharedfuzz_shim.cthat provides libc-backed implementations of the AXL mem/str/log primitives so parser .c files can be compiled directly against the host libc without pulling in the freestanding allocator. Not wired into the defaultmaketarget (fuzzing is opt-in) and not wired into CI — a nightly job with crash artifact upload is the likely shape and is a separate follow-up. Remaining parser targets to cover:axl_http_parse_request_line/_header_line,axl-digest-*(block feeding), WebSocket frame parser.[ ] Benchmark suite. No benchmarks for the library. The hash table, radix tree, ring buffer, format engine, and JSON parser are the obvious candidates.
[x]
axl_http_parse_request_linealready public; kernel servers deduped. The parser was already exposed via<axl/axl-http-core.h>(umbrella’d through<axl.h>); the ROADMAP entry was wrong about needing to promote it. Real win: addedaxlk_http_read_request_lineinexperiments/axl-kernel/include/axl-kernel.hthat wraps the read loop + the public parse, and migrated all three kernel POC servers (hwinfo, bootconfig, reqlog) to use it. ~70 LOC of duplicated byte-fiddling removed.[x] AxlJsonWriter — JSON output API. Landed. Renamed
AxlJsonCtx/AxlJsonBuildertoAxlJsonReader/AxlJsonWriterfor symmetry. Writer now AxlString-backed, with orthogonal container/key/atom calls, optionalAXL_JSON_WRITER_PRETTYflag, sticky error flag (covers OOM + structural misuse), andaxl_json_write_tokenbridge for parse → mutate → emit. Migrated tests, fuzz, tools/rfbrowse, sdk/examples. Three kernel POC servers now build their endpoint JSON via the writer, not snprintf chunks.[x] Kernel-server endpoint builder cleanups. Done as part of the AxlJsonWriter migration. axlk-reqlog-server’s manual NUL-terminated copy loops swapped to
axl_strlcpy; all three servers’ endpoint builders now use the writer.[x] AxlRingBuf push/lost stats counters. Landed. Added
pushes_total+pushes_lostas cumulative byte counters on every push path (push, push_msg, push_elem, push_advance — including reject-mode rejection and overwrite-mode input-drop / old-data-displacement). Accessors areaxl_ring_buf_pushes_total/_lost; struct fields are private and reset onaxl_ring_buf_clear/init. axlk-reqlog-server now uses AxlRingBuf for its 8-element log ring (replacing the hand-rolled struct + head index + counters), and computes received/dropped by dividing the byte counters by element size. 19 new unit tests cover reject-rejected, overwrite-displaced, oversized-overwrite-input-drop, element-mode counts, and clear() reset.
Correctness / performance gaps
[ ] AxlLoop event-driven driver mode (eliminate
driver_tick_ms). Driver-mode dispatch is currently polled:axl_loop_attach_driverinstalls a periodicEVT_TIMER | EVT_NOTIFY_SIGNALatTPL_CALLBACKwhose notify drains all sources everytick_ms(default 50 ms). Every source whose readiness IS a UEFI event (TCP4 receive completion, EVT_TIMER, protocol-installed notify) currently re-enters via the loop’s polled dispatch cycle, which means up to ~tick_ms / 2average added latency and a TPL_CALLBACK budget that has to be hand-managed (peraxl-loop.h“notify-budget rule”). A genuinely event-driven driver mode would attach each source’s underlying UEFI event to its OWNEVT_NOTIFY_SIGNALcallback that runs that source’s dispatch directly. Net effect: -axl_loop_attach_driver/_detach_drivergo away -AxlService.driver_tick_msfield deleted -AXL_SERVICE_DEFAULT_TICK_MSdeleted - average dispatch latency drops from ~25 ms to firmware- callback latency (~µs) - “AxlLoop is event-driven” stops being a half-truth in driver mode Trade-off: real surgery on the source abstraction. Every source type (TCP, timer, idle, defer, pubsub, raw event) needs its dispatch path rewired to fire from its own firmware notify event. Idle and defer don’t have a UEFI-event analogue and need a different home (idle = “run after current notify drains,” defer = its own EVT_TIMER). Foreground mode still wants the centralized loop cycle so you end up with two source-dispatch paths instead of one. The integration tests are currently load-bearing on tick semantics in places (the drain-cap fix from the b0e567b HTTP-server starvation bug). Not blocking anything today — axl-webfs serve works fine at 50 ms — so this is post-1.0 work. Revisit when latency or conceptual cleanup actually starts costing something.[x] Sync ops busy-poll instead of blocking on events — new AxlCompletion module. Landed April 2026. AxlCompletion + AxlWait helpers (
axl_wait_for_flag/word/ms,axl_wait_for,axl_wait_for_with_tick) on a shared internal primitive_axl_event_wait_timeout_with_tickthat delegates to AxlLoop. Per-protocol Tier 4 wrappers (_axl_{udp,tcp,dns,ip4}_wait) absorb theEFI_*_COMPLETION_TOKEN+ Poll plumbing once. Ten src/net sites and three SSIF sites ported. Two KCS sites left as spin with explanatory comments (100 us cadence is below firmware timer resolution). Async TCP no-mapping retry got a partial fix (CPU-idle sleep; async-start blocking is tracked below as follow-up). Measured test-axl.sh CPU dropped from ~70% avg to 22% avg; wall-clock unchanged because the remaining time is legitimate protocol timeouts. AARCH64 QEMU-TCG flake rate went from 30% baseline (3/10) to 0/5 with deterministic 61.0s ±0.05s wall-clock — the stable timing is itself a signature of idle-CPU waits (old busy-polls created guest/host scheduler contention noise). AxlTestCompletion runs via an auxiliary runner (test/integration/test-axl-completion.sh) because AxlTestNet has a pre-existing FAT-image-timing UAF in its UDP-async teardown path — tracked as a follow-up below.**Follow-ups from this rework (tracked separately):** - [x] **AxlTestNet UDP-async teardown UAF.** Fixed 2026-04-18. The test was calling `axl_loop_free(loop)` before `axl_socket_free(receiver)`. The socket's UDP async receive state still held `sock->loop` (now dangling) and a stale source id; the subsequent `axl_udp_recv_stop` → `axl_loop_remove_source` dereferenced freed loop memory (filled with `0xAF` poison by AXL_MEM_DEBUG), which then propagated into `UDP4->Cancel` via a corrupted token event pointer and tripped a #GP in DxeCore. Fix: swapped the free order in `test/unit/axl-test-net.c`'s UDP async recv test so the socket is freed before the loop it was registered against. AxlTestCompletion folded back into `test/integration/test-axl.sh`'s TEST_APPS; auxiliary runner deleted. Full suite: 1277/1277 on both X64 and AARCH64. - [x] **Sync TCP wrappers orphan their async socket on timeout.** Fixed via `AxlCancellable`, landed 2026-04-18. `axl_tcp_{connect,accept,send,recv}_async` grew an optional `AxlCancellable *` parameter; sync wrappers now allocate an ephemeral cancellable, wire it to the 10 s timeout, and let the async op's cancel path handle uniform teardown (cancel UEFI token, drop loop sources, close events, fire user cb with `AXL_CANCELLED`). All four loop_free external-source warnings cleared. `AxlCompletion` + `axl_wait_*` also accept a cancellable for symmetry. Original description retained below. Surfaced by the `axl_loop_free` diagnostic added in `3216c86`. `axl_tcp_connect` (and by extension the other sync wrappers: accept/send/recv) creates an ephemeral loop, calls `axl_tcp_connect_async` (which allocates an `AxlTcp` and registers a `connect_source` on the loop), and runs the loop with a 10-second timeout. On timeout, the sync wrapper has no handle to the in-flight AxlTcp (connect_async only exposes it to the user callback, which never fires on timeout) and no public cancel API, so the AxlTcp + its loop source + the UEFI event leak. The ephemeral loop is freed with the source still active — would have been a UAF if anyone else held the source id. Run `TEST_KEEP_LOG=/tmp/out.log ./test/integration/test-axl.sh` and grep for "axl_loop_free: caller-owned event source" to see 4 hits per run (TCP connect to ports 9999, 9998, 9996, 9994 all fail under SLIRP, exercising this path). Fix: either pass an out-pointer back through `connect_async` so the sync wrapper can close on timeout, or add a public `axl_tcp_connect_cancel` function. Same pattern likely applies to the other three sync wrappers. Discovered 2026-04-18. - [ ] **Async TCP `Configure` retry blocks its caller (API-contract issue; no observed impact).** [src/net/axl-tcp-async.c:540](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-tcp-async.c#L540). The no-mapping retry loop inside an async-start function still blocks the caller for up to `TCP_MAPPING_RETRIES * TCP_MAPPING_DELAY` (~10 s) while DHCP is pending. Commit `7c98082` swapped `axl_backend_stall` for `axl_wait_ms` so the CPU idles and Ctrl-C still works — the user-visible symptoms are gone. What remains is a pure API-contract violation: async-start shouldn't block. In practice no caller observes this: the only one that reaches Configure during the DHCP window is the sync wrapper, which wants to block. **Not fixing preemptively.** Revisit when a real caller runs `axl_tcp_connect_async` on a shared loop and observes other sources going silent during the Configure-retry window (candidates: axl-webfs long-running server mode, SoftBMC-on-AXL). Proper fix shape: on `EFI_NO_MAPPING` store pending config in `AxlTcp`, register `axl_loop_add_timeout` for re-Configure, return 0 from the async-start call, and report final success/failure through the user callback. Discovered 2026-04-18; documented and deferred 2026-04-19. - [ ] **AxlAsync dogfooding of AxlEvent.** [src/task/axl-async.c](https://github.com/aximcode/axl-sdk-releases/blob/main/src/task/axl-async.c) currently hand-rolls its completion reporting via idle-source polling and the defer queue. Natural consumer of the `AxlEvent` primitive — signal-from-worker, wait- from-caller is the exact pattern. Additive (no API change), not a refactor. Low priority; raise only if someone touches AxlAsync for another reason or if a consumer needs a wait-for-async-work primitive. Discovered 2026-04-18. - [ ] **`axl_yield()` instrumentation of AXL APIs.** [docs/AXL-Lifecycle.md §3.1](https://github.com/aximcode/axl-sdk-releases/blob/main/docs/AXL-Lifecycle.md#31-where-axl-apis-inject-yields-automatically) lists the targets: file I/O (`axl_file_get_contents` / `axl_fread` / directory iteration), HTTP body-read loops in `src/net/axl-http-client.c`, `axl_digest_update` on large buffers, IPMI KCS 100 µs busy polls, SMBIOS table walks, `axl_array_sort`, hash-table rehash. Grep shows zero call sites in `src/` as of Phase A7 landing. Consequence: Ctrl-C works through any code path that goes through `AxlLoop` (HTTP server, sync TCP wrappers, `axl_wait_*`) but is silently ignored by CPU-bound or retry-loop paths that don't. Scope: seed `axl_yield()` at outer-loop boundaries in ~10–15 sites, tested by running a CPU-heavy call under QEMU and verifying Ctrl-C terminates it. Discovered 2026-04-20. - [ ] **Minimal runtime opt-out via `axl-cc --minimal-runtime`.** CRT0 unconditionally installs the registry, atexit list, signal notify, and default loop during `_axl_init`. [§9 of `AXL-Lifecycle.md`](https://github.com/aximcode/axl-sdk-releases/blob/main/docs/AXL-Lifecycle.md#9-design-decisions-locked-in) locked in "registry is always on" with the rationale that drivers don't link CRT0 anyway; that's true but leaves size-constrained or exit-managed apps with no way out. Ship `axl-crt0-minimal.o` as a peer to `axl-crt0-native.o`: sets firmware globals, inits console for `axl_printf`, parses argv, calls `main`, returns. No registry, no atexit, no signal notify, no default loop. The registry and atexit APIs already no-op safely when their storage is NULL, so `libaxl.a` stays unchanged. Consumers pick via `axl-cc --minimal-runtime`. Discovered 2026-04-20. - [ ] **Idle source fires on non-blocking dispatch — revisit when a real caller bites.** [src/loop/axl-loop.c:241-254](https://github.com/aximcode/axl-sdk-releases/blob/main/src/loop/axl-loop.c#L241-L254). Idle callbacks run once per `axl_loop_next_event` pass regardless of `blocking`. Under `axl_loop_run` that's naturally throttled by `WaitForEvent`; under an `axl_yield`-driven tight CPU loop it fires every yield — potentially millions of times per second. Matches GLib / libuv / Node convention, and two in-tree consumers depend on it: `src/task/axl-async.c`'s AP completion poll registers an idle source that expects to fire every tick, and `test/unit/axl-test-runtime.c` has `test_yield_dispatches_ready_work` explicitly asserting the current semantics. Kept as-is; documented as a footgun in [`AXL-Lifecycle.md` §2.6](https://github.com/aximcode/axl-sdk-releases/blob/main/docs/AXL-Lifecycle.md#26-idle-callbacks-and-yield-driven-loops) (recommend `axl_loop_add_timer` or `axl_defer` for tight-yield apps). Revisit if a real caller hits unresponsive idles or unwanted saturation — at which point the right fix is probably to skip idle when `blocking == false` and update the test + AxlAsync together. Discovered 2026-04-20. Original description (retained for context): Multiple call sites across `src/net/` and `src/ipmi/` wait for UEFI operations by repeatedly calling `protocol->Poll()` separated by `axl_backend_stall(1000)`. As a spec-mandated busy-wait, `Stall` burns host CPU for the entire timeout window. Measured impact: AxlTestNet pins 99-100% QEMU CPU for 28 consecutive seconds during DNS / UDP / HTTP-setup tests whose intervals match UEFI network timeouts; the AARCH64 QEMU-TCG integration run flakes ~30% of the time because the busy-waits race other timers under slower emulation. **Full `_stall` site audit (April 2026).** 15 calls in the tree: 4 legitimate busy-wait by design, 11 misuse candidates. Priority by measured impact. _src/net (high — this is the 99% CPU burn AxlTestNet shows):_ - [src/net/axl-udp.c:244](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-udp.c#L244) — `axl_udp_send_to` timeout loop (10s default) - [src/net/axl-udp.c:319](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-udp.c#L319) — `axl_udp_receive` timeout loop - [src/net/axl-net-resolve.c:184](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-net-resolve.c#L184) — DNS 5s primary poll - [src/net/axl-net-resolve.c:202](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-net-resolve.c#L202) — DNS secondary poll - [src/net/axl-net-dhcp.c:198](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-net-dhcp.c#L198) — DHCP first-stage wait (100ms stalls) - [src/net/axl-net-dhcp.c:250](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-net-dhcp.c#L250) — DHCP IP-assignment wait (1s stalls) - ~~src/net/axl-tcp-sync.c — TCP close drain~~ FIXED 2026-04-28 in b46686d: close finalizes via the loop when the firmware signals SockConnClosed (no per-close busy-wait); the only remaining `_axl_tcp_wait` site is the sync fallback used when no event loop is running (shutdown / sync-only CLI use). - [src/net/axl-tcp-sync.c:209](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-tcp-sync.c#L209) — Configure mapping retry (TCP_MAPPING_DELAY) - [src/net/axl-tcp-async.c:540](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-tcp-async.c#L540) — same Configure retry on the async path (extra-bad: async code should never block the loop) - [src/net/axl-net-ping.c:260](https://github.com/aximcode/axl-sdk-releases/blob/main/src/net/axl-net-ping.c#L260) — ping response wait (1ms stalls, full timeout) _src/ipmi (medium — SSIF's 60ms inter-command delay is the largest):_ - [src/ipmi/axl-ipmi-ssif.c:109](https://github.com/aximcode/axl-sdk-releases/blob/main/src/ipmi/axl-ipmi-ssif.c#L109) — SSIF write-retry delay (60ms) - [src/ipmi/axl-ipmi-ssif.c:124](https://github.com/aximcode/axl-sdk-releases/blob/main/src/ipmi/axl-ipmi-ssif.c#L124) — SSIF read-retry exponential backoff (starts 60ms) - [src/ipmi/axl-ipmi-ssif.c:304](https://github.com/aximcode/axl-sdk-releases/blob/main/src/ipmi/axl-ipmi-ssif.c#L304) — SSIF 60ms inter-command delay (spec-mandated for iDRAC/Grace) - [src/ipmi/axl-ipmi-kcs.c:93](https://github.com/aximcode/axl-sdk-releases/blob/main/src/ipmi/axl-ipmi-kcs.c#L93) — KCS IBF-clear poll (100µs cadence, 5s timeout) - [src/ipmi/axl-ipmi-kcs.c:125](https://github.com/aximcode/axl-sdk-releases/blob/main/src/ipmi/axl-ipmi-kcs.c#L125) — KCS OBF-set poll (100µs cadence) KCS's 100µs interval sits at the edge of `gBS` timer granularity (firmware timers typically snap to 100µs–1ms). Evaluate per-platform before converting; leaving the two KCS sites as spin is defensible on latency grounds. _Shim / tests (low — negligible wall-time contribution):_ - [sdk/examples/smbus-hc-shim.c:267](https://github.com/aximcode/axl-sdk-releases/blob/main/sdk/examples/smbus-hc-shim.c#L267) — SMBus wait-ready poll (1ms × 1s) - [sdk/examples/smbus-hc-shim.c:287](https://github.com/aximcode/axl-sdk-releases/blob/main/sdk/examples/smbus-hc-shim.c#L287) — SMBus run-and-wait poll (1ms × 1s) - [test/unit/axl-test-net.c:247](https://github.com/aximcode/axl-sdk-releases/blob/main/test/unit/axl-test-net.c#L247) — `axl_spin_usleep(10000)` in socket accept test - [test/unit/axl-test-net.c:1502](https://github.com/aximcode/axl-sdk-releases/blob/main/test/unit/axl-test-net.c#L1502) — same pattern in socket stream test _Legitimate busy-wait (leave alone):_ - [src/backend/native/axl-backend-native.c:1162](https://github.com/aximcode/axl-sdk-releases/blob/main/src/backend/native/axl-backend-native.c#L1162) — the `gBS->Stall` wrapper itself - [src/util/axl-time.c:47](https://github.com/aximcode/axl-sdk-releases/blob/main/src/util/axl-time.c#L47), [:56](https://github.com/aximcode/axl-sdk-releases/blob/main/src/util/axl-time.c#L56) — `timer_sleep_us` fallback when timer creation fails - [src/util/axl-time.c:140-152](https://github.com/aximcode/axl-sdk-releases/blob/main/src/util/axl-time.c#L140-L152) — `axl_spin_{sleep,msleep,usleep}` public busy-wait API - [src/util/axl-sys.c:79](https://github.com/aximcode/axl-sdk-releases/blob/main/src/util/axl-sys.c#L79) — `axl_stall()` public busy-wait wrapper **Fix direction — layered API built on top of AxlLoop.** AxlLoop already multiplexes arbitrary EFI_EVENTs through `gBS->WaitForEvent` with the shell-break event appended to every wait, so Ctrl-C detection is built-in. Its FUSE-style `axl_loop_next_event` / `axl_loop_dispatch_event` primitives let a caller drive one iteration at a time without running the loop to completion. The sync-wait primitives below should be thin wrappers around AxlLoop, NOT a parallel implementation of event multiplexing + break handling. See `include/axl/axl-loop.h` and `axl_backend_shell_break_event` / `axl_backend_shell_break_flag` in `src/backend/axl-backend.h` for the primitives to reuse. **Tier 1 — AxlCompletion (public, zero callbacks, signal/wait):** ```c /* include/axl/axl-completion.h */ typedef struct AxlCompletion AxlCompletion; AxlCompletion *axl_completion_new(void); void axl_completion_free(AxlCompletion *c); AXL_DEFINE_AUTOPTR_CLEANUP(AxlCompletion, axl_completion_free) void axl_completion_signal(AxlCompletion *c); /* idempotent */ void axl_completion_reset(AxlCompletion *c); /* reusable */ int axl_completion_wait(AxlCompletion *c); /* infinite, 0/-2 */ int axl_completion_wait_timeout(AxlCompletion *c, uint64_t timeout_us); ``` Parallels Linux kernel `struct completion`. Internally: wraps an EFI_EVENT (signal = `SignalEvent`) and implements `wait*` by creating an ephemeral AxlLoop, adding the event as a source, running until fired. Ctrl-C handling comes for free from the loop. Valuable beyond this refactor for AxlDefer completion, AxlAsync result reporting, any cross-BSP/AP signaling. **Tier 2 — zero-callback convenience (most call sites):** ```c /* Wait until *flag becomes true. */ int axl_wait_for_flag(volatile const bool *flag, uint64_t timeout_us); /* Wait until *word stops matching not_ready_value. Covers the UEFI-token Status pattern, DMA flags, etc. */ int axl_wait_for_word(volatile const uint64_t *word, uint64_t not_ready_value, uint64_t timeout_us); /* Interruptible sleep — what today's axl_msleep should have been. */ int axl_wait_ms(uint64_t ms); ``` Zero callbacks, zero allocations at the callsite, no session object. Covers pure sleeps and simple flag/word waits. **Tier 3 — callback form for genuinely complex conditions:** ```c typedef bool (*AxlCondFn)(void *ctx); typedef void (*AxlTickFn)(void *ctx); int axl_wait_for(AxlCondFn cond_fn, void *cond_ctx, uint64_t timeout_us); int axl_wait_for_with_tick( AxlCondFn cond_fn, void *cond_ctx, AxlTickFn tick_fn, void *tick_ctx, uint64_t tick_us, uint64_t timeout_us); ``` The `_with_tick` form is the actual vehicle for the sync-net refactor — `tick_fn` drives the UEFI protocol state machine forward between waits. All return values follow the same convention (0 = condition, -1 = timeout, -2 = interrupted). **Tier 4 — internal per-protocol wrappers (one-liner callsites):** Each sync-net module gets a single helper that absorbs the tick + cond callbacks once. These live in internal headers (UEFI types allowed), so the 11 callsites become a single function call with no user-written predicates: ```c /* src/net/axl-net-internal.h */ int _axl_tcp_token_wait(EFI_TCP4_COMPLETION_TOKEN *t, EFI_TCP4_PROTOCOL *tcp4, uint64_t timeout_us); int _axl_udp_token_wait(EFI_UDP4_COMPLETION_TOKEN *t, EFI_UDP4_PROTOCOL *udp4, uint64_t timeout_us); int _axl_dns_token_wait(EFI_DNS4_COMPLETION_TOKEN *t, EFI_DNS4_PROTOCOL *dns4, uint64_t timeout_us); ``` Each implementation is ~5 lines, built on Tier 3 with the protocol-specific Poll as the tick callback. Every `src/net/` sync callsite collapses from 5 lines to 1: ```c /* Before — 5 lines, 100% CPU: */ while (elapsed < UDP_SEND_TIMEOUT_US) { axl_efi_call(udp4->Poll, 1, udp4); if (tx_token.Status != EFI_NOT_READY) break; axl_backend_stall(1000); elapsed += 1000; } /* After — 1 line, idle CPU, zero callbacks: */ _axl_udp_token_wait(&tx_token, udp4, UDP_SEND_TIMEOUT_US); ``` **Why not just `axl_usleep`:** a minimum `axl_backend_stall → axl_usleep` swap drops CPU to ~0% during the wait, but each iteration creates + destroys a one-shot timer, there's up-to-1ms latency between completion and wake, and Ctrl-C still can't interrupt the loop. **Open question for the implementation session: how should these layer on AxlLoop?** Two candidate shapes, discuss at session start before writing code: 1. **Each wait creates an ephemeral AxlLoop.** Simple, no caller state. Costs a few allocations per call. The ephemeral loop inherits shell-break handling automatically. Clean separation: sync callers never touch a loop object. 2. **Add `axl_loop_wait_condition(loop, cond, ctx, tick_us, timeout_us)` as a new AxlLoop method.** Tier 3 becomes a one-liner that creates a throwaway loop and forwards. Callers who already own a loop can skip the allocation by calling the method directly. More composable but bigger API surface. Option 2 feels like the right direction (more composable, AxlLoop users reuse their loop, also better integrates with AxlDefer/AxlPubsub which live inside a loop), but confirm by sketching both and looking at what AxlDefer/AxlPubsub do with their own loop handles. The `axl_loop_next_event` / `axl_loop_dispatch_event` FUSE primitives already allow driving a loop iteration-by-iteration — that's what `wait_condition` would be built on. Zero new multiplexing code. **Work plan for the dedicated session:** 1. Discuss and pick ephemeral-loop vs loop-method shape. Sketch each with one callsite ported both ways, compare the internal-header shape. 2. Implement chosen shape in `src/util/axl-completion.c` + `src/util/axl-waiter.c` (Tier 2/3), reusing AxlLoop's event multiplexing via `axl_loop_next_event` / `axl_loop_add_event` / `axl_loop_add_timer`. Never re-implement `gBS->WaitForEvent` or break-flag handling. 3. Unit tests in `test/unit/axl-test-completion.c`: signal before wait, signal after wait, timeout path, reset + reuse, wait_for_flag, wait_for_word, wait_for_with_tick on a mock state machine, NULL-safety on all entry points, Ctrl-C interruption (simulate via `axl_backend_shell_break_flag` injection). 4. Write per-protocol helpers in `src/net/axl-net-internal.h` + `src/ipmi/axl-ipmi-internal.h`. One each for TCP4, UDP4, DNS4, DHCP4, IP4 (ping), SSIF. 5. Port the 10 `src/net/` sites. Verify the async TCP path (`axl-tcp-async.c:540`) threads through AxlLoop rather than blocking — it currently busy-waits regardless of context. 6. Port the 3 SSIF sites in `src/ipmi/axl-ipmi-ssif.c`. Leave the 2 KCS sites with an explanatory comment (100µs cadence below firmware timer resolution). 7. Convert the shim (2 sites) and tests (2 sites) — minor. 8. Update `docs/AXL-Design.md` §Async Work Phases with AxlCompletion + wait helpers alongside AxlBufPool / AxlAsync / AxlDefer / AxlPubsub; new `src/util/README.md` section; Sphinx page. 9. Re-measure: AxlTestNet guest CPU ~72% → sub-20% avg; AARCH64 flake rate 3/10 → expected 0/10; Net binary wall time may also drop (state machine advances per event instead of per 1ms tick). 10. Ratchet bump per new test count. **Out of scope for the rework:** the AxlTestNet DNS tests that time out (no loopback DNS under `-netdev user` in typical config) — those SHOULD time out, and the framework skips them with `SKIP: …`. The rework just stops the timeouts from burning CPU. Discovered 2026-04-18 while investigating AARCH64 test flakes during the AxlSmbus Phase B1a work. Phases B1a and B1b have since landed; this AxlCompletion rework is next in the queue.
API / packaging gaps
[x]
AXL_VERSIONmacro ininclude/axl/axl-version.hwithAXL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBERandAXL_VERSION_AT_LEAST(M, m, p). Kept in sync with the repo-rootVERSIONfile via the Makefile’scheck-versiontarget (hard-errors on drift) andscripts/bump-version.sh. Shipped with v0.1.1.[x] pkg-config file (
axl.pc, plus per-archaxl-x64.pcandaxl-aa64.pc) generated byinstall.shat<prefix>/lib/pkgconfig/. Relocatable via pkg-config’s${pcfiledir}. Consumers canpkg-config --cflags --libs axlonce the SDK is installed.[x] CMake package config for consumers using CMake.
install.shgenerateslib/cmake/axl/axl-config.cmakewith a relocatableAXL_SDK_DIRlookup, so consumers canfind_package(axl REQUIRED)and callaxl_add_app(). Still a raw macro (no imported targets), which is fine for the non-library image.efioutput shape — add imported targets only if someone actually wants them.[ ] Proper
-develsplit for distro upstream submission. Today we ship a singleaxl-sdkpackage withProvides: axl-sdk-develsodnf install axl-sdk-develresolves via the RPM alias. That’s sufficient for self-distribution via GitHub Releases, but Fedora/Debian upstream reviewers will ask for a real split before acceptance. Mirror gnu-efi’s shape: -axl-sdk:/usr/lib/axl/<arch>/*.o(CRT0, reloc, debug) +/usr/lib/axl/elf_*_efi.lds(linker scripts) — the firmware glue. -axl-sdk-devel(Requires: axl-sdk): headers,libaxl.a,axl-cc,pe-set-debug, pkg-config, cmake, docs, examples — everything a developer directly touches. ~30 lines inrelease.yml+scripts/build-packages.shto split onefpminvocation into two per matrix entry with correct--excludepatterns. Defer until actually submitting upstream — not worth the complexity while self-hosting.[ ]
debian/directory +.specfile for distro submission. Required for official Debian/Fedora upstream packages. Separate from CI.deb/.rpmgeneration — those will happily keep usingfpm, but distros want native spec + rules files they can review and patch. A few days’ polishing to passlintian/rpmlintclean. Ties directly to the-develsplit above.[ ] Migrate library build to meson+ninja or cmake+ninja. Parked April 2026 as “wait for a concrete pain point”; the first one landed during the v0.7.3 AxlArgs unification: restructuring a public-header struct (
AxlArgsApp/AxlVerb→AxlArgsNode) produced an inconsistent incremental build wherelibaxl.ahad members rebuilt against the new header but the test binaries linked a.asnapshot that the runtime hit as stale state —axl_mem_fail_next_alloctest counter drifted, AxlMem stalled.make clean && makecleared it; the fix went into project CLAUDE.md as a manual workaround. Root cause isar rcsnot tracking per-member timestamps cleanly + GNU make’s-MDdeps not catching cross-archive dependencies. Both meson and cmake (with ninja generator) handle this naturally via their internal dep DBs. Preference: meson — it’s lighter, the toolchain-file story for freestanding UEFI cross-compile is cleaner than CMake’s, and it generates compile_commands.json for free. Open question: do we keep the.so → .efi objcopystep in the build system or move it to a shared script that both meson and the consumer-sideaxl-ccinvoke. Real work (multi-day); revisit only when this bites again or a consumer asks. Triggering condition: another stale-build incident, OR landing IDE integration that needscompile_commands.json(clangd works without it but the experience is degraded).
Documentation gaps
[ ] Design doc for the coding style already exists, but there is no “how to add a new module” walkthrough. Useful when onboarding contributors or adding modules like
axl-ipmi.[x] Changelog.
CHANGELOG.mdat the repo root, maintained per-release. Entries moved under a version heading at tag time (see the v0.1.2 entry for an example). Release notes inrelease.ymllink to it rather than duplicating content.
Real-hardware findings — Dell PowerEdge XE7745 (iDRAC10), May 2026
Empirical session against actual hardware (svc tag KCH0TD1) shook out a number of discoveries and followups that would never have surfaced in QEMU. Tracked as a single section so the cross-cutting fixes don’t get lost in per-phase backlogs.
Confirmed working on hardware
[x] axl-sdk USB-NIC bundle (
NetworkCommon.efi+UsbRndis.efi, ~20KB total) brings up the iDRAC virtual USB-NIC on the host UEFI shell. iDRAC enumerates as Microsoft RNDIS, NOT CDC-ECM/NCM. Dell’s BIOS DXE volume on this platform has the full TCP/IP stack above SNP and the full USB stack below, but no USB-NIC class driver in between — that’s exactly the gap axl-sdk fills. Verified empirically (5 independent checks includingdh -p,dh -p LoadedImage,bcfg driver dump, explicitconnect <usb-io-handle>against every USB endpoint).[x] axl-webfs
--helpandlist-nicswork cleanly on real hardware (mount/serve blocked by L3 routing — see below).[x]
racadm console com2works from the laptop SSH wrapper as long asBIOS.SerialCommSettings.SerialComm = OnConRediris applied. Disconnect:Ctrl-\\.
Bugs found and fixed (2026-05-04)
[x] iPXE leaves boot-services watchdog armed. When axl-sdk loads
ipxe-intel.efior any iPXE-derived driver viaaxl_net_ensure_drivers, iPXE’sefi_watchdog.carms a 5-minSetWatchdogTimerand re-arms every 10s while alive. iPXE’s shutdown handler only disarms when chaining to an OS (booting==true); when iPXE exits without booting (the axl-sdk SNP-binding case), the watchdog stays armed → host resets ~5 min later (Dell BIOS extends the timeout to ~30 min in BDS phase). Fixed by addingaxl_watchdog_disarm()at end ofaxl_net_ensure_drivers. Cannot be unit-tested in QEMU (real hardware only); regression covered via on-hardware cycle test.[x]
lsusbleaks per-entry device-path bytes.axl_memdupatsrc/usb/axl-usb.c:354allocates one buffer per USB interface entry; entries array is process-lifetime by design but cleanup was missing →axl_mem_dump_leaks()flagged ~2.7KB on a real Dell with 16+ USB devices. Fixed by registering anaxl_atexitcleanup that frees each entry’sbus_key(alias ofdev_key) and the array itself.[x]
lspcisidecar singleton ctx leaks ~32 bytes. The atexit-thunk insrc/data/axl-sidecar.cexplicitly didn’t free itsSingletonAtexitCtx, on the rationale “process exit reclaims the small allocation.” But ouraxl_mem_dump_leaks()runs at axl_runtime_cleanup BEFORE that, so the comment was contradictory in our environment. Fixed by freeing ctx in the thunk (callers are forbidden from running_freeafter atexit-thunks have fired, which is the established axl-sdk convention).[x] AXL_TLS toggle silently produced non-TLS libaxl.a + tools. Two related Makefile bugs surfaced while wiring TLS into uefi-devkit’s tool build pipeline. (1) Toggling
AXL_TLS=1growsLIB_OBJSwith ~50 mbedtls .o files but make sees libaxl.a (from prior non-TLS run) as up-to-date and skipsar rcs— TLS symbols never enter the archive. (2) Tool .efis from prior runs are NEWER than the (about-to-be-rebuilt) libaxl.a, so make decides they’re up-to-date and skips re-linking — observedfetch.efi22:23, libaxl.a 22:25. Both produce non-TLS binaries with no warning. Fixed by detecting the AXL_TLS state-change at make parse time (via$(BUILDDIR)/.axl-tls-state) and wiping.os, libaxl.a, and tools when it flips. Also added the missingclean-toolstarget uefi-devkit references in itstools-cleanrecipe. Detected viafetch.efisize: 185KB (non-TLS) vs 433KB (TLS).
Bugs found while exercising tools end-to-end (2026-05-05/06 sweep)
Re-tested every shipped tool against XE7745 via the axl-webfs PUT
loop (docs/HW-Testing-Workflow.md). Catalog of new findings:
[x] Dell IPMI
chassis statusreturned all zeros. Verified fixed on hardware:Power State: on,Current Power State: 0x2dafter the buffer-shift fix below. Was: all-zero response on iDRAC10 (server obviously powered on) — root cause was the Dell vendor protocol writing intoResponse[0]not&Response[1]. Reference comment inuefi-ipmitool/IpmiToolPkg/Library/ IpmiTransportLib/IpmiTransportLib.c:184-188documents the exact empirical finding (“Previous testing showed that &Response[1] returns zeros”). Fix: hand the firmwareresp[0..N-1], shift right by one after the call, synthesize CC=0x00 atresp[0]. Also fixed theDELL_IPMI_SEND_COMMANDtypedef which was missing the mandatoryLunparameter at slot 3 — without it every following arg slid into the wrong register/stack slot. Regression test intest/unit/axl-test-ipmi.c(test_dell_transport_dispatch) installs a mock vtable, runsaxl_ipmi_get_device_id, and asserts both quirks are honored.[x] IPMI auto-detect now follows the spec: SMBIOS Type 38 first, vendor protocols as fallback. Previously the chain was EDKII → Dell → SMBIOS → default-KCS, so on Dell hardware the vendor protocol always won even when SMBIOS Type 38 explicitly pointed at a working KCS port. New chain: SMBIOS Type 38 → EDKII → Dell → default-KCS. Also added a public
axl_ipmi_session_new_with_transport(hint)API and anipmi.efi --transport kcs|ssif|edkii|dellflag so users can pin a specific transport (escape hatch when auto-detect misbehaves on a quirky platform).[x] iDRAC10 IPMI now works via KCS — fixed 2026-05-06 with a run of four state-machine fixes against the live hardware (see commits 2aecf16 + the staging-buffer + read-loop cleanup). Get Device ID returns byte-for-byte the same response Linux’s
ipmitool raw 6 1does (20 81 01 20 02 df a2 02 00 00 01 00 08 3c 37); chassis status, sel list, raw passthrough all functional with auto- detect (no--transportflag needed) since the SMBIOS Type 38 path now picks the right ports + drives the state machine compatibly. Original investigation findings on the Dell-vendor-protocol garbage problem preserved below for historical context — that path is still broken on iDRAC10 but no longer matters since the spec-defined KCS works.**Resolved root causes** (each one would have masked the next if not all four had been fixed): 1. SMBIOS Type 38 BaseAddressModifier register-spacing bits live at bits 7..6, not bits 1..0 as I'd memorized from a stale read of the spec. `(modifier >> 6) & 3` matches dmidecode + Linux ipmi_si. Wrong bits gave 16-byte stride (cmd at 0xCB8, unmapped) instead of 4-byte (cmd at 0xCAC). 2. Missing `clear_obf` calls during write phase. iDRAC10 sets OBF=1 during the WRITE_START echo; without draining, the BMC sees OBF=1 while we drive the next write and treats it as a protocol violation (status went to 0xC1 = ERROR). Linux's `ipmi_kcs_sm.c` drains OBF at every state-machine transition. 3. `kcs_wait_obf_set` rejected `state != expected` even when OBF=0 (BMC still transitioning). Real BMCs have a window between a status field flipping and OBF asserting. 4. Read loop assumed every byte arrived in `state == READ`. Last response byte routinely arrives with `state == IDLE` already (BMC set the byte then immediately transitioned). New loop matches Linux's WAIT_READ pattern: handle `state in {READ, IDLE}`, drain final OBF, complete. Plus a structural fix: `kcs_send_raw` now stages the wire response into an internal 258-byte buffer (echo + max IPMI response) and only copies post-echo body bytes into the caller's buffer. Previously the typed wrappers' small buffers (chassis_status used resp[5]) had no room for the 2-byte echo + body. **Original (now mostly historical) capture state on iDRAC10:** * Dell EFI_IPMI_TRANSPORT (vendor) — published, accepts commands, returns EFI_SUCCESS. **chassis_status (3-byte response) returns real BMC data** (Power State: on, byte 0x21 = power_on + restore_policy bits set). **Get Device ID (11-byte response) returns garbage** — the buffer is filled with stack contents (Dell protocol GUID bytes `7409d614-5abf-4869` are observable mid-buffer across runs). uefi-ipmitool's reference impl shows the same brokenness (`mc info: failed to get device ID (Time out)`, `raw 6 1: transport error (Time out)`), so this isn't an axl-sdk bug per se — Dell changed something between iDRAC9 (which uefi-ipmitool was developed against on PowerEdge XE8712 ARM64) and iDRAC10. * KCS at SMBIOS Type 38 port (0xCA8/0xCA9) — port-IO read returns 0xFF idle. The phantom-BMC guard now retries with a Get Status command (write 0x60 to cmd port, wait 1ms, re-read) per the IPMI spec — designed to catch BMCs that *do* idle at 0xFF but respond to commands. **iDRAC10 fails even this**: the byte stays 0xFF after the Get Status write, confirming the port is truly unmapped. Bypassing the guard entirely and forcing the KCS sequence hits IBF-clear timeout. The LPC/KCS path is genuinely locked down on iDRAC10 (likely intentional — production servers commonly disable host→BMC I/O port access for security). * SSIF (I2C @ slave 0x20) — uefi-ipmitool's I2C probe shows write succeeds but read returns 32 bytes of 0xFF on the one I2C master that responds (handle [4]). Disconnected bus or unprogrammed slave; SSIF not viable. * **NEW:** `DELL_IDRAC_INTERFACE_PROTOCOL` (E0E4EBAD-A45E-419E-852E-AEDE2C2BDE6C) is published on iDRAC10. uefi-ipmitool labels it "non-IPMI" — probably the new iDRAC config/management interface that supersedes the legacy 7409D614 Dell IPMI vendor protocol. No public docs found; understanding its API would require Dell datasheets or reverse engineering. Bottom line: on iDRAC10 from the host UEFI shell, the only reliable BMC path is the Dell vendor protocol limited to small-response commands. Linux on iDRAC10 likely uses either LANplus over the iDRAC USB-NIC (port 623) or the new Dell iDRAC interface protocol. Suggested next step: add an out-of-band IPMI-over-LANplus transport to axl-sdk (it would reuse the existing TLS+TCP stack and would work on any iDRAC where the USB-NIC is functional, sidestepping the broken in-band path entirely).[x]
memspd listfinds no DIMMs on AMD EPYC — resolved 2026-05-06 with two changes:1. New public API `axl_smbus_new_with_probe(probe, user)` + `axl_smbus_visit_all(visit, user)` walk every published `EFI_SMBUS_HC_PROTOCOL` and `EFI_I2C_MASTER_PROTOCOL` handle (Dell PowerEdge XE7745 publishes 1 HC + 12 I2C masters). axl-spd's `ensure_session` now uses the probe API with a strict predicate: claim a controller iff some slave at 0x50..0x57 returns a plausible SPD type byte (0x09..0x12 per JEDEC spec table 4). New `memspd scan` verb dumps SPD-range byte 0 from every visited controller — diagnostic gold for "which bus has my SPDs?" investigations. 2. **Empirical finding from running the scan on iDRAC10**: *no* I2C bus reachable from UEFI carries the DIMM SPDs. 13 controllers visited (1 HC + 12 I2C masters); only one I2C master returned anything at 0x50, and that was 0xFF (bus floating, no slave). The DIMM SPDs are gated behind BMC/SMM on iDRAC10 — same lockdown pattern as KCS. memspd's `list` therefore falls back to enumerating SMBIOS Type 17 records (BIOS-populated at POST) and emits the standard size/speed/manufacturer/part-number summary. HW-validated on XE7745: 4 DDR5 64GB SK Hynix HMCG94AHBRA487N @ 6400 MT/s in slots A1/A2/B1/B2, matching the BIOS DIMM map. The `axl_smbus_new_with_probe` API is reusable for any caller that needs to pick a specific SMBus controller — e.g., a future SSIF backend that needs to find the BMC's bus rather than just the first I2C master.
[x]
netinfo diagIPv4 column blank for DHCP-bound NICs. Resolved 2026-05-05:axl_net_list_interfacesnow enumerates IP4Config2 handles separately and correlates to SNP by MAC, so it picks up Dell’s child-handle binding correctly. HW- validated on XE7745 — eth0 (DHCP) now shows 10.9.177.98 with netmask 255.255.254.0; eth1 shows static 169.254.1.2 with gateway 169.254.1.1.[x]
netinfoDEBUG file-open spam. Resolved 2026-05-05:axl_backend_file_opennow suppresses the DEBUG log line forEFI_NOT_FOUND(the normal “file doesn’t exist” case flooded byaxl_driver_locateprobing every mounted volume). Real errors (media, permissions) still surface. HW-validated —netinfo diagDriver Bundle section is now clean.[x]
memspdandmkrddumped--- AXL diag ---block on every run unconditionally.axl_diag_startupis documented inaxl-diag.handsrc/util/README.mdas-v/--verbosegated, but both tools called it frommain()before arg parse. Fixed: gated behind the parsed verbose flag (mkrd had the flag; memspd grew one).[N/A] fetch “eager TLS init” was actually iDRAC HTTP→HTTPS redirect.
fetch.efi --head http://169.254.1.1/showstls: initialized (mbedTLS)because iDRAC10’s HTTP server 301-redirects to HTTPS, and fetch transparently follows the redirect — the TLS init fires for the second (HTTPS) hop. Verified empirically 2026-05-05 by inspecting the response headers: the< HTTP 401reply includedstrict-transport-security: max-age=...; includeSubDomains; preload, which only an HTTPS server emits. Not a bug.[x]
make toolsfrom a fresh state fails on missing crt0/reloc/ debug-info.ofiles. Resolved 2026-05-05:toolstarget now depends onall, so the per-arch object set + libaxl.a are built before any tool tries to link.[ ] HTTPS to iDRAC eth0 routed address times out cleanly.
https://10.215.120.97/from the host’s RTL8153 NIC stalls ~10s then “request failed”.http://10.215.120.97/, ICMP, andhttps://169.254.1.1/(same iDRAC, different NIC) all work — so axl-sdk’s TLS path is healthy. Strongly suggests corp network policy blocks 443/tcp on the segment between the host eth0 and 10.215.120.97. Captured here so a future investigator doesn’t waste time chasing it as an axl-sdk bug.
Confirmed-still-working on real hardware (2026-05-05/06 sweep)
End-to-end re-tested via the axl-webfs PUT loop. All passed (no leaks, real microsec timestamps in tool output):
[x]
lspci— both PCI segments enumerated (118 entries in seg 0, 87 in seg 1). The earlier-flagged “lspci skips segment 0” is no longer reproducible on tip; segment 0 prints without the0000:prefix.[x]
lsusbflat +--tree— full hierarchy (RTL8153, iDRAC USB composite, hubs, HID, CDC interfaces). Per-entry leak from handoff fixed by commit 50aed78.[x]
sysinfo— full CPU/memory/firmware/SMBIOS block.[x]
dmidecode --type 42— VLAN sentinel<none>(not4294967295), full IPv4 + IPv6 record bodies decoded.[x]
cat,hexdump— files from cross-volume paths.[x]
fetchover both HTTP and HTTPS via the iDRAC USB-NIC.
Pre-existing open followups
[x]
dmidecode --type 42decode bodies. Resolved 2026-05-05 in commits 6b256ad + e9ec5c8 and HW-validated on XE7745 2026-05-06: dmidecode now decodes both Type 42 records cleanly. IPv4 record: Host DHCP → 169.254.1.2/24, Service static → 169.254.1.1:443, hostnameidrac.local. IPv6 record: Host AutoConfigure, Service staticfde1:53ba:e9a0:de11::1port 443, same hostname. New public API:AxlSmbiosRedfishOverIp+axl_smbios_read_redfish_over_ip(parses the 91-byte fixed prefix + variable hostname per SMBIOS 3.x §7.43.3); plusaxl_ipv6_formatsibling ofaxl_ipv4_format(RFC 5952 canonical form). dmidecode prints structured Redfish-over-IP fields, hex-dumpsinterface_dataand unrecognized protocol payloads. 2591/2591 unit tests both arches; original entry kept below for posterity.[x] OBSOLETE —
dmidecode --type 42decode bodies. Currently prints only the header (Interface Type: Network Host Interface (0x40), protocol count, lengths). The 7-byte interface-data and 102-byte Redfish-over-IP protocol-data bodies aren’t parsed.axl-smbios.calready hasaxl_smbios_get_host_interface()that returns a parsedAxlSmbiosHostInterfacewith the Redfish-over-IP fields (host MAC, IP, port, hostname, service UUID); the dmidecode tool just doesn’t call it. Extendtools/dmidecode.ccase 42 to print: - For interface-type 0x40 (Network): USB vendor:product or PCI BDF - For protocol 0x04 (Redfish over IP): host IP/mask/gateway, service port, hostname, service UUID Use case: dmidecode –type 42 should tell you which USB device is the iDRAC Redfish gateway and how to reach it.[ ] USB-NIC driver image-unload entry points. Per the
third_party/edk2/README.md, these drivers are upstream EDK2 NetworkPkg vendored binaries — axl-sdk doesn’t own the source. AddingImage Unloadwould have to be upstreamed. Also worth noting:axl_net_ensure_driversdeliberately leaves these loaded between tool invocations (so subsequent tools find SNP already up). An unload entry-point that actually disconnected SNP would defeat that contract. So the practical fix is “live with EFI_UNSUPPORTED onunload, it’s the right behavior for the ensure pattern” — the original framing of this as a bug was overcautious. Mark the entry to make this clear next time.NetworkCommon.efi,UsbCdcEcm.efi,UsbCdcNcm.efi,UsbRndis.efiall returnEFI_UNSUPPORTEDonunload <handle>because they don’t register an unload entry-point. OnlyRtkUsbUndiDxe.efi(third-party Realtek) gets it right. Each affected driver’sDriverEntryshould: 1. Register an unload handler thatDisconnectController’s every controller it bound, thenUninstallMultipleProtocolInterfacesfor any image-handle-installed protocols 2. Verify byload X.efi+connect -r+unload <handle>expecting Success Source for these is likely EDK2USBNetworkPkgderivatives — patches go upstream-of-axl-sdk. Quality-of-life for development testing without rebooting the host.[x]
fetch.efi --secureflag (and flip default to insecure). Resolved in commit 4120a6a:fetch.efinow defaults totls.verify=falsewhen built withAXL_TLS=1, prints “TLS certificate verification disabled (use –secure to enable)” in verbose mode, and the--secureshort--Sflag opts back into verification. Long-term Mozilla CA bundle remains a future enhancement.[x]
lspcidoesn’t enumerate PCI segment 0 — NOT A BUG. Diagnostic logging from commit 48f5b39 confirms both segments enumerate cleanly on XE7745:MCFG[0]: seg=0x0000 base=0x60000000 bus=0x00..0xffMCFG[1]: seg=0x0001 base=0x70000000 bus=0x00..0xffTree output shows entries from both: segment 0 prints in the barebus:dev.funcformat (00:00.0), segment 1 prints with the explicit prefix (0001:80:02.0). The original observation was an output-reading artifact —grep '^[0-9a-f]{4}:'skips the bare-format segment-0 entries because lspci omits the0000:prefix unless--show-domainis passed. Lesson: when lspci output looks suspicious, verify with--show-domain(force the seg prefix) or--debug(which prints the MCFG table) before filing a bug. Original entry preserved below.[x] OBSOLETE —
lspcidoesn’t enumerate PCI segment 0 (filed sawMCFG: 2 segment(s)in debug log but the lspci output only listed segment0001:devices — every host bridge, bridge, and endpoint missing from segment 0. Bug inaxl-pci.cenumeration walk; either off-by-one on segment iteration or assumes single-segment.[x]
netinfo --no-loaddoes not stop ConnectController. Resolved:--no-loadnow still callsaxl_driver_connect(NULL)to bind firmware-provided drivers — only the disk-driver loading is skipped. The two operations were bundled insideaxl_net_ensure_drivers; netinfo’s--no-loadearly-return bypassed both. Now the early-return path explicitly runs the connect, so “use only firmware-provided drivers” is reachable via the flag (originally needed a manual shellconnect -r).[ ] AXL_TLS=1 in uefi-devkit’s pinned axl-sdk build. Tracked as a uefi-devkit-side task: enable
AXL_TLS=1so the bundledfetch.efiand friends gain HTTPS support. iDRAC Redfish is HTTPS-only; without this flag,fetch.efierrors with “HTTPS requires AXL_TLS=1 build.” Source already supports TLS via mbedtls (src/net/axl-tls.c); just a build-flag flip.[ ] Stage
rfbrowse.efiin uefi-devkit images. rfbrowse is built and works (Phase B2 done) but not staged inuefi-devkit/build/staging/<arch>/. Add a manifest entry so it ships with uefi-devkit ISOs.[x]
UsbRndis.efidata plane silently drops packets — RESOLVED via axl-sdk-side workaround. Tip 2acf40f, HW-validated on XE7745 2026-05-06: -RndisFix.efiwalks USB interfaces, finds RNDIS comms (vid:pid 413c:a102, class 02/02/ff), sendsREMOTE_NDIS_SET_MSG / OID_GEN_CURRENT_PACKET_FILTER = 0x0DviaSEND_ENCAPSULATED_COMMANDdirectly, bypassing the EDK2SetUsbRndisPacketFilterstub. - AfterRndisFix,ping 169.254.1.1goes from 100% loss to 0% loss (same TCP/HTTPS works too). - Auto-pick TCP routing in commit 22410f8 makesRfBrowse -b 169.254.1.1zero-config: subnet-match picks eth1 (169.254.1.0/24). Full Redfish service-root JSON returned. ---sourceflag tested with valid pin (works), invalid IP string (hard error, no silent fallback), and IP no interface owns (hard error). The EDK2 stub remains a defect upstream — see commit message for details. axl-sdk’s workaround makes the issue invisible to consumers without requiring an EDK2 build environment.[ ] OBSOLETE —
UsbRndis.efidata plane silently drops packets — root cause identified. EDK2’sMdeModulePkg/Bus/Usb/UsbNetwork/UsbRndis/driver has a stubSetUsbRndisPacketFilteratUsbRndisFunction.c:903-911that justreturn EFI_SUCCESSwithout ever sending aREMOTE_NDIS_SET_MSGto the device. Wiring atUsbRndis.c:669exposes this stub through theEDKII_USB_ETHERNET_PROTOCOL.SetUsbEthPacketFiltervtable slot; bothRndisUndiReceiveFilter(viaPxeFunction.c:854) and any caller hoping to set the packet filter call into the no-op. Result: the iDRAC’s RNDIS endpoint stays in its post-INITIALIZE default state with packet filter = 0; it accepts our TX bulk-out frames but never delivers RX frames up the bulk-in pipe. SNP comes UP, link reports “Media present”,ifconfigaccepts a static IP — but ICMP / TCP / DHCP / anything to 169.254.1.1 gets 100% packet loss, exactly what we see on XE7745. Linux’sdrivers/net/usb/rndis_host.calways sendsRNDIS_MSG_SETwithOID_GEN_CURRENT_PACKET_FILTER = NDIS_PACKET_TYPE_DIRECTED | _BROADCAST | _ALL_MULTICAST = 0x0Dduringrndis_bind()— that is the step EDK2 omits.Confirmed empirically 2026-05-06: with eth1 statically assigned 169.254.1.2/24 + default gateway 169.254.1.1, both UEFI shell `ping -s 169.254.1.2 169.254.1.1` and `NetInfo.efi ping 169.254.1.1` time out 100%. Fix paths, in increasing order of effort: 1. **EDK2 upstream patch** (~25 lines): implement `SetUsbRndisPacketFilter` to build a `REMOTE_NDIS_SET_MSG` (struct already exists at `UsbRndis.h:469`), populate `Oid = OID_GEN_CURRENT_PACKET_FILTER (0x0001010E)`, payload = 0x0D, send via the existing `RndisControlMsg` path (model on `RndisUndiInitialize` at `UsbRndisFunction.c:1063-1116`). Ship a fixed `UsbRndis.efi` in `third_party/edk2/`. 2. **axl-sdk-side workaround**: post-load, walk USB-IO handles bound by UsbRndis, issue the `SEND_ENCAPSULATED_COMMAND` USB control transfer directly (bmRequestType=0x21, bRequest=0x00) carrying the `REMOTE_NDIS_SET_MSG`. Doesn't require modifying the vendored binary; can ship as a small `axl-rndis-fix.c` in `src/net/`. Calling `EDKII_USB_ETHERNET_PROTOCOL.SetUsbEthPacketFilter` from axl-sdk is useless — that's the stub. Sources: - Linux canonical: `drivers/net/usb/rndis_host.c` — `RNDIS_DEFAULT_FILTER` definition and `rndis_bind()` sequence - Microsoft NDIS: OID_GEN_CURRENT_PACKET_FILTER spec - EDK2 source: `MdeModulePkg/Bus/Usb/UsbNetwork/UsbRndis/` in edk2-stable202505 (same defect in edk2-stable202408 — long-standing, not a regression)
[ ] OBSOLETE —
UsbRndis.efidata plane silently drops packets on iDRAC10 USB-NIC (was filed as “L3 to iDRAC USB-NIC fails,” now narrowed). Confirmed 2026-05-04 on XE7745: - Bind chain is correct: withUsbCdcEcm+UsbCdcNcm+UsbRndisall loaded and competing on the iDRAC USB-IO handle, RNDIS wins (ECM/NCMSupported()both return NOT_FOUND for this device descriptor). So the class is genuinely RNDIS. - SNP comes UP, MNP children spawn, EDK2ifconfig -s eth0 static 169.254.1.2 255.255.0.0 169.254.1.1applies cleanly (verified viaifconfig -l: routes set, gateway set). - Both ICMP ping and TCP/80 + TCP/443 connect attempts to iDRAC’s169.254.1.1produce 100% packet loss / “login request failed.” -delldiagslinuxlibredfish on the same hardware works fine from a booted Linux (kernelcdc_rndis) — so iDRAC backend IS responsive; the gap is purely in our UEFI RNDIS data plane.Most likely root causes inside `UsbRndis.efi`: - `REMOTE_NDIS_INITIALIZE_MSG` over the RNDIS control endpoint either not sent or not negotiated correctly (without it the device buffers and discards frames silently) - `REMOTE_NDIS_SET_MSG` with `OID_GEN_CURRENT_PACKET_FILTER` not configured (default filter rejects all received frames) - MTU / max-transfer-size mismatch from a partial init Audit `UsbRndis` source against Linux's `drivers/net/usb/rndis_host.c` and Microsoft's RNDIS spec sections 2.2.4–2.2.7. Adding verbose RNDIS-init log lines behind a debug build flag would make the next investigation tractable. Without USB packet capture, this is otherwise blind. Side note: axl-sdk's `netinfo list` doesn't reflect static IP set by `ifconfig` either (column shows `IPv4=-` when ifconfig reports `169.254.1.2`); netinfo is reading from a different source than IPv4Config2. Filed separately as a netinfo display bug.
[x] axl-webfs serve volume listing corrupted (multi-volume) + teardown leaves loop with active event sources. RESOLVED 2026-05-05, HW-validated on XE7745 2026-05-06: serve lists
fs0:throughfs4:cleanly (all five ASCII names, no garbage, fs4 present), and ESC-stop produces justmem: no leaks detectedwith no AxlLoop-free-with-active- events warning. Three commits: - axl-webfsc5328acaliases FtVolume to AxlVolume so the struct layout can’t drift. The 8-byte size mismatch caused every entry past index 0 in the volume listing to read garbage (because file-transfer.c casts mVolumes directly to AxlVolume*). - axl-webfsa3c3611reverses the cmd-serve teardown order so the HTTP server frees BEFORE its parent AxlLoop, which clears the TCP listener event sources. - axl-sdke642c9cadds<image_fs>:<name>to the driver candidate search list so “drop driver next to app at volume root” works (covers both axl-webfs’s mount tests in QEMU and the user-friendly install pattern). Tests: 47/47 X64 + 67/67 X64+AARCH64 axl-webfs full suite.[ ] axl-webfs serve volume listing is corrupted on real hardware (XE7745, 2026-05-05). axl-webfs’s volume enumeration emits volumes with mangled labels: expected fs0/fs1/fs2/fs3/fs4, actual
fs0,<garbage>2,<garbage>2,,fs3,<garbage>p1, AND fs4 (the volume with the actual tools) is missing entirely.curl /fs4/returns “Volume not found.” Looks like volume names are read as multi-byte chars (likely UCS-2 from EFI_FILE_INFO->VolumeLabel) and emitted raw, producing invalid UTF-8 over HTTP. fs0 and fs3 happen to survive because their labels are ASCII-clean coincidentally. Fix lives in axl-webfs (its volume enumerator) — tracking here since it surfaced during axl-sdk hardware validation.[x]
rfbrowseagainst iDRAC10 over TLS-RSA crashes host with #GP — RESOLVED in commit 33d5d55. Original entry preserved below for context; root cause was rsa.c not being in MBEDTLS_SOURCES, leaving mbedtls_rsa_init as an undefined-but- PLT-stubbed symbol whose GOT slot held the link-time RVA (0x56D00) rather than a runtime address. Empirically confirmed working on real hardware 2026-05-05 against XE7745 (svc tag KCH0TD1, iDRAC10 at 10.215.120.97):rfbrowse.efi -v -u root -p calvin -b 10.215.120.97returned HTTP 200 with full Redfish service-root JSON (1805 bytes, RedfishVersion 1.22.0). TLS handshake completed (tls: initialized (mbedTLS)); SNP came up via core drivers in 4s, iPXE fallback correctly skipped per commit 6bde651. End-to-end validation of the 15-commit chain (build-system + linker + mbedtls + iPXE-fallback + watchdog).[x]
axl_http_clientdoes not decodeTransfer-Encoding: chunkedresponse bodies. Surfaced + RESOLVED 2026-05-05 on XE7745, empirically validated end-to-end against iDRAC10:RfBrowse.efi -v -u root -p calvin -b 10.215.120.97 /redfish/v1/Systems/System.Embedded.1returns the full multi-KB chunked Dell ComputerSystem JSON (PCIe device list, DIMM topology, OEM Dell extensions, etc.). The fix landed in four commits: -b0f5cef—read_chunked_bodystate machine (ST_SIZE → ST_DATA → ST_TRAIL → ST_TRAILERS → ST_DONE) plusaxl_hex_parse_u64public helper, dogfooded byaxl-http-request.candaxl-pci.c. -b9c88dd— don’t treat 0-byte recv as EOF (TLS WANT_READ case). -dfad8a4— first attempt at draining staged TLS data; had a buffer-aliasing flaw. -e9df883— adopt softbmc’s canonical pattern: persistent per-clienttls_rx_buf,tls_drainloop, separate plaintext destination. Cross-checked against EDK2NetworkPkg/HttpDxe/HttpsSupport.c(alternative manual- framing design) and Mongoose’s same-bug fix (cesanta/mongoose#2668). HTTP integration:/chunked,/chunked-ext,/chunked-with-cl. 2569/2569 unit + 72/72 HTTP integration both arches.[x] Content-Length body-read loop hangs on TLS
WANT_READ. Originally filed against the oldclient_recv(TCP→stage→read, one shot per call). The rewrite in commit e9df883 (persistenttls_rx_buf+tls_drainloop) madeclient_recvalways drive progress: each call does a TCP recv that adds bytes to mbedtls’s internal record-assembly state, even if the round doesn’t yet yield plaintext. So the Content-Length loop’srecv_len == 0rounds are guaranteed to be transient — the next iteration’s TCP recv will eventually complete a record. Resolved as a side-effect of the chunked-decode fix.[x] 216-byte leak in
src/net/axl-mbedtls-platform.c:31on rfbrowse exit, surfaced 2026-05-05 by the in-tree leak tracker after the chunked-decode validation succeeded. RESOLVED in commit 4e9ff8d, empirically validated on XE7745: same RfBrowse system-inventory probe now reportsmem: no leaks detectedon exit. Root cause was thataxl_tls_initset up five mbedtls globals (config / cert / PK / CTR-DRBG / entropy) without registering the matchingaxl_tls_cleanupanywhere; commit hooks it viaaxl_atexit, matching the AxlSpd / AxlUsb pattern.[ ] Tools have inline hex-string parsers worth migrating to
axl_hex_parse_u64. Reviewer flagged 2026-05-05:tools/lspci.c(parse_hex_field/pair pattern, same as pre-refactor axl-pci.c),tools/lsusb.c(same),tools/ipmi.caround(v << 4) | nloops,src/data/axl-json-parse.cfor JSON50x...numeric literal parsing. Migrating buys free uint64-overflow detection.axl-url.cand the JSON5\xHHdecoder are lower priority —axl_hex_nibble × 2is arguably clearer for fixed-2-digit decoding.[x] OBSOLETE — original entry kept for posterity:
rfbrowseagainst iDRAC10 over TLS-RSA crashes host with #GP** (XE7745, 2026-05-05, mbedtls config commit a7a7cf2). Now that ECDHE-RSA / RSA-PKCS1 cipher suites are enabled, the handshake gets pastNO_CIPHER_CHOSENand proceeds — but crashes the host:UEFI0011: CPU Exception Type 0x0D: General Protection (Software)from iDRAC lifecycle log timestamped 05:24:56, exactly when rfbrowse was invoked. RSOD text (currently uncaptured) is needed to identify the faulting RIP. Likely candidates inside mbedtls/RSA path: - Misaligned bignum access (mbedtls’smbedtls_mpiarrays need natural alignment; if our PEM/X509 parser hands it unaligned bytes the hardware traps) - Stack overflow — RSA-2048 needs more stack than ECDHE-ECDSA; EDK2 default per-app stack is 128KB, mbedtls might overflow when chains/key-sizes get larger - Buffer overrun in ouraxl-mbedtls-platform.cshim (alloc/ free/snprintf wrappers) that’s only exercised on the RSA path (extra alloc/free pressure from RSA blinding etc.)Plan: capture the RSOD screenshot, run through `~/projects/aximcode/rsod-decode/rsod-decode.py` with the built rfbrowse.efi to map RIP→source line, fix at root cause. Until fixed, ECDHE-RSA / RSA cipher suites should perhaps be enabled behind a build flag (default off) so non-iDRAC consumers don't pay for an unstable codepath.
[ ]
Load Erroron subsequent.efilaunches after running axl-sdk tools. User-attributed 2026-05-05 (corrects two prior wrong hypotheses): the failure is NOT iDRAC-virtual- media-related (user reproduced with the bundle copied to other media), and is NOT iPXE-LoadImage-hook-related (iPXE not loaded in any of the failing sessions). At least three distinct bites observed in this session, all requiring a host reboot to recover.**DO NOT DISMISS — the bug is real and is expected to recur.** User's standing instruction 2026-05-05: keep this open until paired captures definitively pin the cause; the "stress test didn't reproduce it" finding below is data, not a fix. **Empirical findings from a stress test post-commit 4e9ff8d (mbedtls atexit cleanup):** ran 7 distinct tools (NetInfo, LsPci, LsUsb, SysInfo, Dmidecode, RfBrowse, Hexdump) plus 3 sequential RfBrowse invocations — all completed cleanly, 0 Load Errors, every run reported "mem: no leaks detected", and `memmap` totals (LoaderCode/BS_Code/BS_Data/RT_Code/ RT_Data) were byte-identical across captures. **This rules out one specific hypothesis** — "pool fragmentation from cumulative leaks" — because per-run accounting is now exact. It does NOT mean the bug is fixed; the failure is reportedly random and tens of clean runs don't disprove a sporadic failure mode. The user has explicitly observed this symptom with the bundle copied to non-iDRAC media, so the cause is in axl-sdk-tool execution residue of some kind. Remaining candidates worth investigating: 1. Some specific tool sequence that this stress test didn't hit (e.g., killing a tool with Ctrl-C mid-network-op, tools that fail vs. tools that succeed, etc.). 2. State accumulating outside what `memmap` and the AxlMem leak tracker observe — protocol installations on the handle table, event/timer registrations, driver binding state, OpenProtocol agent records that aren't CloseProtocol'd, etc. 3. Race / TOCTOU in the loader itself triggered by axl-sdk driver-binding or signal-handler installation. Capture ritual when it next bites (memorize this, the bug window is short — recovery requires reboot): ``` dh -p LoadedImage # count loaded images dh -p Image # broader image protocol coverage memmap # page totals + free fragmentation ``` Both at a known-good state (after a successful tool run) and at a failing state (right before re-trying a tool that Load-Errors). The diff is the shortest path to root cause; a single failing-state capture without a paired good baseline is much weaker.
[ ] uefi-devkit’s
net-init.nshbypasses the iPXE-fallback protection inaxl_net_ensure_drivers. The script does:nsh for %d in drivers\%arch%\*.efi load %d endfor connect -rThis wildcard-loadsipxe-intel.efi/ipxe-broadcom.efieagerly, regardless of whether SNP comes up from non-iPXE class drivers — so the protection added in axl-sdk commit 6bde651 (load iPXE only as fallback) doesn’t apply when users runnet-init.nshdirectly. Result: same LoadImage poisoning and watchdog-armed states the axl-sdk fix was supposed to prevent.Two options for uefi-devkit-side fix: (a) Drop `net-init.nsh` entirely. Tools like `NetInfo list` already trigger `axl_net_ensure_drivers` with the right fallback semantics — that's the correct entry point. (b) Rewrite `net-init.nsh` to skip iPXE explicitly: ```nsh for %d in drivers\%arch%\*.efi if not %d eq drivers\%arch%\ipxe-intel.efi then if not %d eq drivers\%arch%\ipxe-broadcom.efi then if not %d eq drivers\%arch%\ipxe-all.efidrv then load %d endif endif endif endfor connect -r ``` (a) is cleaner and matches axl-sdk's intent. Track in uefi-devkit, but log here since it surfaced during axl-sdk hardware validation.[ ] axl-webfs serve frees AxlLoop with active TCP event source (XE7745, 2026-05-05). Shutdown emits:
loop: axl_loop_free: caller-owned event source id=2 still active loop: axl_loop_free: 1 caller-owned event source(s) still active — free will proceed but consumers may crash on next useUse-after-free vector on next loop reuse. axl-webfs’s serve cleanup needs to disconnect the listener and remove event sources beforeaxl_loop_free. Real bug; not theoretical.
Test infrastructure — quirky KCS BMC fixture (medium priority)
Our QEMU IPMI testing uses a clean reference BMC simulator that exposes none of the timing/state-transition quirks real Dell hardware has. The four iDRAC10 KCS bugs (commit 2aecf16 and its follow-ups) shipped through QEMU clean and only surfaced on real hardware:
SMBIOS Type 38 BaseAddressModifier in QEMU is
0x00→ 1-byte stride either way (bits 1..0 OR bits 7..6 read as 0). Real Dell publishes0x4A→ 4-byte stride.QEMU’s BMC keeps OBF=0 during the WRITE_START echo phase; real Dell sets OBF=1 as a side effect of state-machine internals.
QEMU transitions OBF + state atomically; real Dell has a few-microsecond window where OBF rises before state flips.
QEMU keeps state==READ until host acks the last byte; real Dell flips state to IDLE at the moment it places the final response byte.
Plus a coverage gap: axl-test-ipmi.c unit tests use
axl_ipmi_session_new_with_callback which bypasses the wire
protocol entirely. The KCS state machine is only exercised by
the test_real_hw path that runs against an external BMC
simulator started by test-ipmi-qemu.sh — and only validates
resp_len >= 12 for Get Device ID, doesn’t stress the quirky
transitions.
Plan: write a “mean BMC” fixture (Python or QEMU device device
patch) that exposes each of the above quirks behind flags, plus
optional misbehaviors (slow OBF, non-spec spacing, more body
bytes than the spec mandates). Wire it into test-ipmi-qemu.sh
as a parallel KCS test job; either as random fuzz or a curated
matrix.
Concrete acceptance: a future change to axl-ipmi-kcs.c that
re-introduces any of the four bugs above MUST cause this test
to fail in CI before it can land.
~~New tool — i2c / SMBus low-level explorer~~ — DONE (commit c3fe679)
Shipped: tools/i2c.efi with verbs list, probe, get, set,
dump and Linux i2cdetect-style argument compatibility (--quick,
--read, --all, optional [first] [last] positionals, AUTO mode
mirroring Linux’s per-address mode selection). Plus three new public
AxlSmbus APIs (axl_smbus_describe, axl_smbus_quick,
axl_smbus_receive_byte) so the tool can do everything Linux’s
i2c-tools does.
Diagnostic value already realized — probing the AMD FCH AUX controller (where DDR5 SPDs live on AMD server boards) in all three modes from UEFI returns zero ACKs, while Linux on the same hardware sees the full address range respond. Confirms the UEFI silent- failure isn’t byte-data-specific; the controller is gated at the protocol level until OS handoff. The original problem section is preserved below for context.
Original problem statement — i2c / SMBus low-level explorer
axl-sdk has the SMBus library piece (AxlSmbus with HC + I2C
master backends, multi-handle walker as of 2026-05-06) but no
tool that exposes it. Linux’s i2c-tools (i2cdetect, i2cget,
i2cset, i2cdump) is the canonical reference for what such a
tool needs to do; the same diagnostic gap is what made the
“memspd finds no DIMMs on AMD EPYC” investigation harder than it
should have been (we had to add an ad-hoc memspd scan verb to
get visibility into per-controller behavior).
Proposed surface (tools/i2c.c):
i2c list— enumerate every publishedEFI_SMBUS_HC_PROTOCOLEFI_I2C_MASTER_PROTOCOLhandle with its kind label and handle pointer (parallelsi2cdetect -l).
i2c probe <bus>— Linux’si2cdetect -y -r <bus>equivalent. Walk every 7-bit slave address, print which respond; mark slaves that look like SPD by reading byte 0 and matching JEDEC type-byte range (0x09..0x12).i2c get <bus> <slave> <reg>— single-byte read.i2c get <bus> <slave> <reg> <count>— block read. Mirrorsi2cget [-y] <bus> <slave> <reg>.i2c set <bus> <slave> <reg> <byte> [<byte>...]— block write. Refuse without an interactive confirmation (writes can brick devices); add--forceto bypass.i2c dump <bus> <slave>— full 256-byte hex dump (parallelsi2cdump).
Why it matters: ANY future tool that needs to read a non-SPD
SMBus device (FRU EEPROMs at 0xA0..0xAE, fan controllers,
voltage monitors, temperature sensors at 0x4C/0x4D, USB-C TCPCs,
PCIe retimers, etc.) needs the same enumerate-and-probe machinery
that memspd scan has. Centralizing it in tools/i2c.c makes
the next investigation 5 minutes instead of an hour.
Empirical motivation captured in DellXE7745/02-spd-and-i2c.log:
on this Dell, Linux’s i2cdetect -l shows 4 buses (3 PIIX4
ports + 1 MGA i2c) but UEFI exposes a different set (1 SMBus HC +
12 I2C masters, none of which carries DIMM SPDs). A tool would
make this immediately visible without writing one-off probe code.
OEM CPLD SMBus adapter — vendor-protocol consumer (medium priority)
On some AMD server platforms the DDR5 SPDs aren’t routed to the FCH SMBus at all — they sit behind an OEM-specific CPLD (“FPGA hub PLD”) accessed via a vendor UEFI protocol. BIOS reads from the CPLD and publishes a subset of the data via SMBIOS Type 17, which is what memspd’s existing fallback path consumes.
The CPLD memory map carries more than DIMM SPDs: common / control / inventory / error-event / misc / sticky / payload / riser-slot-map sections — totalling around 35 KB of platform state. A shaped consumer module would unlock telemetry that isn’t in SMBIOS Type 17 (fan/temp/power detail, riser-slot inventory, PCIe topology hints, etc.).
Reference shape (Dell BIOS reference impl studied 2026-05-06):
DELL_CPLD_SMBUS_PROTOCOLGUID6B14C95E-84FF-477D-16AF-168FCE8B7D99Two function pointers:
DellCpldReadByte(Offset, *Data, Location)andDellCpldWriteByte(Offset, *Data).Location=0returns the byte from a BIOS-cached host memory map;Location=1reads live from the CPLD slave at SMBus address0xC4over SSIF (the EFI_I2C_MASTER protocol path).0x8A10-byte memory map split into named regions (CPLD_COMMON_OFFSET, CPLD_CONTROL_OFFSET, CPLD_INVENTORY, CPLD_MISC, etc.).
Proposed shape — parallel to existing src/ipmi/axl-ipmi-dell.c:
src/oem/axl-oem-cpld-dell.c— vendor adapter consumingDELL_CPLD_SMBUS_PROTOCOLwhen published. Internal-only; no public header — exposes its data through the generic AxlSmbus descriptor or a futureaxl-oem.humbrella.Probe at session-open time. Falls through if the protocol isn’t installed (other platforms, other vendors).
Vendor-name policy applies: file naming is OK (
axl-oem-cpld-dell.cis proper-noun protocol identity, parallel toaxl-ipmi-dell.c); incidental empirical comments stay generic.
Why “medium” not “high”:
SMBIOS Type 17 already covers the common DIMM-info case, so memspd doesn’t need this path
No current consumer needs the extended telemetry
Adding it is ~200 LOC + tests; not free
The mechanism is now documented in
src/smbus/axl-smbus-piix4.cand<axl/axl-spd.h>so future maintainers see the route exists
Lift the priority if a consumer ever needs riser-slot or fine-grained thermal data from UEFI on these platforms.
Console + tooling improvements (lower priority)
[ ] Log timestamp
.usecfield is always.000000.print_console_timestampinsrc/log/axl-log.c:116formats 6 fractional-second digits fromAxlTime.nanosecond / 1000, but the backend atsrc/backend/native/axl-backend-native.c:178copies the rawEFI_TIME.Nanosecondstraight fromgRT->GetTime. Most firmware leaves Nanosecond=0 — the UEFI spec lets firmware populate it but doesn’t require it, and the Dell PowerEdge / OVMF / iDRAC platforms we test on all report 0. Result: every log line ends in.000000which is worse than just hiding the field. Fix options: 1. Detect Nanosecond=0 and fall back to a monotonic counter delta — supplement wallclock with elapsed-since-boot in milliseconds (we can read it via a UEFI Stall(0)-anchored timer or, on x64, the TSC + frequency from CPUID). 2. Hide the fractional field when Nanosecond=0 — minimum viable, no resolution improvement but stops lying. Surfaced 2026-05-06 by user during the in-band Redfish validation session.[ ]
axl-webfs serveshould accept--source <ip>to bind a specific listening interface.axl_tcp_listencurrently auto-picks viatcp_find_service_binding(NULL, NULL, ...)which lands on the first non-zero handle. On a multi-NIC host (laptop curl path needs eth0 = 10.9.177.98; in-band BMC path needs eth1 = 169.254.1.2), the user has no way to pick. The TCP layer already plumbs a source-IP throughaxl_tcp_connect_via; the listen path needs a sibling. Plan: 1. Addaxl_tcp_listen_via(port, source_ip, &listener)to the publicaxl-tcp.h.source_ip == NULLkeeps current auto-pick. 2. Plumb throughaxl_http_serverso the server config exposes a “listen.ip” option. 3. Wire--source <ip>into axl-webfs’s serve subcommand. Surfaced 2026-05-06 during PUT/GET validation: with eth0 and eth1 both configured, auto-pick chose handles[0] consistently which happened to be the correct one — but that’s fragile. Explicit pin is the right shape.[ ] Console-aware tool output mode. Tools like
lspci,drivers,netinfo,dmidecodeemit ANSI escape sequences (color, cursor positioning) that are noise when consumed via IPMI SOL or piped capture. UEFIConOut->Modelets tools detect serial-console mode (or expose an env var); switch to flat output when set.[ ] Real-hardware test runner. Today axl-sdk has 2565 ratcheted unit tests in QEMU. The QEMU↔real-Dell coverage gap is real (mkrd Load Error on the R6725 didn’t reproduce in QEMU;
--no-loadsemantics are hardware-dependent). Ascripts/test-axl-hw.shthat: 1. Mounts the test bundle via iDRAC virtual media 2. Cold powercycles the host 3. Watches IPMI SOL for the UEFI shell prompt 4. Drives test EFIs sequentially, captures output 5. Reports pass/fail in ratchet style Closes the QEMU-coverage gap for pre-release CI.