Skip to content

Commit 5197205

Browse files
committed
arm64: support sub-48 VA_BITS (39/42/47) configs
KIMAGE_VADDR and PAGE_OFFSET on arm64 are functions of VA_BITS_MIN, but KASLD modelled only {48,52}: sub-48 kernels (4K 3-level=39 [common on Android], 64K 2-level=42, 16K 3-level=47) were mis-detected and mis-pinned, while the comments wrongly claimed the pin backstop handled them. - detection: mmap_arm64_va_bits now probes the full boundary ladder (52/48/47/42/39 via MAP_FIXED_NOREPLACE), emitting the exact PAGE_OFFSET instead of collapsing everything <=48 to the 48-bit value - model: VA_BITS_CANDIDATES gains 39/42/47; arm64_page_offset_for/_end_for helpers; Q_VIRT_IMAGE_BASE honest top widened to the union (KASLR_VIRT_TEXT_MAX_WIDE); arm64_coupling_validate and arm64_va_bits_from_directmap generalised to the candidate ladder - new rule arm64_text_base (arm64 opted out of the generic disabled pin): derives VA_BITS_MIN from the resolved PAGE_OFFSET, narrows Q_VIRT_IMAGE_BASE to that config's text band (re-tightening the 48-bit KASLR window too), and pins the no-KASLR base at the correct KIMAGE_VADDR(VA_BITS_MIN). Inferred — a real text leak overrides; no pin when PAGE_OFFSET is unresolved.
1 parent a9f0f75 commit 5197205

11 files changed

Lines changed: 454 additions & 123 deletions

src/components/mmap_arm64_va_bits.c

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,23 @@
22
//
33
// arm64 VA_BITS detection via an mmap boundary probe, emitting PAGE_OFFSET.
44
//
5-
// PROBING-phase component. On arm64
6-
// TASK_SIZE = 1<<VA_BITS and PAGE_OFFSET = -(1<<VA_BITS), so an mmap(MAP_FIXED)
7-
// at the VA_BITS=48 boundary distinguishes the configuration:
5+
// PROBING-phase component. On arm64 TASK_SIZE = 1<<VA_BITS and PAGE_OFFSET =
6+
// -(1<<VA_BITS), both fixed (not randomized). A one-page probe at
7+
// (1<<c) - PAGE_SIZE is mappable iff c <= VA_BITS, so probing the candidate
8+
// ladder largest-first and taking the first that maps yields the exact VA_BITS,
9+
// hence the exact PAGE_OFFSET (= arm64_page_offset_for(VA_BITS)). The engine
10+
// pins Q_PAGE_OFFSET to it (page_offset_from_landmark).
811
//
9-
// probe at 1<<48 fails (ENOMEM): VA_BITS <= 48 -> PAGE_OFFSET
10-
// 0xffff000000000000 probe at 1<<48 succeeds: VA_BITS >= 52 ->
11-
// PAGE_OFFSET 0xfff0000000000000
12-
//
13-
// PAGE_OFFSET is not randomized on arm64, so the detected value is exact; the
14-
// engine pins Q_PAGE_OFFSET to it (page_offset_from_landmark). It is a PROBING
15-
// component: the engine reads component results, and an active probe belongs
16-
// behind the subprocess boundary.
12+
// MAP_FIXED_NOREPLACE distinguishes "beyond TASK_SIZE" (ENOMEM/EINVAL → probe a
13+
// smaller boundary) from "occupied" (EEXIST → the address is within TASK_SIZE)
14+
// and never clobbers a live mapping. If the kernel returns an unrequested
15+
// address (NOREPLACE not honoured, pre-v4.17) the probe is unreliable and emits
16+
// nothing rather than guessing. Likewise an unexpected errno (RLIMIT_AS,
17+
// seccomp) or a VA_BITS below the smallest supported candidate → no emission,
18+
// leaving the engine's honest window (sound but wide).
1719
//
1820
// Leak primitive: virtual (kernel direct-map base) via the mmap syscall;
1921
// unprivileged, no sysctl gate. arm64 only.
20-
//
21-
// Caveat: RLIMIT_AS exhaustion also returns ENOMEM. Unlikely at probe time;
22-
// the same risk mmap_brute_vmsplit accepts.
2322
// ---
2423
// <bcoles@gmail.com>
2524

