Skip to content

Commit d4f8343

Browse files
committed
rules: pin x86_64 direct map under CONFIG_KASAN / nokaslr
kernel_randomize_memory() returns early when !kaslr_memory_enabled(), and kaslr_memory_enabled() = kaslr_enabled() && !IS_ENABLED(CONFIG_KASAN) (identical 6.6..mainline). So page_offset/vmalloc/vmemmap keep their compile-time L4/L5 defaults whenever KASLR is off OR CONFIG_KASAN=y -- and on x86_64 those macros are unconditionally the runtime variables, so the base equals the constant. The non-obvious case is KASAN with CONFIG_RANDOMIZE_MEMORY=y (common on syzkaller / CTF / debug kernels): the config advertises a randomised direct map but KASAN overrides it. directmap_kaslr_disabled_pin pins the three bases on a positive disable signal, choosing L4/L5 from SF_VIRT_ADDR_BITS (cpuinfo, leak-free). New SF_KASAN_ENABLED detected by proc_config / boot_config; L4/L5 base constants in x86_64.h. Reads only scalar facts (no est[Q] -> no self-edge); the meet backstops any out-of-window value. Kernel TEXT KASLR stays randomised.
1 parent 5fac162 commit d4f8343

10 files changed

Lines changed: 214 additions & 3 deletions

File tree

src/components/boot_config.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ int main(void) {
127127
kasld_emit_scalar(SF_PHYS_KASLR_DISABLED, 1, CONF_PARSED);
128128
}
129129

130+
/* CONFIG_KASAN=y forces the direct-map randomisation off at runtime
131+
* (kaslr_memory_enabled() = kaslr_enabled() && !IS_ENABLED(CONFIG_KASAN)), so
132+
* page_offset / vmalloc / vmemmap stay at their compile-time defaults even
133+
* with CONFIG_RANDOMIZE_MEMORY=y. Consumed by directmap_kaslr_disabled_pin.
134+
*/
135+
if (is_kconfig_set(fp, "CONFIG_KASAN")) {
136+
printf("[.] CONFIG_KASAN=y\n");
137+
kasld_emit_scalar(SF_KASAN_ENABLED, 1, CONF_PARSED);
138+
}
139+
130140
fclose(fp);
131141

132142
return 0;

src/components/proc_config.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ int main(void) {
166166
kasld_emit_scalar(SF_PHYS_KASLR_DISABLED, 1, CONF_PARSED);
167167
}
168168

169+
/* CONFIG_KASAN=y forces the direct-map randomisation off at runtime —
170+
* kaslr_memory_enabled() = kaslr_enabled() && !IS_ENABLED(CONFIG_KASAN) —
171+
* so page_offset / vmalloc / vmemmap stay at their compile-time defaults even
172+
* when CONFIG_RANDOMIZE_MEMORY=y. Consumed by directmap_kaslr_disabled_pin
173+
* (x86_64). The fact is arch-neutral; the rule gates on the arch. */
174+
if (is_kconfig_set(fp, "CONFIG_KASAN")) {
175+
printf("[.] CONFIG_KASAN=y\n");
176+
kasld_emit_scalar(SF_KASAN_ENABLED, 1, CONF_PARSED);
177+
}
178+
169179
fclose(fp);
170180

171181
return 0;

src/engine_rules.c

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

6969
/* KASLR-off pin */
7070
rule_virt_kaslr_disabled_pin,
71+
rule_directmap_kaslr_disabled_pin,
7172
rule_phys_kaslr_disabled_pin,
7273
rule_physical_start_lower_bound,
7374

src/include/kasld/api.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,12 @@ enum kasld_scalar_fact {
757757
SF_PHYSICAL_START, /* CONFIG_PHYSICAL_START (kernel's LOAD_PHYSICAL_ADDR */
758758
/* / pref_address; x86). Used to raise the Q_*_TEXT */
759759
/* honest-top floors above their conservative default.*/
760+
SF_KASAN_ENABLED, /* 1 if CONFIG_KASAN=y. On x86_64 KASAN forces */
761+
/* kaslr_memory_enabled()=false (= kaslr_enabled() && */
762+
/* !IS_ENABLED(CONFIG_KASAN)), so the direct map / */
763+
/* vmalloc / vmemmap bases stay at their compile-time */
764+
/* defaults even when CONFIG_RANDOMIZE_MEMORY=y. */
765+
/* Pinned by directmap_kaslr_disabled_pin. */
760766
SF__COUNT,
761767
};
762768

@@ -785,6 +791,7 @@ static const char *const kasld_scalar_fact_wire_table[SF__COUNT] = {
785791
[SF_CMDLINE_HUGEPAGES] = "cmdline_hugepages",
786792
[SF_CMDLINE_MEMMAP_COUNT] = "cmdline_memmap_count",
787793
[SF_PHYSICAL_START] = "physical_start",
794+
[SF_KASAN_ENABLED] = "kasan_enabled",
788795
};
789796
/* Adding an SF_* without a wire token shrinks this below SF__COUNT -> error. */
790797
typedef char kasld_sf_wire_table_complete

src/include/kasld/arch/x86_64.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@
5151
// value.
5252
#define PAGE_OFFSET_RANDOMIZED 1
5353

54+
// Compile-time default region bases — the values page_offset_base /
55+
// vmalloc_base / vmemmap_base are initialised to (head64.c) and KEEP whenever
56+
// kernel_randomize_memory() returns early, i.e. when KASLR is off OR
57+
// CONFIG_KASAN=y (kaslr_memory_enabled() = kaslr_enabled() && !KASAN). On
58+
// x86_64 __PAGE_OFFSET / VMALLOC_START / VMEMMAP_START are unconditionally
59+
// these variables (page_64_types.h, pgtable_64_types.h), so under the disabled
60+
// gate the runtime base IS the constant. Selected by paging level: L4 = 4-level
61+
// (VA 48), L5 = 5-level (VA 57). Consumed by directmap_kaslr_disabled_pin.
62+
#define PAGE_OFFSET_BASE_L4 0xffff888000000000ul
63+
#define PAGE_OFFSET_BASE_L5 0xff11000000000000ul
64+
#define VMALLOC_BASE_L4 0xffffc90000000000ul
65+
#define VMALLOC_BASE_L5 0xffa0000000000000ul
66+
#define VMEMMAP_BASE_L4 0xffffea0000000000ul
67+
#define VMEMMAP_BASE_L5 0xffd4000000000000ul
68+
5469
#define KERNEL_VIRT_VAS_START PAGE_OFFSET
5570
#define KERNEL_VIRT_VAS_END 0xfffffffffffffffful
5671

src/include/kasld/engine_rules.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ R(base_align_cross_validate);
7979

