From: Jennifer Miller <jmill@asu.edu>
To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org,
eddyz87@gmail.com, memxor@gmail.com, kees@kernel.org,
linux-hardening@vger.kernel.org
Cc: jmill@asu.edu, xmei5@asu.edu, samitolvanen@google.com,
peterz@infradead.org, bpf@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: [RFC PATCH bpf-next v1 0/1] Enable BPF JIT hardening by default when x86_64 CFI is enabled
Date: Fri, 10 Jul 2026 12:19:31 -0700 [thread overview]
Message-ID: <20260710191932.120911-1-jmill@asu.edu> (raw)
Hi all,
As part of some academic research we, Xiang (CC'd) and I, found three
(closely-related) methods for bypassing different configurations of
x86-64 CFI schemes (kCFI and FineIBT). The methods have some limitations
which I will provide further details on in the rest of this mail.
We previously reported this to security@k.o and were encouraged to have
a public discussion about hardening against this, rather than dealing
with it in private.
In short, the cBPF JIT code can contain controlled 32-bit immediates
which can be used to partially or fully craft CFI hashes and endbr64
instructions to bypass control flow integrity mitigations. To mitigate
these bypasses we'd recommend enabling JIT Constant Blinding for
unprivileged users by default (bpf_jit_harden=1) when CFI is enabled.
I have included a potential patch that does this, I am not sure though
if this change could somehow be more local to CFI related code, or if
it'd be better to modify the function that controls whether JIT blinding
is enabled (bpf_jit_blinding_enabled) rather than touching the sysctl's
default value.
For some additional context, in the past few years misaligned
instructions in the cBPF JIT region have become a somewhat common target
for Control Flow Hijacking exploits because they provide constrained
shellcode execution, a number of submissions to Google's KernelCTF
program have used this approach[1-3]. There is randomization applied to
start of the JIT code so it is probabilistic whether control flow is
hijacked to the expected code, on a system where panic on oops is not
enabled it is possible to retry until the attack succeeds.
We have a PoC exploit for each method, where we took an existing exploit
and demonstrated that we can get code execution from control flow
hijacking on a kernel with CFI enabled, we can share more details about
those about if desired.
There are three methods, each under different configurations:
1. Full Hash Forgery
- Bypasses kCFI, kernel versions >= 6.2
2. Endbr64 Forgery + Pivot
- Bypasses FineIBT
3. Opcode Collision Forgery
- Bypasses kCFI, for kernel versions < 6.2
1. Full Hash Forgery
This method applies when CONFIG_CFI=y and CONFIG_FINEIBT=n or when
CONFIG_FINEIBT=y but kCFI is being used, either as a fallback (AMD) or
forced via `cfi=kcfi` on the kernel cmdline. However, if
CONFIG_FINEIBT=y then a leak of a kCFI hash or the random value that is
xored with the hashes to randomize them (via an arbitrary read or
otherwise) is necessary for this to work.
CFI hashes are four bytes long and it is only possible to forge at most
four consecutive bytes in JIT code using 32-bit immediates. When
CONFIG_FINEIBT=y, the hash is stored at [dest-0xf] (offset depends on
function alignment config but -0xf is where it is at by default) where
'dest' is the target of indirect control flow. If the target hash is
0x21524111 when kCFI is in use, then the negation of the hash is must
be forged in the immediate (-0x21524111 == 0xdeadbeef).
insn0 = {.code = BPF_LD + BPF_K, .k = 0xdeadbeef}; // hash
insn1 = {.code = BPF_LD + BPF_K, .k = 0xcafeb0ba}; // filler
insn2 = {.code = BPF_LD + BPF_K, .k = 0xcafeb0ba}; // filler
insn3 = {.code = BPF_LD + BPF_K, .k = 0xc3d42948}; // code
Produces:
mov eax, 0xdeadbeef
mov eax, 0xcafeb0ba
mov eax, 0xcafeb0ba
mov eax, 0xc3d42948
At the kCFI protected callsite:
[dest-0xf] == 0xdeadbeef (hash check succeeds)
[dest] == 0xc3d42948 == `sub rsp, rdx; ret;` (pivots the stack)
2. Endbr64 Forgery + Pivot
This method applies when CONFIG_FINEIBT=y and FineIBT is used at
run-time. An endbr64 instruction can be crafted in a 32-bit JIT
immediate and the aligned instructions that follow will be executed
afterwards. This method is *highly* constrained in which callsites can
be used, we have only found one viable callsite so far. If FineIBT
paranoid is enabled, which is true by default for existing Intel CPUs
but not for future CPUs with support for FRED, then a leak of a kCFI
hash is required.
Since the kernel uses rbp as a general purpose register when frame
pointers are omitted it is possible to find call-sites where rbp points
to a heap chunk. BPF JIT code ends in a `leave; ret` sequence, so if rbp
points to the heap then after executing this sequence the heap pointer
will become our new stack when returning from the JIT code, achieving a
stack pivot to the heap. Not only does it need to point to a heap chunk
but [rbp+0x8] must be controlled for the stack pivot to be useful (there
needs to be at least one ROP gadget there to a longer ROP chain
elsewhere in memory). As previously mentioned, we have only found one
case of a call-site where this is possible. Enumerating all call-sites
where this is possible is non-trivial, so we are still unsure of how many
there actually are.
3. Opcode Collision Forgery
This method only really applies when CONFIG_CFI_CLANG=y on kernels with
support for kCFI before FineIBT was merged (pre 6.2). More recent
kernels default to placing padding after the CFI hash rather than before
it. Without FineIBT, CFI hashes are not randomized on boot so a leak is
not necessary.
Prior to 6.2 the padding for CFI stubs was hardcoded to be before the
hash, making it more difficult to forge a full hash in an unaligned JIT
instruction while maintaining control flow hijacking. Using the full
four bytes of a 32-bit immediate to forge a hash would result in aligned
instructions after the hash being executed and would likely result in a
fault when the JIT code tries to return. To maintain code execution
avoid using all four-bytes of the immediate and instead reserve a few
bytes following the hash to craft a jump instruction to the next 4 byte
immediate and continue with the limited shellcode execution in unaligned
instructions. Not crafting all four bytes of the hash in the 32-bit
immediate means that an "opcode-collision" is necessary to craft the
full hash, which looks something like this:
insn0 = {.code = BPF_LD + BPF_K, .k = 0x7fdb0000}; // hash part 1
insn1 = {.code = BPF_LD + BPF_K, .k = 0x9002eb1a}; // hash part 2 + insn
Produces:
mov eax, 0x7fdb0000
mov eax, 0x9002eb1a
Combined:
b8 00 00 [db 7f b8 1a] eb 02 90
At the kCFI protected callsite:
[dest-0x4] == 0x1ab87fdb (hash check succeeds)
[dest] == 0x02eb == `jmp .+4` (continue control flow hijacking)
Since 0xb8 is the opcode byte for the `mov eax` instruction, it is
possible to forge a kCFI hash such as 0x1ab87fdb using two instructions
in the JIT region and still retain limited shellcode execution through
code stored in 32-bit immediates.
There are a number of instructions that take 32-bit immediates that can
be generated in cBPF JIT (e.g., or, xor, and, sub, add, etc...), with
various opcode bytes which can collide with a CFI hash. Xiang enumerated
these instructions and did an analysis to see how many hashes could be
bypassed with this method on a 6.1 LTS kernel (pre-FineIBT so no CFI
hash rerandomization yet, compiled with the KernelCTF config plus the
configs necessary for kCFI), and found that approx 14% of the hashes
were able to be crafted via an opcode collision in the JIT code.
~Jennifer
[1] https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2023-3609_cos_mitigation/docs/exploit.md#spray-ebpf-programs
[2] https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2024-36972_lts_cos/docs/exploit.md#spray-ebpf-programs
[3] https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2025-21756_cos/docs/exploit.md#spray-ebpf-programs
Jennifer Miller (1):
bpf: Enable JIT hardening by default when x86_64 CFI is enabled
kernel/bpf/core.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
--
2.34.1
next reply other threads:[~2026-07-10 19:19 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-10 19:19 Jennifer Miller [this message]
2026-07-10 19:19 ` [RFC PATCH bpf-next v1 1/1] bpf: Enable JIT hardening by default when x86_64 CFI is enabled Jennifer Miller
2026-07-10 19:30 ` sashiko-bot
2026-07-12 6:22 ` bot+bpf-ci
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260710191932.120911-1-jmill@asu.edu \
--to=jmill@asu.edu \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=kees@kernel.org \
--cc=linux-hardening@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=memxor@gmail.com \
--cc=peterz@infradead.org \
--cc=samitolvanen@google.com \
--cc=xmei5@asu.edu \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox