/dev/kmem returns EIO for all kernel addresses: kernel image is not in phys_install, so mdb -k cannot attach #9
/dev/kmem cannot read any kernel-image address, so mdb -k cannot attach. The kernel's own physical pages are not in phys_install.
Symptom
# mdb -k -e '::status'
mdb: failed to read 'platform' string from kernel: I/O error
mdb: failed to initialize target: I/O error
mdb_kvm.c:1538-1547 finds the platform symbol without complaint, then fails
on the read. The symbol is fine -- a single global at 0xfffffffffbcf85e0 in
.bss, present exactly once in both .symtab and .dynsym.
Reproducer, without mdb
fd = open("/dev/kmem", O_RDONLY);
n = pread(fd, buf, 63, (off_t)0xfffffffffbcf85e0); /* `platform` */
| host | result |
|---|---|
| hh-alpha11, kernel of 2026-08-10 (pre-Limine) | n=63, content "amd64" |
hh-alpha12, rev 342 (Limine) |
n=-1, errno 5 (EIO) |
Both .text (limine_entry, 0xfffffffffb82e160) and .bss (platform) fail,
so it is the whole kernel image, not one section.
Root cause
mem.c:290 computes is_memory = pf_is_memory(pfn), and mem.c:314-321:
if (!is_memory) {
if (allowio) { ... } else
error = EIO; /* <- here */
} else
error = uiomove(va + pageoff, nbytes, rw, uio);
EIO rather than EFAULT is the tell: hat_getpfnum() succeeded, so the mapping
is fine; pf_is_memory() is what returns false. That checks the PFN against
phys_install, and the kernel image is not in it.
Measured from the boot log at rev 342 -- the kernel image PFNs against the
published install ranges:
kernel image PA 0xb92f1000 .. 0xb9309000
hh: install ... 0xb7eb0000 .. 0xb92f1000 <- ends exactly where the kernel starts
hh: install ... 0xb97f9000 .. 0xbed3f000 <- resumes after it
24 of 24 sampled kernel pages are outside phys_install. The kernel sits
precisely in the 5.28 MiB hole between two install ranges, and the boundary
coincides exactly with the start of the image -- consistent with the region Limine
reports as its "kernel and modules" memmap type being excluded when phys_install
is built.
Upstream (dboot) loads the kernel into ordinary memory that is in phys_install,
which is why the pre-Limine host reads it fine.
Blast radius
Anything gated on pf_is_memory() for a kernel address. mdb -k is the one that
bites: with #8 fixed,
/dev/ksyms is now correct and well-formed (49712 symtab entries, correct
e_entry), so mdb gets all the way to the first memory read before failing. DTrace
is unaffected -- it does not read through /dev/kmem.
Notes for whoever fixes it
Do not just widen phys_install -- that is the page-accounting surface that
#6 and the rev-284
heaptext bug both live on, and pages that are in phys_install must not also be
reachable on the free list. The kernel image needs to be known as memory without
becoming allocatable. docs/design/LIMINE_ARCHITECTURE.md §6 has the four
existing claims and why each exists; this is a fifth case that was missed because
nothing exercised it until ksyms started working.
Found while fixing #8 -- this is the next layer down, not a regression from it.
Fixed in rev 345 (ee3c758de9b9). Verified on a full hh-build of tip plus a marker-gated 28-boot matrix.
The fix
Limine files the kernel under EXECUTABLE_AND_MODULES, which the adapter's
memmap switch dropped into bi_rsvdmem through its default arm. The adapter
now publishes the kernel's own span as bi_kernel — the entry whose base equals
the executable base from the already-present executable-address request — while
module images (the ~300 MiB inflated boot archive) keep the old treatment.
startup_memlist() merges that span into phys_install only, and no claim
is taken. kphysm_init() builds memsegs and the free list from phys_avail,
so a span present in phys_install and absent from phys_avail can never be
allocated. pf_is_memory() then returns true and mem.c's M_KMEM path stops
returning EIO.
The mistake worth recording
The first version merged the span into bootops->boot_mem->physinstalled. That
is also the source phys_avail is built from, and avail_filter() trims only
the front and back of a range — it cannot punch a hole in the middle. The kernel
span coalesces into the middle of a larger usable range, so that version put
2.4 MiB of live, executing kernel image on the free list.
It booted. 17 services, 0 failed. mdb -k attached. /dev/kmem worked. Every
functional check passed while the kernel's own RAM was allocatable — the rev-284
failure mode exactly. What caught it was a two-line property assertion computed
from the hh: install / hh: avail prints:
kernel span in phys_install: YES (must be YES)
kernel span in phys_avail : YES (must be NO) <- FAIL
The corrected design keeps a separate merged list feeding phys_install alone,
which makes the property structural rather than dependent on a filter happening
to catch it.
Verification (rev 345, full build, hh-alpha12)
hh: kernel span b92ee000+50b000 -> phys_install (not phys_avail)
in phys_install: YES / in phys_avail: NO -> PASS
# mdb -k -e '::status'
debugging live kernel (64-bit) on hammerhead
operating system: 5.11 hammerhead-ee3c758de9b9 (amd64)
pread(/dev/kmem, `platform) -> n=63, "amd64"
Build build_ok=y, 0 errors after "Building OS-Net source". Boot matrix
28/28 — 1 vCPU 4/4, 2 vCPU 12/12, 4 vCPU 12/12 — every boot 17 daemons /
0 failed. DTrace unaffected (212 modules / 0 without CTF). Healthy across
1.2 GiB of kmem churn.
Note on mdb -k
::status works; ::memstat still fails with couldn't walk 'memseg': unknown walk name. That is the separate, pre-existing dmod-relocation problem
(mdb_warn in .symtab but not .dynsym, the GNU-ld --export-dynamic
regression noted 2026-08-10) — mdb's target attaches, its loadable modules do
not. Not tracked here.
Assignees
No assignees
Labels
No labels
Severity
Medium