Skip to content

BIOS: panic on unknown E820 region type violates ACPI §15 (Table 15-374) #581

Open

Description

@batatrax

Hi — reporting a panic hit on real hardware during a BIOS/CSM boot, with a proposed three-line fix. Happy to open a PR against v0.9-base if you agree with the approach.

Symptom

ASUS ROG AMD board, 32 GiB RAM, AMI BIOS 2.20.1271, booting legacy/CSM from a USB stick. The bootloader runs, reaches the memory map parsing, and stops:

panicked at src/bootinfo/memory_map.rs:225:18:
invalid region type 2954887168

The kernel never starts. Nothing indicates which region is at fault, nor how many there are.

What the specification requires

INT 15h AX=E820h is specified by the ACPI Specification, §15 — System Address Map Interfaces. Table 15-374 (Address Range Types) explicitly covers types the OS does not know:

Ranges marked "Reserved for future use" must be treated by OSPM as if the type returned was AddressRangeReserved.

So the specification anticipates unknown types, and mandates reserving them rather than failing. The current behaviour is non-conformant: a bootloader refuses to start on firmware that is within spec.

This is not hypothetical — Linux dealt with the same issue ("e820: Undefined type not treated as AddressRangeReserved"), some firmware emitting type 13 where SeaBIOS emits 2.

What 0.11 already does

The next major version handles this well:

// bootloader-x86_64-bios-stage-4/src/memory_descriptor.rs
fn kind(&self) -> MemoryRegionKind {
    match self.0.region_type {
        1     => MemoryRegionKind::Usable,
        other => MemoryRegionKind::UnknownBios(other),
    }
}

Neither panic nor silence: the region is not reusable, and the raw value is preserved. The proposal below carries that spirit into the constraints of 0.9.

Proposed fix

src/bootinfo/memory_map.rs, around line 219:

 impl From<E820MemoryRegion> for MemoryRegion {
     fn from(region: E820MemoryRegion) -> MemoryRegion {
         let region_type = match region.region_type {
             1 => MemoryRegionType::Usable,
             2 => MemoryRegionType::Reserved,
             3 => MemoryRegionType::AcpiReclaimable,
             4 => MemoryRegionType::AcpiNvs,
             5 => MemoryRegionType::BadMemory,
-            t => panic!("invalid region type {}", t),
+            // ACPI Specification §15, Table 15-374 (Address Range Types): ranges marked
+            // "Reserved for future use" must be treated by OSPM as if the type returned
+            // was AddressRangeReserved. Treating them as fatal makes the bootloader
+            // refuse to start on firmware that is within spec.
+            //
+            // Not reusing the region is both the safe and the conformant behaviour;
+            // `Reserved` expresses exactly that.
+            _ => MemoryRegionType::Reserved,
         };

Why Reserved rather than a variant carrying the value, as 0.11 does with UnknownBios(u32): MemoryRegionType is #[repr(C)] and crosses the boundary between bootloader and kernel, which are compiled separately. Adding a payload-carrying variant would change its representation and break the ABI for every existing kernel. On a maintenance branch that seems out of the question.

If you would rather preserve the value anyway, two options exist — a payload-free variant plus a separate field in MemoryRegion, or a counter exposed through BootInfo — but both touch the ABI and are your call, not mine.

What this fix does not address

In our case the value received is 2954887168 = 0xB0200000. That is not a region type, however exotic: the specification defines only a handful. It is an address. So there is additionally something wrong with how the map is read upstream of this code — entry size, entry count, or buffer overrun — which I have not characterised yet.

The proposed change does not fix that. It keeps the bootloader usable on conformant hardware, and turns a fatal stop into a merely unused region, which is what the specification asks for. I will keep investigating the root cause and report back.

Reproduction

  • bootloader = "0.9.34", custom x86_64 target, image produced by cargo bootimage
  • written with dd to a USB stick (SanDisk 0781:5590), 3,091,968 bytes, checksum verified
  • ASUS ROG AMD board, AMI BIOS 2.20.1271, legacy/CSM boot
  • the same binary runs fine under QEMU: the fault only shows on this real firmware, which is consistent with an E820 map that differs from SeaBIOS's

Activity

  1. Freax13 commented on Aug 21, 2026

    @Freax13
    Member

    This is not hypothetical — Linux dealt with the same issue ("e820: Undefined type not treated as AddressRangeReserved"), some firmware emitting type 13 where SeaBIOS emits 2.

    There's an email thread with this title, but AFAICT no patch related to it was merged into the kernel.

    Proposed fix

    src/bootinfo/memory_map.rs, around line 219:

     impl From<E820MemoryRegion> for MemoryRegion {
         fn from(region: E820MemoryRegion) -> MemoryRegion {
             let region_type = match region.region_type {
                 1 => MemoryRegionType::Usable,
                 2 => MemoryRegionType::Reserved,
                 3 => MemoryRegionType::AcpiReclaimable,
                 4 => MemoryRegionType::AcpiNvs,
                 5 => MemoryRegionType::BadMemory,
    -            t => panic!("invalid region type {}", t),
    +            // ACPI Specification §15, Table 15-374 (Address Range Types): ranges marked
    +            // "Reserved for future use" must be treated by OSPM as if the type returned
    +            // was AddressRangeReserved. Treating them as fatal makes the bootloader
    +            // refuse to start on firmware that is within spec.
    +            //
    +            // Not reusing the region is both the safe and the conformant behaviour;
    +            // `Reserved` expresses exactly that.
    +            _ => MemoryRegionType::Reserved,
             };

    This fix looks good to me.

    What this fix does not address

    In our case the value received is 2954887168 = 0xB0200000. That is not a region type, however exotic: the specification defines only a handful. It is an address. So there is additionally something wrong with how the map is read upstream of this code — entry size, entry count, or buffer overrun — which I have not characterised yet.

    This has me concerned. This isn't just some unknown type, it's likely a corrupted descriptor. Does your proposed fix actually make the bootloader work on your hardware? Is it just this one entry that's corrupted or is the entire memory map made up of corrupted descriptors? Ideally we would like to rule out that this is not a bug in the bootloader especially iff the entire memory map is corrupted.

    Linux dumps the E820 memory map to the kernel logs. Could you see what memory map Linux uses? It would be interesting to see if they also report type 0xB0200000 for one of the descriptors.

  2. batatrax commented on Aug 21, 2026

    @batatrax
    Author

    2026-08-21_1.zip

    Reply to #581 — measured on hardware, 2026-08-21

    Draft. Every number below was measured on the affected machine or under QEMU on
    2026-08-21. Photos: doc/handoff/metal-photos/2026-08-21_*.


    Thanks for the quick review — and for pushing back on the corrupted-descriptor
    question. You were right to insist, but the answer turned out to be neither of the
    two options we discussed. I instrumented the loader and ran it on the affected
    machine.

    The firmware is fine. The bootloader overflows its own stack into the memory map
    it is parsing.

    Your three questions

    "Does your proposed fix actually make the bootloader work on your hardware?"

    Yes, it boots. But it should not be merged on its own — see the last section.

    "Is it just this one entry that's corrupted, or is the entire memory map made up of corrupted descriptors?"

    Neither: the map is clean. I dumped all entries before conversion:

    • count = 35, every type in 1..=4, zero out-of-spec types
    • regions perfectly contiguous, e.g. 0x100000 + 0x9C1F000 = 0x9D1F000 = next base
    • tail entries are ordinary MMIO (fed40000, fedc2000, fedd4000, ff000000)
      and high RAM (0x82f380000) — an unremarkable 32 GiB map

    I also checksummed the entry bytes in real mode, immediately after do_e820
    returns, and recomputed the same checksum in long mode just before conversion:
    identical (0x3c35). The buffer is not modified between collection and parsing.

    And 0xB0200000 is absent from the entire 4 KiB buffer.

    "Could you see what memory map Linux uses?"

    Not needed to reach a conclusion, but I can still do it if you want it for the record.

    What is actually happening

    I filled the unused stack with 0xA5 right after do_e820, then read the
    high-water mark after create_from completes:

    E820 entries stack used vs. 7 168 B available
    QEMU / SeaBIOS 7 8 918 B +1 750 B (24 % over)
    Affected machine (AMI BIOS, AMD) 35 10 038 B +2 870 B (40 % over)

    linker.ld places the stack at _stack_start = 0x6000 … _stack_end = 0x7c00
    (7 168 B), directly above the E820 buffer at _memory_map = 0x5000. The stack
    grows downward, so it grows straight into the map it is about to parse.

    Measured cost: ~40 B of stack per E820 entry. On the affected machine the stack
    reaches 0x54ca while the entries end at 0x5348 — 386 bytes of margin, i.e.
    roughly 9 more entries before the map itself gets overwritten
    .

    This overflow also happens under QEMU. It is invisible there only because the
    region it lands in happens to be empty.

    Two contributing defects

    1. MemoryMap is a 1 544-byte local of create_from (64 × MemoryRegion),
      i.e. 21 % of the entire stack, allocated on it.
    2. add_region() calls self.sort() on every insertion — a full sort of 64
      elements repeated once per entry. Measured: with 35 entries the conversion alone
      consumes 1 120 B more stack than with 7.

    Why the value looked like an address

    0xB0200000 was never a region type — it is stack residue read back as one.

    Two runs on the same machine and same firmware, differing only in the size of
    the compiled bootloader, produced 2954887168 (0xB0200000) and 196608
    (0x30000). A firmware-supplied value would be stable; a value that changes with
    the compiled code comes from the stack.
    That is what convinced me.

    About the ACPI fix

    It remains correct and still worth merging (ACPI §15, Table 15-374 — ranges marked
    "Reserved for future use" must be treated as AddressRangeReserved).

    But merging it alone would turn a hard panic into silence: the loader would boot
    with a stack that has already overflowed into the memory map, and nothing would say
    so. I would suggest pairing it with one or more of:

    • moving MemoryMap off the stack (static / BSS);
    • sorting once after all insertions instead of on every one;
    • enlarging the stack, and/or placing a guard between it and _memory_map.

    Unrelated, but in the same file

    mmap_ent is declared .word (2 bytes) in e820.s but read as
    static mmap_ent: usize (8 bytes) in main.rs. The read pulls in 6 bytes of
    neighbouring data; & 0xff masks it today, but it is a read outside the symbol.
    This is observable: after I added a checksum variable next to mmap_ent, I could
    literally see its value appear in the upper bytes of the "counter".

    Raw output

    Transcribed from the affected machine (the tool's own labels are in French; values
    are verbatim). Photographs of each run are available if you want them.

    Run 1 — stock 0.9.34, no patch. Reproduced identically twice, hours apart:

    panicked at src/bootinfo/memory_map.rs:225:18: invalid region type 2954887168
    

    Run 2 — same machine, instrumented build (dump + real-mode checksum + stack
    canary + ACPI fix). count, sum, canary and stack are the instrumentation:

    mmap_ent raw=0x10bb663c350023   count=35
    buffer=0x5000  end=0x5348  (cap 0x5fff)   stack=0x6000..0x7c00
    idx        base     length  type
     00           0      9d400  t=1
     01       9d400       2c00  t=2
     02       e0000      20000  t=2
     03      100000    9c1f000  t=1
     04     9d1f000     2e1000  t=2
     05     a000000     200000  t=1
     ...  23 middle entries not shown ...
     29    fed40000       5000  t=2
     30    fed80000      10000  t=2
     31    fedc2000       e000  t=2
     32    fedd4000       2000  t=2
     33    ff000000    1000000  t=2
     34   82f380000     c80000  t=2
    
    0xB0200000            : ABSENT from buffer 0x5000..0x6000
    out-of-spec types     : 0
    checksum real=0x3c35 long=0x3c35   IDENTICAL - buffer untouched
    canary                : 472 B altered of 3256 - lowest=0x592a
    STACK low-water=0x54ca  used=10038 B / 7168 B budgeted
      => OVERFLOWS by 2870 B below 0x6000  (40% over)
    

    Run 3 — same instrumented build, but with the original panic! restored
    (i.e. the ACPI fix removed, everything else identical):

    panicked at src/bootinfo/memory_map.rs:238:18: invalid region type 196608
    

    Same machine, same firmware, same E820 map — a different bogus value
    (196608 = 0x30000 instead of 0xB0200000). Only the compiled code differed.

    Under QEMU/SeaBIOS, same instrumented build, for comparison:

    count=7   buffer=0x5000  end=0x50a8
    out-of-spec types     : 0
    checksum real=0x076c long=0x076c   IDENTICAL
    STACK low-water=0x592a  used=8918 B / 7168 B budgeted
      => OVERFLOWS by 1750 B below 0x6000  (24% over)
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions