Skip to content

Commit c177904

Browse files
committed
components: recover the kernel image base from kernfs seek cookies
kernfs derives each /sys directory entry's readdir seek cookie (d_off) from a name hash salted with the entry's namespace tag. For a reader in the initial network namespace the tag of every /sys/class/net entry is &init_net, a KASLR-linked kernel-image global, so the cookies leak a hash of it. The name-hash fold is affine in the salt, so testing each candidate kernel base recovers the &init_net that reproduces every observed cookie; a per-build init_net-from-_text offset then pins the image base. The value is a seek cookie, not a %p, so it is independent of kptr_restrict, and a plain readdir needs no privilege. Fixed upstream in v7.0 (salt switched to the namespace id); live on every earlier kernel. Recovery searches the KASLR base grid and runs where that grid is tractable, folding as hash_32 or hash_64 by word size. s390x and ppc64/ppc64le, whose windows hold billions of bases, are a sound no-op, as is a 32-bit build whose ±2^31 cookie alias is also in-window.
1 parent b384435 commit c177904

7 files changed

Lines changed: 6276 additions & 4 deletions

File tree

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,15 @@ $(TEST_KCORE_BIN): $(TEST_DIR)/test_kcore.c $(SRC_DIR)/components/proc_kcore.c $
639639
$(call ccv,CCLD,$@)
640640
$(Q)$(CC) $(TEST_ALL_CFLAGS) $(ALL_LDFLAGS) -I$(SRC_DIR) $(TEST_DIR)/test_kcore.c -o $@
641641

642+
# kernfs_ns_hash salt recovery: the component #included (main renamed) and its
643+
# pure recovery functions exercised over synthesised cookies — unique recovery,
644+
# the offset-table base pin, the patched-kernel no-op, and salt discrimination.
645+
# Host-agnostic (no live getdents64; the seek-cookie path cannot be staged).
646+
TEST_KERNFS_BIN := $(TEST_OBJ_DIR)/test_kernfs_ns_hash
647+
$(TEST_KERNFS_BIN): $(TEST_DIR)/test_kernfs_ns_hash.c $(SRC_DIR)/components/kernfs_ns_hash.c $(HDRS) | $(TEST_OBJ_DIR)
648+
$(call ccv,CCLD,$@)
649+
$(Q)$(CC) $(TEST_ALL_CFLAGS) $(ALL_LDFLAGS) -I$(SRC_DIR) $(TEST_DIR)/test_kernfs_ns_hash.c -o $@
650+
642651
# sysfs / ACPI / DT leak-parser tests: each component #included (main renamed)
643652
# and driven over a staged KASLD_SYSROOT fixture tree reproducing the kernel ABI.
644653
TEST_PARSERS_SRCS := $(SRC_DIR)/components/sysfs_efi_runtime_map.c \
@@ -697,7 +706,8 @@ TEST_ALL_BINS := $(TEST_BIN) \
697706
$(TEST_DMESG_RESV_BIN) \
698707
$(TEST_BPE820_BIN) \
699708
$(TEST_PARSERS_BIN) \
700-
$(TEST_KCORE_BIN)
709+
$(TEST_KCORE_BIN) \
710+
$(TEST_KERNFS_BIN)
701711

702712
$(TEST_ALL_BINS): $(TEST_HDRS)
703713

src/components/kernfs_ns_hash.c

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.

src/components/offsets/kernfs_ns_hash.inc

Lines changed: 5735 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/check-live-probes

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ COMP=$ROOT/src/components
5858
# (ioctls the running kernel's GPU device)
5959
# /dev/snd/seq — ALSA sequencer data.ext.ptr leak (drives
6060
# the running kernel's sequencer)
61-
SIG='perf_event_open|SYS_perf_event_open|__NR_perf_event_open|rdtsc|sidechannel\.h|mincore[[:space:]]*\(|sidt|/proc/self/syscall|getppid[[:space:]]*\(|PF_CAN|AF_CAN|NILFS_IOCTL|FBIOGET_FSCREENINFO|TIOCGSERIAL|pppd|SYS_bpf|BPF_PROG_LOAD|KBASE_IOCTL|/dev/mali|/dev/snd/seq'
61+
# SYS_getdents64 — kernfs seek-cookie leak (reads the running
62+
# kernel's live /sys/class/net directory)
63+
SIG='perf_event_open|SYS_perf_event_open|__NR_perf_event_open|rdtsc|sidechannel\.h|mincore[[:space:]]*\(|sidt|/proc/self/syscall|getppid[[:space:]]*\(|PF_CAN|AF_CAN|NILFS_IOCTL|FBIOGET_FSCREENINFO|TIOCGSERIAL|pppd|SYS_bpf|BPF_PROG_LOAD|KBASE_IOCTL|/dev/mali|/dev/snd/seq|SYS_getdents64'
6264

