Reproducibility

September 6, 2026 · View on GitHub

KASLD's results can be checked, not just trusted. The core property to verify is that the range KASLD infers for the kernel image base contains the real value:

truth ∈ [virt_image_base_min, virt_image_base_max]

The same property holds on the physical axis — phys_truth ∈ [phys_kaslr_text_min, phys_kaslr_text_max] — on architectures that place the kernel image independently in physical memory (x86-64, arm64, riscv64, s390); elsewhere the physical base is a fixed projection of the virtual one and is covered by the check above.

A range that excludes the truth is a bug; a range that is wider than necessary is not (it just means less was recovered). There are four ways to verify this, from a boot-free structural check up to live-kernel soundness. All are runnable from a clean checkout.

These overlap in breadth — several run KASLD across many architectures — but each answers a different question, so they are complementary, not redundant:

checkruns againstanswerscost
tests/replaycaptured fixtures, offlinedoes KASLD parse and run cleanly over real kernel state? (structural — a regression / robustness check, no truth compared)seconds
tests/validate-fixturesthe shipped truth-bearing captures, offlinedoes the inferred range contain the real base across many captured kernels, without a VM?seconds
extra/validate-bundleone captured bundle, offlinedoes the inferred range contain that system's truth?seconds
tests/vm/runa live kernel booted under qemudoes the inferred range contain a live kernel's truth, across arches and privilege levels?minutes

The key distinction is structural vs. soundness. tests/replay is structural — it confirms the binary runs cleanly over many captured kernels but does not compare against a truth. Soundness is checked directly: tests/validate-fixtures does it offline over the captured kernels that carry ground truth — a real kallsyms _text/_stext or an iomem "Kernel code" line — and tests/vm/run does it on a freshly booted live kernel whose real base it knows directly. Many committed captures keep that truth even though they are shared publicly: the anonymization that prepares a fixture for sharing (extra/collect --anonymize, plus extra/anonymize-fdt for device-tree boards) redacts host-identifying text — hostname, CPU model, UUIDs, MACs, sensitive cmdline values — from cpuinfo/cmdline/version/dmesg, but never touches /proc/kallsyms or the iomem kernel line, so the base survives. A capture's symbol table is reduced to the landmarks a fixture reads by extra/trim-kallsyms, which validates the capture before and after and keeps the reduction only when the soundness result is identical — a component that resolves a symbol by name reads a truncated table as an absence rather than an omission. The offline checks are cheap and continuous; the VM check is slower and run periodically.

Table of Contents

1. On the local kernel

Capture the running system's state and confirm the inferred ranges contain the truth:

extra/collect --kallsyms              # capture a self-contained bundle
extra/validate-bundle kasld-bundle-*  # run kasld over it, check the truth

collect writes a path-preserving copy of the files KASLD reads plus the kernel's real symbol addresses. validate-bundle runs the matching kasld binary over that bundle offline and checks every inferred range against the captured ground truth. It exits non-zero if any range excludes the truth. No root needed.

It also lists which vantage sources the bundle carries — the collecting process's identity, its MAC label, the active LSM list, the group database, and the container markers. Those do not affect an inferred range, so a missing one is reported rather than failed; it is reported at all because its absence is silent in the analysis, where an unread source and an empty answer both present as "unknown".

--kallsyms can only record the ground truth when kallsyms is readable (kptr_restrict=0, or root). Without it the bundle carries no truth and the checks report N/A rather than PASS — still not a failure, just nothing to compare against.

2. Live, across architectures

tests/vm/run boots real, publicly-fetchable kernels under qemu-system, runs KASLD against the running kernel, and applies the same check across architectures and attacker profiles:

make cross            # build the per-arch binaries
tests/vm/run          # boot each supported arch, default profile
tests/vm/run all hardened   # repeat under the unprivileged floor

It needs qemu-system-<arch> and the cross toolchains on PATH; an architecture is skipped (not failed) when either is missing. The analysis always runs as an unprivileged user (uid 1000) — KASLD's threat model is an unprivileged local attacker, so every profile measures what such a user can leak, never what root can. Every profile is a declared sysctl vector — never a "default-config" assumption; the one privileged step is the per-boot ground-truth capture the check compares against.

default is each kernel's own compile-time sysctl posture, read back at boot and left as booted: kptr_restrict=0 and perf_event_paranoid=2 upstream, with dmesg_restrict taking whatever the kernel .config sets (0 on the mainline builds, 1 on Alpine). The other profiles move away from that booted baseline: kptr-hidden, perf-open, dmesg-open and bpf-open each shift a single axis; hardened tightens all of them at once.

  • default — the booted compile-time defaults. kptr_restrict=0 alone does not expose symbol values: at the upstream perf_event_paranoid=2, /proc/kallsyms zeroes every address for an unprivileged reader, because kallsyms_show_value() requires perf_event_paranoid<=1 or CAP_SYSLOG (generic across architectures). On a stock kernel the symbol table therefore yields nothing, and the base comes from sound inference alone.
  • kptr-hiddendefault plus kptr_restrict=2: kernel pointers are hidden. On a stock kernel this changes little over default — perf already gates kallsyms — so it isolates the effect of kptr_restrict by itself.
  • perf-opendefault plus perf_event_paranoid=0. Dropping perf below 1 unlocks two independent sound signals at once: /proc/kallsyms now shows real addresses, and perf_event_open text-poke records leak a symbol pointer. Either pins the base exactly.
  • dmesg-opendefault plus dmesg_restrict=0: the world-readable ring buffer. This differs from default only on kernels that ship dmesg_restrict=1 (Alpine); on the mainline builds dmesg is already open.
  • bpf-opendefault plus unprivileged_bpf_disabled=0: unprivileged bpf() permitted, which the BPF verifier-log leaks require. The perf-open equivalent for the BPF axis. Cells fall into three groups. Where the kernel has CONFIG_BPF_SYSCALL and unprivileged use is locked off — the distro cells ship CONFIG_BPF_UNPRIV_DEFAULT_OFF, the mainline ones are built with it forced on — that lock is what this profile relaxes, and default reflects an attacker who cannot call bpf(). Roughly half the mainline cells do not set CONFIG_BPF_SYSCALL at all — among them every x86_64, i686, armv7, armeb, ppc32 and MIPS build — so there is no bpf() and no unprivileged_bpf_disabled sysctl to relax; the profile is a no-op there, and the boot log records the knob as absent. On the 5.15 mainline cells that do have CONFIG_BPF_SYSCALL the forced lock did not take, so those boot with unprivileged bpf() already permitted and default measures the same vantage bpf-open does. The row is shown on every cell, like the other sysctl profiles, including where it reads the same as default — that is a result, not an absence, and it is the one this profile most often reports. Where the syscall exists, a row matching default says unprivileged bpf() was permitted and still leaked nothing; where it does not, the row says there was never a syscall to permit. A missing row would leave a reader unable to tell either case from a cell nobody ran. On a mainline cell that does have bpf(), the verifier-log components would still report unavailable: their offset table is keyed by uname and built from a corpus of published distro kernels, which a locally built 7.0.0 is not in.
  • hardenedkptr_restrict=2, dmesg_restrict=1, perf_event_paranoid=3, unprivileged_bpf_disabled=2: the realistic attacker floor, where only file-derived facts survive.

Under the tighter profiles the window may widen but must still contain the truth. See tests/vm/README.md for the full arch list and options.

Every architecture is booted on mainline kernel.org kernels — the LTS lines 5.15 and 6.6 and current 7.0 — cross-built from pinned source by tests/vm/build-kernel (a stock upstream defconfig plus fixed config overlays: endianness, devtmpfs, and text KASLR for riscv64 and ppc32). Of the fifteen, eight additionally boot a publicly-fetchable Alpine distro kernel; the other seven — armeb, mips, mipsel, mips64el, powerpc64, ppc32, riscv32 — have no Alpine port, so mainline is their only source. Cells are named <arch>-<distro>-<line>; for a mainline cell the version in the name is a label and LINUX_VERSION sets the source:

LINUX_VERSION=5.15.211 tests/vm/build-kernel ppc32-mainline-5.15  # cross-build -> cache (slow)
tests/vm/run ppc32-mainline-5.15                                  # boot it, verdict

armeb needs -mbe8 on both payloads. The cross set's big-endian arm toolchain emits BE32 by default, which cannot run on the BE8 userspace an arm kernel provides from ARMv6 on — it takes SIGILL on its first instruction. -mbe8 produces BE8 instead, and make cross passes it for the armeb triple, as does the VM harness for the init it stages beside kasld.

Architecture characterization

Which layout quantities KASLR randomizes — and therefore which columns the results tables below can carry at all — is a structural property of each architecture's memory model, not of whether the KASLR feature is enabled. Two architecture flags decide it: whether kernel text tracks the direct map (so a physical leak also fixes the virtual base), and whether the direct map is a static compile-time projection. Together they set, per architecture, whether the physical image base is an independently-randomized quantity and whether the direct-map / vmalloc / vmemmap region bases move on their own. This table is the reason a single flat grid does not fit: different architectures have different quantities to measure.

architecturetext ↔ direct mapdirect mapindependent phys baseindependent region bases
x86_64decoupledrandomizedyesyes (RANDOMIZE_MEMORY)
aarch64decoupledrandomizedyesno †
riscv64decoupledrandomizedyesno †
s390xdecoupledrandomizedyesno †
i686coupledstaticcoupledno
armv7, armebcoupledstaticcoupledno
mips, mipselcoupledstaticcoupledno
mips64elcoupledstaticcoupledno
ppc32coupledstaticcoupledno
ppc64le, powerpc64coupledstaticcoupledno
riscv32coupledstaticcoupledno
loongarch64coupledstaticcoupledno

Coupled means kernel text and the direct map share a fixed offset, so a physical leak reveals the virtual base and there is no separate physical quantity to score — the results matrix marks these coupled. Decoupled means the kernel image is placed independently in physical memory, so the physical image base is its own quantity. † The decoupled non-x86 architectures do randomize the linear map, but by a mechanism (aarch64 memstart_addr, s390 identity base, riscv64 kernel_map.page_offset) that this harness bounds but does not ground-truth; only x86_64's CONFIG_RANDOMIZE_MEMORY region bases are truth-gated (see Memory-KASLR region bases).

Reading the result cells in the tables below:

  • exact — the base was recovered (a byte-exact pin, or a window narrower than one KASLR slot); 0 bits of KASLR entropy remain.
  • <n> bits — KASLR still randomized, n bits (2ⁿ candidate slots) unrecovered.
  • unbounded — the resolved window has an unstated edge, so what remains cannot be counted at all. Weaker than any bit figure, and distinct from : KASLR is on, and nothing bounds the search on one side.
  • — KASLR is off on that cell, so there is no randomization to defeat.
  • coupled — the quantity is a fixed projection of another (not independently randomized), so there is no separate value to score.
  • n/a — the quantity does not exist on that architecture at all.

Soundness itself is not a per-cell column: every published cell is sound on every gated axis, and a cell whose window excludes the truth is withheld, never shown. The tables report how much was recovered; that the window contains the truth is the invariant behind the whole page.

Results matrix: image base

This is the core soundness result — the virtual kernel-text base and, on decoupled architectures, the physical image base. Every cell is sound: the true base lies inside the guaranteed window on both axes, and a cell whose window excludes the truth is withheld, never published. The snapshot is generated by tests/vm/run all <scenario> followed by tests/vm/run table.

The guaranteed window is resolved purely from signals at or above the sound floor and never depends on a timing or microarchitectural side channel — that is what makes the soundness verdict reproducible run to run and machine to machine: every cell contains the truth on every boot, on any host.

The shape of the result, one row per architecture. tests/vm/run chart renders it from the same rows as the table below, so the two cannot disagree:

Residual KASLR entropy by architecture: a horizontal range bar per architecture spanning the guaranteed residual across every kernel line and configuration tested, from s390x at 17-39 bits and aarch64 at 15-32 down to x86_64 at 0-9 and i686 at 5-8; a ring at zero marks the architectures where perf-open recovers the base exactly, the three MIPS architectures are drawn apart because perf-open gains nothing there, and five architectures boot with KASLR off on every kernel

A bar spans every kernel line and configuration tested rather than naming one number, because the spread is real: a different VA width or kernel line is a different window, not noise.

Reading a cell. source is the kernel: alpine (a distro kernel) or mainline (a vanilla kernel.org build via tests/vm/build-kernel). virt residual and phys residual say how much KASLR entropy KASLD could not strip from each axis; the vocabulary (exact / <n> bits / / coupled) is defined in Architecture characterization above. phys residual carries an independent count only on the decoupled arches; elsewhere it reads coupled, and the virtual result already determines it.

CONFIG_PHYSICAL_ALIGN is a build choice, not a property of the architecture. Alpine sets it to 16 MiB on x86 where the mainline builds take the 2 MiB default — eight times coarser, so an Alpine image base starts with 3 bits less entropy for anything to strip. The alpine x86 rows therefore report fewer bits than the mainline rows beside them because of how they were built, not because of the release. Those 3 bits are the whole of the shortfall on i686 and on the x86_64 physical axis; where a gap is wider, the rest is a difference in what that release leaked. See x86 slot granularity (CONFIG_PHYSICAL_ALIGN).

