Skip to content

Commit 4aa8d27

Browse files
committed
engine: project candidate counts, not a second way to count them
compute_kaslr_info() reached across the engine/summary seam to count how many candidates a quantity admits, and under -DKASLD_TESTING that reach was replaced by hand-rolled range arithmetic read off layout. Nothing exercised the substitute; the unit tests asserted on none of the fields it wrote. They were exercising a summary function no shipped binary contains. engine_sync_authoritative() now projects six counts alongside the windows it already wrote -- both image bases, the direct-map base, vmalloc, vmemmap and the module base -- from the same estimates at the same moment, at the engine-resolved alignments. Every #else substitution goes with it: 33 KASLD_TESTING references drop to 24, and 13 blocks inside compute_kaslr_info() to 5. The five that remain are omissions rather than substitutions, so the testing build now runs less of the summary path instead of a different one. quantity_top_slots() turned out to need no engine state at all, reading only the quantity table and the sound floor, so it and its call sites compile in every build. Seeding layout directly is why the tests carried over unchanged, and why they proved nothing. Counting at PAGE_SIZE rather than the resolved KASLR granularity, dropping the constraint set so interior holes survive, or carving at CONF_BRUTE so a heuristic claim narrows the GUARANTEED window each left the whole suite and every replay fixture passing. The alignment mistake reports this host at 241153 slots and 18 bits against a true 472 and 9 -- the headline number doubles its claimed entropy and nothing objects. Replay is crash coverage by construction, so it established that the seam runs on 13 arches, never what it says. test_engine_sync_projects_slot_counts is a counting contract rather than an edge contract: one fixture whose window, resolved alignment, proven point exclude and sub-floor span exclude land all three mistakes on different totals, so a single exact assertion separates them. It kills all three, and deleting the phys count, and passes on every cross target, its literals deriving from the fixture rather than the host.
1 parent 0679257 commit 4aa8d27

3 files changed

Lines changed: 168 additions & 91 deletions

File tree

src/include/kasld/internal.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,28 @@ struct kasld_layout {
119119
* the region's origin. */
120120
unsigned long virt_module_base_min;
121121
unsigned long virt_module_base_max;
122+
123+
/* Candidate counts, projected beside the windows they belong to.
124+
*
125+
* How many placements a quantity still admits is not (hi - lo) / align: the
126+
* estimate's interior C_EXCLUDE holes are carved at READ time, so only
127+
* quantity_slots() over the estimate AND its constraint set can answer it. A
128+
* flat division is hole-blind and over-counts.
129+
*
130+
* They live here because that answer needs the engine, and the summary
131+
* builder is a consumer of this struct rather than of the engine. Projecting
132+
* them at the same moment as the window they count keeps the two describing
133+
* one resolution -- and leaves no computation for a build without an engine
134+
* to substitute a second, hole-blind definition for.
135+
*
136+
* Zero means "no count resolved", which is also the value a caller that seeds
137+
* this struct directly gets without asking. */
138+
unsigned long virt_kaslr_slots;
139+
unsigned long phys_kaslr_slots;
140+
unsigned long virt_page_offset_slots;
141+
unsigned long virt_vmalloc_slots;
142+
unsigned long virt_vmemmap_slots;
143+
unsigned long virt_module_slots;
122144
};
123145

124146
/* =========================================================================

src/orchestrator.c

Lines changed: 67 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2905,23 +2905,23 @@ static int ilog2(unsigned long v) {
29052905
* projection with no engine dependencies); engine_resolve and its engine
29062906
* instance are engine-only (they drive the components + engine.c machinery). */
29072907
static void engine_sync_authoritative(const struct engine *e);
2908-
#ifndef KASLD_TESTING
2909-
static void engine_resolve(struct engine *e);
2910-
static struct engine
2911-
g_auth_engine; /* holds the GUARANTEED (primary) resolution */
29122908

