Skip to content

Commit e7a4e33

Browse files
committed
render: promote a concrete likely direct-map base to a headline
When a POS_BASE timing pin (prefetch_directmap) narrows the direct-map likely window to a single-slot bracket at the base, show that base as a graded "likely (speculative)" headline with the guaranteed window beneath — the same form as the image bases — instead of a "not derandomized" bound row with a dim likely sub-line. A wider likely window, or an interior-only run, still renders as the bounded window.
1 parent 5ac29f3 commit e7a4e33

2 files changed

Lines changed: 148 additions & 34 deletions

File tree

src/render/text.c

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
#include <sys/utsname.h>
2020
#include <unistd.h>
2121

22+
/* Un-randomized direct-map base — the reference for the direct map's
23+
* RANDOMIZE_MEMORY offset in the readout. x86_64-only; 0 elsewhere, where the
24+
* direct-map base promotion that uses it never fires (gated on
25+
* RANDOMIZE_MEMORY_ALIGN > 0). */
26+
#ifndef PAGE_OFFSET_BASE_L4
27+
#define PAGE_OFFSET_BASE_L4 0ul
28+
#endif
29+
2230
/* Group key for "already printed" tracking. Sections are short, fixed
2331
* strings from region_info[].section_name — copy by pointer (those are
2432
* static literals owned by region_info.c). */
@@ -1170,24 +1178,25 @@ static void readout_guaranteed_window_row(unsigned long lo, unsigned long hi,
11701178
"", lo, hi, c(C_DIM), c(C_RESET), bits, slots);
11711179
}
11721180

1173-
/* A concrete LIKELY base: the best-guess address, its slide (base - default,
1174-
* inheriting the point's speculative grade), and the "likely (speculative)"
1175-
* label. The label is right-padded so it lands in the same column as
1176-
* "guaranteed" on the window row that follows: both rows share the
1177-
* " <label> 0x<addr>" prefix, and this row's " slide " runs 15 columns short
1178-
* of where the window row's " - 0x<addr> " puts "guaranteed", so pad the
1179-
* slide value out to width 15. */
1181+
/* A concrete LIKELY base graded "likely (speculative)", annotated with its
1182+
* displacement from the un-randomized base: `disp` labelled `disp_label`
1183+
* ("slide" for the kernel image bases; "off" for the direct map's independent
1184+
* RANDOMIZE_MEMORY offset). The " <label> <value>" field is a fixed 24
1185+
* visible columns (the value padded to fill), so "likely (speculative)" lands
1186+
* in the column shared with "guaranteed" on the window row beneath — both rows
1187+
* share the " <label> 0x<addr>" prefix, and 24 columns matches where the
1188+
* window row's " - 0x<addr> " puts "guaranteed". */
11801189
static void readout_likely_base_row(const char *label, unsigned long base,
1181-
long slide) {
1182-
unsigned long mag = slide < 0 ? (unsigned long)-slide : (unsigned long)slide;
1183-
char sb[24];
1184-
int sn = snprintf(sb, sizeof(sb), "%s0x%lx", slide < 0 ? "-" : "+", mag);
1185-
int pad = 15 - sn;
1190+
const char *disp_label, long disp) {
1191+
unsigned long mag = disp < 0 ? (unsigned long)-disp : (unsigned long)disp;
1192+
char vb[24];
1193+
int vn = snprintf(vb, sizeof(vb), "%s0x%lx", disp < 0 ? "-" : "+", mag);
1194+
int pad = 20 - (int)strlen(disp_label) - vn; /* " <label> " + value == 24 */
11861195
if (pad < 1)
1187-
pad = 1; /* keep one space if the slide is unexpectedly wide */
1188-
printf(" %-19s %s0x%016lx%s slide %s%s%s%*s%slikely (speculative)%s\n",
1189-
label, c(C_GREEN), base, c(C_RESET), c(C_CYAN), sb, c(C_RESET), pad,
1190-
"", c(C_DIM), c(C_RESET));
1196+
pad = 1; /* keep one space if the value is unexpectedly wide */
1197+
printf(" %-19s %s0x%016lx%s %s %s%s%s%*s%slikely (speculative)%s\n", label,
1198+
c(C_GREEN), base, c(C_RESET), disp_label, c(C_CYAN), vb, c(C_RESET),
1199+
pad, "", c(C_DIM), c(C_RESET));
11911200
}
11921201

11931202
static void render_readout(const struct summary *s) {
@@ -1269,7 +1278,7 @@ static void render_readout(const struct summary *s) {
12691278
} else if (v_likely_base) {
12701279
/* Concrete base + its slide, both graded likely; the proven window follows
12711280
* on the guaranteed row beneath. */
1272-
readout_likely_base_row("Virtual image base", s->kaslr.vtext,
1281+
readout_likely_base_row("Virtual image base", s->kaslr.vtext, "slide",
12731282
s->kaslr.vslide);
12741283
if (s->kaslr.vstext && s->kaslr.vstext != s->kaslr.vtext)
12751284
printf(" %-19s %s0x%016lx%s %slikely%s\n", "Virtual _stext",
@@ -1295,7 +1304,7 @@ static void render_readout(const struct summary *s) {
12951304
printf(" %-19s %s0x%016lx%s\n", "Physical _stext", c(C_GREEN),
12961305
s->kaslr.pstext, c(C_RESET));
12971306
} else if (p_likely_base) {
1298-
readout_likely_base_row("Physical image base", s->kaslr.ptext,
1307+
readout_likely_base_row("Physical image base", s->kaslr.ptext, "slide",
12991308
s->kaslr.pslide);
13001309
if (s->kaslr.pstext && s->kaslr.pstext != s->kaslr.ptext)
13011310
printf(" %-19s %s0x%016lx%s %slikely%s\n", "Physical _stext",
@@ -1320,28 +1329,52 @@ static void render_readout(const struct summary *s) {
13201329
{
13211330
unsigned long lo = s->kaslr.virt_page_offset_min;
13221331
unsigned long hi = s->kaslr.virt_page_offset_max;
1323-
if (lo && hi && hi >= lo) {
1324-
unsigned long slots = s->kaslr.virt_page_offset_slots;
1325-
int bits = slots > 0 ? ilog2_ul(slots) : 0;
1326-
readout_bound_row("Direct map base", lo, hi, slots, bits,
1327-
(unsigned long)RANDOMIZE_MEMORY_ALIGN);
1328-
} else if (lo && !hi) {
1329-
printf(" %-19s >= %s0x%016lx%s\n", "Direct map base", c(C_CYAN), lo,
1330-
c(C_RESET));
1331-
} else if (!lo && hi) {
1332-
printf(" %-19s <= %s0x%016lx%s\n", "Direct map base", c(C_CYAN), hi,
1333-
c(C_RESET));
1332+
unsigned long llo = s->kaslr.virt_page_offset_likely_min;
1333+
unsigned long lhi = s->kaslr.virt_page_offset_likely_max;
1334+
unsigned long align = (unsigned long)RANDOMIZE_MEMORY_ALIGN;
1335+
unsigned long slots = s->kaslr.virt_page_offset_slots;
1336+
int bits = slots > 0 ? ilog2_ul(slots) : 0;
1337+
/* A concrete likely base — a POS_BASE timing pin (prefetch_directmap)
1338+
* narrowed the likely window to a single-slot bracket at the base — is
1339+
* promoted to a graded headline with the guaranteed window beneath, the
1340+
* same form as the image bases. The best-guess base is the bracket's top
1341+
* (lhi); the slide-less row is used since the direct map has no default to
1342+
* slide from. A wider likely narrowing is not a base pin and stays a dim
1343+
* range sub-line under the plain bounded row. */
1344+
int likely_base =
1345+
lo && hi && hi >= lo && lhi && align && (lhi - llo) <= align;
1346+
if (likely_base) {
1347+
readout_likely_base_row("Direct map base", lhi, "off",
1348+
(long)(lhi - PAGE_OFFSET_BASE_L4));
1349+
readout_guaranteed_window_row(lo, hi, slots, bits, align);
1350+
} else {
1351+
if (lo && hi && hi >= lo)
1352+
readout_bound_row("Direct map base", lo, hi, slots, bits, align);
1353+
else if (lo && !hi)
1354+
printf(" %-19s >= %s0x%016lx%s\n", "Direct map base", c(C_CYAN), lo,
1355+
c(C_RESET));
1356+
else if (!lo && hi)
1357+
printf(" %-19s <= %s0x%016lx%s\n", "Direct map base", c(C_CYAN), hi,
1358+
c(C_RESET));
1359+
readout_likely_row(llo, lhi);
13341360
}
1335-
readout_likely_row(s->kaslr.virt_page_offset_likely_min,
1336-
s->kaslr.virt_page_offset_likely_max);
13371361
}
13381362

13391363
/* Coupling closes the bounds table as a single dim line: it is a static
13401364
* arch property (not a measured quantity), so it recedes from the green/
13411365
* magenta measured rows and explains why physical and virtual bases resolve
1342-
* as separate (or shared) quantities above. */
1343-
printf(" %-19s %s%s%s\n", "Phys/Virt coupling", c(C_DIM), coupling_descr(),
1344-
c(C_RESET));
1366+
* as separate (or shared) quantities above. Its job is to relate the physical
1367+
* and virtual text bases, so it only earns its place when there is a physical
1368+
* dimension to relate to: always on coupled arches (where one leak yields
1369+
* both — the exploitation-relevant case), and on decoupled arches only when a
1370+
* physical image base row was actually rendered. Suppressed where no physical
1371+
* base is shown, so it never asserts a relationship the reader can't see. */
1372+
int phys_row_shown = (s->kaslr.has_phys && ppin) || p_likely_base ||
1373+
s->kaslr.pslots > 0 || layout.phys_kaslr_text_min ||
1374+
layout.phys_kaslr_text_max;
1375+
if (TEXT_TRACKS_DIRECTMAP || phys_row_shown)
1376+
printf(" %-19s %s%s%s\n", "Phys/Virt coupling", c(C_DIM), coupling_descr(),
1377+
c(C_RESET));
13451378
printf("\n");
13461379

13471380
readout_print_leaks();

tests/test_render.c

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,85 @@ static void test_render_memory_likely_window(void) {
437437
set_render_mode(0, 0, 0);
438438
}
439439

440+
/* A concrete likely direct-map base — a POS_BASE timing pin
441+
* (prefetch_directmap) narrowed the likely window to a single-slot bracket at
442+
* the base — is promoted in the default readout to a graded headline (the base
443+
* address graded "likely (speculative)") with the guaranteed window beneath it
444+
* ("guaranteed"), the same form as the image bases, rather than a bound row +
445+
* dim sub-line. The "guaranteed" label is unique to that promoted window row,
446+
* so its presence proves the promotion fired. RANDOMIZE_MEMORY arches only
447+
* (align > 0). */
448+
static void test_render_directmap_base_promoted(void) {
449+
#if RANDOMIZE_MEMORY_ALIGN > 0
450+
struct summary s;
451+
reset_results();
452+
num_comp_logs = 0;
453+
num_scalar_facts = 0;
454+
memset(&s, 0, sizeof(s));
455+
456+
s.kaslr.vslots = 60; /* keep the regular KASLR readout path */
457+
s.kaslr.vbits = 6;
458+
/* Base above the un-randomized direct-map base (PAGE_OFFSET_BASE_L4), so the
459+
* displayed offset is the realistic small positive value it always is on a
460+
* live kernel (page_offset_base >= PAGE_OFFSET_BASE_L4). */
461+
unsigned long align = (unsigned long)RANDOMIZE_MEMORY_ALIGN;
462+
unsigned long base = (unsigned long)PAGE_OFFSET_BASE_L4 + 20ul * align;
463+
s.kaslr.virt_page_offset_min = (unsigned long)PAGE_OFFSET_BASE_L4;
464+
s.kaslr.virt_page_offset_max = base; /* guaranteed window top */
465+
s.kaslr.virt_page_offset_slots = 21;
466+
s.kaslr.virt_page_offset_likely_min = base - align; /* one-slot bracket */
467+
s.kaslr.virt_page_offset_likely_max = base; /* best-guess base */
468+
469+
set_render_mode(0, 0, 0); /* default text readout */
470+
capture_stdout(wrap_render_summary, &s);
471+
char hex[32], off[32];
472+
snprintf(hex, sizeof(hex), "0x%016lx", base);
473+
snprintf(off, sizeof(off), "off +0x%lx", 20ul * align); /* base - default */
474+
assert(strstr(render_cap, hex) != NULL); /* headline base */
475+
assert(strstr(render_cap, off) != NULL); /* RM offset */
476+
assert(strstr(render_cap, "likely (speculative)") != NULL); /* graded */
477+
assert(strstr(render_cap, "guaranteed") != NULL); /* window beneath */
478+
set_render_mode(0, 0, 0);
479+
#endif
480+
}
481+
482+
/* The Phys/Virt coupling note relates the physical and virtual text bases. On a
483+
* decoupled arch it is gated on a physical image base row actually being
484+
* present — shown when one is, suppressed when there is nothing to relate to.
485+
* (Coupled arches always show it; this checks the decoupled gate.) */
486+
static void test_render_coupling_gated(void) {
487+
#if !TEXT_TRACKS_DIRECTMAP
488+
struct summary s;
489+
unsigned long sp_lo = layout.phys_kaslr_text_min;
490+
unsigned long sp_hi = layout.phys_kaslr_text_max;
491+
492+
reset_results();
493+
num_comp_logs = 0;
494+
num_scalar_facts = 0;
495+
memset(&s, 0, sizeof(s));
496+
s.kaslr.vslots = 60; /* keep the regular KASLR readout path */
497+
s.kaslr.vbits = 6;
498+
set_render_mode(0, 0, 0);
499+
500+
/* No physical base anywhere → the coupling note is suppressed. */
501+
layout.phys_kaslr_text_min = 0;
502+
layout.phys_kaslr_text_max = 0;
503+
s.kaslr.pslots = 0;
504+
capture_stdout(wrap_render_summary, &s);
505+
assert(strstr(render_cap, "Phys/Virt coupling") == NULL);
506+
507+
/* A physical image base row present → the note is shown. */
508+
layout.phys_kaslr_text_min = 0x01000000ul;
509+
layout.phys_kaslr_text_max = 0x10000000ul;
510+
capture_stdout(wrap_render_summary, &s);
511+
assert(strstr(render_cap, "Phys/Virt coupling") != NULL);
512+
513+
layout.phys_kaslr_text_min = sp_lo;
514+
layout.phys_kaslr_text_max = sp_hi;
515+
set_render_mode(0, 0, 0);
516+
#endif
517+
}
518+
440519
/* The verbose Memory-KASLR candidate count comes from the engine's hole-aware
441520
* slot field (s->kaslr.virt_page_offset_slots), NOT a renderer-local
442521
* (max-min)/align. Set a slot count the naive formula could never produce for
@@ -1526,6 +1605,8 @@ int main(void) {
15261605
RUN(test_render_likely_window);
15271606
RUN(test_render_vtext_speculative);
15281607
RUN(test_render_memory_likely_window);
1608+
RUN(test_render_directmap_base_promoted);
1609+
RUN(test_render_coupling_gated);
15291610
RUN(test_render_memory_kaslr_uses_stored_slots);
15301611
RUN(test_render_disabled_base_not_labeled_likely);
15311612
RUN(test_render_leak_discloses_interior);

0 commit comments

Comments
 (0)