The bits are a floor, not a ceiling: they are the most KASLR entropy that survives sound, reproducible inference, and a microarchitectural oracle can strip more on capable hardware — see Speculative narrowing below. They are also one boot's sample rather than a constant: a row whose ceiling comes from a leak positioned relative to the kernel image has a span that tracks that boot's KASLR draw, so it can move a bit or two between runs, in either direction. Read a profile's effect from the component it unlocks, not from a small step.

Every profile is run on every cell and gets a row, including where it reads the same as default — a profile that changed nothing is a result, and the table would otherwise leave that indistinguishable from a cell nobody ran. Cells carrying a configuration in the release column (7.0.0 (va39), 7.0.0 (vmsplit2g)) are purpose-built kernels on the config axis; see Kernel-configuration sensitivity.

Two conventions explain rows that would otherwise look like gaps. An architecture predating a kernel line has no row for it (LoongArch, mainlined in 6.1, has no 5.15 row). And a cell may show KASLR: off where its neighbours on the same architecture show on: the three MIPS cells on the 6.6 line are built without CONFIG_RANDOMIZE_BASE, because on that line MIPS Oopses in the exception path once the kernel actually relocates. Those cells still test soundness — an unprivileged process cannot read CONFIG_RANDOMIZE_BASE, so KASLD reports the conservative KASLR-possible window, which must contain the fixed base like any other.

The summary names the two scenarios that carry the result: default is the ordinary unprivileged vantage, and perf-open is the one that moves the answer on most architectures. The remaining scenarios — kptr-hidden, dmesg-open, bpf-open, hardened, and the x86 paging modes — restate their cell's default in all but 12 rows, and the fold beneath carries every one of them.