29132909
/* Sound floor for the guaranteed window: inputs below this are out of scope, so
29142910
* the window is derived purely from >= floor signals. CONF_INFERRED admits
29152911
* parsed/derived/inferred (proven); heuristic/timing/brute reach the likely
29162912
* window only. The two-window POLICY (which floors, what they mean) lives here;
2917-
* the engine is floor-agnostic. */
2913+
* the engine is floor-agnostic.
2914+
*
2915+
* Outside the engine-only block: this names a confidence level and has no
2916+
* engine dependency, and the projection above -- which every build compiles --
2917+
* counts candidates at this floor. */
29182918
#define KASLD_SOUND_FLOOR CONF_INFERRED
29192919

2920-
#ifndef KASLD_TESTING
29212920
/* Candidate count over q's honest compile-time top — the entropy this
29222921
* architecture's KASLR had before any evidence narrowed it. Counted through
29232922
* quantity_slots() at the same alignment as the residual, so the two are
2924-
* directly comparable. */
2923+
* directly comparable. Its inputs are the quantity table and the sound floor;
2924+
* no engine state, so every build has it. */
29252925
static unsigned long quantity_top_slots(enum kasld_quantity q,
29262926
unsigned long align) {
29272927
if (!align || !quantities[q].init_top)
@@ -2930,7 +2930,11 @@ static unsigned long quantity_top_slots(enum kasld_quantity q,
29302930
quantities[q].init_top(&top);
29312931
return quantity_slots(q, &top, KASLD_SOUND_FLOOR, NULL, 0, align);
29322932
}
2933-
#endif
2933+
2934+
#ifndef KASLD_TESTING
2935+
static void engine_resolve(struct engine *e);
2936+
static struct engine
2937+
g_auth_engine; /* holds the GUARANTEED (primary) resolution */
29342938

29352939
/* Snapshot of the LIKELY resolution (floor CONF_BRUTE — all signals): the est +
29362940
* constraints quantity_slots() needs, plus the resolver's rejected-constraint
@@ -3100,27 +3104,12 @@ void compute_kaslr_info(struct summary *s) {
31003104
s->kaslr.has_phys = 0;
31013105
s->kaslr.pstext = observed_stext_base(KASLD_TYPE_PHYS, ptext);
31023106

3103-
/* Hole-aware slot count: route via quantity_slots() so interior C_EXCLUDE
3104-
* holes and any C_STRIDE residue class are reflected in the headline entropy
3105-
* number. Flat (hi-lo)/align is the no-constraints fallback for KASLD_TESTING
3106-
* builds (the engine instance is compiled out there). */
3107-
#ifndef KASLD_TESTING
3108-
s->kaslr.vslots =
3109-
quantity_slots(Q_VIRT_IMAGE_BASE, &g_auth_engine.est[Q_VIRT_IMAGE_BASE],
3110-
KASLD_SOUND_FLOOR, g_auth_engine.constraints,
3111-
g_auth_engine.n_constraints, layout.virt_kaslr_align);
3112-
#else
3113-
{
3114-
unsigned long text_range =
3115-
layout.virt_kaslr_text_max - layout.virt_kaslr_text_min;
3116-
/* Closed window: + 1 counts the floor slot, matching quantity_slots(). */
3117-
s->kaslr.vslots = (layout.virt_kaslr_align && layout.virt_kaslr_text_max)
3118-
? text_range / layout.virt_kaslr_align + 1
3119-
: 0;
3120-
}
3121-
#endif
3107+
/* Hole-aware slot counts, projected beside the windows they count (see
3108+
* struct kasld_layout). Interior C_EXCLUDE holes and any C_STRIDE residue
3109+
* class are already reflected, so the headline entropy follows the estimate
3110+
* rather than the width of its convex hull. */
3111+
s->kaslr.vslots = layout.virt_kaslr_slots;
31223112
s->kaslr.vbits = s->kaslr.vslots > 0 ? ilog2(s->kaslr.vslots) : 0;
3123-
#ifndef KASLD_TESTING
31243113
{
31253114
/* The starting candidate count, from the window the kernel draws the image
31263115
* base from -- KASLR_VIRT_TEXT_MIN..MAX, not the quantity's honest top.
@@ -3141,31 +3130,15 @@ void compute_kaslr_info(struct summary *s) {
31413130
unsigned long top = (a && hi > lo) ? (hi - lo) / a + 1 : 0;
31423131
s->kaslr.vtop_slots = top;
31433132
s->kaslr.vbits_top = top > 0 ? ilog2(top) : 0;
3144-
#ifndef KASLD_TESTING
31453133
s->kaslr.varch_slots = quantity_top_slots(Q_VIRT_IMAGE_BASE, a);
3146-
#endif
31473134
}
3148-
#endif
31493135

31503136
#ifdef KASLR_PHYS_MIN
31513137
{
3152-
#ifndef KASLD_TESTING
3153-
s->kaslr.pslots =
3154-
quantity_slots(Q_PHYS_IMAGE_BASE, &g_auth_engine.est[Q_PHYS_IMAGE_BASE],
3155-
KASLD_SOUND_FLOOR, g_auth_engine.constraints,
3156-
g_auth_engine.n_constraints, layout.phys_kaslr_align);
3157-
#else
3158-
unsigned long phys_range =
3159-
layout.phys_kaslr_text_max - layout.phys_kaslr_text_min;
3160-
s->kaslr.pslots = (layout.phys_kaslr_align && layout.phys_kaslr_text_max)
3161-
? phys_range / layout.phys_kaslr_align + 1
3162-
: 0;
3163-
#endif
3138+
s->kaslr.pslots = layout.phys_kaslr_slots;
31643139
s->kaslr.pbits = s->kaslr.pslots > 0 ? ilog2(s->kaslr.pslots) : 0;
3165-
#ifndef KASLD_TESTING
31663140
s->kaslr.parch_slots =
31673141
quantity_top_slots(Q_PHYS_IMAGE_BASE, layout.phys_kaslr_align);
3168-
#endif
31693142
}
31703143
#endif
31713144

@@ -3280,64 +3253,28 @@ void compute_kaslr_info(struct summary *s) {
32803253
? layout.virt_module_base_max
32813254
: 0;
32823255

3283-
/* Hole-aware residual slot counts for the memory-KASLR regions, mirroring the
3284-
* headline vslots/pslots: routed through quantity_slots() so interior
3285-
* C_EXCLUDE holes (and any stride class) are reflected in the entropy the
3286-
* renderer prints, rather than a hole-blind (max-min)/align. Flat division is
3287-
* the KASLD_TESTING fallback (the engine instance is compiled out there).
3288-
* Only the both-sided window displays a slot count, so gate on min && max. */
3289-
#ifndef KASLD_TESTING
3256+
/* Residual slot counts for the memory-KASLR regions, mirroring the headline
3257+
* vslots/pslots: the projected count already reflects interior C_EXCLUDE
3258+
* holes and any stride class, so the entropy the renderer prints follows the
3259+
* estimate rather than the width of its hull. Only a both-sided window
3260+
* displays a count, so gate on min && max -- a presentation rule, asked here
3261+
* where those edges are in hand. */
32903262
s->kaslr.virt_page_offset_slots =
32913263
(s->kaslr.virt_page_offset_min && s->kaslr.virt_page_offset_max)
3292-
? quantity_slots(Q_PAGE_OFFSET, &g_auth_engine.est[Q_PAGE_OFFSET],
3293-
KASLD_SOUND_FLOOR, g_auth_engine.constraints,
3294-
g_auth_engine.n_constraints, RANDOMIZE_MEMORY_ALIGN)
3264+
? layout.virt_page_offset_slots
32953265
: 0;
32963266
s->kaslr.virt_vmalloc_slots =
32973267
(s->kaslr.virt_vmalloc_min && s->kaslr.virt_vmalloc_max)
3298-
? quantity_slots(Q_VMALLOC_BASE, &g_auth_engine.est[Q_VMALLOC_BASE],
3299-
KASLD_SOUND_FLOOR, g_auth_engine.constraints,
3300-
g_auth_engine.n_constraints, RANDOMIZE_MEMORY_ALIGN)
3268+
? layout.virt_vmalloc_slots
33013269
: 0;
33023270
s->kaslr.virt_vmemmap_slots =
33033271
(s->kaslr.virt_vmemmap_min && s->kaslr.virt_vmemmap_max)
3304-
? quantity_slots(Q_VMEMMAP_BASE, &g_auth_engine.est[Q_VMEMMAP_BASE],
3305-
KASLD_SOUND_FLOOR, g_auth_engine.constraints,
3306-
g_auth_engine.n_constraints, RANDOMIZE_MEMORY_ALIGN)
3272+
? layout.virt_vmemmap_slots
33073273
: 0;
3308-
/* The module base is page-granular on every arch that randomizes it (x86_64
3309-
* draws a whole number of pages; the arm64 bounding box and the
3310-
* PAGE_OFFSET-derived bands are page-aligned), so PAGE_SIZE is the pitch
3311-
* rather than RANDOMIZE_MEMORY_ALIGN, which is an x86_64 memory-KASLR
3312-
* constant and 0 elsewhere. */
33133274
s->kaslr.virt_module_slots =
33143275
(s->kaslr.virt_module_min && s->kaslr.virt_module_max)
3315-
? quantity_slots(Q_MODULE_BASE, &g_auth_engine.est[Q_MODULE_BASE],
3316-
KASLD_SOUND_FLOOR, g_auth_engine.constraints,
3317-
g_auth_engine.n_constraints, PAGE_SIZE)
3276+
? layout.virt_module_slots
33183277
: 0;
3319-
#else
3320-
{
3321-
unsigned long a = (unsigned long)RANDOMIZE_MEMORY_ALIGN;
3322-
s->kaslr.virt_page_offset_slots =
3323-
(a && s->kaslr.virt_page_offset_max > s->kaslr.virt_page_offset_min)
3324-
? (s->kaslr.virt_page_offset_max - s->kaslr.virt_page_offset_min) /
3325-
a
3326-
: 0;
3327-
s->kaslr.virt_vmalloc_slots =
3328-
(a && s->kaslr.virt_vmalloc_max > s->kaslr.virt_vmalloc_min)
3329-
? (s->kaslr.virt_vmalloc_max - s->kaslr.virt_vmalloc_min) / a
3330-
: 0;
3331-
s->kaslr.virt_vmemmap_slots =
3332-
(a && s->kaslr.virt_vmemmap_max > s->kaslr.virt_vmemmap_min)
3333-
? (s->kaslr.virt_vmemmap_max - s->kaslr.virt_vmemmap_min) / a
3334-
: 0;
3335-
s->kaslr.virt_module_slots =
3336-
(s->kaslr.virt_module_max > s->kaslr.virt_module_min)
3337-
? (s->kaslr.virt_module_max - s->kaslr.virt_module_min) / PAGE_SIZE
3338-
: 0;
3339-
}
3340-
#endif
33413278
s->kaslr.virt_page_offset_bits = s->kaslr.virt_page_offset_slots > 0
33423279
? ilog2(s->kaslr.virt_page_offset_slots)
33433280
: 0;
@@ -4358,6 +4295,45 @@ static void engine_sync_authoritative(const struct engine *e) {
43584295
layout.modules_end = obs_hi;
43594296
}
43604297
}
4298+
4299+
/* Candidate counts, last: every window and alignment this reads is final by
4300+
* here, and the count must be taken from the same resolution as the window it
4301+
* counts. quantity_slots() is the only answer that carves the estimate's
4302+
* interior C_EXCLUDE holes; the alignments are the engine-resolved ones
4303+
* projected above, not the compile-time defaults.
4304+
*
4305+
* Counted unconditionally. Whether a count is worth PRESENTING is a question
4306+
* about the window's shape -- both edges known, and so on -- which the
4307+
* summary builder asks where those edges are in hand. Gating here instead
4308+
* would put a presentation rule in the projection and answer it from fields
4309+
* that do not exist yet. */
4310+
layout.virt_kaslr_slots = quantity_slots(
4311+
Q_VIRT_IMAGE_BASE, &e->est[Q_VIRT_IMAGE_BASE], KASLD_SOUND_FLOOR,
4312+
e->constraints, e->n_constraints, layout.virt_kaslr_align);
4313+
/* Both image-base counts are taken here, on every arch. The phys window is
4314+
* projected under !TEXT_TRACKS_DIRECTMAP above because only a decoupled arch
4315+
* resolves it separately; the count is still meaningful on a coupled arch,
4316+
* where the estimate carries the locked-to-virt window. */
4317+
layout.phys_kaslr_slots = quantity_slots(
4318+
Q_PHYS_IMAGE_BASE, &e->est[Q_PHYS_IMAGE_BASE], KASLD_SOUND_FLOOR,
4319+
e->constraints, e->n_constraints, layout.phys_kaslr_align);
4320+
layout.virt_page_offset_slots =
4321+
quantity_slots(Q_PAGE_OFFSET, &e->est[Q_PAGE_OFFSET], KASLD_SOUND_FLOOR,
4322+
e->constraints, e->n_constraints, RANDOMIZE_MEMORY_ALIGN);
4323+
layout.virt_vmalloc_slots =
4324+
quantity_slots(Q_VMALLOC_BASE, &e->est[Q_VMALLOC_BASE], KASLD_SOUND_FLOOR,
4325+
e->constraints, e->n_constraints, RANDOMIZE_MEMORY_ALIGN);
4326+
layout.virt_vmemmap_slots =
4327+
quantity_slots(Q_VMEMMAP_BASE, &e->est[Q_VMEMMAP_BASE], KASLD_SOUND_FLOOR,
4328+
e->constraints, e->n_constraints, RANDOMIZE_MEMORY_ALIGN);
4329+
/* The module base is page-granular on every arch that randomizes it (x86_64
4330+
* draws a whole number of pages; the arm64 bounding box and the
4331+
* PAGE_OFFSET-derived bands are page-aligned), so PAGE_SIZE is the pitch
4332+
* rather than RANDOMIZE_MEMORY_ALIGN, which is an x86_64 memory-KASLR
4333+
* constant and 0 elsewhere. */
4334+
layout.virt_module_slots =
4335+
quantity_slots(Q_MODULE_BASE, &e->est[Q_MODULE_BASE], KASLD_SOUND_FLOOR,
4336+
e->constraints, e->n_constraints, PAGE_SIZE);
43614337
}
43624338
#ifndef KASLD_TESTING
43634339