6365
# Reviewed live probes whose liveness is not greppable: mmap VA-boundary sweeps
6466
# (mmap alone is too common a call to signature on) and the iret CPU probe. Keep

tests/check-text-provenance

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ allow_file() {
6262
proc_kallsyms.c) return 0 ;; # _stext, read by name
6363
pppd_kallsyms.c) return 0 ;; # a named symbol's address
6464
bpf_verifier_ksym.c) return 0 ;; # ksym resolved through the uname-keyed table
65+
kernfs_ns_hash.c) return 0 ;; # &init_net, the unique salt of the net-tagged sysfs hashes
6566
dmesg_android_ion_snapshot.c) return 0 ;; # the last_ion_buf symbol's address
6667
dmesg_mmu_idmap.c) return 0 ;; # virt_to_phys(__idmap_text), a named section
6768
# An instruction address: whatever executed was text.
@@ -133,7 +134,7 @@ done
133134

134135
# An allowlist entry naming a component that no longer claims text is stale, and
135136
# a stale entry is how the next real offender gets waved through.
136-
for stale in proc_kallsyms pppd_kallsyms bpf_verifier_ksym dmesg_android_ion_snapshot \
137+
for stale in proc_kallsyms pppd_kallsyms bpf_verifier_ksym kernfs_ns_hash dmesg_android_ion_snapshot \
137138
dmesg_mmu_idmap perf_event_open perf_lbr_sampling perf_amd_branch_user \
138139
perf_text_poke_leak \
139140
dmesg_backtrace dmesg_ex_handler_msr qemu_tcg_iret proc_stat_wchan proc_kcore \

tests/run-all

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ else
2828
fi
2929

3030
# Suite binaries (smallest first).
31-
SUITES="test_evidence test_estimate test_align test_addr_parse test_target_width test_task_size test_prefetch_scan test_cpu test_outcome test_text_order test_kernel_image test_engine_integration test_kasld test_render test_engine test_dmesg_layout test_dmesg_backtrace test_boot_config test_dmesg_kaslr_disabled test_sysfs_devicetree_memory test_proc_net_sock_ptr test_proc_timer_list test_proc_kallsyms test_tracefs_available_filter_addrs test_btf test_dmesg_reservations test_boot_params_e820 test_sysfs_parsers test_kcore"
31+
SUITES="test_evidence test_estimate test_align test_addr_parse test_target_width test_task_size test_prefetch_scan test_cpu test_outcome test_text_order test_kernel_image test_engine_integration test_kasld test_render test_engine test_dmesg_layout test_dmesg_backtrace test_boot_config test_dmesg_kaslr_disabled test_sysfs_devicetree_memory test_proc_net_sock_ptr test_proc_timer_list test_proc_kallsyms test_tracefs_available_filter_addrs test_btf test_dmesg_reservations test_boot_params_e820 test_sysfs_parsers test_kcore test_kernfs_ns_hash"
3232

3333
total_pass=0
3434
total_count=0

