Skip to content

Commit 11f9e3f

Browse files
committed
components: read the image base and a direct-map bound from debugfs
Two components read /sys/kernel/debug, reachable on a relaxed mount, a container bind-mount, or a privileged vantage. ptdump_kernel_page_tables parses the CONFIG_PTDUMP_DEBUGFS page-table walk: in the x86 high kernel mapping an unmapped gap precedes the image, so the first mapped run, found via the protection column, begins at _text and is pinned as the image base at parsed confidence. It declines where no mapped text run is identifiable, since the dump format carries no ABI guarantee, and is gated to the arches whose text has a dedicated high mapping. kmemleak reads the leaked-object report, whose addresses print raw with no kptr gate; the lowest direct-map object, classified through kasld_addr_classify rather than a window test, bounds page_offset from above in the likely window and needs objects to be present. Both split absence from denial on errno and emit a disposition on every gated path. check-text-provenance allowlists ptdump: the marker and first mapped run establish image membership by structure, as proc_kcore's phdr does.
1 parent ccf036a commit 11f9e3f

3 files changed

Lines changed: 288 additions & 0 deletions

File tree

src/components/kmemleak.c

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// This file is part of KASLD - https://github.com/bcoles/kasld
2+
//
3+
// Leak a direct-map witness from the kmemleak debugfs report.
4+
//
5+
// With CONFIG_DEBUG_KMEMLEAK the kernel exposes /sys/kernel/debug/kmemleak.
6+
// Each currently-unreferenced (leaked) object is reported as:
7+
//
8+
// unreferenced object 0x<addr> (size <n>):
9+
// <backtrace ...>
10+
//
11+
// The object address is printed raw (0x%08lx) with no kptr_restrict / %pK /
12+
// kallsyms_show_value gate; access is bounded only by the file mode (0644) and
13+
// the debugfs mount. Because the file is 0644, it is the one address-bearing
14+
// debugfs file that becomes readable to an unprivileged process when the mount
15+
// is relaxed (mode=/gid=), where the 0400 files stay root-only.
16+
//
17+
// A leaked slab object lives in the linear (direct) map, so its virtual address
18+
// is an interior witness of the direct map: page_offset_base <= addr. The
19+
// lowest such address bounds the direct-map base from above. This is a bound,
20+
// not a pin — the object sits an unknown distance above the base — so it is
21+
// emitted as an interior sample, never a base.
22+
//
23+
// Data leaked: an interior direct-map virtual address (bounds
24+
// page_offset_base from above)
25+
// Kernel subsystem: mm/kmemleak — the leaked-object report
26+
// Address type: virtual (direct map)
27+
// Method: parsed (debugfs text report)
28+
// Gate: file mode 0644 and the debugfs mount (0700 by default);
29+
// no kptr_restrict / kallsyms_show_value check.
30+
// Config: CONFIG_DEBUG_KMEMLEAK; requires leaked objects to be
31+
// present (the report is empty on a system with none).
32+
//
33+
// Non-direct-map objects (vmalloc, percpu, physical) are skipped: they give no
34+
// clean bound on a quantity resolved here. The report is frequently empty, in
35+
// which case the component declines.
36+
// ---
37+
// <bcoles@gmail.com>
38+
39+
#define _GNU_SOURCE
40+
#include "include/kasld/api.h"
41+
#include "include/kasld/cli.h"
42+
43+
#include <errno.h>
44+
#include <fcntl.h>
45+
#include <stdio.h>
46+
#include <string.h>
47+
#include <unistd.h>
48+
49+
KASLD_EXPLAIN(
50+
"Reads /sys/kernel/debug/kmemleak, the report of currently-leaked kernel "
51+
"objects. Each 'unreferenced object 0x<addr>' line carries a raw object "
52+
"address with no kptr_restrict gate. A leaked slab object lives in the "
53+
"direct map, so the lowest reported direct-map address bounds "
54+
"page_offset_base from above. Requires CONFIG_DEBUG_KMEMLEAK and leaked "
55+
"objects present; access is bounded by the 0644 file mode and the debugfs "
56+
"mount.");
57+
58+
KASLD_META("method:parsed\n"
59+
"phase:inference\n"
60+
"discloses:virtual\n"
61+
"config:CONFIG_DEBUG_KMEMLEAK\n");
62+
63+
int main(int argc, char **argv) {
64+
kasld_cli(argc, argv);
65+
66+
int fd = kasld_open("/sys/kernel/debug/kmemleak", O_RDONLY);
67+
if (fd < 0) {
68+
if (errno == EACCES || errno == EPERM)
69+
return kasld_disp_mitigation_denied(
70+
"debugfs", "/sys/kernel/debug/kmemleak not readable");
71+
return kasld_disp_absent("no kmemleak report (CONFIG_DEBUG_KMEMLEAK)");
72+
}
73+
FILE *f = fdopen(fd, "r");
74+
if (!f) {
75+
close(fd);
76+
return kasld_disp_inconclusive("could not read the kmemleak report");
77+
}
78+
79+
char line[1024];
80+
unsigned long lowest = ~0ul;
81+
int found = 0;
82+
while (fgets(line, sizeof(line), f)) {
83+
char *p = strstr(line, "unreferenced object");
84+
if (!p)
85+
continue;
86+
/* Skip past any qualifier (e.g. "(percpu)") to the address token. */
87+
p = strstr(p, "0x");
88+
if (!p)
89+
continue;
90+
unsigned long addr;
91+
if (!kasld_addr_parse(p + 2, 16, &addr, NULL))
92+
continue;
93+
/* Decide the region from the classifier, not a bare window test: the
94+
* direct-map window overlaps the text band on some arches, so a range test
95+
* could misattribute a text address as direct-map. Only a clean direct-map
96+
* classification is kept; a vmalloc/percpu object, or an address ambiguous
97+
* with the text band, is skipped. */
98+
if (kasld_addr_classify(addr) != REGION_DIRECTMAP_BAND)
99+
continue;
100+
if (addr < lowest)
101+
lowest = addr;
102+
found = 1;
103+
}
104+
fclose(f);
105+
106+
if (!found)
107+
return kasld_disp_inconclusive(
108+
"no direct-map objects in the kmemleak report");
109+
110+
kasld_found("lowest kmemleak direct-map object: 0x%016lx", lowest);
111+
/* A range-classified witness, not a source-established one: emitted as the
112+
* band region, which bounds page_offset from above in the likely window. */
113+
kasld_result_sample(KASLD_TYPE_VIRT, REGION_DIRECTMAP_BAND, lowest, NULL,
114+
CONF_PARSED);
115+
return 0;
116+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// This file is part of KASLD - https://github.com/bcoles/kasld
2+
//
3+
// Leak the kernel virtual text base (_text) from the debugfs page-table dump.
4+
//
5+
// With CONFIG_PTDUMP_DEBUGFS=y the kernel exposes a walk of init_mm's page
6+
// tables. On x86 it is /sys/kernel/debug/page_tables/kernel; on arm64
7+
// /sys/kernel/debug/kernel_page_tables. The walker prints one line per
8+
// contiguous run of same-protection mappings as a raw virtual range
9+
// "0x<start>-0x<end>", with region headers "---[ name ]---" between runs. The
10+
// addresses are printed raw (%lx), with no kptr_restrict / kallsyms_show_value
11+
// gate — the only access control is the file mode (0400) and the debugfs mount.
12+
//
13+
// The randomized text base is recovered from the x86 "High Kernel Mapping"
14+
// region. That region begins at the fixed __START_KERNEL_map and holds an
15+
// unmapped gap [__START_KERNEL_map, _text) followed by the mapped kernel image
16+
// [_text, ...) — the gap width is exactly the KASLR slide. The run addresses
17+
// alone do not distinguish the gap from the image (both are printed), so the
18+
// image is located by the protection column: a present entry prints one of
19+
// "RW "/"ro ", a non-present entry prints only spaces. The first run in the
20+
// region that carries a protection flag starts at _text.
21+
//
22+
// Data leaked: _text (virtual text base)
23+
// Kernel subsystem: mm/ptdump + arch page-table dumper
24+
// Address type: virtual (kernel text)
25+
// Method: parsed (debugfs page-table dump)
26+
// Gate: file mode 0400 and the debugfs mount (0700 by default);
27+
// no kptr_restrict / kallsyms_show_value check. Reachable
28+
// from an already-privileged vantage, a debugfs mount
29+
// relaxed by mode=/gid=, or a container bind-mount.
30+
// Config: CONFIG_PTDUMP_DEBUGFS
31+
//
32+
// The dump format carries no ABI guarantee, so parsing is conservative: a base
33+
// is pinned only when a mapped run is unambiguously located inside the kernel-
34+
// text window; anything else declines rather than risk a wrong pin. Only the
35+
// text base is recoverable — the direct-map / vmalloc / vmemmap region bases
36+
// are held in the walker's marker table and never printed, and the first mapped
37+
// run of those regions sits above the region base, so no exact base is
38+
// available there. proc_kcore recovers the same text base (and page_offset)
39+
// from a stable binary format; this covers the vantage where kcore is masked
40+
// but debugfs is readable.
41+
//
42+
// Sound only where the kernel text has a dedicated high mapping distinct from
43+
// the direct map (TEXT_TRACKS_DIRECTMAP == 0). On arm64 the kernel image is
44+
// mapped inside the vmalloc region with no distinct header, so its runs cannot
45+
// be told from module/vmalloc runs — the parse finds no "High Kernel Mapping"
46+
// region and declines there.
47+
// ---
48+
// <bcoles@gmail.com>
49+
50+
#define _GNU_SOURCE
51+
#include "include/kasld/api.h"
52+
#include "include/kasld/cli.h"
53+
54+
#include <errno.h>
55+
#include <fcntl.h>
56+
#include <stdio.h>
57+
#include <string.h>
58+
#include <unistd.h>
59+
60+
KASLD_EXPLAIN(
61+
"Reads the debugfs page-table dump (/sys/kernel/debug/page_tables/kernel "
62+
"on "
63+
"x86, kernel_page_tables on arm64), a raw walk of the kernel page tables "
64+
"with CONFIG_PTDUMP_DEBUGFS. In the x86 'High Kernel Mapping' region an "
65+
"unmapped gap precedes the kernel image, so the first mapped run — found "
66+
"via "
67+
"the protection column — starts at the randomized _text. Addresses are "
68+
"printed raw with no kptr_restrict gate; access is bounded by the 0400 "
69+
"file "
70+
"mode and the debugfs mount.");
71+
72+
KASLD_META("method:parsed\n"
73+
"phase:inference\n"
74+
"discloses:virtual\n"
75+
"config:CONFIG_PTDUMP_DEBUGFS\n");
76+
77+
#if !TEXT_TRACKS_DIRECTMAP
78+
79+
/* x86 dumps init_mm under page_tables/kernel; arm64 uses kernel_page_tables.
80+
* The arm64 file is opened too, but its output carries no "High Kernel Mapping"
81+
* region, so the parse below declines on it. */
82+
static const char *const PATHS[] = {
83+
"/sys/kernel/debug/page_tables/kernel",
84+
"/sys/kernel/debug/kernel_page_tables",
85+
NULL,
86+
};
87+
88+
/* A present page-table entry prints exactly one of "RW "/"ro " in the
89+
* protection column; a non-present entry prints only spaces. Either token
90+
* marks a mapped run. */
91+
static int line_is_mapped(const char *line) {
92+
return strstr(line, "RW ") != NULL || strstr(line, "ro ") != NULL;
93+
}
94+
95+
int main(int argc, char **argv) {
96+
kasld_cli(argc, argv);
97+
98+
FILE *f = NULL;
99+
int denied = 0;
100+
for (int i = 0; PATHS[i]; i++) {
101+
int fd = kasld_open(PATHS[i], O_RDONLY);
102+
if (fd < 0) {
103+
if (errno == EACCES || errno == EPERM)
104+
denied = 1;
105+
continue;
106+
}
107+
f = fdopen(fd, "r");
108+
if (!f) {
109+
close(fd);
110+
continue;
111+
}
112+
kasld_info("reading kernel page-table dump from %s", PATHS[i]);
113+
break;
114+
}
115+
if (!f)
116+
return denied ? kasld_disp_mitigation_denied(
117+
"debugfs", "kernel page-table dump not readable")
118+
: kasld_disp_absent(
119+
"no kernel page-table dump (CONFIG_PTDUMP_DEBUGFS)");
120+
121+
char line[512];
122+
int in_text_region = 0;
123+
unsigned long text = 0;
124+
while (fgets(line, sizeof(line), f)) {
125+
/* Region header: "---[ name ]---". Only the x86 high-kernel-image region
126+
* bounds the randomized text run. */
127+
if (strstr(line, "---[")) {
128+
in_text_region = strstr(line, "High Kernel Mapping") != NULL;
129+
continue;
130+
}
131+
if (!in_text_region)
132+
continue;
133+
134+
/* Run lines start with the raw range "0x<start>-0x<end>"; headers and
135+
* "... skipped ..." lines do not. Only the start is needed. */
136+
if (strncmp(line, "0x", 2) != 0)
137+
continue;
138+
unsigned long a;
139+
const char *end;
140+
if (!kasld_addr_parse(line + 2, 16, &a, &end) || *end != '-')
141+
continue;
142+
/* First mapped run in the region starts at _text (nothing below _text in
143+
* the high mapping is mapped). An unmapped gap line is skipped. */
144+
if (line_is_mapped(line)) {
145+
text = a;
146+
break;
147+
}
148+
}
149+
fclose(f);
150+
151+
if (text == 0 || !kasld_addr_is_kernel_text(text))
152+
return kasld_disp_inconclusive(
153+
"no mapped kernel-text run in the page-table dump");
154+
155+
kasld_found("kernel _text from page-table dump: 0x%lx", text);
156+
/* The first mapped run of the high kernel mapping begins at _text, the image
157+
* base — the start of the image, not _stext (which sits past the head gap).
158+
* REGION_KERNEL_IMAGE names the image base directly; REGION_KERNEL_TEXT would
159+
* be read as _stext and shifted down by the head gap. */
160+
kasld_result_base(KASLD_TYPE_VIRT, REGION_KERNEL_IMAGE, text, "_text",
161+
CONF_PARSED);
162+
return 0;
163+
}
164+
165+
#else /* TEXT_TRACKS_DIRECTMAP: the kernel text sits inside the direct map, so \
166+
* a page-table run in the text window can start below _text — no sound \
167+
* pin. Inert on coupled arches. */
168+
169+
int main(void) { return 0; }
170+
171+
#endif

tests/check-text-provenance

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ allow_file() {
7676
proc_stat_wchan.c) return 0 ;; # the function the task sleeps in
7777
# A structure the kernel published as the image itself.
7878
proc_kcore.c) return 0 ;; # the text segment's ELF program header
79+
ptdump_kernel_page_tables.c) return 0 ;; # first mapped run of the high kernel mapping = _text
7980
sysfs_kernel_notes_xen.c) return 0 ;; # XEN_ELFNOTE_ENTRY, the entry point
8081
dmesg_riscv_relocation.c) return 0 ;; # the relocated base the kernel printed
8182
tracefs_printk_formats.c) return 0 ;; # format strings, in the image's rodata

0 commit comments

Comments
 (0)