@@ -43,6 +43,11 @@ kernel's real symbol addresses. `validate-bundle` runs the matching kasld binary
4343over that bundle offline and checks every inferred range against the captured
4444ground truth. It exits non-zero if any range excludes the truth. No root needed.
4545
46+ ` --kallsyms ` can only record the ground truth when kallsyms is readable
47+ (` kptr_restrict=0 ` , or root). Without it the bundle carries no truth and the
48+ checks report ` N/A ` rather than ` PASS ` — still not a failure, just nothing to
49+ compare against.
50+
4651## 2. Live, across architectures
4752
4853[ ` tests/vm/run ` ] ( ../tests/vm ) boots real, publicly-fetchable kernels under
@@ -56,62 +61,135 @@ tests/vm/run all hardened # repeat under the unprivileged floor
5661```
5762
5863It needs ` qemu-system-<arch> ` and the cross toolchains on PATH; an architecture
59- is skipped (not failed) when either is missing. Profiles range from ` default `
60- (kallsyms readable) to ` hardened ` (uid 1000, ` kptr_restrict=2 ` ,
61- ` dmesg_restrict=1 ` , ` perf_event_paranoid=3 ` — the realistic unprivileged floor,
62- where the range widens but must still contain the truth). See
63- [ tests/vm/README.md] ( ../tests/vm/README.md ) for the full arch list and options.
64+ is skipped (not failed) when either is missing. The reader profiles (the
65+ ` scenario ` column of the matrix) escalate how little kasld is allowed to read:
66+
67+ - ` default ` — root, ` kptr_restrict=0 ` : kallsyms and everything else readable.
68+ - ` hide ` — root, but ` kptr_restrict=2 ` : kernel pointers are * hidden* , so kallsyms
69+ is unusable and the base must come from inference or other leaks (e.g. dmesg),
70+ not the symbol table.
71+ - ` hardened ` — unprivileged (uid 1000) with ` kptr_restrict=2 ` ,
72+ ` dmesg_restrict=1 ` , ` perf_event_paranoid=3 ` : the realistic attacker floor,
73+ where only file-derived facts survive.
74+ - ` nokaslr ` — KASLR disabled on the kernel command line.
75+
76+ Under the tighter profiles the window may widen but must still contain the truth.
77+ See [ tests/vm/README.md] ( ../tests/vm/README.md ) for the full arch list and options.
78+
79+ Eight architectures boot from publicly-fetchable Alpine kernels. Four more that
80+ Alpine does not port — ` mips ` , ` mipsel ` , ` riscv32 ` , ` ppc32 ` — are built from a
81+ pinned kernel.org source by ` tests/vm/build-kernel ` (a stock upstream defconfig
82+ plus a fixed endianness/devtmpfs overlay) and booted the same way:
83+
84+ ``` sh
85+ tests/vm/build-kernel mips mipsel riscv32 ppc32 # cross-build -> cache (slow)
86+ tests/vm/run mips # boot it, verdict
87+ ```
88+
89+ ` armeb ` is not covered: the only big-endian arm toolchain in the cross set emits
90+ ARMv5 BE32 code, which can neither run on an ARMv7 BE8 kernel nor boot a BE32
91+ kernel under qemu.
6492
6593### Results matrix
6694
6795The snapshot below is generated by ` tests/vm/run all <scenario> ` followed by
6896` tests/vm/run table ` . ` recovered ` = the window collapsed to the exact base;
69- ` residual bits ` = remaining KASLR entropy in the inferred window (` 0 ` when
70- recovered, ` — ` when KASLR is off so the figure does not apply); ` sound ` = the
71- true base lies inside the inferred window. The axis is architecture × reader
72- profile; a kernel-config axis is out of scope (see Scope below).
97+ ` residual bits ` = remaining KASLR entropy in the inferred window: ` 0 ` when none
98+ is left — KASLR off (no entropy to begin with) or a randomized base fully
99+ recovered; a positive number when randomized and the window is wider than a
100+ point; ` — ` only when randomized-but-not-recovered * and* the architecture emits no
101+ separate virtual-KASLR entropy figure (the count is unavailable, not zero — see
102+ ` mips ` /` mipsel ` under ` hide ` ). ` sound ` = the true base lies inside the inferred
103+ window. The axis is architecture × reader profile; a kernel-config axis is out
104+ of scope (see Scope below). The first eight architectures boot Alpine kernels;
105+ the last four (` mips ` , ` mipsel ` , ` riscv32 ` , ` ppc32 ` ) are kernel.org builds from
106+ ` tests/vm/build-kernel ` .
73107
74108| arch | release | scenario | KASLR | recovered | residual bits | sound |
75109| ------| ---------| ----------| -------| -----------| ---------------| -------|
76- | x86_64 | 6.12.81 | default | on | yes | 0 | yes |
77- | x86_64 | 6.12.81 | hide | on | yes | 0 | yes |
78- | x86_64 | 6.12.81 | hardened | on | yes | 0 | yes |
79- | x86_64 | 6.12.81 | nokaslr | off | yes | 0 | yes |
80- | i686 | 6.12.81 | default | on | yes | 0 | yes |
81- | i686 | 6.12.81 | hide | on | yes | 0 | yes |
82- | i686 | 6.12.81 | hardened | on | no | 5 | yes |
83- | i686 | 6.12.81 | nokaslr | off | yes | 0 | yes |
84- | aarch64 | 6.12.81 | default | on | yes | 0 | yes |
85- | aarch64 | 6.12.81 | hide | on | yes | 0 | yes |
86- | aarch64 | 6.12.81 | hardened | on | no | 26 | yes |
87- | aarch64 | 6.12.81 | nokaslr | off | yes | 0 | yes |
88- | armv7 | 6.12.81 | default | off | yes | 0 | yes |
89- | armv7 | 6.12.81 | hide | off | yes | 0 | yes |
90- | armv7 | 6.12.81 | hardened | off | no | — | yes |
91- | armv7 | 6.12.81 | nokaslr | off | yes | 0 | yes |
92- | riscv64 | 6.18.35 | default | off | yes | 0 | yes |
93- | riscv64 | 6.18.35 | hide | off | yes | 0 | yes |
94- | riscv64 | 6.18.35 | hardened | off | yes | 0 | yes |
95- | riscv64 | 6.18.35 | nokaslr | off | yes | 0 | yes |
96- | loongarch64 | 6.18.35 | default | on | yes | 0 | yes |
97- | loongarch64 | 6.18.35 | hide | on | no | 3 | yes |
98- | loongarch64 | 6.18.35 | hardened | on | no | 14 | yes |
99- | loongarch64 | 6.18.35 | nokaslr | off | yes | 0 | yes |
100- | ppc64le | 6.12.81 | default | off | yes | 0 | yes |
101- | ppc64le | 6.12.81 | hide | off | no | 0 | yes |
102- | ppc64le | 6.12.81 | hardened | off | no | — | yes |
103- | ppc64le | 6.12.81 | nokaslr | off | yes | 0 | yes |
104- | s390x | 6.12.81 | default | on | yes | 0 | yes |
105- | s390x | 6.12.81 | hide | on | no | 9 | yes |
106- | s390x | 6.12.81 | hardened | on | no | 39 | yes |
107- | s390x | 6.12.81 | nokaslr | off | yes | 0 | yes |
108-
109- All 32 cells are sound. Under ` hardened ` the window widens (s390x to 39 bits) but
110- still contains the truth — the intended outcome at the unprivileged floor. Some
111- architectures show KASLR ` off ` outside the ` nokaslr ` column: under the default
112- qemu machine they receive no KASLR seed, so the kernel boots unrandomized and
113- kasld pins the default base via the disabled-base path — a soundness case, not a
114- gap.
110+ | x86_64 | 6.12.81-0-virt | default | on | yes | 0 | yes |
111+ | x86_64 | 6.12.81-0-virt | hide | on | yes | 0 | yes |
112+ | x86_64 | 6.12.81-0-virt | hardened | on | yes * | 0 | yes |
113+ | x86_64 | 6.12.81-0-virt | nokaslr | off | yes | 0 | yes |
114+ | i686 | 6.12.81-0-lts | default | on | yes | 0 | yes |
115+ | i686 | 6.12.81-0-lts | hide | on | yes | 0 | yes |
116+ | i686 | 6.12.81-0-lts | hardened | on | no | 5 | yes |
117+ | i686 | 6.12.81-0-lts | nokaslr | off | yes | 0 | yes |
118+ | aarch64 | 6.12.81-0-virt | default | on | yes | 0 | yes |
119+ | aarch64 | 6.12.81-0-virt | hide | on | yes | 0 | yes |
120+ | aarch64 | 6.12.81-0-virt | hardened | on | no | 26 | yes |
121+ | aarch64 | 6.12.81-0-virt | nokaslr | off | yes | 0 | yes |
122+ | armv7 | 6.12.81-0-lts | default | off | yes | 0 | yes |
123+ | armv7 | 6.12.81-0-lts | hide | off | yes | 0 | yes |
124+ | armv7 | 6.12.81-0-lts | hardened | off | no | 0 | yes |
125+ | armv7 | 6.12.81-0-lts | nokaslr | off | yes | 0 | yes |
126+ | riscv64 | 6.18.35-0-lts | default | off | yes | 0 | yes |
127+ | riscv64 | 6.18.35-0-lts | hide | off | yes | 0 | yes |
128+ | riscv64 | 6.18.35-0-lts | hardened | off | yes | 0 | yes |
129+ | riscv64 | 6.18.35-0-lts | nokaslr | off | yes | 0 | yes |
130+ | loongarch64 | 6.18.35-0-lts | default | on | yes | 0 | yes |
131+ | loongarch64 | 6.18.35-0-lts | hide | on | no | 3 | yes |
132+ | loongarch64 | 6.18.35-0-lts | hardened | on | no | 14 | yes |
133+ | loongarch64 | 6.18.35-0-lts | nokaslr | off | yes | 0 | yes |
134+ | ppc64le | 6.12.81-0-lts | default | off | yes | 0 | yes |
135+ | ppc64le | 6.12.81-0-lts | hide | off | no | 0 | yes |
136+ | ppc64le | 6.12.81-0-lts | hardened | off | no | 0 | yes |
137+ | ppc64le | 6.12.81-0-lts | nokaslr | off | yes | 0 | yes |
138+ | s390x | 6.12.81-0-lts | default | on | yes | 0 | yes |
139+ | s390x | 6.12.81-0-lts | hide | on | no | 9 | yes |
140+ | s390x | 6.12.81-0-lts | hardened | on | no | 39 | yes |
141+ | s390x | 6.12.81-0-lts | nokaslr | off | yes | 0 | yes |
142+ | mips | 6.15.6 | default | on | yes | 0 | yes |
143+ | mips | 6.15.6 | hide | on | no | — | yes |
144+ | mips | 6.15.6 | hardened | on | no | 9 | yes |
145+ | mips | 6.15.6 | nokaslr | off | yes | 0 | yes |
146+ | mipsel | 6.15.6 | default | on | yes | 0 | yes |
147+ | mipsel | 6.15.6 | hide | on | no | — | yes |
148+ | mipsel | 6.15.6 | hardened | on | no | 9 | yes |
149+ | mipsel | 6.15.6 | nokaslr | off | yes | 0 | yes |
150+ | riscv32 | 6.15.6 | default | off | yes | 0 | yes |
151+ | riscv32 | 6.15.6 | hide | off | yes | 0 | yes |
152+ | riscv32 | 6.15.6 | hardened | off | no | 0 | yes |
153+ | riscv32 | 6.15.6 | nokaslr | off | yes | 0 | yes |
154+ | ppc32 | 6.15.6 | default | on | yes | 0 | yes |
155+ | ppc32 | 6.15.6 | hide | on | no | 2 | yes |
156+ | ppc32 | 6.15.6 | hardened | on | no | 15 | yes |
157+ | ppc32 | 6.15.6 | nokaslr | off | yes | 0 | yes |
158+
159+ ` * ` reached via a timing side-channel (e.g. prefetch) — hardware/KVM-dependent, not available under pure emulation.
160+
161+ All 48 cells are sound. Under ` hardened ` the window widens but still contains the
162+ truth — the intended outcome at the unprivileged floor.
163+
164+ ` recovered ` and ` residual bits ` measure different things, which is why a row can
165+ read ` off / no / 0 ` . ` residual bits ` is * KASLR entropy* left in the window, so it
166+ is ` 0 ` whenever KASLR is off — there was no entropy to start with. ` recovered ` is
167+ whether the window collapsed to a single address. A KASLR-` off ` row that is
168+ ` recovered=no ` (e.g. ` ppc64le ` or ` riscv32 ` under ` hardened ` ) therefore shows
169+ ` 0 ` residual but a non-point window: the base is fixed (0 entropy), yet from
170+ file-only facts kasld can only * bound* it — between the true base and that base
171+ plus the image-base offset — rather than read it exactly. That width is inference
172+ uncertainty, not entropy. It still contains the truth, which is the property
173+ being checked.
174+
175+ Several architectures show KASLR ` off ` outside the ` nokaslr ` column: under the
176+ default qemu machine they receive no KASLR seed (or the port has no text KASLR),
177+ so the kernel boots unrandomized and kasld bounds or pins the base via the
178+ disabled-base path — a soundness case, not a gap. Where that disabled base is a
179+ fixed constant (e.g. seedless ` riscv64 ` under ` -M virt ` ), kasld pins it exactly
180+ from arch constants plus the world-readable device-tree, so ` recovered=yes ` holds
181+ even under ` hardened ` — no kallsyms or leak is needed when there is no randomness
182+ to recover.
183+
184+ x86_64 is the opposite case and deserves a caveat: its exact recovery under
185+ ` hardened ` (KASLR * on* , kallsyms and dmesg both unreadable) comes from the
186+ ` prefetch ` ** timing side-channel** , not a file-derived fact. (Under ` hide ` the
187+ pin is still a parsed leak — root can read dmesg there — so only ` hardened `
188+ leans on timing.) That oracle is hardware- and environment-dependent: it needs a
189+ real or KVM-backed CPU (these boots had ` /dev/kvm ` ) and does not fire under pure
190+ emulation or with some mitigations. Where it is unavailable the x86_64 ` hardened `
191+ window stays wider-but-sound rather than collapsing to the exact base; that
192+ ` 0 ` -bit cell is the oracle's best case, not a guarantee.
115193
116194## 3. Offline, over a captured corpus
117195
0 commit comments