Skip to content

Commit 8f01f6b

Browse files
committed
engine: model PAGE_OFFSET as a per-architecture bracket
PAGE_OFFSET was treated as a constant the analysing build could substitute for the target's. On the architectures where the split is a build choice (x86_32, arm32, ppc32) that is the wrong number, and it leaked into rules, components and renderers alike. Q_PAGE_OFFSET is now resolved within a per-arch bracket, measured at runtime where possible: mmap binary-searches TASK_SIZE to page granularity, and components floor leaked pointers on that measurement rather than on a compile-time value. ppc32 loses its false invariant pin, which widens its guaranteed windows and stops excluding the in-tree 2G and 2.75G builds. Two arch axes replace flags that were standing in for questions they do not answer: LINEAR_MAP_ANCHOR which physical address the kernel maps at PAGE_OFFSET, and so where a rule may read it from. arm64 places memstart_addr away from the DRAM base by an amount nothing unprivileged recovers, so the phys/virt synthesis declines there instead of reconstructing a base that is wrong by gigabytes. MODULES_ANCHOR what the module band is fixed to. Four alternatives that were three booleans plus a magnitude, kept exclusive by hand-written pairwise checks; the checks are gone and a misspelt value no longer compiles to a silent default. Components emit facts, not derivations: the compile-time linear-map projection no longer reaches the evidence set, where it was reconstructed back into "corroboration" for the constant it came from. Rules read the DRAM base through one accessor rather than four copies of a scan. Two soundness violations, both found by live boots and neither visible to the host suite: an s390 bound promoted to a pin kept the slack a bound was entitled to, landing one image offset below _text; and riscv64 derived a text slot from a kaslr-seed the kernel had not consumed, pinning a fresh wrong address on every nokaslr boot. Guards: no rule or component substitutes the compile-time constant, no renderer prints a layout default as an answer, the DRAM base is read only through its accessor, and a guard whose scope comes back empty now fails instead of reporting a clean tree it never examined. The published matrix is regenerated. Every reader profile is now shown on every cell, including where it changed nothing — that is a result, and suppressing it made an untested cell indistinguishable from a tested one.
1 parent 21a042c commit 8f01f6b

111 files changed

Lines changed: 4963 additions & 1389 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,16 @@ $(TEST_ALIGN_BIN): $(TEST_DIR)/test_align.c $(HDRS) | $(TEST_OBJ_DIR)
411411
$(call ccv,CCLD,$@)
412412
$(Q)$(CC) $(ALL_CFLAGS) $(ALL_LDFLAGS) -I$(SRC_DIR) $(TEST_DIR)/test_align.c -o $@
413413