8080
/* KASLR-off pin + learned floor */
8181
R(virt_kaslr_disabled_pin);
82+
R(directmap_kaslr_disabled_pin);
8283
R(phys_kaslr_disabled_pin);
8384
R(physical_start_lower_bound);
8485

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// This file is part of KASLD - https://github.com/bcoles/kasld
2+
//
3+
// Rule: x86_64 direct-map KASLR-disabled pin (page_offset / vmalloc / vmemmap).
4+
//
5+
// arch/x86/mm/kaslr.c:kernel_randomize_memory() returns BEFORE touching any
6+
// base when !kaslr_memory_enabled(), and
7+
//
8+
// kaslr_memory_enabled() = kaslr_enabled() && !IS_ENABLED(CONFIG_KASAN)
9+
//
10+
// (identical in linux-6.6 … 7.0 and mainline). So page_offset_base /
11+
// vmalloc_base / vmemmap_base keep their compile-time defaults whenever KASLR
12+
// is off (SF_VIRT_KASLR_DISABLED) OR CONFIG_KASAN=y (SF_KASAN_ENABLED) — and on
13+
// x86_64 __PAGE_OFFSET / VMALLOC_START / VMEMMAP_START ARE those variables
14+
// unconditionally (page_64_types.h, pgtable_64_types.h), so the runtime base
15+
// equals the constant. The non-obvious case is KASAN with
16+
// CONFIG_RANDOMIZE_MEMORY=y: the config advertises a randomised direct map but
17+
// KASAN overrides it at runtime — common on syzkaller / CTF / debug kernels.
18+
//
19+
// On a positive disable signal, pin all three bases to the paging-level default
20+
// (L4 = VA 48, L5 = VA 57), chosen by SF_VIRT_ADDR_BITS (proc_cpuinfo's "bits
21+
// virtual", which tracks the active level — leak-free, so the pin fires without
22+
// any direct-map leak). CONFIG_RANDOMIZE_BASE is independent of the memory
23+
// randomisation, so kernel TEXT stays randomised; this pins only the
24+
// direct-map side.
25+
//
26+
// Soundness:
27+
// * Fires only on a positive disable signal AND a resolved VA width.
28+
// * The pinned values are exact, non-config-tunable kernel constants for the
29+
// resolved level, so no window-containment read is needed — and an
30+
// out-of-window C_EQUALS is dropped by the engine's meet as a conflict
31+
// anyway (so no est[Q] read, no self-edge).
32+
// * A higher-confidence real direct-map leak still wins via the resolver's
33+
// conflict handling.
34+
// ---
35+
// <bcoles@gmail.com>
36+
37+
#include "../include/kasld/engine_rules.h"
38+
39+
#include <string.h>
40+
41+
int rule_directmap_kaslr_disabled_pin(const struct evidence_set *ev,
42+
const struct estimate *est,
43+
struct constraint *out, int out_max) {
44+
(void)est;
45+
#if defined(__x86_64__) || defined(__amd64__)
46+
uint32_t sig_id = 0, va_id = 0;
47+
enum kasld_confidence sig_conf = CONF_UNKNOWN, va_conf = CONF_UNKNOWN;
48+
unsigned long va_bits = 0;
49+
for (int i = 0; i < ev->n_obs; i++) {
50+
const struct observation *o = &ev->obs[i];
51+
if (!o->valid || o->value_kind != OBS_SCALAR)
52+
continue;
53+
if ((o->scalar_fact == SF_KASAN_ENABLED ||
54+
o->scalar_fact == SF_VIRT_KASLR_DISABLED) &&
55+
o->scalar_value != 0) {
56+
if (sig_id == 0) {
57+
sig_id = o->id;
58+
sig_conf = o->conf;
59+
}
60+
} else if (o->scalar_fact == SF_VIRT_ADDR_BITS && o->scalar_value != 0) {
61+
va_bits = o->scalar_value;
62+
va_id = o->id;
63+
va_conf = o->conf;
64+
}
65+
}
66+
if (sig_id == 0 || va_bits == 0)
67+
return 0;
68+
69+
/* Active paging level: 48-bit VA -> 4-level (L4), 57-bit -> 5-level (L5). */
70+
int l5;
71+
if (va_bits <= 48)
72+
l5 = 0;
73+
else if (va_bits <= 57)
74+
l5 = 1;
75+
else
76+
return 0; /* unexpected width — don't pin. */
77+
78+
struct {
79+
enum kasld_quantity q;
80+
unsigned long value;
81+
} pins[3] = {
82+
{Q_PAGE_OFFSET, l5 ? PAGE_OFFSET_BASE_L5 : PAGE_OFFSET_BASE_L4},
83+
{Q_VMALLOC_BASE, l5 ? VMALLOC_BASE_L5 : VMALLOC_BASE_L4},
84+
{Q_VMEMMAP_BASE, l5 ? VMEMMAP_BASE_L5 : VMEMMAP_BASE_L4},
85+
};
86+
87+
enum kasld_confidence conf = (sig_conf < va_conf) ? sig_conf : va_conf;
88+
int n = 0;
89+
for (int k = 0; k < 3 && n < out_max; k++) {
90+
struct constraint *c = &out[n++];
91+
memset(c, 0, sizeof(*c));
92+
c->q = pins[k].q;
93+
c->op = C_EQUALS;
94+
c->value = pins[k].value;
95+
c->conf = conf;
96+
c->derived_from[0] = sig_id;
97+
c->derived_from[1] = va_id;
98+
c->lineage_count = 2;
99+
snprintf(c->origin, ORIGIN_LEN, "directmap_kaslr_disabled_pin");
100+
}
101+
return n;
102+
#else
103+
(void)ev;
104+
(void)out;
105+
(void)out_max;
106+
return 0;
107+
#endif
108+
}

tests/test_engine.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3134,6 +3134,65 @@ static void test_virt_kaslr_disabled_pin_no_signal_no_pin(void) {
31343134
assert(e.est[Q_VIRT_TEXT_BASE].hi == top.hi);
31353135
}
31363136

