Skip to content

Commit e6a9acd

Browse files
committed
arch: state the 32-bit module bands as the union over splits
kasld_addr_is_module_band() is enforced inside each component, before the engine has resolved anything, so the static MODULES_START/END must be the union the arch-header CONTRACT asks for -- not the band this binary's own default would have. Two arches instantiated it at the compile-time split. x86_32 stated a literal 0xf0000000. The kernel has no fixed module region: MODULES_VADDR is VMALLOC_START, which is PAGE_OFFSET plus the runtime lowmem span plus 8 MiB. That literal is correct only on a 3G split carrying more than ~760 MiB of lowmem; a 512 MiB VM puts the real floor at ~0xe0800000 and every module address was discarded below it. Booted on the i686 cell at mem=512M the band now projects to 0xc0800000, under the truth rather than over it. arm32 had two. The floor was the 3G instance, leaving a VMSPLIT_1G kernel's modules a full 512 MiB below the band. The ceiling was PAGE_OFFSET, which describes only the dedicated window: CONFIG_ARM_MODULE_PLTS is default y and spills overflow into [VMALLOC_START, VMALLOC_END] above PAGE_OFFSET, so the ordinary configuration -- not an exotic one -- allocated modules the band rejected. Same shape as the ppc32 platform split. The rule is now enforced rather than described: the static macros must equal the relation applied to KERNEL_VIRT_VAS_START, the lowest admissible split on every arch in scope. An inequality would have passed on the code being fixed here, since the wrong instantiation is spelled almost exactly like the right one. ppc32 and riscv32 change spelling only -- their split cannot move, so the two arguments coincide. x86_32 is the first arch whose relation ADDS to PAGE_OFFSET, which the wrap guards read as a wrapped floor and rejected. Both now test the direction the relation actually moves, folded at compile time. test-cross caught this.
1 parent 89547d0 commit e6a9acd

9 files changed

Lines changed: 138 additions & 43 deletions

File tree

docs/kaslr.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,15 +299,15 @@ KASLD quantity (`Q_*`) that resolves each region annotated alongside it:
299299
| Architecture | Text ↔ Phys | Text ↔ Direct map | Text ↔ Modules | Notes |
300300
|---|---|---|---|---|
301301
| x86_64 | Independent | Independent | Independent | Three separate randomizations (`CONFIG_RANDOMIZE_MEMORY`) |
302-
| x86_32 | Coupled | Coupled | Fixed module region | Single KASLR offset |
302+
| x86_32 | Coupled | Coupled | Tracks lowmem, not text | Single KASLR offset; the module region starts at `VMALLOC_START`, which is `PAGE_OFFSET` plus the runtime lowmem size plus 8 MiB |
303303
| arm64 | Independent | Independent | Bracketed (within 2 GiB of text) | Separate phys/virt randomization; modules drawn from a window spanning the kernel image, so they move with it |
304-
| arm32 || Coupled | Fixed (PAGE_OFFSET - 16M) | No KASLR |
304+
| arm32 || Coupled | Below PAGE_OFFSET, spills to vmalloc | No KASLR; a dedicated 16 MiB window below `PAGE_OFFSET`, with `CONFIG_ARM_MODULE_PLTS` (default y) spilling overflow into vmalloc above it |
305305
| MIPS32/64 | Coupled | Coupled (kseg0) | Fixed module region | Hardware-defined mapping |
306-
| PowerPC32 | Coupled | Coupled | Fixed (PAGE_OFFSET - 256M) | |
306+
| PowerPC32 | Coupled | Coupled | Platform-dependent | A dedicated window below `PAGE_OFFSET` on 8xx / book3s32; every other platform, 85xx included, allocates from the shared vmalloc window |
307307
| PowerPC64 || Coupled | Shared VAS | No KASLR |
308308
| LoongArch64 | Coupled | Coupled | Fixed module region | Direct-mapped windows |
309309
| RISC-V64 | Virtual only | Decoupled | Coupled (shifts with kernel) | Module region anchored to kernel `_end`; text ↔ directmap coupled on legacy pre-v5.10 kernels (no KASLR) |
310-
| RISC-V32 || Coupled | Same as PAGE_OFFSET | No KASLR |
310+
| RISC-V32 || Coupled | Below PAGE_OFFSET (vmalloc) | No KASLR; modules share the vmalloc window, which on sv32 is the 512 MiB below `PAGE_OFFSET` |
311311

312312
On coupled architectures, all sections are at fixed offsets from each other:
313313
a physical address reveals the virtual text base via

