Skip to content

Commit d73908f

Browse files
committed
inference: chain page_offset_base → vmalloc_base → vmemmap_base
Add two inference plugins that propagate page_offset_base bounds through the fixed CONFIG_RANDOMIZE_MEMORY region ordering: x86_64_vmalloc_base_bound: vmalloc_base_min = page_offset_min + directmap_size_tb * 1 TiB + PUD_SIZE (directmap_size_tb derived from /proc/zoneinfo max_pfn) x86_64_vmemmap_base_bound: vmemmap_base_min = vmalloc_base_min + VMALLOC_SIZE_TB * 1 TiB + PUD_SIZE vmemmap_base_max = CPU_ENTRY_AREA_BASE - vmemmap_size (L4 vs L5 paging detected from page_offset_min) Adds vmalloc_base_{min,max} and vmemmap_base_{min,max} to kasld_analysis_ctx, threads them through snap_bounds / bounds_changed, and surfaces the three bounded regions in render_kaslr_text and JSON output under a new "Memory KASLR" section. Also derive a ret2dir physmap alias of kernel text in compute_derived_addrs when both PHYS/TEXT and a pinned page_offset_base are available: page_offset_base + (P text − PHYS_OFFSET). The subtraction is a no-op on x86_64 / arm64 (PHYS_OFFSET = 0) and significant on riscv64 (= 0x80000000).
1 parent 0e075e7 commit d73908f

6 files changed

Lines changed: 515 additions & 5 deletions

File tree