archreleasesourceKASLRdefault (virt / phys)perf-open (virt / phys)
aarch646.12.81-0-virtalpineon31 bits / 14 bitsexact / 9 bits
aarch645.15.211mainlineon31 bits / 14 bitsexact / 9 bits
aarch646.6.144mainlineon31 bits / 14 bitsexact / 9 bits
aarch647.0.0mainlineon31 bits / 14 bitsexact / 9 bits
aarch647.0.0 (va39)mainlineon22 bits / 14 bitsexact / 9 bits
aarch647.0.0 (va47)mainlineon32 bits / 14 bitsexact / 9 bits
aarch647.0.0 (va42)mainlineon24 bits / 13 bitsexact / 9 bits
aarch647.0.0 (va36)mainlineon19 bits / 14 bitsexact / 9 bits
armeb5.15.211mainlineoff— / —— / —
armeb6.6.144mainlineoff— / —— / —
armeb7.0.0mainlineoff— / —— / —
armv76.12.81-0-ltsalpineoff— / —— / —
armv75.15.211mainlineoff— / —— / —
armv76.6.144mainlineoff— / —— / —
armv77.0.0mainlineoff— / —— / —
armv77.0.0 (vmsplit2g)mainlineoff— / —— / —
i6866.12.81-0-ltsalpineon5 bits / coupledexact / coupled
i6865.15.211mainlineon8 bits / coupledexact / coupled
i6866.6.144mainlineon8 bits / coupledexact / coupled
i6867.0.0mainlineon8 bits / coupledexact / coupled
i6867.0.0 (vmsplit2g)mainlineon8 bits / coupledexact / coupled
loongarch646.18.44-0-ltsalpineon10 bits / coupledexact / coupled
loongarch646.6.144mainlineon10 bits / coupledexact / coupled
loongarch647.0.0mainlineon16 bits / coupledexact / coupled
mips5.15.211mainlineon8 bits / coupled8 bits / coupled
mips6.6.144mainlineoff— / —— / —
mips7.0.0mainlineon13 bits / coupled13 bits / coupled
mips64el5.15.211mainlineon8 bits / coupled8 bits / coupled
mips64el6.6.144mainlineoff— / —— / —
mips64el7.0.0mainlineon14 bits / coupled14 bits / coupled
mipsel5.15.211mainlineon8 bits / coupled8 bits / coupled
mipsel6.6.144mainlineoff— / —— / —
mipsel7.0.0mainlineon13 bits / coupled13 bits / coupled
powerpc645.15.211mainlineoff— / —— / —
powerpc646.6.144mainlineoff— / —— / —
powerpc647.0.0mainlineoff— / —— / —
ppc325.15.211mainlineon12 bits / coupledexact / coupled
ppc326.6.144mainlineon14 bits / coupledexact / coupled
ppc327.0.0mainlineon14 bits / coupledexact / coupled
ppc64le6.12.81-0-ltsalpineoff— / —— / —
ppc64le5.15.211mainlineoff— / —— / —
ppc64le6.6.144mainlineoff— / —— / —
ppc64le7.0.0mainlineoff— / —— / —
riscv325.15.211mainlineoff— / —— / —
riscv326.6.144mainlineoff— / —— / —
riscv327.0.0mainlineoff— / —— / —
riscv646.18.44-0-ltsalpineoff— / —— / —
riscv645.15.211mainlineoff— / —— / —
riscv646.6.144mainlineonexact / 9 bitsexact / 9 bits
riscv647.0.0mainlineon16 bits / 9 bitsexact / 9 bits
s390x6.12.81-0-ltsalpineon39 bits / 10 bitsexact / 10 bits
s390x5.15.211mainlineon17 bits / 16 bitsexact / exact
s390x6.6.144mainlineon17 bits / 16 bitsexact / exact
s390x7.0.0mainlineon39 bits / 16 bitsexact / 10 bits
x86_646.12.81-0-virtalpineon2 bits / 6 bitsexact / 6 bits
x86_645.15.211mainlineon5 bits / 9 bitsexact / 9 bits
x86_646.6.144mainlineon5 bits / 9 bitsexact / 9 bits
x86_647.0.0mainlineon9 bits / 9 bitsexact / 9 bits
Full results matrix — every scenario, 364 rows
archreleasesourcescenarioKASLRvirt residualphys residual
aarch646.12.81-0-virtalpinedefaulton31 bits14 bits
aarch646.12.81-0-virtalpinekptr-hiddenon31 bits14 bits
aarch646.12.81-0-virtalpineperf-openonexact9 bits
aarch646.12.81-0-virtalpinedmesg-openon31 bits14 bits
aarch646.12.81-0-virtalpinebpf-openon15 bits14 bits
aarch646.12.81-0-virtalpinehardenedon31 bits14 bits
aarch645.15.211mainlinedefaulton31 bits14 bits
aarch645.15.211mainlinekptr-hiddenon31 bits14 bits
aarch645.15.211mainlineperf-openonexact9 bits
aarch645.15.211mainlinedmesg-openon31 bits14 bits
aarch645.15.211mainlinebpf-openon31 bits14 bits
aarch645.15.211mainlinehardenedon31 bits14 bits
aarch646.6.144mainlinedefaulton31 bits14 bits
aarch646.6.144mainlinekptr-hiddenon31 bits14 bits
aarch646.6.144mainlineperf-openonexact9 bits
aarch646.6.144mainlinedmesg-openon31 bits14 bits
aarch646.6.144mainlinebpf-openon31 bits14 bits
aarch646.6.144mainlinehardenedon31 bits14 bits
aarch647.0.0mainlinedefaulton31 bits14 bits
aarch647.0.0mainlinekptr-hiddenon31 bits14 bits
aarch647.0.0mainlineperf-openonexact9 bits
aarch647.0.0mainlinedmesg-openon31 bits14 bits
aarch647.0.0mainlinebpf-openon31 bits14 bits
aarch647.0.0mainlinehardenedon31 bits14 bits
aarch647.0.0 (va39)mainlinedefaulton22 bits14 bits
aarch647.0.0 (va39)mainlinekptr-hiddenon22 bits14 bits
aarch647.0.0 (va39)mainlineperf-openonexact9 bits
aarch647.0.0 (va39)mainlinedmesg-openon22 bits14 bits
aarch647.0.0 (va39)mainlinebpf-openon22 bits14 bits
aarch647.0.0 (va39)mainlinehardenedon22 bits14 bits
aarch647.0.0 (va47)mainlinedefaulton32 bits14 bits
aarch647.0.0 (va47)mainlinekptr-hiddenon32 bits14 bits
aarch647.0.0 (va47)mainlineperf-openonexact9 bits
aarch647.0.0 (va47)mainlinedmesg-openon32 bits14 bits
aarch647.0.0 (va47)mainlinebpf-openon32 bits14 bits
aarch647.0.0 (va47)mainlinehardenedon32 bits14 bits
aarch647.0.0 (va42)mainlinedefaulton24 bits13 bits
aarch647.0.0 (va42)mainlinekptr-hiddenon24 bits13 bits
aarch647.0.0 (va42)mainlineperf-openonexact9 bits
aarch647.0.0 (va42)mainlinedmesg-openon24 bits13 bits
aarch647.0.0 (va42)mainlinebpf-openon24 bits13 bits
aarch647.0.0 (va42)mainlinehardenedon24 bits13 bits
aarch647.0.0 (va36)mainlinedefaulton19 bits14 bits
aarch647.0.0 (va36)mainlinekptr-hiddenon19 bits14 bits
aarch647.0.0 (va36)mainlineperf-openonexact9 bits
aarch647.0.0 (va36)mainlinedmesg-openon19 bits14 bits
aarch647.0.0 (va36)mainlinebpf-openon19 bits14 bits
aarch647.0.0 (va36)mainlinehardenedon19 bits14 bits
armeb5.15.211mainlinedefaultoff
armeb5.15.211mainlinekptr-hiddenoff
armeb5.15.211mainlineperf-openoff
armeb5.15.211mainlinedmesg-openoff
armeb5.15.211mainlinebpf-openoff
armeb5.15.211mainlinehardenedoff
armeb6.6.144mainlinedefaultoff
armeb6.6.144mainlinekptr-hiddenoff
armeb6.6.144mainlineperf-openoff
armeb6.6.144mainlinedmesg-openoff
armeb6.6.144mainlinebpf-openoff
armeb6.6.144mainlinehardenedoff
armeb7.0.0mainlinedefaultoff
armeb7.0.0mainlinekptr-hiddenoff
armeb7.0.0mainlineperf-openoff
armeb7.0.0mainlinedmesg-openoff
armeb7.0.0mainlinebpf-openoff
armeb7.0.0mainlinehardenedoff
armv76.12.81-0-ltsalpinedefaultoff
armv76.12.81-0-ltsalpinekptr-hiddenoff
armv76.12.81-0-ltsalpineperf-openoff
armv76.12.81-0-ltsalpinedmesg-openoff
armv76.12.81-0-ltsalpinebpf-openoff
armv76.12.81-0-ltsalpinehardenedoff
armv75.15.211mainlinedefaultoff
armv75.15.211mainlinekptr-hiddenoff
armv75.15.211mainlineperf-openoff
armv75.15.211mainlinedmesg-openoff
armv75.15.211mainlinebpf-openoff
armv75.15.211mainlinehardenedoff
armv76.6.144mainlinedefaultoff
armv76.6.144mainlinekptr-hiddenoff
armv76.6.144mainlineperf-openoff
armv76.6.144mainlinedmesg-openoff
armv76.6.144mainlinebpf-openoff
armv76.6.144mainlinehardenedoff
armv77.0.0mainlinedefaultoff
armv77.0.0mainlinekptr-hiddenoff
armv77.0.0mainlineperf-openoff
armv77.0.0mainlinedmesg-openoff
armv77.0.0mainlinebpf-openoff
armv77.0.0mainlinehardenedoff
armv77.0.0 (vmsplit2g)mainlinedefaultoff
armv77.0.0 (vmsplit2g)mainlinekptr-hiddenoff
armv77.0.0 (vmsplit2g)mainlineperf-openoff
armv77.0.0 (vmsplit2g)mainlinedmesg-openoff
armv77.0.0 (vmsplit2g)mainlinebpf-openoff
armv77.0.0 (vmsplit2g)mainlinehardenedoff
i6866.12.81-0-ltsalpinedefaulton5 bitscoupled
i6866.12.81-0-ltsalpinekptr-hiddenon5 bitscoupled
i6866.12.81-0-ltsalpineperf-openonexactcoupled
i6866.12.81-0-ltsalpinedmesg-openon5 bitscoupled
i6866.12.81-0-ltsalpinebpf-openon5 bitscoupled
i6866.12.81-0-ltsalpinehardenedon5 bitscoupled
i6865.15.211mainlinedefaulton8 bitscoupled
i6865.15.211mainlinekptr-hiddenon8 bitscoupled
i6865.15.211mainlineperf-openonexactcoupled
i6865.15.211mainlinedmesg-openon8 bitscoupled
i6865.15.211mainlinebpf-openon8 bitscoupled
i6865.15.211mainlinehardenedon8 bitscoupled
i6866.6.144mainlinedefaulton8 bitscoupled
i6866.6.144mainlinekptr-hiddenon8 bitscoupled
i6866.6.144mainlineperf-openonexactcoupled
i6866.6.144mainlinedmesg-openon8 bitscoupled
i6866.6.144mainlinebpf-openon8 bitscoupled
i6866.6.144mainlinehardenedon8 bitscoupled
i6867.0.0mainlinedefaulton8 bitscoupled
i6867.0.0mainlinekptr-hiddenon8 bitscoupled
i6867.0.0mainlineperf-openonexactcoupled
i6867.0.0mainlinedmesg-openon8 bitscoupled
i6867.0.0mainlinebpf-openon8 bitscoupled
i6867.0.0mainlinehardenedon8 bitscoupled
i6867.0.0 (vmsplit2g)mainlinedefaulton8 bitscoupled
i6867.0.0 (vmsplit2g)mainlinekptr-hiddenon8 bitscoupled
i6867.0.0 (vmsplit2g)mainlineperf-openonexactcoupled
i6867.0.0 (vmsplit2g)mainlinedmesg-openon8 bitscoupled
i6867.0.0 (vmsplit2g)mainlinebpf-openon8 bitscoupled
i6867.0.0 (vmsplit2g)mainlinehardenedon8 bitscoupled
loongarch646.18.44-0-ltsalpinedefaulton10 bitscoupled
loongarch646.18.44-0-ltsalpinekptr-hiddenon10 bitscoupled
loongarch646.18.44-0-ltsalpineperf-openonexactcoupled
loongarch646.18.44-0-ltsalpinedmesg-openon10 bitscoupled
loongarch646.18.44-0-ltsalpinebpf-openon10 bitscoupled
loongarch646.18.44-0-ltsalpinehardenedon11 bitscoupled
loongarch646.6.144mainlinedefaulton10 bitscoupled
loongarch646.6.144mainlinekptr-hiddenon10 bitscoupled
loongarch646.6.144mainlineperf-openonexactcoupled
loongarch646.6.144mainlinedmesg-openon10 bitscoupled
loongarch646.6.144mainlinebpf-openon10 bitscoupled
loongarch646.6.144mainlinehardenedon11 bitscoupled
loongarch647.0.0mainlinedefaulton16 bitscoupled
loongarch647.0.0mainlinekptr-hiddenon16 bitscoupled
loongarch647.0.0mainlineperf-openonexactcoupled
loongarch647.0.0mainlinedmesg-openon16 bitscoupled
loongarch647.0.0mainlinebpf-openon16 bitscoupled
loongarch647.0.0mainlinehardenedon16 bitscoupled
mips5.15.211mainlinedefaulton8 bitscoupled
mips5.15.211mainlinekptr-hiddenon8 bitscoupled
mips5.15.211mainlineperf-openon8 bitscoupled
mips5.15.211mainlinedmesg-openon8 bitscoupled
mips5.15.211mainlinebpf-openon8 bitscoupled
mips5.15.211mainlinehardenedon8 bitscoupled
mips6.6.144mainlinedefaultoff
mips6.6.144mainlinekptr-hiddenoff
mips6.6.144mainlineperf-openoff
mips6.6.144mainlinedmesg-openoff
mips6.6.144mainlinebpf-openoff
mips6.6.144mainlinehardenedoff
mips7.0.0mainlinedefaulton13 bitscoupled
mips7.0.0mainlinekptr-hiddenon13 bitscoupled
mips7.0.0mainlineperf-openon13 bitscoupled
mips7.0.0mainlinedmesg-openon13 bitscoupled
mips7.0.0mainlinebpf-openon13 bitscoupled
mips7.0.0mainlinehardenedon13 bitscoupled
mips64el5.15.211mainlinedefaulton8 bitscoupled
mips64el5.15.211mainlinekptr-hiddenon8 bitscoupled
mips64el5.15.211mainlineperf-openon8 bitscoupled
mips64el5.15.211mainlinedmesg-openon8 bitscoupled
mips64el5.15.211mainlinebpf-openon8 bitscoupled
mips64el5.15.211mainlinehardenedon8 bitscoupled
mips64el6.6.144mainlinedefaultoff
mips64el6.6.144mainlinekptr-hiddenoff
mips64el6.6.144mainlineperf-openoff
mips64el6.6.144mainlinedmesg-openoff
mips64el6.6.144mainlinebpf-openoff
mips64el6.6.144mainlinehardenedoff
mips64el7.0.0mainlinedefaulton14 bitscoupled
mips64el7.0.0mainlinekptr-hiddenon14 bitscoupled
mips64el7.0.0mainlineperf-openon14 bitscoupled
mips64el7.0.0mainlinedmesg-openon14 bitscoupled
mips64el7.0.0mainlinebpf-openon14 bitscoupled
mips64el7.0.0mainlinehardenedon14 bitscoupled
mipsel5.15.211mainlinedefaulton8 bitscoupled
mipsel5.15.211mainlinekptr-hiddenon8 bitscoupled
mipsel5.15.211mainlineperf-openon8 bitscoupled
mipsel5.15.211mainlinedmesg-openon8 bitscoupled
mipsel5.15.211mainlinebpf-openon8 bitscoupled
mipsel5.15.211mainlinehardenedon8 bitscoupled
mipsel6.6.144mainlinedefaultoff
mipsel6.6.144mainlinekptr-hiddenoff
mipsel6.6.144mainlineperf-openoff
mipsel6.6.144mainlinedmesg-openoff
mipsel6.6.144mainlinebpf-openoff
mipsel6.6.144mainlinehardenedoff
mipsel7.0.0mainlinedefaulton13 bitscoupled
mipsel7.0.0mainlinekptr-hiddenon13 bitscoupled
mipsel7.0.0mainlineperf-openon13 bitscoupled
mipsel7.0.0mainlinedmesg-openon13 bitscoupled
mipsel7.0.0mainlinebpf-openon13 bitscoupled
mipsel7.0.0mainlinehardenedon13 bitscoupled
powerpc645.15.211mainlinedefaultoff
powerpc645.15.211mainlinekptr-hiddenoff
powerpc645.15.211mainlineperf-openoff
powerpc645.15.211mainlinedmesg-openoff
powerpc645.15.211mainlinebpf-openoff
powerpc645.15.211mainlinehardenedoff
powerpc646.6.144mainlinedefaultoff
powerpc646.6.144mainlinekptr-hiddenoff
powerpc646.6.144mainlineperf-openoff
powerpc646.6.144mainlinedmesg-openoff
powerpc646.6.144mainlinebpf-openoff
powerpc646.6.144mainlinehardenedoff
powerpc647.0.0mainlinedefaultoff
powerpc647.0.0mainlinekptr-hiddenoff
powerpc647.0.0mainlineperf-openoff
powerpc647.0.0mainlinedmesg-openoff
powerpc647.0.0mainlinebpf-openoff
powerpc647.0.0mainlinehardenedoff
ppc325.15.211mainlinedefaulton12 bitscoupled
ppc325.15.211mainlinekptr-hiddenon14 bitscoupled
ppc325.15.211mainlineperf-openonexactcoupled
ppc325.15.211mainlinedmesg-openon14 bitscoupled
ppc325.15.211mainlinebpf-openon14 bitscoupled
ppc325.15.211mainlinehardenedon14 bitscoupled
ppc326.6.144mainlinedefaulton14 bitscoupled
ppc326.6.144mainlinekptr-hiddenon14 bitscoupled
ppc326.6.144mainlineperf-openonexactcoupled
ppc326.6.144mainlinedmesg-openon14 bitscoupled
ppc326.6.144mainlinebpf-openon14 bitscoupled
ppc326.6.144mainlinehardenedon14 bitscoupled
ppc327.0.0mainlinedefaulton14 bitscoupled
ppc327.0.0mainlinekptr-hiddenon13 bitscoupled
ppc327.0.0mainlineperf-openonexactcoupled
ppc327.0.0mainlinedmesg-openon14 bitscoupled
ppc327.0.0mainlinebpf-openon14 bitscoupled
ppc327.0.0mainlinehardenedon14 bitscoupled
ppc64le6.12.81-0-ltsalpinedefaultoff
ppc64le6.12.81-0-ltsalpinekptr-hiddenoff
ppc64le6.12.81-0-ltsalpineperf-openoff
ppc64le6.12.81-0-ltsalpinedmesg-openoff
ppc64le6.12.81-0-ltsalpinebpf-openoff
ppc64le6.12.81-0-ltsalpinehardenedoff
ppc64le5.15.211mainlinedefaultoff
ppc64le5.15.211mainlinekptr-hiddenoff
ppc64le5.15.211mainlineperf-openoff
ppc64le5.15.211mainlinedmesg-openoff
ppc64le5.15.211mainlinebpf-openoff
ppc64le5.15.211mainlinehardenedoff
ppc64le6.6.144mainlinedefaultoff
ppc64le6.6.144mainlinekptr-hiddenoff
ppc64le6.6.144mainlineperf-openoff
ppc64le6.6.144mainlinedmesg-openoff
ppc64le6.6.144mainlinebpf-openoff
ppc64le6.6.144mainlinehardenedoff
ppc64le7.0.0mainlinedefaultoff
ppc64le7.0.0mainlinekptr-hiddenoff
ppc64le7.0.0mainlineperf-openoff
ppc64le7.0.0mainlinedmesg-openoff
ppc64le7.0.0mainlinebpf-openoff
ppc64le7.0.0mainlinehardenedoff
riscv325.15.211mainlinedefaultoff
riscv325.15.211mainlinekptr-hiddenoff
riscv325.15.211mainlineperf-openoff
riscv325.15.211mainlinedmesg-openoff
riscv325.15.211mainlinebpf-openoff
riscv325.15.211mainlinehardenedoff
riscv326.6.144mainlinedefaultoff
riscv326.6.144mainlinekptr-hiddenoff
riscv326.6.144mainlineperf-openoff
riscv326.6.144mainlinedmesg-openoff
riscv326.6.144mainlinebpf-openoff
riscv326.6.144mainlinehardenedoff
riscv327.0.0mainlinedefaultoff
riscv327.0.0mainlinekptr-hiddenoff
riscv327.0.0mainlineperf-openoff
riscv327.0.0mainlinedmesg-openoff
riscv327.0.0mainlinebpf-openoff
riscv327.0.0mainlinehardenedoff
riscv646.18.44-0-ltsalpinedefaultoff
riscv646.18.44-0-ltsalpinekptr-hiddenoff
riscv646.18.44-0-ltsalpineperf-openoff
riscv646.18.44-0-ltsalpinedmesg-openoff
riscv646.18.44-0-ltsalpinebpf-openoff
riscv646.18.44-0-ltsalpinehardenedoff
riscv646.18.44-0-ltsalpineno5lvloff
riscv646.18.44-0-ltsalpineno4lvloff
riscv645.15.211mainlinedefaultoff
riscv645.15.211mainlinekptr-hiddenoff
riscv645.15.211mainlineperf-openoff
riscv645.15.211mainlinedmesg-openoff
riscv645.15.211mainlinebpf-openoff
riscv645.15.211mainlinehardenedoff
riscv645.15.211mainlineno5lvloff
riscv645.15.211mainlineno4lvloff
riscv646.6.144mainlinedefaultonexact9 bits
riscv646.6.144mainlinekptr-hiddenonexact9 bits
riscv646.6.144mainlineperf-openonexact9 bits
riscv646.6.144mainlinedmesg-openonexact9 bits
riscv646.6.144mainlinebpf-openonexact9 bits
riscv646.6.144mainlinehardenedon4 bits9 bits
riscv646.6.144mainlineno5lvlonexact9 bits
riscv646.6.144mainlineno4lvlonexact9 bits
riscv647.0.0mainlinedefaulton16 bits9 bits
riscv647.0.0mainlinekptr-hiddenon16 bits9 bits
riscv647.0.0mainlineperf-openonexact9 bits
riscv647.0.0mainlinedmesg-openon16 bits9 bits
riscv647.0.0mainlinebpf-openon16 bits9 bits
riscv647.0.0mainlinehardenedon16 bits9 bits
riscv647.0.0mainlineno5lvlon16 bits9 bits
riscv647.0.0mainlineno4lvlon16 bits9 bits
s390x6.12.81-0-ltsalpinedefaulton39 bits10 bits
s390x6.12.81-0-ltsalpinekptr-hiddenon39 bits10 bits
s390x6.12.81-0-ltsalpineperf-openonexact10 bits
s390x6.12.81-0-ltsalpinedmesg-openon39 bits10 bits
s390x6.12.81-0-ltsalpinebpf-openon28 bits10 bits
s390x6.12.81-0-ltsalpinehardenedon39 bits10 bits
s390x5.15.211mainlinedefaulton17 bits16 bits
s390x5.15.211mainlinekptr-hiddenon17 bits16 bits
s390x5.15.211mainlineperf-openonexactexact
s390x5.15.211mainlinedmesg-openon17 bits16 bits
s390x5.15.211mainlinebpf-openon17 bits16 bits
s390x5.15.211mainlinehardenedon17 bits16 bits
s390x6.6.144mainlinedefaulton17 bits16 bits
s390x6.6.144mainlinekptr-hiddenon17 bits16 bits
s390x6.6.144mainlineperf-openonexactexact
s390x6.6.144mainlinedmesg-openon17 bits16 bits
s390x6.6.144mainlinebpf-openon17 bits16 bits
s390x6.6.144mainlinehardenedon17 bits16 bits
s390x7.0.0mainlinedefaulton39 bits16 bits
s390x7.0.0mainlinekptr-hiddenon39 bits16 bits
s390x7.0.0mainlineperf-openonexact10 bits
s390x7.0.0mainlinedmesg-openon39 bits16 bits
s390x7.0.0mainlinebpf-openon39 bits16 bits
s390x7.0.0mainlinehardenedon39 bits41 bits
x86_646.12.81-0-virtalpinedefaulton2 bits6 bits
x86_646.12.81-0-virtalpinekptr-hiddenon2 bits6 bits
x86_646.12.81-0-virtalpineperf-openonexact6 bits
x86_646.12.81-0-virtalpinedmesg-openon2 bits6 bits
x86_646.12.81-0-virtalpinebpf-openonexact6 bits
x86_646.12.81-0-virtalpinehardenedon2 bits6 bits
x86_646.12.81-0-virtalpineno5lvlon2 bits6 bits
x86_646.12.81-0-virtalpinela57on2 bits6 bits
x86_645.15.211mainlinedefaulton5 bits9 bits
x86_645.15.211mainlinekptr-hiddenon5 bits9 bits
x86_645.15.211mainlineperf-openonexact9 bits
x86_645.15.211mainlinedmesg-openon5 bits9 bits
x86_645.15.211mainlinebpf-openon5 bits9 bits
x86_645.15.211mainlinehardenedon5 bits9 bits
x86_645.15.211mainlineno5lvlon5 bits9 bits
x86_645.15.211mainlinela57on5 bits9 bits
x86_646.6.144mainlinedefaulton5 bits9 bits
x86_646.6.144mainlinekptr-hiddenon5 bits9 bits
x86_646.6.144mainlineperf-openonexact9 bits
x86_646.6.144mainlinedmesg-openon5 bits9 bits
x86_646.6.144mainlinebpf-openon5 bits9 bits
x86_646.6.144mainlinehardenedon5 bits9 bits
x86_646.6.144mainlineno5lvlon5 bits9 bits
x86_646.6.144mainlinela57on5 bits9 bits
x86_647.0.0mainlinedefaulton9 bits9 bits
x86_647.0.0mainlinekptr-hiddenon9 bits9 bits
x86_647.0.0mainlineperf-openonexact9 bits
x86_647.0.0mainlinedmesg-openon9 bits9 bits
x86_647.0.0mainlinebpf-openon9 bits9 bits
x86_647.0.0mainlinehardenedon9 bits9 bits
x86_647.0.0mainlineno5lvlon9 bits9 bits
x86_647.0.0mainlinela57on9 bits9 bits