src/include/kasld/api.h

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,25 @@ __extension__ _Static_assert((unsigned long)KERNEL_PHYS_MAX >
271271

272272
/* MODULES_RELATIVE_TO_PAGE_OFFSET is opt-in: an arch declares it 1 when its
273273
* module band is defined as a DELTA FROM PAGE_OFFSET rather than at fixed
274-
* addresses. The compile-time MODULES_START/END then encode nothing but the
275-
* band's position under the compile-time PAGE_OFFSET, and an arch whose
276-
* PAGE_OFFSET moves at runtime (arm32 VMSPLIT) leaves them pointing at a place
277-
* the modules are not. Such an arch also supplies MODULES_START_FOR(po) /
278-
* MODULES_END_FOR(po) -- the same arithmetic parameterised on PAGE_OFFSET -- so
279-
* the orchestrator can re-derive the band once the engine resolves it, and so
280-
* the static macros cannot drift from the relation they are an instance of.
274+
* addresses. Such an arch supplies MODULES_START_FOR(po) / MODULES_END_FOR(po)
275+
* -- the same arithmetic parameterised on PAGE_OFFSET -- so the orchestrator
276+
* can re-derive the band once the engine resolves it, and so the static macros
277+
* cannot drift from the relation they are an instance of.
278+
*
279+
* WHICH INSTANCE: the static MODULES_START/END must apply the relation at the
280+
* LOWEST PAGE_OFFSET the arch admits, not at the compile-time one. The two
281+
* differ wherever the split is a build choice the analysing binary cannot see
282+
* (arm32 and x86_32 VMSPLIT), and there the compile-time instance is a band
283+
* this binary's own default would have -- not the union the CONTRACT above
284+
* requires. It matters because the static macros are what
285+
* kasld_addr_is_module_band() enforces INSIDE each component, which runs
286+
* before the engine has resolved anything: a band drawn at the wrong split
287+
* discards every genuine module address at the source, and the orchestrator's
288+
* re-derivation comes too late to recover it. On every arch in scope the
289+
* lowest admissible split is KERNEL_VIRT_VAS_START, so that is the argument to
290+
* instantiate with; where PAGE_OFFSET cannot move the two coincide and the
291+
* choice is a no-op. The assertions below hold the property rather than the
292+
* spelling.
281293
*
282294
* Both accessors must stay integer CONSTANT EXPRESSIONS when applied to the
283295
* compile-time PAGE_OFFSET: MODULES_START/END appear in static initialisers and
@@ -296,6 +308,20 @@ __extension__ _Static_assert((unsigned long)KERNEL_PHYS_MAX >
296308
#if MODULES_RELATIVE_TO_TEXT
297309
#error "module band cannot follow both PAGE_OFFSET and the text base"
298310
#endif
311+
/* The union-vs-instance rule, enforced rather than merely documented: writing
312+
* the floor as the relation applied to the compile-time PAGE_OFFSET is the
313+
* mistake, and it is spelled almost identically to the correct form, so the
314+
* check has to be equality against the right argument. Both accessors are
315+
* monotone in PAGE_OFFSET, so this also fixes the floor as the widest the
316+
* relation can produce, whether it adds to PAGE_OFFSET or subtracts from it.
317+
* An arch needing a wider band widens the RELATION, which keeps the static
318+
* macros and the runtime projection describing the same geometry. */
319+
#if MODULES_START != MODULES_START_FOR(KERNEL_VIRT_VAS_START)
320+
#error "MODULES_START must instantiate the relation at the lowest split"
321+
#endif
322+
#if MODULES_END < MODULES_END_FOR(PAGE_OFFSET)
323+
#error "MODULES_END is below the band's own ceiling at the compile-time split"
324+
#endif
299325
#endif
300326

301327
/* MODULES_BRACKET_TEXT is opt-in: an arch declares it as a byte size W when
@@ -698,6 +724,30 @@ static inline int kasld_addr_in_range(unsigned long long addr,
698724
return addr >= lo && addr <= hi;
699725
}
700726

727+
#if MODULES_RELATIVE_TO_PAGE_OFFSET
728+
/* Guard for projecting the module band onto a resolved PAGE_OFFSET.
729+
*
730+
* MODULES_START_FOR either adds to PAGE_OFFSET or subtracts from it, so the
731+
* arithmetic can wrap at whichever end of the address space it moves toward: a
732+
* subtracting relation underflows past 0 when the split reaches the lowest one
733+
* admissible, an adding relation overflows past ULONG_MAX. Both present the
734+
* same way -- the projected floor lands on the far side of the PAGE_OFFSET it
735+
* was derived from. Which side that is depends only on the relation, so the
736+
* test is a compile-time constant and folds away.
737+
*
738+
* A wrapped floor is rejected, never clamped: on an arch that carves the band
739+
* out of the address space below PAGE_OFFSET, clamping to the bottom of the
740+
* kernel VAS collapses the band onto a point and rejects every genuine module
741+
* leak. */
742+
static inline int kasld_module_band_floor_sane(unsigned long po,
743+
unsigned long floor) {
744+
return ((unsigned long)MODULES_START_FOR((unsigned long)PAGE_OFFSET) <=
745+
(unsigned long)PAGE_OFFSET)
746+
? floor <= po
747+
: floor >= po;
748+
}
749+
#endif
750+
701751
/* Predicate: is `va` plausibly inside the kernel module BAND on this arch?
702752
*
703753
* Wraps the MODULES_START/END validation union (see the CONTRACT in the

src/include/kasld/arch/arm32.h

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,33 @@
6565
// Above this, addresses fall in the vectors/fixmap region.
6666
#define KERNEL_VIRT_TEXT_MAX 0xf0000000ul
6767

68-
// Modules are located below kernel: PAGE_OFFSET - 16MiB (0x01000000)
69-
// https://elixir.bootlin.com/linux/v6.1.1/source/arch/arm/include/asm/memory.h#L51
70-
// Stated as a relation to PAGE_OFFSET, not as fixed addresses: VMSPLIT chooses
71-
// PAGE_OFFSET at build time and the band moves with it, so the compile-time
72-
// instance below is only where the modules sit under THIS binary's default.
73-
// MODULES_RELATIVE_TO_PAGE_OFFSET lets the orchestrator re-derive the band once
74-
// the engine resolves the running kernel's PAGE_OFFSET.
68+
// arm32 gives modules a dedicated window immediately below the linear map:
69+
// MODULES_VADDR is PAGE_OFFSET - 16 MiB (8 MiB on a Thumb-2 kernel, whose
70+
// relocations reach less far, so the 16 MiB floor covers both), and
71+
// MODULES_END is PAGE_OFFSET, or one PMD below it under CONFIG_HIGHMEM.
72+
//
73+
// That window is not the whole story. CONFIG_ARM_MODULE_PLTS -- `default y`,
74+
// so this is the ordinary case rather than an exotic one -- gives the
75+
// allocator a fallback of [VMALLOC_START, VMALLOC_END] for modules that no
76+
// longer fit, reached via PLT veneers (arch/arm/mm/init.c execmem_arch_setup).
77+
// VMALLOC_START derives from runtime high_memory and therefore lies ABOVE
78+
// PAGE_OFFSET, so a band that stops at PAGE_OFFSET describes only the first
79+
// window and rejects every module that spilled into the second. VMALLOC_END is
80+
// 0xff800000, but a band is the union over what a kernel MIGHT do, and
81+
// CONFIG_XIP_KERNEL re-points MODULES_VADDR at the execute-in-place ROM
82+
// entirely -- so the ceiling is the top of the address space.
83+
//
84+
// The floor is the only PAGE_OFFSET relation of the two, and it is
85+
// instantiated at the lowest admissible split rather than this binary's:
86+
// VMSPLIT is a build choice of the kernel under analysis, and
87+
// kasld_addr_is_module_band() runs inside components, before the engine has
88+
// resolved which split that kernel used.
89+
// https://elixir.bootlin.com/linux/v7.2/source/arch/arm/include/asm/memory.h
90+
// https://elixir.bootlin.com/linux/v7.2/source/arch/arm/mm/init.c
7591
#define MODULES_RELATIVE_TO_PAGE_OFFSET 1
7692
#define MODULES_START_FOR(po) ((po) - 0x01000000ul)
77-
#define MODULES_END_FOR(po) (po)
78-
#define MODULES_START MODULES_START_FOR(PAGE_OFFSET) // 0xbf000000ul
93+
#define MODULES_END_FOR(po) (KERNEL_VIRT_VAS_END)
94+
#define MODULES_START MODULES_START_FOR(KERNEL_VIRT_VAS_START) // 0x3f000000ul
7995
#define MODULES_END MODULES_END_FOR(PAGE_OFFSET)
8096
// Module region is fixed below PAGE_OFFSET; does not shift with KASLR.
8197
#define MODULES_RELATIVE_TO_TEXT 0

src/include/kasld/arch/ppc32.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@
6666
#define MODULES_RELATIVE_TO_PAGE_OFFSET 1
6767
#define MODULES_START_FOR(po) ((po) - 0x10000000ul)
6868
#define MODULES_END_FOR(po) (KERNEL_VIRT_VAS_END)
69-
#define MODULES_START MODULES_START_FOR(PAGE_OFFSET) // 0xb0000000ul
69+
// Instantiated at the lowest admissible split, per the union rule in api.h;
70+
// ppc32 has only one, so this is PAGE_OFFSET spelled as the rule requires.
71+
#define MODULES_START MODULES_START_FOR(KERNEL_VIRT_VAS_START) // 0xb0000000ul
7072
#define MODULES_END MODULES_END_FOR(PAGE_OFFSET)
7173
#define MODULES_RELATIVE_TO_TEXT 0
7274

src/include/kasld/arch/riscv32.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@
5757
#define MODULES_RELATIVE_TO_PAGE_OFFSET 1
5858
#define MODULES_START_FOR(po) ((po) - 0x20000000ul)
5959
#define MODULES_END_FOR(po) (po)
60-
#define MODULES_START MODULES_START_FOR(PAGE_OFFSET)
60+
// Instantiated at the lowest admissible split, per the union rule in api.h;
61+
// riscv32 has only one, so this is PAGE_OFFSET spelled as the rule requires.
62+
#define MODULES_START MODULES_START_FOR(KERNEL_VIRT_VAS_START)
6163
#define MODULES_END MODULES_END_FOR(PAGE_OFFSET)
6264
#define MODULES_RELATIVE_TO_TEXT 0
6365

src/include/kasld/arch/x86_32.h

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,40 @@
5656
// configurations. The orchestrator adjusts at runtime once vmsplit is detected.
5757
#define KERNEL_VIRT_VAS_START KERNEL_VIRT_TEXT_MIN
5858
#define KERNEL_VIRT_VAS_END 0xfffffffful
59-
// Above this, addresses fall in the module/fixmap region.
59+
// Kernel text ceiling, and a deliberate over-estimate rather than a region
60+
// boundary: the image sits within KERNEL_IMAGE_SIZE (512 MiB) of PAGE_OFFSET,
61+
// so even the highest split puts it no higher than 0xe0000000. What lies above
62+
// the image is the rest of the linear map, whose top is the runtime lowmem
63+
// size -- there is no fixed address at which the module region begins.
6064
#define KERNEL_VIRT_TEXT_MAX 0xf0000000ul
6165

62-
// Modules placed in high memory above kernel text.
63-
// https://elixir.bootlin.com/linux/v6.1.1/source/arch/x86/kernel/module.c
64-
#define MODULES_START 0xf0000000ul
65-
#define MODULES_END 0xfffffffful
66+
// x86_32 has no module region at a fixed address. MODULES_VADDR is
67+
// VMALLOC_START, which is `high_memory + VMALLOC_OFFSET` (8 MiB), and
68+
// high_memory is PAGE_OFFSET plus the span of lowmem:
69+
//
70+
// MODULES_VADDR = PAGE_OFFSET + lowmem_span + 8 MiB
71+
//
72+
// Two runtime terms, and only the first is a PAGE_OFFSET relation. The lowmem
73+
// span is a property of how much RAM the machine has (capped by MAXMEM), which
74+
// nothing here observes, so the floor states the limit it approaches as that
75+
// span goes to zero: PAGE_OFFSET + 8 MiB. Sound under the older definition
76+
// too, which rounded down to an 8 MiB boundary -- every admissible PAGE_OFFSET
77+
// is already 8 MiB-aligned, so the rounding cannot land below.
78+
//
79+
// The ceiling is VMALLOC_END, which moves with NR_CPUS through
80+
// CPU_ENTRY_AREA_PAGES and is no more a compile-time constant than the floor,
81+
// so it is the top of the address space.
82+
//
83+
// A band stated as a literal cannot hold here: the floor is correct only for a
84+
// 3G split carrying more than ~760 MiB of lowmem, and drops below any fixed
85+
// value on a smaller machine or a lower split.
86+
// https://elixir.bootlin.com/linux/v7.2/source/arch/x86/include/asm/pgtable_32_areas.h
87+
// https://elixir.bootlin.com/linux/v7.2/source/arch/x86/mm/init_32.c
88+
#define MODULES_RELATIVE_TO_PAGE_OFFSET 1
89+
#define MODULES_START_FOR(po) ((po) + 0x00800000ul)
90+
#define MODULES_END_FOR(po) (KERNEL_VIRT_VAS_END)
91+
#define MODULES_START MODULES_START_FOR(KERNEL_VIRT_VAS_START) // 0x40800000ul
92+
#define MODULES_END MODULES_END_FOR(PAGE_OFFSET)
6693
// Module region is fixed; does not shift with KASLR.
6794
#define MODULES_RELATIVE_TO_TEXT 0
6895

src/orchestrator.c

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3960,16 +3960,14 @@ static void engine_sync_authoritative(const struct engine *e) {
39603960
if (po->lo && po->lo <= po->hi && (pinned || default_excluded)) {
39613961
unsigned long lo = MODULES_START_FOR(po->lo);
39623962
unsigned long hi = MODULES_END_FOR(po->hi);
3963-
/* MODULES_START_FOR subtracts, so a PAGE_OFFSET window reaching down to
3964-
* the lowest admissible split can wrap the floor past 0 and leave it
3965-
* near ULONG_MAX. Reject a wrapped floor; do NOT clamp it to
3966-
* KERNEL_VIRT_VAS_START. On every arch that carves the module band out
3967-
* of vmalloc the band legitimately sits BELOW PAGE_OFFSET, and on those
3968-
* KERNEL_VIRT_VAS_START *is* PAGE_OFFSET -- clamping there would discard
3969-
* the whole band and reject every genuine module leak, the exact failure
3970-
* the union contract above exists to prevent. Same wrap test that
3963+
/* Reject a wrapped floor; do NOT clamp it to KERNEL_VIRT_VAS_START. On
3964+
* every arch that carves the module band out of vmalloc the band
3965+
* legitimately sits BELOW PAGE_OFFSET, and on those KERNEL_VIRT_VAS_START
3966+
* *is* PAGE_OFFSET -- clamping there would discard the whole band and
3967+
* reject every genuine module leak, the exact failure the union contract
3968+
* above exists to prevent. Same wrap test that
39713969
* rules/module_base_bounds.c applies to these edges. */
3972-
if (lo <= po->lo && hi > lo) {
3970+
if (kasld_module_band_floor_sane(po->lo, lo) && hi > lo) {
39733971
mod_union_lo = lo;
39743972
mod_union_hi = hi;
39753973
layout.modules_start = lo;

src/rules/module_base_bounds.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ int rule_module_base_bounds(const struct evidence_set *ev,
127127
if (po->lo && po->hi && po->lo <= po->hi) {
128128
unsigned long band_lo = (unsigned long)MODULES_START_FOR(po->lo);
129129
unsigned long band_hi = (unsigned long)MODULES_END_FOR(po->hi);
130-
/* MODULES_START_FOR subtracts, so guard the wrap a low PAGE_OFFSET
131-
* would produce rather than emitting a floor near ULONG_MAX. */
132-
if (band_lo <= po->lo && n < out_max)
130+
/* Guard the wrap an extreme PAGE_OFFSET would produce rather than
131+
* emitting a floor at the wrong end of the address space. */
132+
if (kasld_module_band_floor_sane(po->lo, band_lo) && n < out_max)
133133
mbb_emit(&out[n++], C_LOWER_BOUND, band_lo, 0);
134134
if (band_hi >= po->hi && n < out_max)
135135
mbb_emit(&out[n++], C_UPPER_BOUND, band_hi, 0);

tests/test_kasld.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,12 +1920,12 @@ static void test_engine_sync_module_band_follows_page_offset(void) {
19201920
* KERNEL_VIRT_VAS_START. Arches that carve the module band out of vmalloc
19211921
* put it below PAGE_OFFSET, and on those KERNEL_VIRT_VAS_START *is*
19221922
* PAGE_OFFSET -- clamping there would collapse the band and reject every
1923-
* genuine module leak. Only a wrapped floor (a PAGE_OFFSET window reaching
1924-
* below the band's own width) is rejected, and `moved` is above the
1925-
* compile-time PAGE_OFFSET so it cannot wrap here. */
1923+
* genuine module leak. Only a wrapped floor is rejected, and `moved` sits
1924+
* well inside the address space, so the relation cannot wrap here whether it
1925+
* adds to PAGE_OFFSET or subtracts from it. */
19261926
unsigned long want_lo = MODULES_START_FOR(moved);
19271927
unsigned long want_hi = MODULES_END_FOR(moved);
1928-
assert(want_lo <= moved); /* no wrap in this fixture */
1928+
assert(kasld_module_band_floor_sane(moved, want_lo)); /* no wrap here */
19291929
assert(want_hi > want_lo);
19301930
assert(layout.modules_start == want_lo);
19311931
assert(layout.modules_end == want_hi);

0 commit comments

Comments
 (0)