Skip to content

Commit 860fb4e

Browse files
committed
inference: audit fixes for soundness and clean-up
Tighten unsound formulas in meminfo_phys_ceiling (coupled-arch phys_floor), directmap_page_offset_bounds (anchor lower bound on observed dram_floor), randomize_memory_page_offset (require RAM_BASE-tagged witness), and text_cluster_filter (raise CLUSTER_MIN to 5). Move boot_params_align to LAYOUT_ADJUST. Fix layout_adjust conflict warning, harden alignment masks against kaslr_align==0, correct LoongArch behaviour claim and riscv64 image-size docs.
1 parent 41751ef commit 860fb4e

13 files changed

Lines changed: 197 additions & 87 deletions

src/inference/arm64_memstart_align.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
//
2727
// The minimum DIRECTMAP result V_min therefore provides a tight upper bound:
2828
//
29-
// page_offset_max = min(page_offset_max, round_down(V_min, ARM64_MEMSTART_ALIGN))
29+
// page_offset_max = min(page_offset_max, round_down(V_min,
30+
// ARM64_MEMSTART_ALIGN))
3031
//
3132
// This is strictly tighter than the V_min bound already set by
3233
// directmap_page_offset_bounds.c whenever the physical address corresponding

src/inference/arm64_phys_kaslr_align.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ static void arm64_phys_kaslr_align_run(struct kasld_analysis_ctx *ctx) {
5050

5151
/* Derive EFI_KIMG_ALIGN = max(THREAD_ALIGN, SEGMENT_ALIGN=64KiB).
5252
* 64K pages: THREAD_ALIGN = 2*(1<<16) = 128KiB → EFI_KIMG_ALIGN = 128KiB.
53-
* 4K/16K: THREAD_ALIGN ≤ 32KiB → EFI_KIMG_ALIGN = 64KiB (SEGMENT_ALIGN). */
53+
* 4K/16K: THREAD_ALIGN ≤ 32KiB → EFI_KIMG_ALIGN = 64KiB (SEGMENT_ALIGN).
54+
*/
5455
if (pagesize == 65536)
5556
efi_kimg_align = 131072ul; /* 128 KiB */
5657
else if (pagesize == 4096 || pagesize == 16384)

src/inference/boot_params_align.c

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@
3333
// The ceiling from init_size is applied after the alignment snap so a
3434
// single pass handles both constraints.
3535
//
36-
// Phase: PRE_COLLECTION — boot_params is a static file (set at boot); no
37-
// component results are needed. Reading before components also means the
38-
// corrected alignment can contribute to kaslr_ceiling's bound via convergence.
36+
// Phase: LAYOUT_ADJUST — must be LAYOUT_ADJUST (not PRE_COLLECTION) because
37+
// the plugin writes ctx->layout->kaslr_align and ctx->layout->phys_kaslr_align;
38+
// per kasld_inference.h, layout is writable only in LAYOUT_ADJUST. Reading
39+
// boot_params requires no component results, so any phase from LAYOUT_ADJUST
40+
// onward is correct; LAYOUT_ADJUST is chosen so that subsequent
41+
// POST_COLLECTION plugins see the corrected alignment via the orchestrator's
42+
// per-phase refresh of ctx->arch->{kaslr_align, phys_kaslr_align}.
3943
//
4044
// Applicable: x86-64 only.
4145
// ---
@@ -123,7 +127,7 @@ static void boot_params_align_run(struct kasld_analysis_ctx *ctx) {
123127
if (kernel_alignment > ctx->layout->kaslr_align) {
124128
if (verbose && !quiet)
125129
fprintf(stdout,
126-
"[infer] boot_params_align: virt_kaslr_align updated"
130+
"[infer] virt_kaslr_align tightened by boot_params_align:"
127131
" %#lx -> %#lx\n",
128132
ctx->layout->kaslr_align, kernel_alignment);
129133
ctx->layout->kaslr_align = kernel_alignment;
@@ -132,7 +136,7 @@ static void boot_params_align_run(struct kasld_analysis_ctx *ctx) {
132136
kernel_alignment > ctx->layout->phys_kaslr_align) {
133137
if (verbose && !quiet)
134138
fprintf(stdout,
135-
"[infer] boot_params_align: phys_kaslr_align updated"
139+
"[infer] phys_kaslr_align tightened by boot_params_align:"
136140
" %#lx -> %#lx\n",
137141
ctx->layout->phys_kaslr_align, kernel_alignment);
138142
ctx->layout->phys_kaslr_align = kernel_alignment;
@@ -205,7 +209,7 @@ static void boot_params_align_run(struct kasld_analysis_ctx *ctx) {
205209

206210
static const struct kasld_inference boot_params_align = {
207211
.name = "boot_params_align",
208-
.phase = KASLD_INFER_PHASE_PRE_COLLECTION,
212+
.phase = KASLD_INFER_PHASE_LAYOUT_ADJUST,
209213
.run = boot_params_align_run,
210214
};
211215

src/inference/directmap_page_offset_bounds.c

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -101,41 +101,81 @@ static void directmap_page_offset_bounds_run(struct kasld_analysis_ctx *ctx) {
101101
ctx->page_offset_max = vdmap_min;
102102
}
103103

104-
/* Lower bound: PAGE_OFFSET > V_min - MemTotal.
105-
* Only sound when all physical RAM lies within [PHYS_OFFSET,
106-
* PHYS_OFFSET + MemTotal). On NUMA systems with memory holes the highest
107-
* DRAM address can exceed this range: a directmap leak from a high-address
108-
* node would then give V_min > PAGE_OFFSET + MemTotal, making the lower
109-
* bound unsound. Detect this by scanning PHYS/DRAM results and skip. */
104+
/* Lower bound: PAGE_OFFSET > V_min - phys_span,
105+
* where phys_span is the highest physical address mapped in the directmap.
106+
*
107+
* Anchored at PHYS_OFFSET, phys_span = (P_floor - phys_offset) + mem_bytes.
108+
* On systems where the firmware reports DRAM starting at phys_offset,
109+
* P_floor == phys_offset and phys_span == mem_bytes. On systems with a
110+
* low-memory hole — e.g. arm64/MIPS boards where PHYS_OFFSET=0 but DRAM
111+
* begins at 1 GiB — P_floor > phys_offset and phys_span > mem_bytes. The
112+
* earlier formulation `lower = V_min - mem_bytes` (which assumed
113+
* P_floor == phys_offset) could push page_offset_min above the true
114+
* PAGE_OFFSET when V_min came from a high-physical-address leak.
115+
*
116+
* To stay sound we use the *observed* DRAM floor from PHYS/DRAM results.
117+
* If no PHYS/DRAM evidence exists we cannot bound P_floor and must skip
118+
* the lower-bound update.
119+
*
120+
* On NUMA systems with memory holes the highest physical DRAM address can
121+
* exceed P_floor + mem_bytes: detect via PHYS/DRAM scan and skip rather
122+
* than risk excluding the true PAGE_OFFSET. */
110123
unsigned long mem_bytes = read_memtotal_bytes();
111124
if (mem_bytes == 0)
112125
return;
113126

114-
if (ctx->arch->phys_offset <= ULONG_MAX - mem_bytes) {
115-
unsigned long phys_limit = ctx->arch->phys_offset + mem_bytes;
116-
for (size_t i = 0; i < ctx->result_count; i++) {
117-
const struct result *r = &ctx->results[i];
118-
if (!r->valid)
119-
continue;
120-
if (r->type == KASLD_ADDR_PHYS &&
121-
strcmp(r->section, KASLD_SECTION_DRAM) == 0 && r->raw >= phys_limit)
122-
return; /* NUMA hole detected; lower bound would be unsound */
123-
}
127+
unsigned long phys_floor = ULONG_MAX;
128+
for (size_t i = 0; i < ctx->result_count; i++) {
129+
const struct result *r = &ctx->results[i];
130+
if (!r->valid)
131+
continue;
132+
if (r->type != KASLD_ADDR_PHYS)
133+
continue;
134+
if (strcmp(r->section, KASLD_SECTION_DRAM) != 0)
135+
continue;
136+
if (r->raw < phys_floor)
137+
phys_floor = r->raw;
138+
}
139+
140+
if (phys_floor == ULONG_MAX)
141+
return; /* No PHYS/DRAM witness — cannot bound P_floor soundly. */
142+
143+
if (phys_floor < ctx->arch->phys_offset)
144+
return; /* Inconsistent: a leak below PHYS_OFFSET indicates misclassified
145+
* results. Skip rather than produce a wrong bound. */
146+
147+
unsigned long phys_floor_offset = phys_floor - ctx->arch->phys_offset;
148+
149+
if (phys_floor_offset > ULONG_MAX - mem_bytes)
150+
return; /* Overflow guard. */
151+
152+
unsigned long phys_span = phys_floor_offset + mem_bytes;
153+
154+
/* NUMA-hole guard: any PHYS/DRAM result above phys_floor + mem_bytes
155+
* indicates a hole that exceeds our phys_span estimate. Skip in that case. */
156+
unsigned long phys_limit = phys_floor + mem_bytes;
157+
for (size_t i = 0; i < ctx->result_count; i++) {
158+
const struct result *r = &ctx->results[i];
159+
if (!r->valid)
160+
continue;
161+
if (r->type == KASLD_ADDR_PHYS &&
162+
strcmp(r->section, KASLD_SECTION_DRAM) == 0 && r->raw >= phys_limit)
163+
return;
124164
}
125165

126-
/* Guard against unsigned underflow when MemTotal > V_min. */
127-
if (vdmap_min < mem_bytes)
166+
/* Guard against unsigned underflow when phys_span > V_min. */
167+
if (vdmap_min < phys_span)
128168
return;
129169

130-
unsigned long lower = vdmap_min - mem_bytes;
170+
unsigned long lower = vdmap_min - phys_span;
131171

132172
if (lower > ctx->page_offset_min && lower < ctx->page_offset_max) {
133173
if (verbose && !quiet)
134174
fprintf(stdout,
135175
"[infer] virt_page_offset_min tightened by"
136176
" directmap_page_offset_bounds: %#lx -> %#lx"
137-
" (MemTotal=%lu bytes)\n",
138-
ctx->page_offset_min, lower, mem_bytes);
177+
" (phys_floor=%#lx phys_span=%#lx)\n",
178+
ctx->page_offset_min, lower, phys_floor, phys_span);
139179
ctx->page_offset_min = lower;
140180
}
141181
}

src/inference/dram_bound.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,12 @@ static void dram_bound_run(struct kasld_analysis_ctx *ctx) {
8989
if (pdram_lo < phys_offset)
9090
return;
9191

92-
/* Align down to the nearest slot boundary to stay conservative. */
93-
unsigned long virt_lo =
94-
(pdram_lo - phys_offset + page_offset + text_offset) &
95-
~(kaslr_align - 1);
92+
/* Align down to the nearest slot boundary to stay conservative. Skip the
93+
* mask when kaslr_align is zero (KASLR-disabled config) — masking with
94+
* ~(0 - 1) == 0 would zero the address. */
95+
unsigned long virt_lo = pdram_lo - phys_offset + page_offset + text_offset;
96+
if (kaslr_align > 0)
97+
virt_lo &= ~(kaslr_align - 1);
9698

9799
if (virt_lo > kaslr_min && virt_lo > ctx->text_base_min &&
98100
virt_lo < ctx->text_base_max) {

src/inference/dram_ceiling.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ static void dram_ceiling_run(struct kasld_analysis_ctx *ctx) {
138138
* that fits entirely within RAM; rounding down keeps the bound sound. */
139139
unsigned long phys_ceiling = dram_top - kernel_size;
140140
unsigned long virt_ceiling =
141-
(phys_ceiling - phys_offset + page_offset + text_offset) &
142-
~(kaslr_align - 1);
141+
phys_ceiling - phys_offset + page_offset + text_offset;
142+
if (kaslr_align > 0)
143+
virt_ceiling &= ~(kaslr_align - 1);
143144

144145
if (virt_ceiling > kaslr_min && virt_ceiling < ctx->text_base_max) {
145146
if (verbose && !quiet)

src/inference/layout_adjust.c

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,28 @@ static void layout_adjust_run(struct kasld_analysis_ctx *ctx) {
7979
po_vals[po_n++] = r->aligned;
8080
}
8181

82+
/* 2. Apply consensus PAGE_OFFSET when it differs from compile-time default.
83+
* adjust_for_page_offset() shifts all dependent layout fields atomically.
84+
* Compute first so the conflict warning below can report the value
85+
* actually being applied rather than a separately-derived "min of first
86+
* two values" approximation. */
87+
unsigned long detected_po =
88+
group_consensus(KASLD_ADDR_VIRT, KASLD_SECTION_PAGEOFFSET);
89+
8290
if (po_n > 1 && !po_conflict_warned) {
8391
po_conflict_warned = 1;
8492
if (!quiet) {
8593
fprintf(stdout, "[infer] layout_adjust: conflicting PAGE_OFFSET sources"
8694
" (possible legacy kernel layout):");
8795
for (int i = 0; i < po_n; i++)
8896
fprintf(stdout, " 0x%016lx", po_vals[i]);
89-
fprintf(stdout, "; using 0x%016lx (modern layout assumed)\n",
90-
po_vals[0] < po_vals[1] ? po_vals[0] : po_vals[1]);
97+
if (detected_po)
98+
fprintf(stdout, "; using 0x%016lx (group_consensus)\n", detected_po);
99+
else
100+
fprintf(stdout, "; no consensus, retaining compile-time default\n");
91101
}
92102
}
93103

94-
/* 2. Apply consensus PAGE_OFFSET when it differs from compile-time default.
95-
* adjust_for_page_offset() shifts all dependent layout fields atomically.
96-
*/
97-
unsigned long detected_po =
98-
group_consensus(KASLD_ADDR_VIRT, KASLD_SECTION_PAGEOFFSET);
99104
if (detected_po && detected_po != ctx->layout->page_offset)
100105
adjust_for_page_offset(detected_po);
101106

src/inference/meminfo_phys_ceiling.c

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,25 @@ static void meminfo_phys_ceiling_run(struct kasld_analysis_ctx *ctx) {
113113
ctx->phys_base_max = ceiling;
114114
}
115115
} else {
116-
/* Coupled: phys_to_virt(ceiling) = page_offset + (ceiling - phys_offset)
117-
* = page_offset + mem_bytes - MIN_IMAGE_SIZE.
118-
* text_base = phys_to_virt(phys_base) + TEXT_OFFSET, so add text_offset.
116+
/* Coupled: phys_to_virt(P) = page_offset + (P - phys_offset). The phys
117+
* ceiling is phys_floor + mem_bytes - MIN_IMAGE_SIZE, where phys_floor
118+
* is the *observed* DRAM start (which may be ≥ arch->phys_offset on
119+
* systems with a low-memory hole, e.g. arm/MIPS boards where DRAM
120+
* begins at 0x40000000 but PHYS_OFFSET=0). So:
121+
* virt_ceiling = page_offset + (phys_floor - phys_offset) + mem_bytes
122+
* - MIN_IMAGE_SIZE + text_offset.
123+
* Using only `mem_bytes - MIN_IMAGE_SIZE` (the previous formulation,
124+
* which assumed phys_floor == phys_offset) understated the ceiling on
125+
* such systems and could exclude the true text base.
119126
* Use the runtime page_offset from layout (set by layout_adjust). */
120127
unsigned long page_offset = ctx->layout->page_offset;
121128
unsigned long text_offset = ctx->arch->text_offset;
122-
unsigned long virt_ceiling =
123-
page_offset + mem_bytes - MIN_IMAGE_SIZE + text_offset;
129+
unsigned long phys_offset = ctx->arch->phys_offset;
130+
unsigned long phys_floor = dram_floor(ctx, phys_offset);
131+
unsigned long phys_floor_offset =
132+
(phys_floor > phys_offset) ? (phys_floor - phys_offset) : 0;
133+
unsigned long virt_ceiling = page_offset + phys_floor_offset + mem_bytes -
134+
MIN_IMAGE_SIZE + text_offset;
124135

125136
unsigned long kaslr_min = ctx->arch->kaslr_base_min;
126137
unsigned long kaslr_align = ctx->arch->kaslr_align;

src/inference/min_offset_from_image_size.c

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
// Inference plugin: forbidden lower slots from kernel image size
44
// (POST_COLLECTION)
55
//
6-
// Both MIPS and LoongArch enforce a minimum KASLR offset:
6+
// Both MIPS and LoongArch enforce a minimum KASLR offset using identical
7+
// arithmetic in their respective `relocate_kernel.c`:
78
//
89
// offset &= (CONFIG_RANDOMIZE_BASE_MAX_OFFSET - 1);
910
// if (offset < kernel_length)
10-
// offset += ALIGN(kernel_length, KERNEL_ALIGN); // MIPS
11-
// // or, on LoongArch: offset = ALIGN(kernel_length, KERNEL_ALIGN)
11+
// offset += ALIGN(kernel_length, KERNEL_ALIGN);
12+
//
13+
// (Verified against arch/mips/kernel/relocate.c and
14+
// arch/loongarch/kernel/relocate.c on Linux 6.17.)
1215
//
1316
// Any randomly-drawn offset in [0, kernel_length) is bumped upward. The
1417
// range [0, kernel_length) is a forbidden zone: no valid text base falls
@@ -23,22 +26,19 @@
2326
// sound: the kernel cannot load within the forbidden zone, and the zone
2427
// extends at least gap bytes from KASLR_BASE_MIN.
2528
//
26-
// LoongArch extension:
29+
// LoongArch diagnostic:
2730
// When kernel_length ≥ CONFIG_RANDOMIZE_BASE_MAX_OFFSET, every offset
28-
// drawn from [0, max_offset) satisfies offset < kernel_length, so the
29-
// bump always fires. On LoongArch the bump is an assignment (not an
30-
// addition), making the result a single fixed value:
31-
//
32-
// text_base = KASLR_BASE_MIN + ALIGN(kernel_length, KERNEL_ALIGN)
33-
//
34-
// When kernel_length_estimate ≥ max_offset this plugin emits a verbose
35-
// diagnostic. A bilateral pin is not applied: the gap is a lower bound
36-
// (gap ≤ true kernel_length), so ALIGN(gap) ≤ ALIGN(true_kernel_length),
37-
// and setting text_base_max = KASLR_BASE_MIN + ALIGN(gap) could exclude
38-
// the true text base if gap underestimates by more than one KERNEL_ALIGN
39-
// step. Implementing the bilateral pin soundly requires exact kernel_length
40-
// (e.g. from /boot/Image header — see riscv64_fdt_kaslr_seed inference for
41-
// the Image-header approach; a LoongArch variant is a future enhancement).
31+
// drawn from [0, max_offset) satisfies `offset < kernel_length`, so the
32+
// bump always fires. The post-bump offset lands in
33+
// [ALIGN(kernel_length, KERNEL_ALIGN),
34+
// max_offset + ALIGN(kernel_length, KERNEL_ALIGN)) — *not* a single
35+
// deterministic value. We emit a verbose diagnostic noting that the
36+
// randomization range has been shifted into a higher window. No further
37+
// bound tightening is applied because the gap is only a lower bound on
38+
// kernel_length; soundly pinning text_base_max requires the exact
39+
// kernel_length (e.g. from /boot/Image header — see
40+
// riscv64_fdt_kaslr_seed for the Image-header approach; a LoongArch
41+
// variant is a future enhancement).
4242
//
4343
// Note: config_max_offset_bound (PRE_COLLECTION) sets
4444
// text_base_max = KASLR_BASE_MIN + max_offset.
@@ -162,10 +162,12 @@ static void min_offset_from_image_size_run(struct kasld_analysis_ctx *ctx) {
162162
}
163163

164164
#if defined(__loongarch__)
165-
/* LoongArch extension: when kernel_length >= max_offset the bump always
166-
* fires (assignment semantics), making KASLR deterministic. Log this
167-
* condition for awareness; a bilateral pin is not applied because gap is
168-
* a lower bound on the true kernel_length (see file header). */
165+
/* LoongArch diagnostic: when kernel_length >= max_offset, every random
166+
* draw triggers the bump branch, shifting the slot window from
167+
* [0, max_offset) to [ALIGN(kernel_length), max_offset +
168+
* ALIGN(kernel_length)). The kernel is still randomized within this shifted
169+
* window — not a single deterministic point. Log for awareness; no bound pin
170+
* (see file header for why). */
169171
struct utsname uts;
170172
if (uname(&uts) == 0) {
171173
FILE *fp = open_boot_config(uts.release);
@@ -179,13 +181,11 @@ static void min_offset_from_image_size_run(struct kasld_analysis_ctx *ctx) {
179181
: gap;
180182

181183
if (verbose && !quiet)
182-
fprintf(
183-
stdout,
184-
"[infer] min_offset_from_image_size: LoongArch KASLR"
185-
" deterministic (kernel_length_estimate=%#lx >= max_offset=%#lx);"
186-
" deterministic virt_text_base >= %#lx;"
187-
" bilateral pin requires exact kernel_length\n",
188-
gap, max_offset, kaslr_min + aligned_gap);
184+
fprintf(stdout,
185+
"[infer] min_offset_from_image_size: LoongArch KASLR window"
186+
" shifted (kernel_length_estimate=%#lx >= max_offset=%#lx);"
187+
" virt_text_base >= %#lx; range shifted, not pinned\n",
188+
gap, max_offset, kaslr_min + aligned_gap);
189189
}
190190
}
191191
}

src/inference/module_text_bound.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ static void module_text_bound_run(struct kasld_analysis_ctx *ctx) {
5959
unsigned long kaslr_align = ctx->arch->kaslr_align;
6060
unsigned long kaslr_min = ctx->arch->kaslr_base_min;
6161

62+
/* Refuse to operate on a zero alignment — every formula below uses
63+
* `~(kaslr_align - 1)` which would be 0 (i.e., zero-out the address).
64+
* This guards against KASLR-disabled or otherwise-zero arch configs. */
65+
if (kaslr_align == 0)
66+
return;
67+
6268
/* Find minimum and maximum valid aligned virtual module addresses. */
6369
unsigned long vmod_lo = ULONG_MAX;
6470
unsigned long vmod_hi = 0;

0 commit comments

Comments
 (0)