414+
# TASK_SIZE probe test (header-only): drives the boundary search and gap
415+
# detection in task_size.h with a synthetic address space (an injected step, no
416+
# mmap), so the pure logic runs on any host. Covers the porous / untrusted paths
417+
# a normal-kernel VM boot cannot reach. No .c sources to link.
418+
TEST_TS_BIN := $(TEST_OBJ_DIR)/test_task_size
419+
420+
$(TEST_TS_BIN): $(TEST_DIR)/test_task_size.c $(HDRS) | $(TEST_OBJ_DIR)
421+
$(call ccv,CCLD,$@)
422+
$(Q)$(CC) $(ALL_CFLAGS) $(ALL_LDFLAGS) -I$(SRC_DIR) $(TEST_DIR)/test_task_size.c -o $@
423+
414424
# Prefetch scan edge-detection test (header-only): drives
415425
# prefetch_scan_find_edge() with synthetic timing profiles. The x86_64-only
416426
# header makes the suite inert on other hosts. No .c sources to link.
@@ -600,7 +610,7 @@ $(TEST_PARSERS_BIN): $(TEST_DIR)/test_sysfs_parsers.c $(TEST_PARSERS_SRCS) $(HDR
600610
$(Q)$(CC) $(ALL_CFLAGS) $(ALL_LDFLAGS) -I$(SRC_DIR) $(TEST_DIR)/test_sysfs_parsers.c -o $@
601611

602612
.PHONY: test
603-
test : $(KASLD_BIN) $(TEST_BIN) $(TEST_RENDER_BIN) $(TEST_EST_BIN) $(TEST_EV_BIN) $(TEST_ALIGN_BIN) $(TEST_PREFETCH_SCAN_BIN) $(TEST_CPU_BIN) $(TEST_OUTCOME_BIN) $(TEST_TEXT_ORDER_BIN) $(TEST_KIMG_BIN) $(TEST_ENG_BIN) $(TEST_INT_BIN) $(TEST_DMESG_BIN) $(TEST_BACKTRACE_BIN) $(TEST_BOOTCFG_BIN) $(TEST_KASLRDIS_BIN) $(TEST_DTMEM_BIN) $(TEST_SOCKPTR_BIN) $(TEST_TIMERLIST_BIN) $(TEST_BTF_BIN) $(TEST_DMESG_RESV_BIN) $(TEST_BPE820_BIN) $(TEST_PARSERS_BIN) $(TEST_KCORE_BIN)
613+
test : $(KASLD_BIN) $(TEST_BIN) $(TEST_RENDER_BIN) $(TEST_EST_BIN) $(TEST_EV_BIN) $(TEST_ALIGN_BIN) $(TEST_TS_BIN) $(TEST_PREFETCH_SCAN_BIN) $(TEST_CPU_BIN) $(TEST_OUTCOME_BIN) $(TEST_TEXT_ORDER_BIN) $(TEST_KIMG_BIN) $(TEST_ENG_BIN) $(TEST_INT_BIN) $(TEST_DMESG_BIN) $(TEST_BACKTRACE_BIN) $(TEST_BOOTCFG_BIN) $(TEST_KASLRDIS_BIN) $(TEST_DTMEM_BIN) $(TEST_SOCKPTR_BIN) $(TEST_TIMERLIST_BIN) $(TEST_BTF_BIN) $(TEST_DMESG_RESV_BIN) $(TEST_BPE820_BIN) $(TEST_PARSERS_BIN) $(TEST_KCORE_BIN)
604614
@$(TEST_DIR)/run-all
605615
@$(TEST_DIR)/check-render-width
606616
@$(MAKE) --no-print-directory lint
@@ -625,7 +635,10 @@ lint :
625635
@$(TEST_DIR)/check-text-region
626636
@$(TEST_DIR)/check-confidence-floor
627637
@$(TEST_DIR)/check-lattice-seam
638+
@$(TEST_DIR)/check-page-offset-substitution
639+
@$(TEST_DIR)/check-render-default
628640
@$(TEST_DIR)/check-image-size
641+
@$(TEST_DIR)/check-dram-base
629642
@$(TEST_DIR)/check-fdt-unflatten
630643
@$(TEST_DIR)/check-ksymoff
631644
@$(TEST_DIR)/check-manpages

docs/bypass-techniques.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ space directly, without reading any files or exploiting vulnerabilities.
382382

383383
The following KASLD components use brute-force probing:
384384

385-
* [mmap_brute_vmsplit.c](../src/components/mmap_brute_vmsplit.c)determines `PAGE_OFFSET` (vmsplit) on 32-bit systems by mapping pages across the address space until failure
385+
* [mmap_brute_vmsplit.c](../src/components/mmap_brute_vmsplit.c)locates the user/kernel split (`TASK_SIZE`) on 32-bit systems by binary-searching the address space for the lowest page `mmap` refuses, which is `PAGE_OFFSET` (vmsplit) itself on x86_32 and a lower bound on it elsewhere
386386

387387
## Weak entropy
388388

docs/kaslr.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ KERNEL_IMAGE_SIZE` with no gap, so the ceiling is hard.
143143
— the common one. Sub-48 configs place the image higher and randomize over a
144144
smaller window, since `KIMAGE_VADDR = _PAGE_END(VA_BITS_MIN) + module-region
145145
size` and `_PAGE_END = -(1 << (VA_BITS_MIN − 1))`: 4K 3-level (`VA_BITS = 39`,
146-
common on Android) → `0xffffffc080000000`, 64K 2-level (42), 16K 3-level (47).
146+
common on Android) → `0xffffffc080000000`, 64K 2-level (42), 16K 3-level (47),
147+
and 16K 2-level (36, `EXPERT`-gated).
147148
KASLD detects the running `VA_BITS` with an mmap boundary probe
148149
(`mmap_arm64_va_bits`) and resolves the per-config text band and entropy
149150
accordingly.

docs/reproducibility.md

Lines changed: 258 additions & 97 deletions
Large diffs are not rendered by default.

docs/testing.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,12 @@ when available (`HAVE_PTHREAD`), matching the normal build.
8989

9090
### Static guards (`make lint`)
9191

92-
`make test` finishes by running `make lint` — static-analysis guards that assert
93-
source invariants the unit tests can't, with no compiled test binary. Run them
92+
`make test` finishes by running `make lint` — guards that assert source
93+
invariants the unit tests can't. Most are pure text over `src/`, so they need no
94+
build and run in a second; three are not, and it matters when the tree must stay
95+
frozen: `check-truncation` compiles a translation unit for i686,
96+
`check-hash-parity` builds `tests/check_hash_parity.c`, and `check-render-width`
97+
and `check-baseline` execute already-built binaries. Run them
9498
alone with `make lint` (fast; no driver build). Each exits non-zero on failure,
9599
and `make` halts on the first.
96100

@@ -108,9 +112,12 @@ and `make` halts on the first.
108112
| `check-text-floor` | no component rolls its own text-base floor — they must use the `api.h` helper |
109113
| `check-shellcheck` | shellcheck over the `extra/` helper scripts |
110114
| `check-confidence-floor` | no engine rule pins the *guaranteed* window from a guess — a sub-floor signal may shape `likely` only, outside the reviewed allowlist |
111-
| `check-lattice-seam` | quantities whose lattice varies by architecture (`Q_PAGE_OFFSET`, `Q_VA_BITS`) are read through `quantity_pinned/window/admits/narrowed`, never through `.lo` / `.hi`. `struct estimate` means different things per lattice — on a finite set `lo` is a live-candidate bitmask and `hi` is unused — so a direct read is correct only for the lattice it was written against, which is not a property of the source when the same file compiles both ways. Nothing would fail loudly: a bitmask read as an address is a small integer, so the result is a plausible wrong answer rather than a crash. The pointer alias is discovered from its binding rather than assumed to be named `po`, so renaming it cannot slip a read past |
115+
| `check-lattice-seam` | the quantities held to the estimate accessors (`Q_PAGE_OFFSET`, `Q_VA_BITS`) are read through `quantity_pinned/window/admits/narrowed`, never through `.lo` / `.hi`. `struct estimate` means different things per lattice — on a finite set `lo` is a live-candidate bitmask and `hi` is unused — and which lattice a quantity uses is declared once in the quantity table, so a direct read hard-codes an answer the reader never asked for. Nothing would fail loudly: a bitmask read as an address is a small integer, so the result is a plausible wrong answer rather than a crash. The pointer alias is discovered from its binding rather than assumed to be named `po`, so renaming it cannot slip a read past |
116+
| `check-page-offset-substitution` | no engine rule or leak component substitutes the compile-time `PAGE_OFFSET` for the target's linear-map base. That constant describes the analysing build, not the kernel under examination, and on the VMSPLIT arches the two differ routinely — code that reaches for it is asserting the split it was compiled with. The failure is invisible: it compiles everywhere, passes on the whole default-split corpus, and is off by exactly the gap between two build configurations, which is zero on every machine anyone tests. In a rule, an equality must read the resolved `Q_PAGE_OFFSET` via `quantity_pinned()`, and a bound may instead use `PAGE_OFFSET_MAX` (upper) or `PAGE_OFFSET_MIN` (lower), which hold against every target and need no resolution. A component runs before inference and can never see an estimate, so it measures the boundary instead — `kasld_kernel_pointer_floor()` for the user/kernel split, `kasld_page_offset_floor()` for a region-tagged bound. Comments and string literals are stripped first, and `#if` / `#elif` lines are exempt by construction (a constant expression cannot call an accessor, which is why the band assertions keep `PAGE_OFFSET` a plain scalar), so only C code counts |
117+
| `check-render-default` | no output format names a compile-time layout default (`PAGE_OFFSET`, `KERNEL_VIRT_TEXT_DEFAULT`) in code. A renderer printing an address asserts it, and these are link-time constants of the analysing build rather than measurements of the target — presenting one as the answer states a wrong address at full confidence on any kernel built differently, which has happened twice in two different renderers. Showing a default *as* a default is fine via the published layout field; using the linear-map base as an answer goes through `kasld_page_offset_if_known()`, which yields the constant only where a single base is admissible. No exceptions — a new one means that accessor needs extending |
112118
| `check-text-region` | the `KERNEL_TEXT` vs `KERNEL_IMAGE` base contract holds — only reviewed emitters may publish a `_stext` base |
113119
| `check-image-size` | the kernel image size is read only through the evidence accessors, never re-derived in a component |
120+
| `check-dram-base` | where physical RAM begins is read only through `evidence_lowest_dram_base()`, never re-scanned in a rule. Four rules need it, and on the architectures whose kernel sets its physical offset from the base of DRAM that value *is* the address mapped at `PAGE_OFFSET` — so two rules disagreeing about it anchor the linear map differently and shift a guaranteed window rather than widening one. The filter is the substance: `REGION_RAM` with `POS_BASE` and nothing else, which is the kernel's account of its own memory rather than firmware's account of the board, and a bank the kernel rejected would drag the anchor below the real one — the dangerous direction, since one consumer emits `C_EQUALS`. Before the accessor existed the same loop was copied into every caller and the comments promised an agreement nothing enforced |
114121
| `check-hash-parity` | every hashed offset-table row's key recomputes to the stored value under the shipped `kasld_fnv1a64()`, so the runtime hash and the offline generator's cannot drift apart |
115122
| `check-manpages` | the set of long options in each program's `--help` exactly matches the set its man page documents, so a new or removed flag cannot skip its manual entry |
116123
| `check-version` | the version-carrying files stay in step, so a release cannot ship a binary claiming one version while the man pages claim another |
@@ -122,6 +129,8 @@ and `make` halts on the first.
122129
| `check-render-parity` | the text readout, the markdown report and JSON name the same set of resolved quantities for a given run. The Layout row model exists so no two formats can describe one resolved state differently, but it only binds a format that consults it: the no-randomization postures once returned before the model was built and then hardcoded the kernel image base, so a quantity the engine had pinned reached JSON while both readouts omitted it. Compares names, never values — formats may present the same bound differently (the text block snaps a window to the alignment grid, markdown prints the raw edges) — and requires every quantity to have a name mapping, so adding one forces stating how each format names it |
123130
| `check-guard-docs` | this table lists exactly the guards `make lint` runs — the same parity check `check-manpages` applies to flags, applied to the guard list itself |
124131
| `check-readout-docs` | documented sample output uses the renderer's current vocabulary and fits 100 columns (live output is measured separately by `check-render-width`) — the README and `docs/` carry hand-maintained copies of rendered output with nothing tying them to the renderer, so a rename or column change silently leaves them describing a version of the tool that no longer exists |
132+
| `hardening-fixtures` | the `-H` hardening advisor holds its structural invariants when driven over the captured x86_64 sysroots. `test_render.c` covers the meta → gate → suggestion logic by seeding component logs synthetically; this drives the REAL binary over real captures, which is the path that regressed before. Not named `check-*`: it exercises behaviour over fixtures rather than asserting a source invariant, but `make lint` runs it and it is part of that contract |
133+
| `cli-flags` | the argument parser, chiefly short-flag bundling (`-fq` == `-f -q`), which `main()`'s option loop cannot be unit-tested for (`main` is compiled out under `-DKASLD_TESTING`). Same note on the name as above |
125134

126135
`check-truncation` needs `i686-linux-gnu-gcc` and `check-shellcheck` needs
127136
`shellcheck`; both **skip cleanly** (exit 0) when their tool is absent, so
@@ -339,7 +348,7 @@ inferred range contains the kernel's true text base. Where
339348
`extra/validate-bundle` validates a single captured system offline, this
340349
validates live kernels
341350
across architectures and reader-privilege profiles
342-
(`default` / `kptr-hidden` / `perf-open` / `dmesg-open` / `hardened` / `nokaslr`).
351+
(`default` / `kptr-hidden` / `perf-open` / `dmesg-open` / `bpf-open` / `hardened` / `nokaslr`).
343352

344353
Unlike replay (layer 2) — which runs offline over captured fixtures and
345354
only checks that KASLD parses and runs — this boots a real kernel, so it

src/components/acpi_mrrm.c

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,6 @@ int main(void) {
121121
kasld_result_sample(KASLD_TYPE_PHYS, REGION_RAM, (unsigned long)addr, label,
122122
CONF_PARSED);
123123
count++;
124-
125-
#ifdef phys_to_directmap_virt
126-
unsigned long virt = phys_to_directmap_virt((unsigned long)addr);
127-
kasld_found("acpi_mrrm %s: directmap va = 0x%016lx", label, virt);
128-
kasld_result_sample(KASLD_TYPE_VIRT, REGION_DIRECTMAP, virt, label,
129-
CONF_PARSED);
130-
#endif
131124
}
132125
closedir(d);
133126

src/components/boot_config.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ int main(void) {
6565

6666
#if PAGE_OFFSET_FROM_CONFIG
6767
/* Detect PAGE_OFFSET (32-bit vmsplit). CONFIG_PAGE_OFFSET equals the runtime
68-
* page_offset only on PAGE_OFFSET_FROM_CONFIG arches (x86_32, arm32); pinning
69-
* Q_PAGE_OFFSET to it via page_offset_from_landmark's C_EQUALS would exclude
70-
* the truth on arches whose CONFIG_PAGE_OFFSET differs from the running base.
68+
* page_offset only on PAGE_OFFSET_FROM_CONFIG arches (x86_32, arm32, ppc32);
69+
* pinning Q_PAGE_OFFSET to it via page_offset_from_landmark's C_EQUALS would
70+
* exclude the truth on arches whose CONFIG_PAGE_OFFSET differs from the
71+
* running base.
7172
* (The properly gated scalar path is bootconfig_facts ->
7273
* page_offset_from_config.) */
7374
unsigned long virt_page_offset = get_kconfig_page_offset(fp);

src/components/boot_params_e820.c

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -272,27 +272,6 @@ int main(void) {
272272
CONF_PARSED);
273273
}
274274
}
275-
276-
#ifdef phys_to_directmap_virt
277-
if (lo != ~0ul) {
278-
unsigned long virt = phys_to_directmap_virt(lo);
279-
kasld_info("possible direct-map virtual address (low): 0x%016lx",
280-
virt);
281-
kasld_result_base(KASLD_TYPE_VIRT, REGION_DIRECTMAP, virt, NULL,
282-
CONF_PARSED);
283-
}
284-
if (hi) {
285-
unsigned long virt = phys_to_directmap_virt(hi);
286-
kasld_info("possible direct-map virtual address (high): 0x%016lx",
287-
virt);
288-
kasld_result_top(KASLD_TYPE_VIRT, REGION_DIRECTMAP, virt, NULL,
289-
CONF_PARSED);
290-
}
291-
#else
292-
kasld_info(
293-
"note: phys and virt KASLR are decoupled on this arch; "
294-
"cannot derive kernel text virtual address from physical leak");
295-
#endif
296275
}
297276
}
298277