tests/test_kernfs_ns_hash.c

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// This file is part of KASLD - https://github.com/bcoles/kasld
2+
//
3+
// Unit tests for kernfs_ns_hash's salt recovery, driven directly over the
4+
// component's pure functions (the live getdents64 path cannot be staged through
5+
// a KASLD_SYSROOT, which stages file contents, not directory seek cookies).
6+
//
7+
// Pins four behaviours:
8+
// 1. Unique recovery. Cookies synthesised from a known kernel-VA salt recover
9+
// exactly that salt from the surrounding window.
10+
// 2. Offset-table pin. Walking the KASLR base grid with a build offset
11+
// recovers base + off, so the caller derives the true base.
12+
// 3. Patched no-op. Cookies salted with a small ns_id (the post-fix salt)
13+
// have NO kernel-VA solution, so recovery emits nothing.
14+
// 4. Discrimination. A wrong salt does not reproduce the cookies.
15+
// ---
16+
// <bcoles@gmail.com>
17+
#define _GNU_SOURCE
18+
19+
int kernfs_ns_hash_main(int argc, char **argv);
20+
#define main kernfs_ns_hash_main
21+
#include "../src/components/kernfs_ns_hash.c"
22+
#undef main
23+
24+
#include "test_harness.h"
25+
26+
#include <assert.h>
27+
#include <stdint.h>
28+
#include <string.h>
29+
30+
/* Build the (name, cookie) constraints a reader would harvest if every entry
31+
* were tagged with `salt`. */
32+
static int build_pairs(unsigned long salt, const char *const *names, int n,
33+
struct kernfs_pair *pr) {
34+
for (int i = 0; i < n; i++) {
35+
kernfs_affine(names[i], &pr[i].pow11, &pr[i].cname);
36+
pr[i].hash = kernfs_fold((uint64_t)salt * pr[i].pow11 + pr[i].cname);
37+
}
38+
return n;
39+
}
40+
41+
static const char *const NAMES[] = {"lo", "ens33", "docker0", "virbr0"};
42+
43+
/* A word-aligned synthetic &init_net and a 2 MiB-aligned base, sized to the
44+
* target word width so the 32-bit builds (run under test-cross) exercise the
45+
* hash_32 fold and the 64-bit build exercises hash_64. The values are
46+
* arbitrary; only their recovery matters. */
47+
#if __SIZEOF_LONG__ == 8
48+
#define SALT_A 0xffffffff8e773d40UL
49+
#define BASE_B 0xffffffff8c000000UL
50+
#else
51+
#define SALT_A 0x8e773d40UL
52+
#define BASE_B 0x8c000000UL
53+
#endif
54+
55+
/* Cookies from a known salt recover exactly that salt from its window. */
56+
static void test_recovers_known_salt(void) {
57+
struct kernfs_pair pr[4];
58+
int n = build_pairs(SALT_A, NAMES, 3, pr);
59+
unsigned long got = 0;
60+
int ok = kernfs_scan(pr, n, SALT_A - 0x2000, SALT_A + 0x2000, sizeof(long), 0,
61+
&got);
62+
assert(ok == 1);
63+
assert(got == SALT_A);
64+
}
65+
66+
/* With a build offset, walking the aligned base grid recovers base + off. */
67+
static void test_offset_table_pin(void) {
68+
unsigned long base = BASE_B; /* 2 MiB aligned */
69+
uint32_t off = 0x00773d40; /* init_net - _text (word aligned) */
70+
unsigned long salt = base + off;
71+
struct kernfs_pair pr[4];
72+
int n = build_pairs(salt, NAMES, 3, pr);
73+
unsigned long align = 0x200000UL;
74+
unsigned long got = 0;
75+
int ok =
76+
kernfs_scan(pr, n, base - 4 * align, base + 4 * align, align, off, &got);
77+
assert(ok == 1);
78+
assert(got == salt); /* == &init_net */
79+
assert(got - off == base); /* caller derives the true base */
80+
}
81+
82+
/* Post-fix the salt is a small ns_id integer; no kernel-VA candidate reproduces
83+
* those cookies, so recovery finds nothing (a sound no-op). */
84+
static void test_patched_is_noop(void) {
85+
unsigned long ns_id = 0x4002; /* small non-pointer salt */
86+
struct kernfs_pair pr[4];
87+
int n = build_pairs(ns_id, NAMES, 3, pr);
88+
unsigned long got = 0;
89+
int ok = kernfs_scan(pr, n, SALT_A - 0x4000, SALT_A + 0x4000, sizeof(long), 0,
90+
&got);
91+
assert(ok == 0);
92+
}
93+
94+
/* The true salt passes the cookie check; an adjacent one does not. */
95+
static void test_wrong_salt_rejected(void) {
96+
struct kernfs_pair pr[4];
97+
int n = build_pairs(SALT_A, NAMES, 3, pr);
98+
assert(kernfs_salt_ok(SALT_A, pr, n) == 1);
99+
assert(kernfs_salt_ok(SALT_A + sizeof(long), pr, n) == 0);
100+
assert(kernfs_salt_ok(SALT_A - sizeof(long), pr, n) == 0);
101+
}
102+
103+
int main(void) {
104+
TEST_SUITE("kernfs_ns_hash");
105+
BEGIN_CATEGORY("salt recovery");
106+
RUN(test_recovers_known_salt);
107+
RUN(test_offset_table_pin);
108+
BEGIN_CATEGORY("soundness");
109+
RUN(test_patched_is_noop);
110+
RUN(test_wrong_salt_rejected);
111+
return TEST_DONE();
112+
}

0 commit comments

Comments
 (0)