Skip to content

Commit d313dab

Browse files
committed
rules: require structural provenance for module-derived text bounds
module_text_bound moves Q_VIRT_IMAGE_BASE at the sound floor but accepted REGION_MODULE_REGION, which means "assumed to be a module because it fell inside the band". On both architectures it runs on, that band contains the whole kernel-text range — riscv64 [ffffffde.., ffffffffc0000000] over text [ffffffe0.., ffffffffc0000000], s390 [0, 20000000000000] over text [100000, 20000000000000] — so a range-classified kernel address is indistinguishable from a module one and arrives as a text-base bound. sysfs_iscsi_transport_handle reaches that state: it tests the band before text, so on those arches its kernel-image branch is unreachable and a built-in transport's .data pointer is tagged as module region. On s390 modules sit below the image, so such an address raises the C_LOWER_BOUND above the true _text and carves truth out of the guaranteed window. Read REGION_MODULE only, as module_text_bracket and module_base_bounds already do, removing the class rather than relying on every emitter to filter. The iscsi ordering is left alone and documented: reversing it would tag genuine riscv64 module addresses KERNEL_DATA and feed image_size_text_data_gap the bogus gap that ordering exists to prevent. Where the two ranges overlap, no ordering is correct; what makes the ambiguity harmless is that the weak tag no longer reaches a text base. Separately, module_base_bounds discarded the best answer it had. The mem_init() layout block prints "modules : 0x..." and the landmark parser emits it as POS_BASE on the region — the region's start stated by the kernel itself. Consume it as a pin. This is the one place the weak tag is the right thing to read: with POS_BASE it means "this IS where the band starts", and only the landmark parser emits that pair. The two riscv64 tests that fed module-region leaks now feed REGION_MODULE, which is what proc_modules and sysfs_module_sections emit; they were written before those components were reclassified, and the tag was incidental to what they assert.
1 parent 16da7a2 commit d313dab

5 files changed

Lines changed: 131 additions & 19 deletions

File tree