@@ -329,11 +308,5 @@ int main(void) {
329308
(unsigned long)initrd_start, (unsigned long)initrd_end,
330309
NULL, CONF_PARSED);
331310

332-
#ifdef phys_to_directmap_virt
333-
unsigned long virt = phys_to_directmap_virt((unsigned long)initrd_start);
334-
kasld_info("possible direct-map virtual address: 0x%016lx", virt);
335-
kasld_result_base(KASLD_TYPE_VIRT, REGION_DIRECTMAP, virt, NULL, CONF_PARSED);
336-
#endif
337-
338311
return 0;
339312
}

src/components/dmesg_acpi_dynamic_ssdt.c

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,6 @@ int main(void) {
167167
* instance name so the table reads "acpi_table:Cpu0Ist". */
168168
kasld_result_sample(KASLD_TYPE_VIRT, REGION_ACPI_TABLE, s.addr, s.label,
169169
CONF_PARSED);
170-
#ifdef directmap_virt_to_phys
171-
{
172-
unsigned long phys = directmap_virt_to_phys(s.addr);
173-
kasld_info(" possible physical address: 0x%016lx", phys);
174-
kasld_result_sample(KASLD_TYPE_PHYS, REGION_ACPI_TABLE, phys, s.label,
175-
CONF_PARSED);
176-
}
177-
#endif
178170

179171
return 0;
180172
}

src/components/dmesg_backtrace.c

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -418,19 +418,6 @@ int main(void) {
418418
kasld_result_sample(KASLD_TYPE_PHYS, REGION_RAM, ctx.cr3, "cr3",
419419
CONF_PARSED);
420420
}
421-
#if defined(phys_to_directmap_virt) && TEXT_TRACKS_DIRECTMAP
422-
/* Coupled arches: project the CR3 phys to its direct-map virtual address.
423-
* Defensive — CR3 is x86-only and x86 is decoupled, so this is unreached in
424-
* practice — but kept correct: swapper → .bss virt, otherwise a generic
425-
* direct-map landmark. */
426-
{
427-
unsigned long virt = phys_to_directmap_virt(ctx.cr3);
428-
enum kasld_region r =
429-
ctx.cr3_swapper ? REGION_KERNEL_BSS : REGION_DIRECTMAP;
430-
kasld_info("possible direct-map virtual address: %lx", virt);
431-
kasld_result_sample(KASLD_TYPE_VIRT, r, virt, "cr3", CONF_PARSED);
432-
}
433-
#endif
434421
}
435422

436423
if (ctx.directmap) {

0 commit comments

Comments
 (0)