tests/test_kasld.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,6 +1877,84 @@ static void test_engine_sync_projects_all_fields(void) {
18771877
assert(layout.virt_kernel_vas_start == vas_floor_before);
18781878
}
18791879

1880+
/* Contract test for the candidate COUNTS engine_sync_authoritative() projects.
1881+
* Separate from the edge test above because a count answers a different
1882+
* question than a window edge: not "did the field get written" but "was it
1883+
* counted at the resolved alignment, over evidence at the sound floor, with
1884+
* interior holes removed".
1885+
*
1886+
* Those three are independent, and each has a wrong answer that a window
1887+
* assertion cannot see -- counting at PAGE_SIZE rather than the resolved
1888+
* KASLR granularity, ignoring the constraint set so interior holes survive, or
1889+
* carving at CONF_BRUTE so a heuristic claim narrows the GUARANTEED window.
1890+
* The fixture is built so all three land on different totals and one exact
1891+
* assertion separates them. */
1892+
static void test_engine_sync_projects_slot_counts(void) {
1893+
struct engine e;
1894+
memset(&e, 0, sizeof(e));
1895+
1896+
e.est[Q_VIRT_IMAGE_BASE].lo = FX_TEXT;
1897+
e.est[Q_VIRT_IMAGE_BASE].hi = FX_TEXT + 0x0e000000ul;
1898+
e.est[Q_VIRT_KASLR_ALIGN].lo = 0x200000ul;
1899+
e.est[Q_PHYS_IMAGE_BASE].lo = 0x4000000ul;
1900+
e.est[Q_PHYS_IMAGE_BASE].hi = 0x3c000000ul;
1901+
e.est[Q_PHYS_KASLR_ALIGN].lo = 0x200000ul;
1902+
e.est[Q_VMALLOC_BASE].lo = (unsigned long)PAGE_OFFSET + 0x11000000ul;
1903+
e.est[Q_VMALLOC_BASE].lo_binding = 1;
1904+
e.est[Q_VMALLOC_BASE].hi = (unsigned long)PAGE_OFFSET + 0x12000000ul;
1905+
e.est[Q_VMALLOC_BASE].hi_binding = 1;
1906+
1907+
/* A proven hole over one aligned candidate. Excluding a single point rather
1908+
* than a span is what shows up in the total: splitting a range removes the
1909+
* excluded candidate but the new range contributes its own floor, so a
1910+
* point exclude nets exactly -1 while a span nets its own width. */
1911+
e.constraints[0].q = Q_VIRT_IMAGE_BASE;
1912+
e.constraints[0].op = C_EXCLUDE;
1913+
e.constraints[0].value = FX_TEXT + 0x02000000ul;
1914+
e.constraints[0].value2 = FX_TEXT + 0x02000000ul;
1915+
e.constraints[0].conf = CONF_PARSED;
1916+
e.constraints[0].id = 1;
1917+
1918+
/* A heuristic hole below the sound floor: the guaranteed count must ignore
1919+
* it. Deliberately wide -- a below-floor hole that happened to leave the
1920+
* total unchanged would make the assertion pass for the wrong reason. */
1921+
e.constraints[1].q = Q_VIRT_IMAGE_BASE;
1922+
e.constraints[1].op = C_EXCLUDE;
1923+
e.constraints[1].value = FX_TEXT + 0x04000000ul;
1924+
e.constraints[1].value2 = FX_TEXT + 0x06000000ul - 1;
1925+
e.constraints[1].conf = CONF_HEURISTIC;
1926+
e.constraints[1].id = 2;
1927+
e.n_constraints = 2;
1928+
1929+
layout.virt_kaslr_align = 0;
1930+
layout.virt_kaslr_slots = 0;
1931+
layout.phys_kaslr_slots = 0;
1932+
layout.virt_vmalloc_slots = 0;
1933+
1934+
engine_sync_authoritative(&e);
1935+
1936+
/* 0x0e000000 of window at 2 MiB granularity is 112 whole strides plus the
1937+
* floor itself, less the one proven hole. Counting at PAGE_SIZE, dropping
1938+
* the constraint set, or carving at CONF_BRUTE each lands elsewhere. */
1939+
assert(layout.virt_kaslr_align == 0x200000ul);
1940+
assert(layout.virt_kaslr_slots == 112);
1941+
1942+
/* Taken on every arch, not only the decoupled ones that resolve a separate
1943+
* phys window: 0x38000000 at the same granularity, no holes. The align a
1944+
* coupled arch mirrors from the virt side is the same 2 MiB. */
1945+
assert(layout.phys_kaslr_slots == 449);
1946+
1947+
#if RANDOMIZE_MEMORY_ALIGN
1948+
/* Memory KASLR moves the direct map, vmalloc and vmemmap bases on a coarser
1949+
* pitch than the image base: 16 MiB of window at that pitch is the floor and
1950+
* nothing above it. */
1951+
assert(layout.virt_vmalloc_slots == 0x1000000ul / RANDOMIZE_MEMORY_ALIGN + 1);
1952+
#else
1953+
/* No memory KASLR here, so no pitch to count on. */
1954+
assert(layout.virt_vmalloc_slots == 0);
1955+
#endif
1956+
}
1957+
18801958
/* engine_sync_authoritative tightens layout.modules_start/end from observed
18811959
* VIRT/REGION_MODULE_BAND addresses (when inside the validation union),
18821960
* so the rendered band reflects the actual runtime module range rather than
@@ -2337,6 +2415,7 @@ int main(void) {
23372415

23382416
BEGIN_CATEGORY("engine_sync_authoritative");
23392417
RUN(test_engine_sync_projects_all_fields);
2418+
RUN(test_engine_sync_projects_slot_counts);
23402419
RUN(test_engine_sync_anchors_module_band_to_observations);
23412420
RUN(test_engine_sync_module_band_rejects_out_of_union);
23422421
RUN(test_engine_sync_module_band_never_degenerate);

0 commit comments

Comments
 (0)