What the profiles do is described in §2 above; what the table adds is where a profile makes no difference, and why.

On a stock kernel default is not exact: at the upstream perf_event_paranoid=2, /proc/kallsyms is zeroed even with kptr_restrict=0 (kallsyms_show_value() needs perf_event_paranoid<=1 or CAP_SYSLOG), so the base comes from sound inference alone. kptr-hidden therefore lands on the same numbers — it removes a source that was already dark.

perf-open is the profile that recovers the base, and it does so on every KASLR-on cell whose kernel is built with CONFIG_PERF_EVENTS. The MIPS cells are the exception, and they show why its two unlocks are really one lever: malta_defconfig does not set CONFIG_PERF_EVENTS, so perf_event_open is absent — and kallsyms_show_value() reaches its kallsyms_for_perf() shortcut only inside that same #ifdef, leaving kptr_restrict=0 to fall through to a CAP_SYSLOG check an unprivileged reader fails. The symbol table is readable and wholly zeroed at once, the sysctl the profile writes does not exist, and every profile reports the same window there.

The phys residual moves independently, because /proc/iomem zeroes its Kernel code addresses for any non-root reader regardless of kptr_restrict or perf_event_paranoid. So the physical axis is largely unmoved by the profiles: the perf unlock recovers the virtual text base, not the physical one.

Architectures showing KASLR off receive no seed under the default qemu machine (or the port has no text KASLR), so the kernel boots unrandomized and residual reads . KASLD still bounds the fixed base soundly via the disabled-base path; it is simply not a KASLR-defeat result, so the column does not score it. riscv64 is the mixed case: its text KASLR needs both a kernel built for it and a boot-supplied seed, so the mainline 6.6 and 7.0 cells are built with RANDOMIZE_BASE and booted with a per-boot kaslr-seed spliced into the device tree, while 5.15 (predating riscv KASLR) and the Alpine kernel stay honestly off.

Memory-KASLR region bases

On x86_64, CONFIG_RANDOMIZE_MEMORY randomizes the direct-map (page_offset_base), vmalloc (vmalloc_base) and vmemmap (vmemmap_base) region bases independently of the kernel text. Each live boot captures their true values from /proc/kcore — the resolved variable's word, never the terabyte-sparse whole — as ground truth, and gates the engine's resolved window for each against it. A cell whose region window excludes the true base is withheld exactly like a text-base violation; this gate runs in the live matrix and offline via validate-bundle, and every gated cell is sound.

The per-region residual entropy is not yet tabulated here: the harness gates each region window for soundness but does not yet emit its residual bit-count. That is a reporting gap, not a soundness gap — the windows are checked to contain the truth today; only the "how many bits survived" number for these three quantities is still to be surfaced.