src/components/sysfs_iscsi_transport_handle.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,18 @@ static enum handle_read read_transport_handle(const char *transport,
207207
* in. Classify by range so a module pointer is tagged REGION_MODULE_REGION, not
208208
* an image region: a KERNEL_DATA tag on a module pointer feeds
209209
* image_size_text_data_gap a bogus (>1 GiB) text..data gap, which pushes the
210-
* Q_VIRT_IMAGE_BASE ceiling below the true base and excludes it. */
210+
* Q_VIRT_IMAGE_BASE ceiling below the true base and excludes it.
211+
*
212+
* The module test comes FIRST deliberately, and must stay that way. Reversing
213+
* it looks tempting on riscv64 and s390, where the module band contains the
214+
* whole kernel-text range and so the second branch below is unreachable -- but
215+
* on those same arches a genuine module address also falls inside that text
216+
* range, so a text-first order would tag real module pointers KERNEL_DATA and
217+
* reproduce exactly the ceiling bug described above. Range classification
218+
* cannot separate the two where the ranges overlap; what makes the ambiguity
219+
* harmless is that REGION_MODULE_REGION no longer reaches any rule that moves
220+
* a text base (module_text_bound and module_text_bracket both require
221+
* REGION_MODULE), so a mis-tag here is presentational. */
211222
static void emit_iscsi_transport(unsigned long addr, const char *name) {
212223
if (kasld_addr_is_module_region(addr))
213224
kasld_result_sample(KASLD_TYPE_VIRT, REGION_MODULE_REGION, addr, name,

src/rules/module_base_bounds.c

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
// Rule: bound Q_MODULE_BASE — the lowest address the module allocator can hand
44
// out — from module observations and from the architecture's module band.
55
//
6-
// Two independent bounds, either of which may be absent:
6+
// Three independent sources, any of which may be absent:
7+
//
8+
// PIN, from the kernel's own layout block: a POS_BASE observation of the
9+
// module region states where the region starts, so it fixes the quantity
10+
// outright rather than bounding it.
711
//
812
// UPPER, from evidence: the allocator cannot have returned an address below
913
// its own region, so the lowest observed module address is an upper bound on
@@ -19,13 +23,16 @@
1923
// the compile-time split only, and on a moved VMSPLIT they name a place the
2024
// modules are not.
2125
//
22-
// PROVENANCE: reads VIRT REGION_MODULE only, never REGION_MODULE_REGION. A
23-
// range-classified address is inside the band by construction, so bounding the
24-
// base with it looks safe — but that safety is inherited from the band, and on
25-
// an arch whose band spans most of the address space (arm64) it is worth
26-
// nothing. Requiring structural provenance keeps the rule sound on its own
27-
// terms rather than on a per-arch argument, and keeps it sound if the module
28-
// base is ever coupled to another quantity. See the region note in api.h.
26+
// PROVENANCE: module ADDRESSES are read as REGION_MODULE only, never
27+
// REGION_MODULE_REGION. A range-classified address is inside the band by
28+
// construction, so bounding the base with it looks safe — but that safety is
29+
// inherited from the band, and on an arch whose band spans most of the address
30+
// space (arm64) it is worth nothing. Requiring structural provenance keeps the
31+
// rule sound on its own terms rather than on a per-arch argument.
32+
//
33+
// The one exception is a POS_BASE observation OF THE REGION, which is not an
34+
// address that might be a module but the region's own start — see the pin
35+
// below. See the region note in api.h.
2936
// ---
3037
// <bcoles@gmail.com>
3138

@@ -61,6 +68,34 @@ int rule_module_base_bounds(const struct evidence_set *ev,
6168
if (out_max < 1)
6269
return 0;
6370

71+
/* --- The region base, stated outright ---------------------------------
72+
* A POS_BASE observation of the module REGION is not a module address at
73+
* all: it is the region's own start, which the kernel prints in its
74+
* mem_init() layout block ("modules : 0x..."). That is the most direct
75+
* possible answer to this quantity, so it pins rather than bounds.
76+
*
77+
* This is the one place REGION_MODULE_REGION is the RIGHT tag to read.
78+
* Elsewhere the weak tag means "assumed to be a module because it fell in
79+
* the band"; combined with POS_BASE it means "this IS where the band
80+
* starts", and only the landmark parser emits that pair -- every other
81+
* band emitter reports interior samples. */
82+
for (int i = 0; i < ev->n_obs; i++) {
83+
const struct observation *o = &ev->obs[i];
84+
if (!o->valid || o->value_kind != OBS_ADDRESS ||
85+
o->eff_type != KASLD_TYPE_VIRT || o->pos != POS_BASE)
86+
continue;
87+
if (o->eff_region != REGION_MODULE_REGION)
88+
continue;
89+
unsigned long a = obs_anchor(o);
90+
if (!a)
91+
continue;
92+
if (n < out_max)
93+
mbb_emit(&out[n++], C_LOWER_BOUND, a, o->id);
94+
if (n < out_max)
95+
mbb_emit(&out[n++], C_UPPER_BOUND, a, o->id);
96+
break;
97+
}
98+
6499
/* --- Upper bound from the lowest structurally-known module address ----- */
65100
unsigned long vmod_lo = ULONG_MAX;
66101
uint32_t lo_src = 0;

src/rules/module_text_bound.c

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
// virt_kaslr_align
1717
// + IMAGE_BASE_OFFSET
1818
//
19-
// Reads VIRT REGION_MODULE / REGION_MODULE_REGION leaks; aligns to the
20-
// resolved Q_VIRT_KASLR_ALIGN. Inert where MODULES_RELATIVE_TO_TEXT==0, and
21-
// inert when no module-region observation is present.
19+
// Reads VIRT REGION_MODULE leaks ONLY -- never REGION_MODULE_REGION; see the
20+
// provenance note at the filter below. Aligns to the resolved
21+
// Q_VIRT_KASLR_ALIGN. Inert where MODULES_RELATIVE_TO_TEXT==0, and inert when
22+
// no structurally-known module observation is present.
2223
// ---
2324
// <bcoles@gmail.com>
2425

@@ -50,7 +51,21 @@ int rule_module_text_bound(const struct evidence_set *ev,
5051
if (!o->valid || o->value_kind != OBS_ADDRESS ||
5152
o->eff_type != KASLD_TYPE_VIRT)
5253
continue;
53-
if (o->eff_region != REGION_MODULE && o->eff_region != REGION_MODULE_REGION)
54+
/* REGION_MODULE only, never REGION_MODULE_REGION. This rule moves
55+
* Q_VIRT_IMAGE_BASE at the sound floor, so it may consume only addresses
56+
* whose source KNOWS they belong to a module -- not ones classified as
57+
* module because they fell inside the band. On both arches this rule runs
58+
* on, the band CONTAINS the whole kernel-text range (riscv64
59+
* [ffffffde.., ffffffffc0000000] over text [ffffffe0.., ffffffffc0000000];
60+
* s390 [0, 20000000000000] over text [100000, 20000000000000]), so a
61+
* range-classified kernel address is indistinguishable from a module one
62+
* and reaches here as a text-base bound. On s390 that is unsound in the
63+
* dangerous direction: modules sit BELOW the image, so a kernel .data
64+
* address raises the C_LOWER_BOUND above the true _text and carves truth
65+
* out of the guaranteed window. Requiring structural provenance removes
66+
* the whole class, rather than relying on every emitter to filter -- see
67+
* the region note in api.h. */
68+
if (o->eff_region != REGION_MODULE)
5469
continue;
5570
unsigned long a = obs_anchor(o);
5671
if (a < vmod_lo) {

tests/test_engine.c

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6962,6 +6962,51 @@ static void test_module_text_bracket_real_arm64_witness(void) {
69626962
#endif
69636963
}
69646964

6965+
/* module_text_bound must not take a range-classified address. This rule moves
6966+
* Q_VIRT_IMAGE_BASE at the sound floor, and on BOTH arches it runs on the
6967+
* module band contains the whole kernel-text range — so a kernel address that
6968+
* some component classified by range is indistinguishable from a module one.
6969+
* On s390 that direction is unsound: modules sit below the image, so such an
6970+
* address raises the lower bound above the true _text. Asserted against a run
6971+
* with no observation at all, so it holds on every arch. */
6972+
static void test_module_text_bound_ignores_range_classified(void) {
6973+
struct engine e;
6974+
const rule_fn rules[] = {rule_kaslr_align_arch_default,
6975+
rule_module_text_bound};
6976+
engine_init(&e);
6977+
engine_run(&e, rules, 2);
6978+
unsigned long bare_lo = e.est[Q_VIRT_IMAGE_BASE].lo;
6979+
unsigned long bare_hi = e.est[Q_VIRT_IMAGE_BASE].hi;
6980+
6981+
engine_init(&e);
6982+
struct estimate top;
6983+
quantities[Q_VIRT_IMAGE_BASE].init_top(&top);
6984+
struct observation m =
6985+
mk_obs(KASLD_TYPE_VIRT, REGION_MODULE_REGION, top.lo + 0x100000ul,
6986+
LO_SET | SAMPLE_SET, POS_INTERIOR, CONF_PARSED);
6987+
evidence_add(&e.ev, &m);
6988+
engine_run(&e, rules, 2);
6989+
assert(e.est[Q_VIRT_IMAGE_BASE].lo == bare_lo);
6990+
assert(e.est[Q_VIRT_IMAGE_BASE].hi == bare_hi);
6991+
}
6992+
6993+
/* The kernel prints its own module-region start in the mem_init() layout block,
6994+
* which the landmark parser emits as POS_BASE on the REGION. That is the region
6995+
* itself, not an address that might be a module, so it pins the quantity. The
6996+
* one place the weak tag is the correct thing to read. */
6997+
static void test_module_base_pinned_by_region_landmark(void) {
6998+
struct engine e;
6999+
engine_init(&e);
7000+
unsigned long band_base = (unsigned long)MODULES_START + 0x200000ul;
7001+
struct observation o = mk_obs(KASLD_TYPE_VIRT, REGION_MODULE_REGION,
7002+
band_base, LO_SET, POS_BASE, CONF_PARSED);
7003+
evidence_add(&e.ev, &o);
7004+
const rule_fn rules[] = {rule_module_base_bounds};
7005+
engine_run(&e, rules, 1);
7006+
assert(e.est[Q_MODULE_BASE].lo == band_base);
7007+
assert(e.est[Q_MODULE_BASE].hi == band_base);
7008+
}
7009+
69657010
/* module_base_bounds. MODULES_START + 1 MiB is inside the module band on every
69667011
* supported arch, so one address exercises the rule everywhere without a
69677012
* per-arch table. */
@@ -7450,7 +7495,10 @@ static void test_riscv64_text_base_legacy(void) {
74507495
* legacy-region bound (the WIDE min), or the window stays uselessly wide. */
74517496
unsigned long vmod = legacy_po - 0x7f6de000ul; /* ~just under 2 GiB below */
74527497
struct observation mod =
7453-
mk_obs(KASLD_TYPE_VIRT, REGION_MODULE_REGION, vmod, LO_SET | SAMPLE_SET,
7498+
/* REGION_MODULE: a real module leak, which is what proc_modules and
7499+
sysfs_module_sections emit. module_text_bound requires structural
7500+
provenance -- a range-classified address must not move a text base. */
7501+
mk_obs(KASLD_TYPE_VIRT, REGION_MODULE, vmod, LO_SET | SAMPLE_SET,
74547502
POS_INTERIOR, CONF_PARSED);
74557503
evidence_add(&e.ev, &mod);
74567504
const rule_fn rules[] = {rule_lobound_page_offset, rule_riscv64_text_base,
@@ -7763,6 +7811,8 @@ int main(void) {
77637811
RUN(test_module_text_bracket_contains_truth);
77647812
RUN(test_module_text_bracket_ignores_range_classified);
77657813
RUN(test_module_text_bracket_real_arm64_witness);
7814+
RUN(test_module_text_bound_ignores_range_classified);
7815+
RUN(test_module_base_pinned_by_region_landmark);
77667816
RUN(test_module_base_upper_from_observation);
77677817
RUN(test_module_base_ignores_range_classified);
77687818
RUN(test_module_base_band_bounds);

tests/test_engine_integration.c

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,11 +1096,12 @@ static void test_full_engine_riscv64_legacy_no_kaslr(void) {
10961096
0xffffffd800000000ul, CONF_INFERRED, NULL);
10971097
add_addr(&e, KASLD_TYPE_VIRT, REGION_PAGE_OFFSET, 0xffffffe000000000ul, 0,
10981098
NULL);
1099-
/* module-region leaks (~2 GiB below text). */
1100-
add_addr(&e, KASLD_TYPE_VIRT, REGION_MODULE_REGION, 0xffffffdf80922000ul, 0,
1101-
NULL);
1102-
add_addr(&e, KASLD_TYPE_VIRT, REGION_MODULE_REGION, 0xffffffdf80d99000ul, 0,
1103-
NULL);
1099+
/* Module leaks (~2 GiB below text), REGION_MODULE as proc_modules and
1100+
* sysfs_module_sections emit them: module_text_bound requires structural
1101+
* provenance, since on this arch the module band contains the whole text
1102+
* range and a range-classified address is indistinguishable from a module. */
1103+
add_addr(&e, KASLD_TYPE_VIRT, REGION_MODULE, 0xffffffdf80922000ul, 0, NULL);
1104+
add_addr(&e, KASLD_TYPE_VIRT, REGION_MODULE, 0xffffffdf80d99000ul, 0, NULL);
11041105
/* DRAM. */
11051106
add_addr(&e, KASLD_TYPE_PHYS, REGION_RAM, 0x80000000ul, 0, NULL);
11061107
add_addr_top(&e, KASLD_TYPE_PHYS, REGION_RAM, 0x9fe00000ul);

0 commit comments

Comments
 (0)