3137+
/* directmap_kaslr_disabled_pin: KASAN (or nokaslr) leaves page_offset / vmalloc
3138+
* / vmemmap at their compile-time L4/L5 defaults; the level comes from
3139+
* SF_VIRT_ADDR_BITS. x86_64 only. */
3140+
int rule_directmap_kaslr_disabled_pin(const struct evidence_set *ev,
3141+
const struct estimate *est,
3142+
struct constraint *out, int out_max);
3143+
3144+
static void test_directmap_kaslr_disabled_pin(void) {
3145+
#if defined(__x86_64__)
3146+
const rule_fn rules[] = {rule_directmap_kaslr_disabled_pin};
3147+
struct estimate top;
3148+
quantities[Q_PAGE_OFFSET].init_top(&top);
3149+
3150+
/* KASAN + 4-level (VA 48): all three bases pinned to the L4 defaults. */
3151+
struct engine e;
3152+
engine_init(&e);
3153+
struct observation k = mk_scalar(SF_KASAN_ENABLED, 1, CONF_PARSED);
3154+
struct observation vb = mk_scalar(SF_VIRT_ADDR_BITS, 48, CONF_PARSED);
3155+
evidence_add(&e.ev, &k);
3156+
evidence_add(&e.ev, &vb);
3157+
engine_run(&e, rules, 1);
3158+
assert(e.est[Q_PAGE_OFFSET].lo == PAGE_OFFSET_BASE_L4 &&
3159+
e.est[Q_PAGE_OFFSET].hi == PAGE_OFFSET_BASE_L4);
3160+
assert(e.est[Q_VMALLOC_BASE].lo == VMALLOC_BASE_L4 &&
3161+
e.est[Q_VMALLOC_BASE].hi == VMALLOC_BASE_L4);
3162+
assert(e.est[Q_VMEMMAP_BASE].lo == VMEMMAP_BASE_L4 &&
3163+
e.est[Q_VMEMMAP_BASE].hi == VMEMMAP_BASE_L4);
3164+
3165+
/* nokaslr + 5-level (VA 57): page_offset pinned to the L5 default. */
3166+
struct engine e2;
3167+
engine_init(&e2);
3168+
struct observation d = mk_scalar(SF_VIRT_KASLR_DISABLED, 1, CONF_PARSED);
3169+
struct observation vb2 = mk_scalar(SF_VIRT_ADDR_BITS, 57, CONF_PARSED);
3170+
evidence_add(&e2.ev, &d);
3171+
evidence_add(&e2.ev, &vb2);
3172+
engine_run(&e2, rules, 1);
3173+
assert(e2.est[Q_PAGE_OFFSET].lo == PAGE_OFFSET_BASE_L5 &&
3174+
e2.est[Q_PAGE_OFFSET].hi == PAGE_OFFSET_BASE_L5);
3175+
3176+
/* Negative: VA width but no disable signal — no pin. */
3177+
struct engine e3;
3178+
engine_init(&e3);
3179+
struct observation vb3 = mk_scalar(SF_VIRT_ADDR_BITS, 48, CONF_PARSED);
3180+
evidence_add(&e3.ev, &vb3);
3181+
engine_run(&e3, rules, 1);
3182+
assert(e3.est[Q_PAGE_OFFSET].lo == top.lo &&
3183+
e3.est[Q_PAGE_OFFSET].hi == top.hi);
3184+
3185+
/* Negative: disable signal but no VA width — no pin (can't pick L4/L5). */
3186+
struct engine e4;
3187+
engine_init(&e4);
3188+
struct observation k4 = mk_scalar(SF_KASAN_ENABLED, 1, CONF_PARSED);
3189+
evidence_add(&e4.ev, &k4);
3190+
engine_run(&e4, rules, 1);
3191+
assert(e4.est[Q_PAGE_OFFSET].lo == top.lo &&
3192+
e4.est[Q_PAGE_OFFSET].hi == top.hi);
3193+
#endif
3194+
}
3195+
31373196
/* SF_PHYS_KASLR_DISABLED pins Q_PHYS_TEXT_BASE on arches where the kernel's
31383197
* decompressor/relocator keeps the image at its compile-time physical default
31393198
* under nokaslr (KASLR_DISABLED_PINS_PHYS=1). Per-quantity window-containment
@@ -4485,6 +4544,7 @@ int main(void) {
44854544
BEGIN_CATEGORY("KASLR-off pin");
44864545
RUN(test_virt_kaslr_disabled_pin);
44874546
RUN(test_virt_kaslr_disabled_pin_no_signal_no_pin);
4547+
RUN(test_directmap_kaslr_disabled_pin);
44884548
RUN(test_phys_kaslr_disabled_pin);
44894549
RUN(test_phys_kaslr_disabled_pin_defers_to_real_leak);
44904550
RUN(test_phys_kaslr_disabled_pin_inert_on_decoupled);

tests/test_orch_common.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
/* Seed the engine-bounds carrier the same way orchestrator's main() does.
1818
* Under KASLD_TESTING the orchestrator's main() is excluded, so suites that
1919
* read layout (compute_kaslr_info coverage, the renderer's derived paths) must
20-
* start it from the honest window themselves. Call once at the top of main(). */
20+
* start it from the honest window themselves. Call once at the top of main().
21+
*/
2122
static void test_init_layout_engine_bounds(void) {
2223
layout.virt_page_offset_min = layout.virt_kernel_vas_start;
2324
layout.virt_page_offset_max = layout.virt_kernel_vas_end;

tests/test_render.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
#include <string.h>
3131
#include <unistd.h>
3232

33-
3433
/* =========================================================================
3534
* Renderer unit tests.
3635
*
@@ -1025,7 +1024,6 @@ static void test_render_hardening_text_no_rand_failed_silent(void) {
10251024
assert(strstr(render_cap, "KASLR posture") == NULL);
10261025
}
10271026

1028-
10291027
int main(void) {
10301028
TEST_SUITE("render — renderer unit suite");
10311029
test_init_layout_engine_bounds();

0 commit comments

Comments
 (0)