|
| 1 | +// This file is part of KASLD - https://github.com/bcoles/kasld |
| 2 | +// |
| 3 | +// Parser unit tests for the sysfs / ACPI / device-tree leak components. Each |
| 4 | +// component is #included with its main() (and its static read_file_line, where |
| 5 | +// present) renamed so the parser is in scope, then driven over a staged |
| 6 | +// KASLD_SYSROOT tree of hand-built fixture files. The fixtures reproduce the |
| 7 | +// exact text/byte format the kernel exposes for each interface, so a test |
| 8 | +// failing here means the parser no longer matches the kernel ABI it targets: |
| 9 | +// |
| 10 | +// acpi_mrrm /sys/firmware/acpi/memory_ranges/rangeN/base |
| 11 | +// sysfs_cbmem_address /sys/bus/coreboot/devices/cbmem-*/address |
| 12 | +// sysfs_cxl_region /sys/bus/cxl/devices/regionN/resource |
| 13 | +// sysfs_qcom_rmtfs_mem /sys/class/rmtfs/qcom_rmtfs_memN/phys_addr |
| 14 | +// sysfs_iommu_reserved_.. /sys/kernel/iommu_groups/N/reserved_regions |
| 15 | +// sysfs_efi_runtime_map /sys/firmware/efi/runtime-map/N/{virt,phys}_addr |
| 16 | +// sysfs_devicetree_elf.. /sys/firmware/devicetree/base/chosen/... |
| 17 | +// |
| 18 | +// The components route their directory and file reads through the KASLD_SYSROOT |
| 19 | +// wrappers, so the fixture tree is read in place of the live system. Each |
| 20 | +// parser's main() is captured on stdout (the wire channel) and the emitted |
| 21 | +// P/V record is checked. |
| 22 | +// --- |
| 23 | +// <bcoles@gmail.com> |
| 24 | +#define _GNU_SOURCE |
| 25 | + |
| 26 | +/* Pull in the public API once, then neutralise the two ELF-section macros so |
| 27 | + * the per-component KASLD_EXPLAIN/KASLD_META definitions do not collide when |
| 28 | + * several components are included into this single translation unit. */ |
| 29 | +#include "../src/include/kasld/api.h" |
| 30 | +#undef KASLD_EXPLAIN |
| 31 | +#undef KASLD_META |
| 32 | +/* Expand to a uniquely-named file-scope declaration (not a definition) so |
| 33 | + * several components coexist in one TU without colliding ELF-section arrays or |
| 34 | + * redundant redeclarations, and the trailing ';' after each KASLD_EXPLAIN(...) |
| 35 | + * / KASLD_META(...) is absorbed. __COUNTER__ makes every name distinct. */ |
| 36 | +#define KASLD_CAT_(a, b) a##b |
| 37 | +#define KASLD_CAT(a, b) KASLD_CAT_(a, b) |
| 38 | +#define KASLD_EXPLAIN(t) \ |
| 39 | + extern char KASLD_CAT(kasld_explain_unused_, __COUNTER__)[] |
| 40 | +#define KASLD_META(t) extern char KASLD_CAT(kasld_meta_unused_, __COUNTER__)[] |
| 41 | + |
| 42 | +/* Forward declarations for the renamed component entry points (avoids |
| 43 | + * -Wmissing-prototypes; the includes below define them). */ |
| 44 | +int efi_main(void); |
| 45 | +int acpi_main(void); |
| 46 | +int cbmem_main(void); |
| 47 | +int cxl_main(void); |
| 48 | +int qcom_main(void); |
| 49 | +int iommu_main(void); |
| 50 | +int dt_main(void); |
| 51 | +int nd_main(void); |
| 52 | +int uio_main(void); |
| 53 | +int iscsi_main(void); |
| 54 | + |
| 55 | +#define main efi_main |
| 56 | +#define read_file_line efi_read_file_line |
| 57 | +#include "../src/components/sysfs_efi_runtime_map.c" |
| 58 | +#undef read_file_line |
| 59 | +#undef main |
| 60 | + |
| 61 | +#define main acpi_main |
| 62 | +#define read_file_line acpi_read_file_line |
| 63 | +#include "../src/components/acpi_mrrm.c" |
| 64 | +#undef read_file_line |
| 65 | +#undef main |
| 66 | + |
| 67 | +#define main cbmem_main |
| 68 | +#define read_file_line cbmem_read_file_line |
| 69 | +#include "../src/components/sysfs_cbmem_address.c" |
| 70 | +#undef read_file_line |
| 71 | +#undef main |
| 72 | + |
| 73 | +#define main cxl_main |
| 74 | +#define read_file_line cxl_read_file_line |
| 75 | +#include "../src/components/sysfs_cxl_region.c" |
| 76 | +#undef read_file_line |
| 77 | +#undef main |
| 78 | + |
| 79 | +#define main qcom_main |
| 80 | +#define read_file_line qcom_read_file_line |
| 81 | +#include "../src/components/sysfs_qcom_rmtfs_mem.c" |
| 82 | +#undef read_file_line |
| 83 | +#undef main |
| 84 | + |
| 85 | +#define main iommu_main |
| 86 | +#include "../src/components/sysfs_iommu_reserved_regions.c" |
| 87 | +#undef main |
| 88 | + |
| 89 | +#define main dt_main |
| 90 | +#include "../src/components/sysfs_devicetree_elfcorehdr.c" |
| 91 | +#undef main |
| 92 | + |
| 93 | +#define main nd_main |
| 94 | +#include "../src/components/sysfs_nd_region.c" |
| 95 | +#undef main |
| 96 | + |
| 97 | +#define main uio_main |
| 98 | +#define read_file_line uio_read_file_line |
| 99 | +#include "../src/components/sysfs_uio_map.c" |
| 100 | +#undef read_file_line |
| 101 | +#undef main |
| 102 | + |
| 103 | +#define main iscsi_main |
| 104 | +#include "../src/components/sysfs_iscsi_transport_handle.c" |
| 105 | +#undef main |
| 106 | + |
| 107 | +#include "test_harness.h" |
| 108 | + |
| 109 | +#include <assert.h> |
| 110 | +#include <fcntl.h> |
| 111 | +#include <stdint.h> |
| 112 | +#include <sys/stat.h> |
| 113 | +#include <sys/types.h> |
| 114 | +#include <unistd.h> |
| 115 | + |
| 116 | +static char g_root[256]; |
| 117 | + |
| 118 | +/* mkdir -p of the parent directory of <g_root><rel>, then return the full |
| 119 | + * path in `out`. */ |
| 120 | +static void full_path(const char *rel, char *out, size_t n) { |
| 121 | + snprintf(out, n, "%s%s", g_root, rel); |
| 122 | +} |
| 123 | + |
| 124 | +static void mkparents(const char *path) { |
| 125 | + char buf[512]; |
| 126 | + snprintf(buf, sizeof(buf), "%s", path); |
| 127 | + for (char *p = buf + 1; *p; p++) { |
| 128 | + if (*p == '/') { |
| 129 | + *p = '\0'; |
| 130 | + mkdir(buf, 0755); |
| 131 | + *p = '/'; |
| 132 | + } |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +/* Write `len` bytes to the sysroot-relative path. */ |
| 137 | +static void stage(const char *rel, const void *data, size_t len) { |
| 138 | + char path[512]; |
| 139 | + full_path(rel, path, sizeof(path)); |
| 140 | + mkparents(path); |
| 141 | + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); |
| 142 | + assert(fd >= 0); |
| 143 | + assert(write(fd, data, len) == (ssize_t)len); |
| 144 | + close(fd); |
| 145 | +} |
| 146 | + |
| 147 | +static void stage_text(const char *rel, const char *text) { |
| 148 | + stage(rel, text, strlen(text)); |
| 149 | +} |
| 150 | + |
| 151 | +static void put_be64(unsigned char *p, uint64_t v) { |
| 152 | + for (int i = 0; i < 8; i++) |
| 153 | + p[i] = (unsigned char)(v >> (56 - 8 * i)); |
| 154 | +} |
| 155 | + |
| 156 | +/* Run a renamed component main(), capturing its stdout (the wire channel) into |
| 157 | + * `cap`. Diagnostics go to stderr and are left alone. */ |
| 158 | +static char cap[16384]; |
| 159 | +static void run_capture(int (*fn)(void)) { |
| 160 | + fflush(stdout); |
| 161 | + char tmpl[] = "/tmp/kasld_parser_capXXXXXX"; |
| 162 | + int fd = mkstemp(tmpl); |
| 163 | + assert(fd >= 0); |
| 164 | + int saved = dup(1); |
| 165 | + dup2(fd, 1); |
| 166 | + /* Silence the component's stderr diagnostics (kasld_info / kasld_err / |
| 167 | + * kasld_found) so they do not bleed into the test harness output. */ |
| 168 | + fflush(stderr); |
| 169 | + int saved_err = dup(2); |
| 170 | + int devnull = open("/dev/null", O_WRONLY); |
| 171 | + if (devnull >= 0) |
| 172 | + dup2(devnull, 2); |
| 173 | + fn(); |
| 174 | + fflush(stdout); |
| 175 | + fflush(stderr); |
| 176 | + dup2(saved, 1); |
| 177 | + close(saved); |
| 178 | + dup2(saved_err, 2); |
| 179 | + close(saved_err); |
| 180 | + if (devnull >= 0) |
| 181 | + close(devnull); |
| 182 | + lseek(fd, 0, SEEK_SET); |
| 183 | + ssize_t n = read(fd, cap, sizeof(cap) - 1); |
| 184 | + cap[n > 0 ? n : 0] = '\0'; |
| 185 | + close(fd); |
| 186 | + unlink(tmpl); |
| 187 | +} |
| 188 | + |
| 189 | +/* --- ACPI MRRM: base is "0x%llx" text ----------------------------------- */ |
| 190 | +static void test_acpi_mrrm_base(void) { |
| 191 | + stage_text("/sys/firmware/acpi/memory_ranges/range0/base", "0x100000000\n"); |
| 192 | + run_capture(acpi_main); |
| 193 | + assert(strstr(cap, "P pmem:range0") != NULL); |
| 194 | + assert(strstr(cap, "sample=0x100000000") != NULL); |
| 195 | +} |
| 196 | + |
| 197 | +/* --- coreboot CBMEM: address is "0x%llx" text --------------------------- */ |
| 198 | +static void test_cbmem_address(void) { |
| 199 | + stage_text("/sys/bus/coreboot/devices/cbmem-00000abc/address", |
| 200 | + "0x100000000\n"); |
| 201 | + run_capture(cbmem_main); |
| 202 | + assert(strstr(cap, "P reserved_mem:cbmem-00000abc") != NULL); |
| 203 | + assert(strstr(cap, "sample=0x100000000") != NULL); |
| 204 | +} |
| 205 | + |
| 206 | +/* --- CXL region: resource is "%#llx" text; -1 means unallocated ---------- */ |
| 207 | +static void test_cxl_region(void) { |
| 208 | + stage_text("/sys/bus/cxl/devices/region0/resource", "0x100000000\n"); |
| 209 | + /* An unallocated region reports 0xff..ff and must be skipped. */ |
| 210 | + stage_text("/sys/bus/cxl/devices/region1/resource", "0xffffffffffffffff\n"); |
| 211 | + run_capture(cxl_main); |
| 212 | + assert(strstr(cap, "sample=0x100000000") != NULL); |
| 213 | + assert(strstr(cap, "ffffffffffffffff") == NULL); |
| 214 | +} |
| 215 | + |
| 216 | +/* --- Qualcomm RMTFS: phys_addr is "%pa" text ("0x%llx") ------------------ */ |
| 217 | +static void test_qcom_rmtfs(void) { |
| 218 | + stage_text("/sys/class/rmtfs/qcom_rmtfs_mem0/phys_addr", "0x100000000\n"); |
| 219 | + run_capture(qcom_main); |
| 220 | + assert(strstr(cap, "P reserved_mem:qcom_rmtfs_mem0") != NULL); |
| 221 | + assert(strstr(cap, "sample=0x100000000") != NULL); |
| 222 | +} |
| 223 | + |
| 224 | +/* --- IOMMU reserved_regions: "0x%016llx 0x%016llx <type>" lines. |
| 225 | + * "msi" entries are skipped by type (even at a DRAM address); "reserved" |
| 226 | + * entries in plausible DRAM are emitted. ---------------------------------- */ |
| 227 | +static void test_iommu_reserved_regions(void) { |
| 228 | + stage_text("/sys/kernel/iommu_groups/0/reserved_regions", |
| 229 | + "0x0000000100000000 0x000000010000ffff msi\n" |
| 230 | + "0x0000000200000000 0x000000020000ffff reserved\n"); |
| 231 | + run_capture(iommu_main); |
| 232 | + /* the reserved DRAM range is emitted (start and end) */ |
| 233 | + assert(strstr(cap, "sample=0x200000000") != NULL); |
| 234 | + /* the msi range is skipped despite its DRAM address */ |
| 235 | + assert(strstr(cap, "0x100000000") == NULL); |
| 236 | +} |
| 237 | + |
| 238 | +/* --- device-tree elfcorehdr: two big-endian u64 (address, size) --------- */ |
| 239 | +static void test_devicetree_elfcorehdr(void) { |
| 240 | + unsigned char blob[16]; |
| 241 | + put_be64(blob + 0, 0x100000000ULL); /* address */ |
| 242 | + put_be64(blob + 8, 0x10000ULL); /* size */ |
| 243 | + stage("/sys/firmware/devicetree/base/chosen/linux,elfcorehdr", blob, |
| 244 | + sizeof(blob)); |
| 245 | + run_capture(dt_main); |
| 246 | + assert(strstr(cap, "P crashkernel:elfcorehdr") != NULL); |
| 247 | + /* big-endian decode: address 0x100000000, hi = addr + size - 1 */ |
| 248 | + assert(strstr(cap, "lo=0x100000000 hi=0x10000ffff") != NULL); |
| 249 | +} |
| 250 | + |
| 251 | +/* --- EFI runtime-map: virt_addr / phys_addr "0x%llx" text; the parser |
| 252 | + * derives virt_page_offset = virt - phys for a direct-map virtual address. |
| 253 | + * Uses host-arch direct-map constants (this interface is x86-only). ------- */ |
| 254 | +static void test_efi_runtime_map(void) { |
| 255 | + unsigned long virt = 0xffff888000001000UL; |
| 256 | + unsigned long phys = 0x1000UL; |
| 257 | + /* Only meaningful where the chosen virt is in the host's direct-map window |
| 258 | + * and virt-phys lands in the page-offset window (true on x86_64). */ |
| 259 | + if (!kasld_addr_is_directmap(virt)) |
| 260 | + return; |
| 261 | + stage_text("/sys/firmware/efi/runtime-map/0/virt_addr", |
| 262 | + "0xffff888000001000\n"); |
| 263 | + stage_text("/sys/firmware/efi/runtime-map/0/phys_addr", "0x1000\n"); |
| 264 | + run_capture(efi_main); |
| 265 | + assert(strstr(cap, "V virt_page_offset") != NULL); |
| 266 | + char want[64]; |
| 267 | + snprintf(want, sizeof(want), "sample=0x%lx", virt - phys); |
| 268 | + assert(strstr(cap, want) != NULL); |
| 269 | +} |
| 270 | + |
| 271 | +/* --- libnvdimm nd_region: resource is "%#llx" text ---------------------- */ |
| 272 | +static void test_nd_region(void) { |
| 273 | + stage_text("/sys/bus/nd/devices/ndregion0/resource", "0x4000000000\n"); |
| 274 | + run_capture(nd_main); |
| 275 | + assert(strstr(cap, "P pmem:ndregion0") != NULL); |
| 276 | + assert(strstr(cap, "sample=0x4000000000") != NULL); |
| 277 | +} |
| 278 | + |
| 279 | +/* --- UIO map: maps/mapN/addr is "%pa" text ("0x%llx"); region defaults to |
| 280 | + * mmio when /proc/iomem does not place the address in System RAM. ---------- */ |
| 281 | +static void test_uio_map(void) { |
| 282 | + stage_text("/sys/class/uio/uio0/maps/map0/addr", "0x90000000\n"); |
| 283 | + stage_text("/sys/class/uio/uio0/maps/map0/name", "uio-mem\n"); |
| 284 | + run_capture(uio_main); |
| 285 | + assert(strstr(cap, "P mmio:uio0/map0") != NULL); |
| 286 | + assert(strstr(cap, "sample=0x90000000") != NULL); |
| 287 | +} |
| 288 | + |
| 289 | +/* --- iSCSI transport handle (CVE-2021-27363): the "handle" attribute is a |
| 290 | + * decimal kernel pointer. KASLD_SYSROOT short-circuits the netlink autoload, |
| 291 | + * so the fixture is read directly. Uses a host-arch kernel-text address. --- */ |
| 292 | +static void test_iscsi_transport_handle(void) { |
| 293 | + unsigned long addr = |
| 294 | + 0xffffffff81000000UL; /* in the host kernel-text window */ |
| 295 | + if (!kasld_addr_is_kernel_text(addr)) |
| 296 | + return; |
| 297 | + char dec[32]; |
| 298 | + snprintf(dec, sizeof(dec), "%lu\n", addr); |
| 299 | + stage_text("/sys/class/iscsi_transport/iser/handle", dec); |
| 300 | + stage_text("/sys/class/iscsi_transport/tcp/handle", dec); |
| 301 | + run_capture(iscsi_main); |
| 302 | + assert(strstr(cap, "V kernel_data:iscsi_iser_transport") != NULL); |
| 303 | + char want[64]; |
| 304 | + snprintf(want, sizeof(want), "sample=0x%lx", addr); |
| 305 | + assert(strstr(cap, want) != NULL); |
| 306 | +} |
| 307 | + |
| 308 | +int main(void) { |
| 309 | + /* One sysroot for the whole suite: each parser reads a distinct path, and |
| 310 | + * kasld_sysroot() caches its value process-wide, so a single root must be |
| 311 | + * set before any component runs. */ |
| 312 | + char tmpl[] = "/tmp/kasld_parser_rootXXXXXX"; |
| 313 | + char *r = mkdtemp(tmpl); |
| 314 | + assert(r != NULL); |
| 315 | + snprintf(g_root, sizeof(g_root), "%s", r); |
| 316 | + setenv("KASLD_SYSROOT", g_root, 1); |
| 317 | + |
| 318 | + TEST_SUITE("test_sysfs_parsers"); |
| 319 | + BEGIN_CATEGORY("sysfs / ACPI / DT leak parsers"); |
| 320 | + RUN(test_acpi_mrrm_base); |
| 321 | + RUN(test_cbmem_address); |
| 322 | + RUN(test_cxl_region); |
| 323 | + RUN(test_qcom_rmtfs); |
| 324 | + RUN(test_iommu_reserved_regions); |
| 325 | + RUN(test_devicetree_elfcorehdr); |
| 326 | + RUN(test_efi_runtime_map); |
| 327 | + RUN(test_nd_region); |
| 328 | + RUN(test_uio_map); |
| 329 | + RUN(test_iscsi_transport_handle); |
| 330 | + return TEST_DONE(); |
| 331 | +} |
0 commit comments