Skip to content

Commit 53c4788

Browse files
committed
rules: let directmap pin take L4/L5 from a resolved Q_VA_BITS
directmap_kaslr_disabled_pin only read SF_VIRT_ADDR_BITS (proc_cpuinfo), so it couldn't fire when cpuinfo was unavailable but a directmap leak had resolved the paging level via x86_64_la57_from_directmap. Fall back to the resolved Q_VA_BITS estimate to pick L4/L5. Reading est[Q_VA_BITS] is cross-quantity (not a self-edge on the bases written) and acyclic. New estimate_finset_value() accessor encapsulates the LK_FINSET live-candidate bitmask so the rule doesn't touch the representation. Confidence and lineage handle the no-observation estimate path (lineage 1, conf = disable signal). Test adds the leak -> la57 -> pin path with no cpuinfo; architecture.md notes the fallback.
1 parent d4f8343 commit 53c4788

5 files changed

Lines changed: 76 additions & 13 deletions

File tree

docs/architecture.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,10 @@ Key rules for cross-region derivation:
250250
(`SF_KASAN_ENABLED`) or KASLR is off (`SF_VIRT_KASLR_DISABLED`) the direct-map
251251
randomisation is suppressed (`kaslr_memory_enabled() = kaslr_enabled() &&
252252
!CONFIG_KASAN`), so `Q_PAGE_OFFSET` / `Q_VMALLOC_BASE` / `Q_VMEMMAP_BASE` are
253-
pinned to their compile-time L4/L5 defaults — the paging level taken from
254-
`SF_VIRT_ADDR_BITS`. Kernel TEXT KASLR is independent and stays randomised.
253+
pinned to their compile-time L4/L5 defaults — the paging level from
254+
`SF_VIRT_ADDR_BITS` (cpuinfo, leak-free) or, when that is unavailable, a
255+
resolved `Q_VA_BITS` (e.g. from a direct-map leak). Kernel TEXT KASLR is
256+
independent and stays randomised.
255257

256258
### Coupled vs decoupled architectures
257259

src/estimate.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,23 @@ int estimate_is_bottom(const struct estimate *e,
253253
return 0;
254254
}
255255

256+
int estimate_finset_value(const struct quantity_def *qd,
257+
const struct estimate *e, unsigned long *out) {
258+
if (qd->lattice != LK_FINSET)
259+
return 0;
260+
unsigned long mask =
261+
e->lo; /* live-candidate bitmask, one bit per candidate */
262+
if (mask == 0 || (mask & (mask - 1)) != 0)
263+
return 0; /* zero, or more than one, candidate still live */
264+
for (int i = 0; i < qd->n_candidates; i++) {
265+
if (mask == (1ul << i)) {
266+
*out = qd->candidates[i];
267+
return 1;
268+
}
269+
}
270+
return 0;
271+
}
272+
256273
/* ------------------------------------------------------------------------
257274
* Greedy resolver.
258275
* ------------------------------------------------------------------------ */