Scope (see Architecture characterization): these three region bases are independently randomized only under CONFIG_RANDOMIZE_MEMORY, i.e. x86_64. The other decoupled architectures randomize the linear map by a different mechanism (aarch64 memstart_addr, s390 identity base, riscv64 kernel_map.page_offset), which the engine bounds but this harness does not ground-truth; a leak of one of those bases only makes the reported residual an upper bound, never unsound. On coupled architectures the direct map is a static projection of kernel text and carries no independent base to randomize.

Kernel-configuration sensitivity

The matrix covers this axis in part. A kernel's paging mode and virtual-address width — x86_64 4- vs 5-level paging, aarch64 VA36 / VA39 / VA42 / VA47 alongside the default VA48, the 32-bit VMSPLIT variants — shift the layout windows and are a distinct axis from the kernel line. This axis is a direct test of the engine's structural soundness: a wrong paging assumption or an over-coarse alignment moves the image-base window, which is exactly the window the soundness gate already checks (it is how past VA-bits and alignment bugs surfaced), so each configuration cell is another soundness data point.

On aarch64 the width fixes the page size, so those four cells also span all three granules — VA39 at 4K, VA36 and VA47 at 16K, VA42 at 64K. VA52 is the one width left uncovered. x86_64 and riscv64 reach the same axis from a single image through the no5lvl / no4lvl / la57 boot parameters, and armv7 and i686 carry a purpose-built 2G/2G split against the default 3G/1G. The configuration axes still outside the matrix are listed under Scope.

Speculative narrowing (the likely window)

residual above is the guaranteed window — resolved only from signals at or above the sound floor, and gate-checked to contain the truth. The engine also resolves a likely window: the guaranteed window narrowed further by sub-floor signals (timing side-channels, a single leaked pointer, config defaults). It is a best-guess, always a subset of guaranteed, and — unlike guaranteed — it is not gated to contain the truth. It surfaces in -j JSON as the likely / likely_physical objects, emitted only when strictly tighter than guaranteed.