src/include/kasld_inference.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@ struct kasld_analysis_ctx {
7171
/* Physical KASLR bounds (PHYS_VIRT_DECOUPLED arches only; zero otherwise). */
7272
unsigned long phys_base_min;
7373
unsigned long phys_base_max;
74+
/* vmalloc region bounds (x86_64 with CONFIG_RANDOMIZE_MEMORY only).
75+
* vmalloc_base_min: architectural lower bound derived from
76+
* page_offset_base + directmap_size_tb * 1TiB + PUD_SIZE.
77+
* Initialised to 0 / ULONG_MAX. Only tightened by
78+
* x86_64_vmalloc_base_bound.c when page_offset_base is known. */
79+
unsigned long vmalloc_base_min;
80+
unsigned long vmalloc_base_max;
81+
/* vmemmap region bounds (x86_64 with CONFIG_RANDOMIZE_MEMORY only).
82+
* vmemmap_base_min: derived from vmalloc_base_min + VMALLOC_SIZE_TB +
83+
* PUD_SIZE inter-region gap.
84+
* vmemmap_base_max: derived from CPU_ENTRY_AREA_BASE − vmemmap_size.
85+
* Initialised to 0 / ULONG_MAX. Only tightened by
86+
* x86_64_vmemmap_base_bound.c. Per-PFN pinning via leaked struct page
87+
* VAs is not feasible on modern kernels (%p hashing since v4.15
88+
* closed the dump_page() source); see
89+
* dev/research/phys-to-virt-bridges.md §B. */
90+
unsigned long vmemmap_base_min;
91+
unsigned long vmemmap_base_max;
7492
const struct kasld_arch_params *arch;
7593
/* Writable layout pointer — set by LAYOUT_ADJUST plugins only.
7694
* POST_COLLECTION and POST_PROBING plugins treat this as read-only. */

src/include/kasld_internal.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ struct kaslr_info {
168168
unsigned long pslots;
169169
int pbits;
170170
int has_phys;
171+
/* Memory KASLR (x86_64 with CONFIG_RANDOMIZE_MEMORY only). Bounds on
172+
* the three independently-randomised memory regions, computed by
173+
* x86_64_vmalloc_base_bound.c and x86_64_vmemmap_base_bound.c via the
174+
* structural placement chain. Zero when no bound was inferred. */
175+
unsigned long page_offset_min;
176+
unsigned long page_offset_max;
177+
unsigned long vmalloc_min;
178+
unsigned long vmalloc_max;
179+
unsigned long vmemmap_min;
180+
unsigned long vmemmap_max;
171181
};
172182

173183
/* Cross-section derived address */
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// This file is part of KASLD - https://github.com/bcoles/kasld
2+
//
3+
// Inference plugin: page_offset_base + directmap_size → vmalloc_base lower
4+
// bound (POST_COLLECTION)
5+
//
6+
// On x86_64 with CONFIG_RANDOMIZE_MEMORY, kernel_randomize_memory() places
7+
// the three virtual memory regions consecutively, each offset by a random
8+
// PUD-aligned value:
9+
//
10+
// kaslr_regions[0]: directmap — base = page_offset_base
11+
// size = directmap_size_tb TiB
12+
// kaslr_regions[1]: vmalloc — base = page_offset_base + directmap_size_tb
13+
// * 1TiB
14+
// + round_up(1, PUD_SIZE) + rand_1
15+
// kaslr_regions[2]: vmemmap — base = vmalloc_base + VMALLOC_SIZE_TB * 1TiB
16+
// + round_up(1, PUD_SIZE) + rand_2
17+
//
18+
// The minimum inter-region gap is exactly PUD_SIZE (1 GiB), because after
19+
// advancing vaddr past each region's size the loop does:
20+
//
21+
// vaddr = round_up(vaddr + 1, PUD_SIZE);
22+
//
23+
// and vaddr is PUD-aligned going in (region sizes are in whole TiB = multiple
24+
// of PUD_SIZE). So the gap is exactly 1 * PUD_SIZE = 1 GiB.
25+
//
26+
// The directmap size is:
27+
//
28+
// memory_tb = DIV_ROUND_UP(max_pfn * 4096, 1 TiB) +
29+
// CONFIG_RANDOMIZE_MEMORY_PHYSICAL_PADDING (default: 10)
30+
// directmap_size_tb = min(4096, memory_tb) [when ZONE_DEVICE disabled]
31+
//
32+
// This gives a sound lower bound:
33+
//
34+
// vmalloc_base_min = page_offset_min + directmap_size_tb * 1TiB + PUD_SIZE
35+
//
36+
// When page_offset_base is exactly pinned (page_offset_min == page_offset_max)
37+
// the bound is tight; otherwise it is a valid-but-loose lower bound.
38+
//
39+
// max_pfn is read from /proc/zoneinfo: max(start_pfn + spanned) across all
40+
// zones. /proc/zoneinfo is world-readable (0444) on all kernel versions.
41+
//
42+
// Phase: POST_COLLECTION — requires page_offset_min from prior plugins
43+
// (directmap_page_offset_bounds, randomize_memory_page_offset).
44+
// Applicable: x86-64 only (other arches use a different memory layout model).
45+
// ---
46+
// <bcoles@gmail.com>
47+
48+
#define _POSIX_C_SOURCE 200809L
49+
50+
#include "../include/kasld_inference.h"
51+
52+
#include <limits.h>
53+
#include <stdio.h>
54+
55+
#define TB_SHIFT 40
56+
#define PUD_SHIFT 30
57+
#define PAGE_SHIFT 12
58+
59+
/* CONFIG_RANDOMIZE_MEMORY_PHYSICAL_PADDING — added to directmap_size_tb.
60+
* Hardcoded to the kernel default (10 TiB). Distro kernels virtually always
61+
* use this value; there is no way to detect it from user space. If a kernel
62+
* uses a larger padding, vmalloc_base_min will be under-estimated (still a
63+
* valid lower bound, just slightly loose). */
64+
#define RANDOMIZE_MEMORY_PHYSICAL_PADDING 10ul
65+
66+
/* Read max_pfn from /proc/zoneinfo: highest (start_pfn + spanned) seen.
67+
* Returns 0 on failure. */
68+
static unsigned long read_max_pfn(void) {
69+
FILE *f = fopen("/proc/zoneinfo", "r");
70+
if (!f)
71+
return 0;
72+
73+
char line[256];
74+
unsigned long max_pfn = 0;
75+
unsigned long cur_spanned = 0;
76+
77+
/* /proc/zoneinfo per-zone layout (relevant fields):
78+
* pages free N
79+
* spanned N ← appears before start_pfn within the zone block
80+
* ...
81+
* start_pfn: N ← zone's base PFN; end = start_pfn + spanned
82+
*/
83+
while (fgets(line, sizeof(line), f)) {
84+
unsigned long val;
85+
86+
if (sscanf(line, " spanned %lu", &val) == 1) {
87+
cur_spanned = val;
88+
continue;
89+
}
90+
91+
if (sscanf(line, " start_pfn: %lu", &val) != 1)
92+
continue;
93+
94+
unsigned long end_pfn = cur_spanned ? val + cur_spanned : val;
95+
if (end_pfn > max_pfn)
96+
max_pfn = end_pfn;
97+
98+
cur_spanned = 0;
99+
}
100+
101+
fclose(f);
102+
return max_pfn;
103+
}
104+
105+
static void x86_64_vmalloc_base_bound_run(struct kasld_analysis_ctx *ctx) {
106+
#if defined(__x86_64__)
107+
/* Only meaningful when page_offset_base is at least partially pinned. */
108+
if (ctx->page_offset_min == 0)
109+
return;
110+
111+
unsigned long max_pfn = read_max_pfn();
112+
if (!max_pfn)
113+
return;
114+
115+
/* directmap_size_tb = DIV_ROUND_UP(max_pfn * PAGE_SIZE, 1 TiB) + padding.
116+
* Cap at 4096 TiB (1 << (MAX_PHYSMEM_BITS - TB_SHIFT), MAX_PHYSMEM_BITS=52).
117+
* This matches kernel_randomize_memory() on kernels without ZONE_DEVICE;
118+
* with ZONE_DEVICE the cap is not applied by the kernel, but 4096 TiB is
119+
* an architectural ceiling regardless. */
120+
unsigned long page_bytes =
121+
max_pfn << PAGE_SHIFT; /* may wrap on 32-bit, but we're x86_64 */
122+
unsigned long one_tb = 1ul << TB_SHIFT;
123+
unsigned long memory_tb =
124+
(page_bytes + one_tb - 1) / one_tb + RANDOMIZE_MEMORY_PHYSICAL_PADDING;
125+
unsigned long directmap_size_tb = memory_tb < 4096ul ? memory_tb : 4096ul;
126+
127+
/* vmalloc_base >= page_offset_base + directmap_size_tb * 1 TiB + PUD_SIZE.
128+
* Use page_offset_min as a conservative substitute for page_offset_base;
129+
* result is always a valid lower bound. */
130+
unsigned long pud_size = 1ul << PUD_SHIFT;
131+
unsigned long candidate =
132+
ctx->page_offset_min + directmap_size_tb * one_tb + pud_size;
133+
134+
/* Sanity: candidate must be above page_offset_min and fit in the kernel VAS.
135+
*/
136+
if (candidate <= ctx->page_offset_min)
137+
return;
138+
139+
if (candidate > ctx->vmalloc_base_min) {
140+
if (verbose && !quiet && !json_output)
141+
printf("[infer] vmalloc_base_min: %#lx "
142+
"(page_offset_min %#lx + %lu TiB directmap + 1 GiB gap;"
143+
" max_pfn %lu)\n",
144+
candidate, ctx->page_offset_min, directmap_size_tb, max_pfn);
145+
ctx->vmalloc_base_min = candidate;
146+
}
147+
#else
148+
(void)ctx;
149+
#endif
150+
}
151+
152+
static const struct kasld_inference x86_64_vmalloc_base_bound = {
153+
.name = "x86_64_vmalloc_base_bound",
154+
.phase = KASLD_INFER_PHASE_POST_COLLECTION,
155+
.run = x86_64_vmalloc_base_bound_run,
156+
};
157+
158+
KASLD_REGISTER_INFERENCE(x86_64_vmalloc_base_bound);
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// This file is part of KASLD - https://github.com/bcoles/kasld
2+
//
3+
// Inference plugin: vmalloc_base + VMALLOC_SIZE_TB → vmemmap_base bounds
4+
// (POST_COLLECTION)
5+
//
6+
// On x86_64 with CONFIG_RANDOMIZE_MEMORY, kernel_randomize_memory() places
7+
// the three memory KASLR regions consecutively, each separated by a random
8+
// PUD-aligned gap of at least PUD_SIZE (1 GiB):
9+
//
10+
// kaslr_regions[0]: directmap — base = page_offset_base
11+
// size = directmap_size_tb * 1 TiB
12+
// kaslr_regions[1]: vmalloc — base = page_offset_base + directmap_size_tb
13+
// * 1 TiB + (>= PUD_SIZE)
14+
// size = VMALLOC_SIZE_TB * 1 TiB
15+
// kaslr_regions[2]: vmemmap — base = vmalloc_base + VMALLOC_SIZE_TB
16+
// * 1 TiB + (>= PUD_SIZE)
17+
// size = directmap_size_tb * 64 / PAGE_SIZE
18+
// TiB
19+
//
20+
// Therefore:
21+
//
22+
// vmemmap_base_min = vmalloc_base_min + VMALLOC_SIZE_TB * 1 TiB + PUD_SIZE
23+
// vmemmap_base_max = CPU_ENTRY_AREA_BASE − vmemmap_size
24+
//
25+
// This is a continuation of x86_64_vmalloc_base_bound.c — together the two
26+
// plugins chain page_offset_base → vmalloc_base → vmemmap_base via the
27+
// fixed inter-region ordering.
28+
//
29+
// VMALLOC_SIZE_TB:
30+
// L4 paging: 32 TiB (kernel constant VMALLOC_SIZE_TB_L4)
31+
// L5 paging: 12800 TiB
32+
// Detected via page_offset_min: < L4 VAS floor (0xffff800000000000) → L5,
33+
// else L4.
34+
//
35+
// CPU_ENTRY_AREA_BASE: 0xfffffe0000000000 on both L4 and L5 (computed as
36+
// -4 << P4D_SHIFT where P4D_SHIFT = 39).
37+
//
38+
// vmemmap_size: derived from max_pfn read from /proc/zoneinfo:
39+
// vmemmap_size = directmap_size_tb * 64 / 4096 TiB
40+
// (each PAGE_SIZE page maps to a 64-byte struct page in vmemmap)
41+
// Rounded up to whole TiB to match kernel_randomize_memory's vmemmap_size
42+
// alignment.
43+
//
44+
// Source data: shared with x86_64_vmalloc_base_bound.c (max_pfn from
45+
// /proc/zoneinfo). Duplicate parse rather than introduce a shared helper —
46+
// keeps each plugin self-contained.
47+
//
48+
// Phase: POST_COLLECTION — requires vmalloc_base_min from
49+
// x86_64_vmalloc_base_bound.c (which itself requires page_offset_min
50+
// from directmap_page_offset_bounds / randomize_memory_page_offset /
51+
// phys_virt_synth).
52+
// Applicable: x86-64 only.
53+
// ---
54+
// <bcoles@gmail.com>
55+
56+
#define _POSIX_C_SOURCE 200809L
57+
58+
#include "../include/kasld_inference.h"
59+
60+
#include <limits.h>
61+
#include <stdio.h>
62+
63+
#define TB_SHIFT 40
64+
#define PUD_SHIFT 30
65+
#define PAGE_SHIFT 12
66+
67+
/* See x86_64_vmalloc_base_bound.c for the padding rationale. */
68+
#define RANDOMIZE_MEMORY_PHYSICAL_PADDING 10ul
69+
70+
/* CPU_ENTRY_AREA_BASE = -4 << P4D_SHIFT = 0xfffffe0000000000 on x86-64
71+
* (L4 and L5). Same value on both paging modes — the CPU_ENTRY_AREA_PGD
72+
* slot is anchored relative to the top of canonical-high. */
73+
#define CPU_ENTRY_AREA_BASE 0xfffffe0000000000ul
74+
75+
/* VMALLOC_SIZE_TB from arch/x86/include/asm/pgtable_64_types.h. */
76+
#define VMALLOC_SIZE_TB_L4 32ul
77+
#define VMALLOC_SIZE_TB_L5 12800ul
78+
79+
/* The L4 vs L5 boundary: __PAGE_OFFSET_BASE_L4 is at 0xffff888000000000.
80+
* Any page_offset below the L4 VAS start (0xffff800000000000) is L5. */
81+
#define X86_64_L4_VAS_START 0xffff800000000000ul
82+
83+
/* Read max_pfn from /proc/zoneinfo (same logic as
84+
* x86_64_vmalloc_base_bound.c — kept inline to avoid a shared helper). */
85+
static unsigned long read_max_pfn(void) {
86+
FILE *f = fopen("/proc/zoneinfo", "r");
87+
if (!f)
88+
return 0;
89+
90+
char line[256];
91+
unsigned long max_pfn = 0;
92+
unsigned long cur_spanned = 0;
93+
94+
while (fgets(line, sizeof(line), f)) {
95+
unsigned long val;
96+
if (sscanf(line, " spanned %lu", &val) == 1) {
97+
cur_spanned = val;
98+
continue;
99+
}
100+
if (sscanf(line, " start_pfn: %lu", &val) != 1)
101+
continue;
102+
unsigned long end_pfn = cur_spanned ? val + cur_spanned : val;
103+
if (end_pfn > max_pfn)
104+
max_pfn = end_pfn;
105+
cur_spanned = 0;
106+
}
107+
fclose(f);
108+
return max_pfn;
109+
}
110+
111+
static void x86_64_vmemmap_base_bound_run(struct kasld_analysis_ctx *ctx) {
112+
#if defined(__x86_64__)
113+
/* Needs vmalloc_base_min already pinned by x86_64_vmalloc_base_bound. */
114+
if (ctx->vmalloc_base_min == 0)
115+
return;
116+
117+
unsigned long pud_size = 1ul << PUD_SHIFT;
118+
unsigned long one_tb = 1ul << TB_SHIFT;
119+
120+
/* Detect paging mode from page_offset_min: L5 if below L4 VAS floor. */
121+
unsigned long vmalloc_size_tb =
122+
(ctx->page_offset_min != 0 && ctx->page_offset_min < X86_64_L4_VAS_START)
123+
? VMALLOC_SIZE_TB_L5
124+
: VMALLOC_SIZE_TB_L4;
125+
126+
/* ---- Lower bound on vmemmap_base ----
127+
* vmemmap_base >= vmalloc_base + VMALLOC_SIZE_TB * 1 TiB + PUD_SIZE.
128+
* Use vmalloc_base_min (the tightest lower bound we have). */
129+
unsigned long lower =
130+
ctx->vmalloc_base_min + vmalloc_size_tb * one_tb + pud_size;
131+
132+
/* Overflow / sanity: result must fit below CPU_ENTRY_AREA_BASE. */
133+
if (lower <= ctx->vmalloc_base_min || lower >= CPU_ENTRY_AREA_BASE)
134+
return;
135+
136+
if (lower > ctx->vmemmap_base_min) {
137+
if (verbose && !quiet && !json_output)
138+
printf("[infer] vmemmap_base_min: %#lx "
139+
"(vmalloc_base_min %#lx + %lu TiB vmalloc + 1 GiB gap)\n",
140+
lower, ctx->vmalloc_base_min, vmalloc_size_tb);
141+
ctx->vmemmap_base_min = lower;
142+
}
143+
144+
/* ---- Upper bound on vmemmap_base ----
145+
* vmemmap_base <= CPU_ENTRY_AREA_BASE − vmemmap_size.
146+
* vmemmap_size matches the kernel's derivation:
147+
* vmemmap_size = round_up(directmap_size_tb * 64 / 4096, 1 TiB)
148+
* Hardcoding the 64-byte struct page size — verifiable via vmlinux's
149+
* CONFIG_MEMCG and tunable group config but defaulting reliably to 64
150+
* on mainline since ~v6.0. */
151+
unsigned long max_pfn = read_max_pfn();
152+
if (max_pfn) {
153+
unsigned long page_bytes = max_pfn << PAGE_SHIFT;
154+
unsigned long memory_tb =
155+
(page_bytes + one_tb - 1) / one_tb + RANDOMIZE_MEMORY_PHYSICAL_PADDING;
156+
unsigned long directmap_size_tb = memory_tb < 4096ul ? memory_tb : 4096ul;
157+
158+
/* vmemmap_size_bytes = directmap_size_tb * 1 TiB * 64 / PAGE_SIZE
159+
* = directmap_size_tb * (1 << 40) * 64 / (1 << 12)
160+
* = directmap_size_tb * (1 << 34) bytes
161+
* = directmap_size_tb * 16 GiB.
162+
* Round up to TiB granularity (kernel uses TB alignment). */
163+
unsigned long vmemmap_size_bytes = directmap_size_tb * (1ul << 34);
164+
unsigned long vmemmap_size_tb = (vmemmap_size_bytes + one_tb - 1) / one_tb;
165+
/* At least 1 TiB to match the kernel's minimum alignment. */
166+
if (vmemmap_size_tb == 0)
167+
vmemmap_size_tb = 1;
168+
169+
unsigned long upper = CPU_ENTRY_AREA_BASE - vmemmap_size_tb * one_tb;
170+
if (upper < ctx->vmemmap_base_max && upper > ctx->vmemmap_base_min) {
171+
if (verbose && !quiet && !json_output)
172+
printf("[infer] vmemmap_base_max: %#lx "
173+
"(CPU_ENTRY_AREA_BASE − %lu TiB vmemmap; max_pfn %lu)\n",
174+
upper, vmemmap_size_tb, max_pfn);
175+
ctx->vmemmap_base_max = upper;
176+
}
177+
}
178+
#else
179+
(void)ctx;
180+
#endif
181+
}
182+
183+
static const struct kasld_inference x86_64_vmemmap_base_bound = {
184+
.name = "x86_64_vmemmap_base_bound",
185+
.phase = KASLD_INFER_PHASE_POST_COLLECTION,
186+
.run = x86_64_vmemmap_base_bound_run,
187+
};
188+
189+
KASLD_REGISTER_INFERENCE(x86_64_vmemmap_base_bound);

0 commit comments

Comments
 (0)