src/include/kasld/estimate.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ void estimate_meet(struct estimate *e, const struct quantity_def *qd,
8383
/* True iff the estimate is lattice-bottom (unsatisfiable / empty). */
8484
int estimate_is_bottom(const struct estimate *e, const struct quantity_def *qd);
8585

86+
/* If a finite-set (LK_FINSET) quantity's estimate has narrowed to exactly one
87+
* live candidate, write its value to *out and return 1. Returns 0 when the
88+
* quantity is not LK_FINSET, or zero / more than one candidate is still live.
89+
* Encapsulates the live-candidate bitmask so callers (e.g. rules reading a
90+
* resolved Q_VA_BITS) don't depend on the LK_FINSET representation. */
91+
int estimate_finset_value(const struct quantity_def *qd,
92+
const struct estimate *e, unsigned long *out);
93+
8694
/* Resolve quantity q over the constraints in cs[0..n_cs), considering only
8795
* those with conf >= floor. Greedy strongest-first (conf DESC, lineage_count
8896
* DESC, id ASC); a constraint that would force bottom is skipped and

src/rules/directmap_kaslr_disabled_pin.c

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@
1717
// KASAN overrides it at runtime — common on syzkaller / CTF / debug kernels.
1818
//
1919
// 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
20+
// (L4 = VA 48, L5 = VA 57). The level comes from SF_VIRT_ADDR_BITS
21+
// (proc_cpuinfo's "bits virtual", which tracks the active level — leak-free, so
22+
// the pin can fire without any direct-map leak), falling back to a resolved
23+
// Q_VA_BITS (e.g. x86_64_la57_from_directmap, from a directmap leak's top bits)
24+
// when cpuinfo is unavailable. CONFIG_RANDOMIZE_BASE is independent of the
25+
// memory randomisation, so kernel TEXT stays randomised; this pins only the
2426
// direct-map side.
2527
//
2628
// Soundness:
2729
// * Fires only on a positive disable signal AND a resolved VA width.
2830
// * 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).
31+
// resolved level — no window-containment read is needed, and an
32+
// out-of-window C_EQUALS is dropped by the engine's meet as a conflict.
33+
// * The only estimate read is est[Q_VA_BITS] (the level): cross-quantity, not
34+
// a self-edge on the bases written here, and acyclic — Q_VA_BITS derives
35+
// from the directmap observation, never from est[Q_PAGE_OFFSET].
3236
// * A higher-confidence real direct-map leak still wins via the resolver's
3337
// conflict handling.
3438
// ---
@@ -41,7 +45,6 @@
4145
int rule_directmap_kaslr_disabled_pin(const struct evidence_set *ev,
4246
const struct estimate *est,
4347
struct constraint *out, int out_max) {
44-
(void)est;
4548
#if defined(__x86_64__) || defined(__amd64__)
4649
uint32_t sig_id = 0, va_id = 0;
4750
enum kasld_confidence sig_conf = CONF_UNKNOWN, va_conf = CONF_UNKNOWN;
@@ -63,7 +66,18 @@ int rule_directmap_kaslr_disabled_pin(const struct evidence_set *ev,
6366
va_conf = o->conf;
6467
}
6568
}
66-
if (sig_id == 0 || va_bits == 0)
69+
if (sig_id == 0)
70+
return 0;
71+
72+
/* Paging level (L4 = VA 48 / L5 = VA 57): prefer the leak-free cpuinfo scalar
73+
* (SF_VIRT_ADDR_BITS); fall back to a resolved Q_VA_BITS — e.g.
74+
* x86_64_la57_from_directmap pins it from a directmap leak's top bits when
75+
* cpuinfo is unavailable. Reading est[Q_VA_BITS] is cross-quantity (not a
76+
* self-edge on the bases this rule writes) and acyclic — Q_VA_BITS derives
77+
* from the directmap observation, never from est[Q_PAGE_OFFSET]. */
78+
if (va_bits == 0)
79+
estimate_finset_value(&quantities[Q_VA_BITS], &est[Q_VA_BITS], &va_bits);
80+
if (va_bits == 0)
6781
return 0;
6882

6983
/* Active paging level: 48-bit VA -> 4-level (L4), 57-bit -> 5-level (L5). */
@@ -84,7 +98,11 @@ int rule_directmap_kaslr_disabled_pin(const struct evidence_set *ev,
8498
{Q_VMEMMAP_BASE, l5 ? VMEMMAP_BASE_L5 : VMEMMAP_BASE_L4},
8599
};
86100

87-
enum kasld_confidence conf = (sig_conf < va_conf) ? sig_conf : va_conf;
101+
/* When the level came from the cpuinfo scalar, corroborate with its
102+
* confidence + id; when it came from the already-resolved estimate there is
103+
* no observation to cite, so the pin rests on the disable signal alone. */
104+
enum kasld_confidence conf =
105+
va_id ? ((sig_conf < va_conf) ? sig_conf : va_conf) : sig_conf;
88106
int n = 0;
89107
for (int k = 0; k < 3 && n < out_max; k++) {
90108
struct constraint *c = &out[n++];
@@ -95,12 +113,13 @@ int rule_directmap_kaslr_disabled_pin(const struct evidence_set *ev,
95113
c->conf = conf;
96114
c->derived_from[0] = sig_id;
97115
c->derived_from[1] = va_id;
98-
c->lineage_count = 2;
116+
c->lineage_count = va_id ? 2 : 1;
99117
snprintf(c->origin, ORIGIN_LEN, "directmap_kaslr_disabled_pin");
100118
}
101119
return n;
102120
#else
103121
(void)ev;
122+
(void)est;
104123
(void)out;
105124
(void)out_max;
106125
return 0;

tests/test_engine.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3190,6 +3190,23 @@ static void test_directmap_kaslr_disabled_pin(void) {
31903190
engine_run(&e4, rules, 1);
31913191
assert(e4.est[Q_PAGE_OFFSET].lo == top.lo &&
31923192
e4.est[Q_PAGE_OFFSET].hi == top.hi);
3193+
3194+
/* Fallback path: NO SF_VIRT_ADDR_BITS, but a directmap leak below the L4 VAS
3195+
* floor resolves Q_VA_BITS=57 via la57_from_directmap; the pin then takes the
3196+
* level from the estimate and still fires (here -> the L5 default). */
3197+
struct engine e5;
3198+
engine_init(&e5);
3199+
struct observation k5 = mk_scalar(SF_KASAN_ENABLED, 1, CONF_PARSED);
3200+
evidence_add(&e5.ev, &k5);
3201+
struct observation dm =
3202+
mk_obs(KASLD_TYPE_VIRT, REGION_DIRECTMAP, 0xff20000000000000ul,
3203+
LO_SET | SAMPLE_SET, POS_INTERIOR, CONF_PARSED);
3204+
evidence_add(&e5.ev, &dm);
3205+
const rule_fn rules_la57[] = {rule_x86_64_la57_from_directmap,
3206+
rule_directmap_kaslr_disabled_pin};
3207+
engine_run(&e5, rules_la57, 2);
3208+
assert(e5.est[Q_PAGE_OFFSET].lo == PAGE_OFFSET_BASE_L5 &&
3209+
e5.est[Q_PAGE_OFFSET].hi == PAGE_OFFSET_BASE_L5);
31933210
#endif
31943211
}
31953212

0 commit comments

Comments
 (0)