tests/vm/run spec-table lists the cells where the likely window beats guaranteed, and by default only the reproducible narrowings: deterministic parsed signals — a file, a leaked pointer, a config default — that beat the sound floor the same way on every run.

Which cells appear, and which component supplies each, is not recorded here. Membership turns over as leaks are patched upstream and as the kernel matrix moves, so the command is the answer; a list transcribed into this document would describe kernels that are no longer in the run.

The shape, however, is stable, and it is the expected outcome once the profiles and rules are correct: few rows, and only from a narrow band of signal. Where a perf or kallsyms signal exists it is a sound pin, so it resolves the guaranteed window under perf-open (to exact), never the likely one — a sound signal has nothing left to narrow. And the memory-map heuristics that bound RAM from world-readable facts (/proc/zoneinfo spans, the device-tree memory node, dmesg zone lines) never tighten past the placement-tracking guaranteed ceiling on any arch here, so they add no likely-only row either. Most parsed signals thus land in the guaranteed matrix or contribute nothing; a signal shows up here only when it is reproducible AND sub-floor, which is a narrow band to occupy. The BPF verifier-log table is the counter-example worth noting: it used to be listed here, and no longer is, because its match now resolves the guaranteed window itself under bpf-open — a signal that graduates out of likely is the outcome to want.

Absolute recovery is monotonic in how much the profile relaxes: perf-open recovers the most, hardened the least, with default/kptr-hidden/dmesg-open between — read that off the guaranteed matrix above, not the likely window.

Timing side-channel narrowings are excluded by default, and no figure for them is published here. The likely window can also be narrowed by a microarchitectural side channel — a cache or speculation oracle such as prefetch or entrybleed that survives even perf_event_paranoid=3, pinning the base where no parsed signal can. Such rows do appear in these runs, and they are withheld for two independent reasons.

The first is reproducibility, and it is the whole reason the two are treated differently. A parsed narrowing reproduces on any machine that boots the same image: the signal is in the file, so the reader gets the same answer as the run that produced it. A timing oracle's success is instead a function of the host CPU — on a mitigated part (microcode or hypervisor Spectre-v2 defences) or a lower microarchitecture the prefetch signal flattens, and the same cell narrows less or not at all. It varies run to run on one machine, so a number derived from one run is not a result a reader can check.

The second is that this harness is the wrong instrument for the measurement. The x86-64 cells boot under KVM, where a prefetch-timing signal can be an artifact of nested paging rather than the behaviour it is taken to demonstrate — so a bit-count obtained here would characterise the hypervisor as much as the technique, in either direction. Neither reason is that the technique fails; these channels are validated separately, against hardware, and are out of the matrix's scope (see Scope). tests/vm/run spec-table --with-timing will list them for anyone reproducing the run locally, with the same caveat attached.

3. Offline, over a captured corpus

The repository ships captured snapshots from real kernels, so the engine can be exercised across hardware that is not otherwise available:

make check            # unit + integration tests (the per-rule checks)
tests/replay          # run kasld over every captured fixture (structural)
make test-fixtures    # assert the resolved window contains the truth, per capture

tests/replay confirms KASLD parses and runs cleanly on each snapshot; make check runs the per-rule unit tests, including the soundness checks. make test-fixtures (tests/validate-fixtures) is the boot-free soundness gate: it runs extra/validate-bundle over every captured kernel that carries ground truth and asserts truth ∈ [min, max], catching the "window excludes the real base" class of bug without a VM. Captures with no recorded truth report N/A rather than pass. The corpus spans 13 architecture variants across distributions (Alpine, Debian, Ubuntu/Raspbian) and kernels from 4.14 to 7.0:

familyexample kernels
x86_64, i6865.15 – 7.0 (Alpine, Debian, Ubuntu)
aarch64, armv74.14 – 7.0
ppc64, ppc325.15 – 7.0
riscv64, riscv325.10 – 6.18
s390x5.15 – 6.19
loongarch646.18
mips32, mips32el, mips64el4.19 – 6.15

Scope

These checks cover soundness — whether the inferred range contains the truth — across a broad set of architectures, kernel versions, and reader-privilege levels. They use one stock kernel configuration per architecture, plus purpose-built cells on the two configuration axes that relocate the kernel memory map: VA-bits / paging mode (aarch64 36/39/42/47, and x86_64 / riscv64 via no5lvl / no4lvl / la57 from a single image) and VMSPLIT (armv7 and i686 at 2G/2G). Those are the axes a wrong answer would be silent on, because every other cell is built at the architecture's default split and paging mode, so a model that assumed the default would pass the whole matrix by coincidence. The remaining configuration axes — endianness, and individual CONFIG_* toggles — are not covered. Timing and side-channel components are validated separately, as their behaviour depends on hardware rather than configuration.

The matrix exercises the kernel, not a distribution's userland. Each cell boots straight into the analysis harness from a minimal initramfs, so no distro init, sysctl.d drop-ins, service sandboxing, or LSM policy (AppArmor / SELinux / seccomp) ever runs. Every profile therefore measures the kernel's own posture — its compile-time sysctl defaults plus the one explicit sysctl vector the harness sets — and nothing a userland would layer on top. This cuts both ways and is a scope boundary in both directions: a real distribution install may enforce controls these cells do not (so an actual system can be stricter than even the hardened column), and userland is itself a potential leak surface — setuid helpers, privileged daemons, and files a running service populates — that the matrix does not exercise (so an actual system may expose more than the default column). The isolation is deliberate: it attributes each result to a named kernel and a declared sysctl vector, keeping the cells reproducible and independent of any particular distribution's userspace.

Neither an unprivileged user namespace nor unprivileged eBPF widens text-base recovery, so neither is a scenario of its own. A user namespace does not help because the symbol table, /proc/iomem, dmesg, and perf_event_open gates all check capable() in the initial user namespace, which the ns_capable() a child namespace grants cannot satisfy — so the default and hardened results already bound a userns-wielding attacker for the text base. A user namespace reaches only ns_capable-gated leaks through created virtual devices (a third-party-driver ioctl surface), which the minimal initramfs does not expose.

eBPF does not help either. Where it is enabled (kernel.unprivileged_bpf_disabled=0; the upstream default flipped to disabled in 5.16), the disclosure KASLD does exploit is the verifier log printing an unmasked struct bpf_map * — a direct-map virtual address, not the text base. Like a %pK /proc/net socket pointer, it bounds page_offset / the direct map — a memory-KASLR region this matrix does not score — so it leaves the text-image-base residual unchanged (the relevant verifier-log hole is version-specific and was masked in 7.2). Both are disclosure channels in their own right, but of kernel data addresses, not the text base measured here.

These are limits on what the checks here verify. For what a KASLD result means when it is run against a target — in particular why a failure to recover the base is not evidence the system is secure — see limitations.md.