Skip to content

Commit 40c4005

Browse files
committed
rules: derive ARM32 PAGE_OFFSET and text base from the VMSPLIT
ARM32 PAGE_OFFSET is a compile-time VMSPLIT choice with no KASLR, so the image sits at PAGE_OFFSET + TEXT_OFFSET. New vmsplit_text_base rule snaps any observed kernel virtual text address to its VMSPLIT boundary, pinning PAGE_OFFSET and the exact _text; agreeing witnesses outrank a raw _stext pin via lineage. Previously the engine kept the 0xc0000000 default, so a 2G/2G kernel reported a virtual text range that excluded the real _text. Also sync the rendered virt_page_offset from the resolved estimate on coupled arches — it was only projected on decoupled ones, so arm32 showed the compile-time seed despite the engine resolving PAGE_OFFSET correctly.
1 parent c4ab31f commit 40c4005

5 files changed

Lines changed: 161 additions & 0 deletions

File tree

src/engine_rules.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ static const rule_fn k_rules[] = {
3030
rule_phys_bits_ceiling,
3131
rule_image_size_text_data_gap,
3232
rule_min_offset_from_image_size,
33+
rule_vmsplit_text_base,
3334
rule_range_from_interior,
3435

3536
/* DRAM bounds */

src/include/kasld/arch/arm32.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@
2727
// VMSPLIT (CONFIG_PAGE_OFFSET) is a compile-time constant, fixed at boot.
2828
#define PAGE_OFFSET_FROM_CONFIG 1
2929

30+
// The runtime PAGE_OFFSET is one of the VMSPLIT boundaries (arch/arm/Kconfig:
31+
// VMSPLIT_3G / 3G_OPT / 2G / 1G), listed high→low for snap-down. arm32 has no
32+
// KASLR, so the kernel image sits at PAGE_OFFSET + TEXT_OFFSET: any observed
33+
// kernel virtual text address V therefore pins PAGE_OFFSET (the largest
34+
// boundary <= V) and hence the exact image base. Consumed by the
35+
// vmsplit_text_base engine rule. The 0xc0000000 default above is only the
36+
// render fallback when no virtual text address is observed.
37+
// https://elixir.bootlin.com/linux/v6.1.1/source/arch/arm/Kconfig#L1116
38+
#define HAVE_VMSPLIT_PAGE_OFFSET 1
39+
#define VMSPLIT_PAGE_OFFSETS \
40+
{0xc0000000ul, 0xb0000000ul, 0x80000000ul, 0x40000000ul}
41+
3042
// https://elixir.bootlin.com/linux/v6.1.1/source/arch/arm/Kconfig#L276
3143
#define PHYS_OFFSET 0ul
3244

src/include/kasld/engine_rules.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ R(virt_ceiling_from_memtotal);
4141
R(phys_bits_ceiling);
4242
R(image_size_text_data_gap);
4343
R(min_offset_from_image_size);
44+
R(vmsplit_text_base);
4445
R(range_from_interior);
4546

4647
/* DRAM bounds (MMIO ceilings, holes, kernel-image phys gap, firmware) */

src/orchestrator.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2550,6 +2550,17 @@ static void engine_sync_authoritative(const struct engine *e) {
25502550
layout.virt_page_offset_min = e->est[Q_PAGE_OFFSET].lo;
25512551
layout.virt_page_offset_max = e->est[Q_PAGE_OFFSET].hi;
25522552

2553+
/* Project the resolved direct-map base onto the rendered singular field
2554+
* whenever the engine has pinned it. On coupled arches PAGE_OFFSET equals
2555+
* the compile-time default on the common configuration, so this is normally
2556+
* a no-op — but a runtime-detected VMSPLIT (arm32 vmsplit_text_base) can pin
2557+
* it to a different boundary, and the render must follow the engine rather
2558+
* than show the compile-time seed. (The decoupled, possibly-unpinned
2559+
* RANDOMIZE_MEMORY case is handled by the block below.) */
2560+
if (e->est[Q_PAGE_OFFSET].lo == e->est[Q_PAGE_OFFSET].hi &&
2561+
e->est[Q_PAGE_OFFSET].lo != 0)
2562+
layout.virt_page_offset = e->est[Q_PAGE_OFFSET].lo;
2563+
25532564
#if !TEXT_TRACKS_DIRECTMAP
25542565
/* On decoupled arches the direct-map base (PAGE_OFFSET) is randomised away
25552566
* from the compile-time floor (x86_64 RANDOMIZE_MEMORY). Anchor the rendered

src/rules/vmsplit_text_base.c

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// This file is part of KASLD - https://github.com/bcoles/kasld
2+
//
3+
// Rule: vmsplit_text_base
4+
//
5+
// On an architecture whose PAGE_OFFSET is a compile-time VMSPLIT choice and
6+
// which has no KASLR (ARM32: arch/arm/Kconfig VMSPLIT_3G/3G_OPT/2G/1G), the
7+
// kernel image base is fixed at PAGE_OFFSET + TEXT_OFFSET. Therefore ANY
8+
// observed kernel virtual text address V determines the whole virtual layout:
9+
//
10+
// PAGE_OFFSET = largest VMSPLIT boundary <= V (V lies in the image,
11+
// which spans [PAGE_OFFSET + TEXT_OFFSET, PAGE_OFFSET +
12+
// 1G))
13+
// virt text base = PAGE_OFFSET + TEXT_OFFSET (== _text, exactly)
14+
//
15+
// This is the runtime "vmsplit adjustment" the arm32 header promises. Without
16+
// it the engine keeps the compile-time PAGE_OFFSET (0xc0000000) default, which
17+
// on a non-3G/1G kernel (e.g. a 2G/2G distro build) is wrong — and the raw
18+
// _stext pin overshoots the real _text by the head/init sections.
19+
//
20+
// We gather every virtual kernel-text witness, snap each to its boundary, and
21+
// take the boundary with the strongest support (highest confidence, then most
22+
// independent witnesses). The witness count becomes the constraint lineage, so
23+
// an agreeing set of leaks outranks a single raw _stext pin in the resolver
24+
// (estimate.c prio_before: confidence DESC, then lineage_count DESC).
25+
//
26+
// Sound only where text == PAGE_OFFSET + TEXT_OFFSET deterministically, hence
27+
// the !KASLR_SUPPORTED gate and the per-arch VMSPLIT_PAGE_OFFSETS opt-in.
28+
// ---
29+
// <bcoles@gmail.com>
30+
31+
#include "include/kasld/engine_rules.h"
32+
#include "include/kasld/regions.h"
33+
#include <string.h>
34+
35+
int rule_vmsplit_text_base(const struct evidence_set *ev,
36+
const struct estimate *est, struct constraint *out,
37+
int out_max) {
38+
(void)est;
39+
#if defined(HAVE_VMSPLIT_PAGE_OFFSET) && !KASLR_SUPPORTED
40+
if (out_max < 2)
41+
return 0;
42+
43+
static const unsigned long cand[] = VMSPLIT_PAGE_OFFSETS; /* high -> low */
44+
const int ncand = (int)(sizeof(cand) / sizeof(cand[0]));
45+
46+
unsigned long best_po = 0;
47+
enum kasld_confidence best_conf = CONF_UNKNOWN;
48+
int best_votes = 0;
49+
uint32_t best_src[MAX_LINEAGE];
50+
int best_nsrc = 0;
51+
52+
for (int c = 0; c < ncand; c++) {
53+
enum kasld_confidence conf = CONF_UNKNOWN;
54+
int votes = 0;
55+
uint32_t src[MAX_LINEAGE];
56+
int nsrc = 0;
57+
58+
for (int i = 0; i < ev->n_obs; i++) {
59+
const struct observation *o = &ev->obs[i];
60+
if (!o->valid || o->value_kind != OBS_ADDRESS ||
61+
o->eff_type != KASLD_TYPE_VIRT)
62+
continue;
63+
if (o->eff_region != REGION_KERNEL_TEXT &&
64+
o->eff_region != REGION_KERNEL_IMAGE)
65+
continue;
66+
unsigned long v = obs_anchor(o);
67+
if (v == 0)
68+
continue;
69+
70+
/* snap v to its VMSPLIT boundary: the largest candidate <= v. A witness
71+
* below every boundary (a stray low value) snaps to nothing and is
72+
* ignored, so it cannot vote. */
73+
unsigned long snap = 0;
74+
for (int k = 0; k < ncand; k++) {
75+
if (v >= cand[k]) {
76+
snap = cand[k];
77+
break;
78+
}
79+
}
80+
if (snap != cand[c])
81+
continue;
82+
83+
votes++;
84+
if (o->conf > conf)
85+
conf = o->conf;
86+
if (nsrc < MAX_LINEAGE)
87+
src[nsrc++] = o->id;
88+
}
89+
90+
/* Prefer the strongest-supported boundary: higher confidence, then more
91+
* independent witnesses. */
92+
if (votes > 0 && (best_votes == 0 || conf > best_conf ||
93+
(conf == best_conf && votes > best_votes))) {
94+
best_po = cand[c];
95+
best_conf = conf;
96+
best_votes = votes;
97+
best_nsrc = nsrc;
98+
memcpy(best_src, src, sizeof(uint32_t) * (size_t)nsrc);
99+
}
100+
}
101+
102+
if (best_votes == 0)
103+
return 0;
104+
105+
int n = 0;
106+
107+
struct constraint *po = &out[n++];
108+
memset(po, 0, sizeof(*po));
109+
po->q = Q_PAGE_OFFSET;
110+
po->op = C_EQUALS;
111+
po->value = best_po;
112+
po->conf = best_conf;
113+
for (int i = 0; i < best_nsrc; i++)
114+
po->derived_from[i] = best_src[i];
115+
po->lineage_count = best_nsrc;
116+
snprintf(po->origin, ORIGIN_LEN, "vmsplit_text_base");
117+
118+
struct constraint *vt = &out[n++];
119+
memset(vt, 0, sizeof(*vt));
120+
vt->q = Q_VIRT_TEXT_BASE;
121+
vt->op = C_EQUALS;
122+
vt->value = best_po + (unsigned long)TEXT_OFFSET;
123+
vt->conf = best_conf;
124+
for (int i = 0; i < best_nsrc; i++)
125+
vt->derived_from[i] = best_src[i];
126+
vt->lineage_count = best_nsrc;
127+
snprintf(vt->origin, ORIGIN_LEN, "vmsplit_text_base");
128+
129+
return n;
130+
#else
131+
(void)ev;
132+
(void)out;
133+
(void)out_max;
134+
return 0;
135+
#endif
136+
}

0 commit comments

Comments
 (0)