@@ -31,37 +30,63 @@
3130
#include <sys/mman.h>
3231
#include <unistd.h>
3332

33+
#ifndef MAP_FIXED_NOREPLACE
34+
#define MAP_FIXED_NOREPLACE 0x100000
35+
#endif
36+
3437
KASLD_EXPLAIN(
35-
"Probes mmap(MAP_FIXED) at 1<<48 on arm64: ENOMEM means VA_BITS<=48 "
36-
"(PAGE_OFFSET 0xffff000000000000), success means VA_BITS>=52 "
37-
"(PAGE_OFFSET 0xfff0000000000000). PAGE_OFFSET is not randomized "
38-
"on arm64, so the value is exact. arm64 only; unprivileged.");
38+
"Probes mmap(MAP_FIXED_NOREPLACE) at the 1<<VA_BITS boundaries on arm64 "
39+
"(52/48/47/42/39): the largest that maps is VA_BITS, giving the exact "
40+
"PAGE_OFFSET = -(1<<VA_BITS) (not randomized on arm64). arm64 only; "
41+
"unprivileged.");
3942

4043
KASLD_META("method:heuristic\n"
4144
"phase:probing\n"
4245
"addr:virtual\n");
4346

4447
int main(void) {
4548
#if defined(__aarch64__)
46-
#define ARM64_VA48_PAGE_OFFSET 0xffff000000000000ul
47-
#define ARM64_VA52_PAGE_OFFSET 0xfff0000000000000ul
48-
#define ARM64_VA_PROBE_ADDR ((void *)(1UL << 48))
49-
#define ARM64_VA_PROBE_LEN 0x1000ul
49+
/* VA_BITS candidates, largest first (must match VA_BITS_CANDIDATES). */
50+
static const unsigned long cands[] = {52ul, 48ul, 47ul, 42ul, 39ul};
51+
const int ncands = (int)(sizeof(cands) / sizeof(cands[0]));
5052

51-
void *p = mmap(ARM64_VA_PROBE_ADDR, ARM64_VA_PROBE_LEN, PROT_READ,
52-
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
53-
unsigned long virt_page_offset;
54-
if (p == MAP_FAILED) {
55-
if (errno != ENOMEM)
56-
return 0; /* a different failure: don't infer */
57-
virt_page_offset = ARM64_VA48_PAGE_OFFSET; /* VA_BITS <= 48 */
58-
kasld_info("mmap(1<<48) failed (ENOMEM): VA_BITS<=48");
59-
} else {
60-
munmap(p, ARM64_VA_PROBE_LEN);
61-
virt_page_offset = ARM64_VA52_PAGE_OFFSET; /* VA_BITS >= 52 */
62-
kasld_info("mmap(1<<48) succeeded: VA_BITS>=52");
53+
long pg = sysconf(_SC_PAGESIZE);
54+
unsigned long page = (pg > 0) ? (unsigned long)pg : 0x1000ul;
55+
56+
unsigned long va_bits = 0;
57+
for (int i = 0; i < ncands; i++) {
58+
unsigned long c = cands[i];
59+
void *want = (void *)((1UL << c) - page);
60+
void *p = mmap(want, (size_t)page, PROT_NONE,
61+
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
62+
if (p == want) { /* mapped exactly here -> within TASK_SIZE */
63+
munmap(p, (size_t)page);
64+
va_bits = c;
65+
break;
66+
}
67+
if (p !=
68+
MAP_FAILED) { /* NOREPLACE not honoured (old kernel) -> unreliable */
69+
munmap(p, (size_t)page);
70+
kasld_info("mmap returned an unrequested address; probe unreliable");
71+
return 0;
72+
}
73+
if (errno == EEXIST) { /* occupied -> addressable -> within TASK_SIZE */
74+
va_bits = c;
75+
break;
76+
}
77+
if (errno == ENOMEM || errno == EINVAL)
78+
continue; /* beyond this boundary; try a smaller VA_BITS */
79+
kasld_info("mmap(1<<%lu): unexpected errno %d; not inferring", c, errno);
80+
return 0; /* RLIMIT_AS / seccomp / etc. — don't guess */
81+
}
82+
83+
if (va_bits == 0) {
84+
kasld_info("VA_BITS below smallest supported candidate; not inferring");
85+
return 0;
6386
}
64-
kasld_info("PAGE_OFFSET: %#lx", virt_page_offset);
87+
88+
unsigned long virt_page_offset = arm64_page_offset_for(va_bits);
89+
kasld_info("VA_BITS=%lu PAGE_OFFSET=%#lx", va_bits, virt_page_offset);
6590
kasld_result_base(KASLD_TYPE_VIRT, REGION_PAGE_OFFSET, virt_page_offset, NULL,
6691
CONF_INFERRED);
6792
return 0;

src/engine_rules.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ static const rule_fn k_rules[] = {
8989
rule_phys_virt_synth,
9090

9191
/* arm64-specific */
92+
rule_arm64_text_base,
9293
rule_arm64_memstart_align,
9394
rule_arm64_va_bits_from_directmap,
9495
rule_arm64_va_bits_from_vmemmap,

src/include/kasld/api.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ __extension__ _Static_assert((unsigned long)KERNEL_PHYS_MAX >
253253
#ifndef KASLR_VIRT_TEXT_MIN_WIDE
254254
#define KASLR_VIRT_TEXT_MIN_WIDE KASLR_VIRT_TEXT_MIN
255255
#endif
256+
/* Honest-top ceiling counterpart: defaults to the KASLR window top; an arch
257+
* whose honest top must span more than one text-base layout (arm64 sub-48
258+
* VA_BITS) widens it. Widen-only — never below KASLR_VIRT_TEXT_MAX. */
259+
#ifndef KASLR_VIRT_TEXT_MAX_WIDE
260+
#define KASLR_VIRT_TEXT_MAX_WIDE KASLR_VIRT_TEXT_MAX
261+
#endif
256262
#if defined(KASLR_PHYS_MIN) && !defined(KASLR_PHYS_MIN_WIDE)
257263
#define KASLR_PHYS_MIN_WIDE KASLR_PHYS_MIN
258264
#endif

src/include/kasld/arch/arm64.h

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,26 @@
4646
#define PAGE_OFFSET 0xfff0000000000000ul
4747
#define PHYS_OFFSET 0ul
4848

49-
// VA_BITS candidates for Q_VA_BITS (finite-set lattice). 48 and 52 are the two
50-
// configurations whose PAGE_OFFSET the directmap-range rule discriminates.
51-
#define VA_BITS_CANDIDATES {48ul, 52ul}
49+
// VA_BITS candidates for Q_VA_BITS (finite-set lattice), smallest first. Each
50+
// arm64 paging config has its own VA_BITS (hence its own PAGE_OFFSET /
51+
// KIMAGE_VADDR geometry): 4K 3-level=39 (common on Android), 64K 2-level=42,
52+
// 16K 3-level=47, 4K/16K 4-level=48, and 52-bit LVA (VA_BITS_MIN still 48).
53+
#define VA_BITS_CANDIDATES {39ul, 42ul, 47ul, 48ul, 52ul}
54+
// Smallest supported VA_BITS — gives the highest (widest-accepting) linear-map
55+
// ceiling for region validation.
56+
#define ARM64_VA_BITS_MIN_SUPPORTED 39ul
57+
58+
// VA_BITS-derived geometry, kept in one place so the layout math is not
59+
// duplicated across mmap_arm64_va_bits, arm64_coupling_validate, and
60+
// arm64_va_bits_from_directmap. arm64 PAGE_OFFSET = -(1<<VA_BITS); the linear
61+
// map occupies [PAGE_OFFSET, _PAGE_END), _PAGE_END = -(1<<(VA_BITS-1)). Pure
62+
// functions of VA_BITS, not randomized.
63+
static inline unsigned long arm64_page_offset_for(unsigned long va_bits) {
64+
return -(1UL << va_bits);
65+
}
66+
static inline unsigned long arm64_page_end_for(unsigned long va_bits) {
67+
return -(1UL << (va_bits - 1));
68+
}
5269

5370
// On arm64, PHYS_OFFSET is runtime (= memstart_addr, randomized at boot), so
5471
// the compile-time formula is NOT a sound runtime directmap projection;
@@ -151,32 +168,29 @@
151168
// https://elixir.bootlin.com/linux/v6.12/source/arch/arm64/include/asm/memory.h#L46
152169
// Use v6.2+ value (2G module region, current default).
153170
#define KIMAGE_VADDR 0xffff800080000000ul
171+
// Module-region size (KIMAGE_VADDR = _PAGE_END(VA_BITS_MIN) + this). v6.2+ uses
172+
// SZ_2G; older kernels used 128M/256M. rule_arm64_text_base derives
173+
// KIMAGE_VADDR for the resolved VA_BITS_MIN as arm64_page_end_for(VA_BITS_MIN)
174+
// + this — for VA_BITS_MIN=48 that reproduces KIMAGE_VADDR above. The version
175+
// spread is the pin's residual imprecision (inferred confidence; a real leak
176+
// overrides).
177+
#define ARM64_MODULE_REGION_SIZE (2ul * GB)
154178

155179
// See docs/kaslr.md "Default text base and KASLR alignment" for all
156180
// architectures. Kernel source: arch/arm64/kernel/vmlinux.lds.S,
157181
// arch/arm64/include/asm/memory.h
158182
#define KERNEL_VIRT_TEXT_DEFAULT (KIMAGE_VADDR + IMAGE_BASE_OFFSET)
159183

160-
/* KASLR-off ⇒ pin contract.
161-
*
162-
* SCOPE: KASLD models arm64 only for VA_BITS_MIN == 48 — the {48, 52} configs
163-
* (4K/16K 4-level, plus 52-bit LVA, whose VA_BITS_MIN is still 48). On those,
164-
* no-KASLR text sits at KIMAGE_VADDR + IMAGE_BASE_OFFSET = the 48-bit default
165-
* below, independent of the runtime VA_BITS (which only moves PAGE_OFFSET / the
166-
* linear map), so this pin is correct.
167-
*
168-
* Sub-48 builds land at a DIFFERENT KIMAGE_VADDR and are NOT supported:
169-
* 4K 3-level → VA_BITS 39 (common on Android), KIMAGE_VADDR
170-
* 0xffffffc080000000 64K 2-level → VA_BITS 42 16K 3-level → VA_BITS 47,
171-
* KIMAGE_VADDR 0xffffc00080000000 KASLD cannot even detect them:
172-
* mmap_arm64_va_bits probes only 1<<48 (so every VA_BITS <= 48 reads as 48) and
173-
* Q_VA_BITS models only {48, 52}. And the pin's window-containment backstop
174-
* does NOT catch the mismatch — the 48-bit default coincides with the
175-
* honest-top floor (KASLR_VIRT_TEXT_MIN_WIDE == KIMAGE_VADDR), so it is always
176-
* "in window." A sub-48 no-KASLR kernel therefore mis-pins (and its window is
177-
* wrong regardless of KASLR). Real support is a VA_BITS overhaul: see
178-
* dev/research/arm64-va-bits-min.md. */
179-
#define KASLR_DISABLED_PINS_VIRT_TEXT 1
184+
/* KASLR-off pin is LAYOUT-DEPENDENT on arm64: KIMAGE_VADDR varies with
185+
* VA_BITS_MIN (= min(VA_BITS, 48)), so the no-KASLR text base is not a single
186+
* compile-time constant. The generic virt_kaslr_disabled_pin (one fixed
187+
* default) is therefore opted OUT; rule_arm64_text_base owns the text base,
188+
* deriving VA_BITS_MIN from the resolved PAGE_OFFSET and narrowing/pinning to
189+
* KIMAGE_VADDR(VA_BITS_MIN) — correct for VA_BITS 39/42/47/48/52, not just 48.
190+
* Same shape as rule_riscv64_text_base. When PAGE_OFFSET is unresolved (no
191+
* probe result, no leak) it does not pin — the honest window stays wide
192+
* (sound). */
193+
#define KASLR_DISABLED_PINS_VIRT_TEXT 0
180194
#define KASLD_ARCH_DEFAULT_TEXT_BASE_DEFINED 1
181195
static inline unsigned long arch_default_text_base(void) {
182196
return KERNEL_VIRT_TEXT_DEFAULT;
@@ -219,6 +233,14 @@ static inline unsigned long arch_default_text_base(void) {
219233
* the v6.6 upper edge (96 TiB) and the v6.12+ upper edge (~94.5 TiB). */
220234
#define KASLR_VIRT_TEXT_MIN_WIDE KIMAGE_VADDR
221235

236+
/* Honest-top CEILING for Q_VIRT_IMAGE_BASE. KASLR_VIRT_TEXT_MAX is the 48-bit
237+
* formula's window top (kept for entropy/slot reporting); it is too low for
238+
* sub-48 configs, whose KIMAGE_VADDR is HIGHER (39-bit → 0xffffffc080000000).
239+
* Widen the honest top to the validation ceiling KERNEL_VIRT_TEXT_MAX, which
240+
* admits every supported VA_BITS_MIN's text base, so a sub-48 text leak is not
241+
* falsely excluded. Widen-only, never-narrow — same discipline as the floor. */
242+
#define KASLR_VIRT_TEXT_MAX_WIDE KERNEL_VIRT_TEXT_MAX
243+
222244
#define KASLR_SUPPORTED 1
223245

224246
#endif /* KASLD_ARM64_H */

src/include/kasld/engine_rules.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ R(text_base_coupling_synth);
100100
R(phys_virt_synth);
101101

102102
/* arm64-specific rules */
103+
R(arm64_text_base);
103104
R(arm64_memstart_align);
104105
R(arm64_va_bits_from_directmap);
105106
R(arm64_va_bits_from_vmemmap);

src/quantities.c

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,16 @@ static void top_interval(struct estimate *e, unsigned long lo,
4646

4747
static void top_virt_image_base(struct estimate *e) {
4848
/* The virtual kernel-text base lives in the virtual KASLR window
49-
* [KASLR_VIRT_TEXT_MIN_WIDE, KASLR_VIRT_TEXT_MAX] — fixed per-arch by the
50-
* kernel's VA layout (unlike the physical base, this does not depend on DRAM
51-
* placement), so it is a sound and tighter honest top than the raw
49+
* [KASLR_VIRT_TEXT_MIN_WIDE, KASLR_VIRT_TEXT_MAX_WIDE] — fixed per-arch by
50+
* the kernel's VA layout (unlike the physical base, this does not depend on
51+
* DRAM placement), so it is a sound and tighter honest top than the raw
5252
* mapping-region bounds KERNEL_VIRT_TEXT_MIN/MAX.
5353
*
54+
* Both edges are the conservative (_WIDE) variants: same value as
55+
* KASLR_VIRT_TEXT_MIN/MAX where the arch's KASLR window already spans every
56+
* layout, wider where it does not (x86_64 CONFIG_PHYSICAL_START at the floor;
57+
* arm64 sub-48 VA_BITS at the ceiling).
58+
*
5459
* The _WIDE floor is the conservative variant of KASLR_VIRT_TEXT_MIN — same
5560
* value where the arch's KASLR_VIRT_TEXT_MIN does not bake in a configurable
5661
* Kconfig knob; *wider* on arches like x86_64 where KASLR_VIRT_TEXT_MIN
@@ -60,7 +65,7 @@ static void top_virt_image_base(struct estimate *e) {
6065
* CONF_HEURISTIC from the compile-time default — overridable by any
6166
* real evidence). */
6267
top_interval(e, (unsigned long)KASLR_VIRT_TEXT_MIN_WIDE,
63-
(unsigned long)KASLR_VIRT_TEXT_MAX);
68+
(unsigned long)KASLR_VIRT_TEXT_MAX_WIDE);
6469
}
6570

6671
static void top_phys_image_base(struct estimate *e) {

src/rules/arm64_coupling_validate.c

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,15 @@
5050

5151
#include <string.h>
5252

53-
/* VA_BITS_MIN=48 anchored constants — see arm64.h for derivations. */
54-
#define ARM64_PAGE_END_VAMIN 0xffff800000000000ul /* _PAGE_END(48) */
55-
#define ARM64_VMEMMAP_END 0xffffffffc0000000ul /* −SZ_1G */
53+
/* The linear map sits in [PAGE_OFFSET, _PAGE_END), both functions of VA_BITS.
54+
* The widest accepting ceiling is _PAGE_END of the SMALLEST supported VA_BITS
55+
* (highest _PAGE_END) — arm64_page_end_for(ARM64_VA_BITS_MIN_SUPPORTED) — so a
56+
* sub-48 PAGE_OFFSET (e.g. 39-bit 0xffffff8000000000) is admitted, not flagged
57+
* as out-of-band. Trade-off: the gap between the 48-bit and smallest-VA_BITS
58+
* _PAGE_END values is no longer policed pre-resolution (a 48-bit heap pointer
59+
* mistagged DIRECTMAP there is admitted); the directmap consumers narrow once
60+
* PAGE_OFFSET resolves. */
61+
#define ARM64_VMEMMAP_END 0xffffffffc0000000ul /* −SZ_1G */
5662
/* VA_BITS=48 VMEMMAP floor; conservative for both VA48 and VA52 (VA52's
5763
* VMEMMAP extends further down, so any address < VA48 floor is consistent
5864
* with VA52 vmemmap — we don't invalidate it). Matches the threshold the
@@ -77,10 +83,11 @@ int rule_arm64_coupling_validate(const struct evidence_set *ev,
7783
switch (o->eff_region) {
7884
case REGION_DIRECTMAP:
7985
case REGION_PAGE_OFFSET:
80-
/* Must be below _PAGE_END(48). The lower edge varies with VA_BITS
81-
* (PAGE_OFFSET = −(1<<VA_BITS)); use the widest plausible floor
82-
* (VA52's, KERNEL_VIRT_VAS_START) as the lower bound. */
83-
bad = (a >= ARM64_PAGE_END_VAMIN) ||
86+
/* Linear map lives in [PAGE_OFFSET, _PAGE_END), both VA_BITS-dependent.
87+
* Widest accepting bounds: floor at the lowest PAGE_OFFSET (VA52's,
88+
* KERNEL_VIRT_VAS_START); ceiling at the highest _PAGE_END (smallest
89+
* supported VA_BITS). */
90+
bad = (a >= arm64_page_end_for(ARM64_VA_BITS_MIN_SUPPORTED)) ||
8491
(a < (unsigned long)KERNEL_VIRT_VAS_START);
8592
break;
8693
case REGION_MODULE:

0 commit comments

Comments
 (0)