-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathproc_zoneinfo.c
More file actions
211 lines (192 loc) · 8.28 KB
/
Copy pathproc_zoneinfo.c
File metadata and controls
211 lines (192 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// This file is part of KASLD - https://github.com/bcoles/kasld
//
// Read zone start_pfn and spanned page counts from /proc/zoneinfo.
// This file is world-readable (0444) and prints, for each populated
// memory zone, the start page frame number (PFN) and the number of
// spanned pages. The physical address of the zone start is:
// start_pfn * PAGE_SIZE. The zone end is: (start_pfn + spanned) * PAGE_SIZE.
//
// Using spanned pages to compute zone ends gives a more accurate DRAM
// upper bound than the highest zone start_pfn alone. For example, on
// a system with 4 GiB DRAM starting at 0x80000000:
// Normal zone start_pfn = 262144 -> 0x100000000
// Normal zone spanned = 524288 -> zone end = 0x180000000
// Without spanned, the :hi result would report 0x100000000 (zone start),
// missing the top 2 GiB of DRAM.
//
// Soundness: /proc/zoneinfo describes USER-ALLOCATABLE memory zones
// (buddy-allocator state), not the full physical-RAM extent. Firmware-
// and kernel-reserved regions (the kernel image, EFI runtime services,
// memblock reservations) live OUTSIDE the published zones. On systems
// where firmware reserves the low-phys range for the kernel image
// (e.g. ppc32 PowerMac with the kernel at phys 0 and the lowest zone
// starting at 0x30000000), treating the lowest zone start as POS_BASE
// would pin dram_floor_bound to a bogus high floor and exclude the
// actual text base. The lowest zone start is therefore emitted as an
// interior SAMPLE — a sound RAM witness, but not a floor pin.
// Authoritative phys floors come from sysfs_devicetree_memory,
// sysfs_firmware_memmap, boot_params_e820 and peers that read the full
// memory map. The HIGHEST zone end IS sound as a TOP bound (the largest
// published zone end ≤ true top of RAM).
//
// Example output (excerpt):
// Node 0, zone DMA
// ...
// spanned 4095
// ...
// start_pfn: 1
// Node 0, zone DMA32
// ...
// spanned 1044480
// ...
// start_pfn: 4096
// Node 0, zone Normal
// ...
// spanned 524288
// ...
// start_pfn: 1048576
//
// Leak primitive:
// Data leaked: physical DRAM base address (zone start PFN × PAGE_SIZE)
// Kernel subsystem: mm/vmstat — /proc/zoneinfo (proc_zoneinfo_show)
// Data structure: struct zone → zone_start_pfn (unsigned long)
// Address type: physical (DRAM)
// Method: parsed (text file)
// Status: unfixed (information exposure by design)
// Access check: none (world-readable /proc/zoneinfo, 0444)
// Source: https://elixir.bootlin.com/linux/v6.12/source/mm/vmstat.c
//
// Mitigations:
// None — /proc/zoneinfo is world-readable (0444); no runtime sysctl can
// restrict access. The start_pfn field is part of core mm and cannot be
// hidden without a kernel patch. On decoupled architectures (x86_64, ARM64,
// RISC-V 64), the physical address cannot derive the virtual text base.
//
// Requires:
// - /proc filesystem
//
// References:
// https://elixir.bootlin.com/linux/v6.12/source/mm/vmstat.c#L1727
// ---
// <bcoles@gmail.com>
#include "include/kasld/api.h"
#include "include/kasld/cli.h"
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
KASLD_EXPLAIN(
"Reads /proc/zoneinfo to extract the start_pfn and spanned page "
"count of each memory zone. Multiplying PFN by page size yields "
"physical-RAM witnesses (interior samples) and a sound top edge of "
"the published RAM extent. World-readable (0444); part of core mm; "
"no sysctl or CONFIG option can hide the start_pfn field.");
KASLD_META("method:parsed\n"
"phase:inference\n"
"discloses:physical\n");
int main(void) {
FILE *f;
const char *path = "/proc/zoneinfo";
char line[512];
unsigned long lo_pfn = 0, hi_pfn = 0, hi_end_pfn = 0;
unsigned long cur_spanned = 0;
int count = 0;
kasld_info("searching %s for zone start_pfn and spanned ...", path);
f = kasld_fopen(path, "r");
if (f == NULL) {
perror("[-] fopen");
return (errno == EACCES || errno == EPERM) ? KASLD_EXIT_NOPERM
: KASLD_EXIT_UNAVAILABLE;
}
while (fgets(line, sizeof(line), f) != NULL) {
unsigned long val;
if (sscanf(line, " spanned %lu", &val) == 1) {
cur_spanned = val;
continue;
}
if (sscanf(line, " start_pfn: %lu", &val) != 1)
continue;
/* `count` is the "no zones seen yet" sentinel — checking lo_pfn == 0
* would conflate "first zone" with "zone genuinely starting at PFN 0"
* (rare but admissible: some hot-plug / embedded boots place a zone at
* PFN 0). The sscanf above already detected parse failure. */
if (count == 0 || val < lo_pfn)
lo_pfn = val;
if (val > hi_pfn)
hi_pfn = val;
unsigned long end_pfn = cur_spanned ? val + cur_spanned : val;
if (end_pfn > hi_end_pfn)
hi_end_pfn = end_pfn;
cur_spanned = 0;
count++;
}
fclose(f);
if (!count) {
kasld_err("no zone start_pfn entries found");
return 0;
}
unsigned long hi_use = hi_end_pfn > hi_pfn ? hi_end_pfn : hi_pfn;
/* A zoneinfo PFN counts the pages of the kernel that published the file, so
* the multiplier must be that kernel's page size.
*
* Where the architecture admits exactly one size that constant is it. Where
* it admits several -- arm64, mips, powerpc, loongarch64 -- a compile-time
* constant would be wrong by up to 64x, so the size is asked of the running
* kernel. That answer describes the running kernel and nothing else: under
* KASLD_SYSROOT the file came from a captured tree whose page size nothing
* here knows, so the conversion is declined rather than made with the
* analysing host's.
*
* The multiply is written out rather than routed through pfn_to_phys()
* because the two want different answers at the top of the range: this site
* CLAMPS an out-of-range PFN to keep a bound, where pfn_to_phys() returns 0
* and declines. Clamping is what keeps a 32-bit (PAE) kernel's high PFNs
* from wrapping unsigned long into a bogus too-low RAM bound. */
#if PAGE_SIZE_KNOWN_AT_BUILD
unsigned long page_size = (unsigned long)PAGE_SIZE_MIN;
#else
unsigned long page_size = 0;
if (!kasld_sysroot()) {
long p = sysconf(_SC_PAGESIZE);
if (p > 0)
page_size = (unsigned long)p;
}
/* Taken as reported, without a plausibility bracket: sysconf answers for the
* kernel running this process, which is more authoritative about that
* kernel's page size than this build's PAGE_SIZE_MIN / PAGE_SIZE_MAX axis
* is. A value outside that bracket would mean the axis is wrong, and
* rejecting it here would hide that rather than surface it. The engine rules
* that read the same quantity off the wire do check it, because there it has
* been through the tagged-line protocol and can be an artefact. */
if (!page_size)
return kasld_disp_absent("page size unknown for a captured tree on an "
"architecture that admits several; zone PFNs "
"cannot be converted to addresses");
#endif
if (lo_pfn > ULONG_MAX / page_size)
lo_pfn = ULONG_MAX / page_size;
if (hi_use > ULONG_MAX / page_size)
hi_use = ULONG_MAX / page_size;
unsigned long lo = lo_pfn * page_size;
unsigned long hi = hi_use * page_size - 1;
/* lo: the start of the lowest published zone — a sound RAM witness but
* NOT a floor pin (reserved low memory below the lowest zone is
* invisible to zoneinfo; see the file header). Emit as an interior
* sample. hi: the end of the highest zone — a sound TOP bound (RAM
* does not extend above the highest published zone end). The component
* currently aggregates across zones rather than reporting per-zone
* DMA_TOP / DMA32_TOP — finer-grained reporting is a future
* enhancement. */
kasld_info("lowest zone start PFN: %lu (phys 0x%016lx)", lo_pfn, lo);
kasld_result_sample(KASLD_TYPE_PHYS, REGION_RAM, lo, NULL, CONF_PARSED);
if (hi_use != lo_pfn) {
if (hi_end_pfn > hi_pfn)
kasld_info("highest zone end PFN: %lu (phys 0x%016lx)", hi_use, hi);
else
kasld_info("highest zone start PFN: %lu (phys 0x%016lx)", hi_use, hi);
kasld_result_top(KASLD_TYPE_PHYS, REGION_RAM, hi, NULL, CONF_PARSED);
}
return 0;
}