From: Ihor Solodrai <ihor.solodrai@linux.dev>
To: "Kumar Kartikeya Dwivedi" <memxor@gmail.com>,
sashiko-reviews@lists.linux.dev,
"Alexis Lothoré (eBPF Foundation)" <alexis.lothore@bootlin.com>
Cc: bpf@vger.kernel.org, Alexei Starovoitov <ast@kernel.org>
Subject: Re: [PATCH bpf-next v7 4/9] bpf, x86: emit KASAN checks in x86 JITed programs
Date: Mon, 24 Aug 2026 17:12:49 -0700 [thread overview]
Message-ID: <fa7cc4ad-6b8d-4bdf-8e2c-66d98ddef7df@linux.dev> (raw)
In-Reply-To: <DKWOG6YKM2NY.1FJHLE7BKZW9Y@gmail.com>
On 8/23/26 3:30 PM, Kumar Kartikeya Dwivedi wrote:
> On Sat Aug 22, 2026 at 1:33 AM CEST, sashiko-bot wrote:
>> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
>> - [High] `emit_kasan_check` fails to save the caller-saved register %r10 (BPF_REG_AX), which corrupts BPF_REG_0 and memory pointers during atomic fetch operations.
>> --
>>
>> --- Patch [4]: [PATCH bpf-next v7 4/9] bpf, x86: emit KASAN checks in x86 JITed programs ---
>> commit 6bca50db6e9af9416363fe2e60f4e42f924950c9
>> Author: Alexis Lothoré (eBPF Foundation) <alexis.lothore@bootlin.com>
>>
>> bpf, x86: emit KASAN checks in x86 JITed programs
>>
>> Insert KASAN shadow memory checks before memory load and store
>> operations in JIT-compiled BPF programs. This helps detect memory safety
>> bugs such as use-after-free and out-of-bounds accesses at runtime.
>>
>>> diff --git a/arch/x86/net/bpf_jit_comp.c b/arch/x86/net/bpf_jit_comp.c
>>> index 13ba3232993ac..8134d028a2f2c 100644
>>> --- a/arch/x86/net/bpf_jit_comp.c
>>> +++ b/arch/x86/net/bpf_jit_comp.c
>>
>> [ ... ]
>>
>>> @@ -1110,6 +1121,92 @@ static void maybe_emit_1mod(u8 **pprog, u32 reg, bool is64)
>>> *pprog = prog;
>>> }
>>>
>>> +static int emit_kasan_check(struct bpf_verifier_env *env, u8 **pprog,
>>> + u32 addr_reg, struct bpf_insn *insn, u8 *ip,
>>> + bool is_write)
>>> +{
>>> +#ifdef CONFIG_BPF_JIT_KASAN
>>> + u32 bpf_size = BPF_SIZE(insn->code);
>>> + s32 off = insn->off;
>>> + u8 *prog = *pprog;
>>> + void *kasan_func;
>>> +
>>> + if (!env)
>>> + return 0;
>>> +
>>> + /* Derive KASAN check function from access type and size */
>>> + switch (bpf_size) {
>>> + case BPF_B:
>>> + kasan_func = is_write ? __asan_store1 : __asan_load1;
>>> + break;
>>> + case BPF_H:
>>> + kasan_func = is_write ? __asan_store2 : __asan_load2;
>>> + break;
>>> + case BPF_W:
>>> + kasan_func = is_write ? __asan_store4 : __asan_load4;
>>> + break;
>>> + case BPF_DW:
>>> + kasan_func = is_write ? __asan_store8 : __asan_load8;
>>> + break;
>>> + default:
>>> + return -EINVAL;
>>> + }
>>> +
>>> + /* Save rax */
>>> + EMIT1(0x50);
>>> + /* Save rcx */
>>> + EMIT1(0x51);
>>> + /* Save rdx */
>>> + EMIT1(0x52);
>>> + /* Save rsi */
>>> + EMIT1(0x56);
>>> + /* Save rdi */
>>> + EMIT1(0x57);
>>> + /* Save r8 */
>>> + EMIT2(0x41, 0x50);
>>> + /* Save r9 */
>>> + EMIT2(0x41, 0x51);
>>> + /*
>>> + * SystemV ABI states that we should also save r10/r11, but in
>>> + * practice those registers are _not_ used by the limited set of
>>> + * kasan helpers we are calling here, so that's fine not to save those.
>>> + */
>>> +
>>> + /* mov rdi, addr_reg */
>>
>> [Severity: High]
>> Is it safe to skip saving %r10 and %r11 here?
>>
>> In the x86 BPF JIT, %r10 is mapped to BPF_REG_AX. During atomic fetch
>> operations, BPF_REG_0 is stashed into BPF_REG_AX before calling
>> emit_kasan_check():
>>
>> arch/x86/net/bpf_jit_comp.c:do_jit() {
>> ...
>> /* Will need RAX as a CMPXCHG operand so save R0 */
>> emit_mov_reg(&prog, true, BPF_REG_AX, BPF_REG_0);
>> if (src_reg == BPF_REG_0)
>> real_src_reg = BPF_REG_AX;
>> if (dst_reg == BPF_REG_0)
>> real_dst_reg = BPF_REG_AX;
>>
>> if (!bpf_atomic_is_load_store(insn)) {
>> if (!accesses_stack_only) {
>> err = emit_kasan_check(env, &prog, real_dst_reg, insn, ip, true);
>> ...
>> }
>>
>> Since the KASAN helpers are standard C functions, they are permitted by the
>> System V AMD64 ABI to clobber caller-saved registers like %r10 (especially if
>> kasan_report is called).
>>
>> If %r10 is clobbered, could this corrupt the stashed BPF_REG_0 and potentially
>> cause a crash when real_dst_reg is used as a memory pointer in the subsequent
>> emit_ldx() call?
>>
>> arch/x86/net/bpf_jit_comp.c:do_jit() {
>> ...
>> /* Load old value */
>> emit_ldx(&prog, BPF_SIZE(insn->code), BPF_REG_0, real_dst_reg, insn->off);
>> ...
>> /* Restore R0 after clobbering RAX */
>> emit_mov_reg(&prog, true, BPF_REG_0, BPF_REG_AX);
>> ...
>> }
>
> This looks like a real bug, R10 is used for both atomics and constant blinding.
> We probably also should save R11.
Hi Kumar, Alexis.
This topic has a bit of history now [1][2].
I don't quite like it, but IMO we should give in to the bots.
Reasons to save r10 and r11:
* SystemV ABI
* bots wouldn't shut up about it (think of future llm bot reports!)
* if our analysis of the kasan helpers is wrong about r10/r11 usage OR
if something changes (the helpers themselves, the compilers, build
flags, JIT's usage of r10/r11 etc.) - we'll get clobbered regs
Reasons to *not* save r10 and r11:
* we make the bpf prog a bit smaller and faster, the win is not very
clear but it's percentages, not a factor
A counter-argument to clobbering is that a kasan bug here most likely
means a bug in the verifier, so it doesn't matter if machine dies.
A counter-argument to that would be: why deliberately increase the bug
surface and let the machine die if we can easily prevent it?
Pasting below a clobbering reproducer from my clanker.
[1] https://lore.kernel.org/all/5f38c9a5-a8a3-4bed-bb8c-b7260a1c1a11@linux.dev/
[2] https://lore.kernel.org/all/CAADnVQ+c9h_wuNwj8pjx885oNErGY7bxxCwKi+DiJ0XKSpyYfg@mail.gmail.com/
diff --git a/tools/testing/selftests/bpf/prog_tests/kasan.c b/tools/testing/selftests/bpf/prog_tests/kasan.c
index 2b424767a0f3..160445395ddc 100644
--- a/tools/testing/selftests/bpf/prog_tests/kasan.c
+++ b/tools/testing/selftests/bpf/prog_tests/kasan.c
@@ -309,6 +309,149 @@ static void run_blinding_subtest(void)
free(ctx);
}
+/*
+ * Value-integrity subtests.
+ *
+ * The suite above only checks that a KASAN report was emitted. A report is not
+ * fatal here (the kernel runs with kasan_multi_shot and panic_on_warn=0), so
+ * the program keeps executing afterwards and its state must be intact. These
+ * two subtests check the two places where the JIT keeps live BPF state in
+ * BPF_REG_AX (x86 r10) across the injected __asan_* call.
+ */
+#define R0_SENTINEL 0x5EEDFACE
+#define ST_SENTINEL 0x0B0BCAFE
+
+static void run_atomic_fetch_integrity_subtest(struct test_ctx *ctx,
+ struct kasan *skel)
+{
+ struct test_spec spec = {
+ .prog_type = "atomic_fetch_r0_integrity",
+ .is_write = true,
+ };
+
+ if (!test__start_subtest("atomic_fetch_r0_integrity"))
+ return;
+
+ strncpy(ctx->prog_name, "atomic_fetch_r0_integrity", PROG_NAME_MAX_LEN);
+
+ /*
+ * Control run: same instruction, same instrumentation, unpoisoned
+ * target. No report, so nothing may be disturbed. If this one fails,
+ * the test itself is wrong.
+ */
+ skel->data->poison_target = false;
+ skel->bss->r0_after_atomic = 0;
+ spec.expect_no_report = true;
+ exec_subtest(ctx, &spec, 8, false);
+ ASSERT_EQ(skel->bss->r0_after_atomic, R0_SENTINEL,
+ "control: r0 preserved when no report fires");
+
+ skel->data->poison_target = true;
+ skel->bss->r0_after_atomic = 0;
+ spec.expect_no_report = false;
+
+ /* Asserts that the report fired: the check must have been emitted. */
+ exec_subtest(ctx, &spec, 8, false);
+
+ /*
+ * ... and that returning from it left r0 alone. The JIT saves r0 into
+ * BPF_REG_AX before the CMPXCHG loop and restores it after, with the
+ * KASAN check in between.
+ */
+ ASSERT_EQ(skel->bss->r0_after_atomic, R0_SENTINEL,
+ "r0 preserved across a KASAN report in a fetch atomic");
+}
+
+static void run_atomic_fetch_addr_subtest(struct test_ctx *ctx,
+ struct kasan *skel)
+{
+ struct test_spec spec = {
+ .prog_type = "atomic_fetch_r0_addr",
+ .is_write = true,
+ };
+
+ if (!test__start_subtest("atomic_fetch_r0_addr"))
+ return;
+
+ strncpy(ctx->prog_name, "atomic_fetch_r0_addr", PROG_NAME_MAX_LEN);
+ exec_subtest(ctx, &spec, 8, false);
+
+ ASSERT_EQ(skel->bss->r0_addr_after, skel->bss->r0_addr_ref,
+ "r10 (used as the atomic base address) preserved");
+}
+
+static void run_atomic_fetch_oob_c_subtest(struct test_ctx *ctx)
+{
+ struct test_spec spec = {
+ .prog_type = "atomic_fetch_oob_c",
+ .is_write = true,
+ };
+
+ if (!test__start_subtest("atomic_fetch_oob_c"))
+ return;
+
+ strncpy(ctx->prog_name, "atomic_fetch_oob_c", PROG_NAME_MAX_LEN);
+ exec_subtest(ctx, &spec, 8, false);
+}
+
+static void run_blinding_integrity_subtest(void)
+{
+ struct test_spec spec = {
+ .prog_type = "st_blinded_integrity",
+ .is_write = true,
+ };
+ char bpf_jit_harden = '2';
+ struct kasan_write_val val;
+ struct kasan_harden *skel;
+ struct test_ctx *ctx;
+ __u32 key = 0;
+ int ret;
+
+ if (!test__start_subtest("st_blinded_integrity"))
+ return;
+
+ ctx = calloc(1, sizeof(*ctx));
+ if (!ASSERT_OK_PTR(ctx, "alloc blinding ctx"))
+ return;
+ ctx->klog_fd = -1;
+
+ ret = set_bpf_jit_harden(&bpf_jit_harden);
+ if (!ASSERT_OK(ret, "set bpf_jit_harden"))
+ goto free_ctx;
+
+ skel = kasan_harden__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "open and load blinded prog"))
+ goto restore;
+
+ ctx->klog_fd = open_kernel_logs();
+ if (!ASSERT_OK_FD(ctx->klog_fd, "open kernel logs"))
+ goto destroy;
+
+ ctx->obj = skel->obj;
+ strncpy(ctx->prog_name, "st_blinded_integrity", PROG_NAME_MAX_LEN);
+
+ /* Asserts that the report fired. */
+ exec_subtest(ctx, &spec, 8, false);
+
+ /* ... and that the blinded store still wrote the right value. */
+ ret = bpf_map__lookup_elem(skel->maps.test_map, &key, sizeof(key),
+ &val, sizeof(val), 0);
+ if (!ASSERT_OK(ret, "read back blinded store target"))
+ goto close_klog;
+ ASSERT_EQ(val.data_8, ST_SENTINEL,
+ "blinded store value preserved across a KASAN report");
+
+close_klog:
+ close(ctx->klog_fd);
+destroy:
+ kasan_harden__destroy(skel);
+restore:
+ ASSERT_OK(set_bpf_jit_harden(&bpf_jit_harden),
+ "restore hardening configuration");
+free_ctx:
+ free(ctx);
+}
+
static struct test_spec tests[] = {
{
.prog_type = "st",
@@ -439,11 +582,16 @@ void test_kasan(void)
run_subtest(ctx, test);
}
+ run_atomic_fetch_integrity_subtest(ctx, skel);
+ run_atomic_fetch_addr_subtest(ctx, skel);
+ run_atomic_fetch_oob_c_subtest(ctx);
+
/*
* Blinding subtest is handled differently as it needs the
* corresponding program to be loaded with bpf_jit_harden raised
*/
run_blinding_subtest();
+ run_blinding_integrity_subtest();
close:
close(ctx->klog_fd);
diff --git a/tools/testing/selftests/bpf/progs/kasan.c b/tools/testing/selftests/bpf/progs/kasan.c
index ea29197646b0..68a041f14353 100644
--- a/tools/testing/selftests/bpf/progs/kasan.c
+++ b/tools/testing/selftests/bpf/progs/kasan.c
@@ -459,4 +459,111 @@ int ldx_oob(struct __sk_buff *skb)
return tmp.data_1;
}
+/*
+ * Value-integrity tests: a KASAN report must not change what the program
+ * computes. The kernel keeps running after a report under kasan_multi_shot,
+ * so every register the JIT holds live across the injected __asan_* call must
+ * come back unchanged.
+ */
+#define R0_SENTINEL 0x5EEDFACE
+
+/*
+ * The JIT lowers an RMW fetch atomic into a CMPXCHG loop and needs RAX as the
+ * CMPXCHG operand, so it stashes BPF r0 into BPF_REG_AX (x86 r10) before the
+ * loop and restores it afterwards. The KASAN check sits between the two, so
+ * r10 holds live BPF state across the call.
+ *
+ * Keep a sentinel in r0 across the atomic and publish it afterwards: if the
+ * check clobbers r10, the restored r0 is garbage.
+ */
+__u64 r0_after_atomic = 0;
+/* Cleared by the control run, which must not produce a report at all. */
+bool poison_target = true;
+
+SEC("tcx/ingress")
+int atomic_fetch_r0_integrity(struct __sk_buff *skb)
+{
+ struct kasan_test_val *val;
+ __u32 key = 0;
+
+ val = bpf_map_lookup_elem(&test_map, &key);
+ if (!val)
+ return 0;
+
+ if (poison_target)
+ bpf_kfunc_kasan_poison(val, sizeof(struct kasan_test_val));
+ asm volatile ("r0 = %[sentinel];"
+ "r1 = 8;"
+ "r1 = atomic_fetch_or((u64 *)(%[val] + 8), r1);"
+ "*(u64 *)(%[out] + 0) = r0;"
+ :
+ : [val] "r" (val), [out] "r" (&r0_after_atomic),
+ [sentinel] "i" (R0_SENTINEL)
+ : "r0", "r1", "memory");
+ if (poison_target)
+ bpf_kfunc_kasan_unpoison(val, sizeof(struct kasan_test_val));
+
+ return 0;
+}
+
+/*
+ * Same lowering, but with the target pointer in r0. The JIT then uses
+ * BPF_REG_AX as real_dst_reg, i.e. r10 is not merely holding a value across
+ * the KASAN check - it is the base address of the ldx and of the locked
+ * cmpxchg that follow it.
+ */
+__u64 r0_addr_after = 0;
+__u64 r0_addr_ref = 0;
+
+SEC("tcx/ingress")
+int atomic_fetch_r0_addr(struct __sk_buff *skb)
+{
+ struct kasan_test_val *val;
+ __u32 key = 0;
+
+ val = bpf_map_lookup_elem(&test_map, &key);
+ if (!val)
+ return 0;
+
+ bpf_kfunc_kasan_poison(val, sizeof(struct kasan_test_val));
+ asm volatile ("r0 = %[val];"
+ "r1 = 8;"
+ "r1 = atomic_fetch_or((u64 *)(r0 + 8), r1);"
+ "*(u64 *)(%[out] + 0) = r0;"
+ "*(u64 *)(%[ref] + 0) = %[val];"
+ :
+ : [val] "r" (val), [out] "r" (&r0_addr_after),
+ [ref] "r" (&r0_addr_ref)
+ : "r0", "r1", "memory");
+ bpf_kfunc_kasan_unpoison(val, sizeof(struct kasan_test_val));
+
+ return 0;
+}
+
+/*
+ * Plain C, no asm: the second lookup returns the map value pointer in r0 and
+ * the compiler leaves it there for the fetch atomic, so the JIT uses
+ * BPF_REG_AX (r10) as the base address of the post-check ldx and cmpxchg.
+ */
+SEC("tcx/ingress")
+int atomic_fetch_oob_c(struct __sk_buff *skb)
+{
+ struct kasan_test_val *val, *val2;
+ __u32 key = 0;
+
+ val = bpf_map_lookup_elem(&test_map, &key);
+ if (!val)
+ return 0;
+
+ bpf_kfunc_kasan_poison(val, sizeof(struct kasan_test_val));
+
+ val2 = bpf_map_lookup_elem(&test_map, &key);
+ if (!val2)
+ return 0;
+ __sync_fetch_and_or(&val2->data_8, 8);
+
+ bpf_kfunc_kasan_unpoison(val, sizeof(struct kasan_test_val));
+ return 0;
+}
+
char LICENSE[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/kasan_harden.c b/tools/testing/selftests/bpf/progs/kasan_harden.c
index a2756bbfd529..9172a1017a8e 100644
--- a/tools/testing/selftests/bpf/progs/kasan_harden.c
+++ b/tools/testing/selftests/bpf/progs/kasan_harden.c
@@ -38,4 +38,36 @@ int st_blinded(struct __sk_buff *skb)
return 0;
}
+/*
+ * Constant blinding lowers a BPF_ST_MEM into
+ * BPF_REG_AX = imm ^ rnd; BPF_REG_AX ^= rnd; *(dst + off) = BPF_REG_AX
+ * and the JIT emits the KASAN check before that store, so BPF_REG_AX (x86 r10)
+ * holds the value to be written across the __asan_* call.
+ *
+ * Written in asm to guarantee a raw BPF_ST opcode regardless of -mcpu: at
+ * -mcpu=v3 clang lowers an immediate store to MOV+STX, which is not blinded
+ * through BPF_REG_AX and does not exercise this path.
+ */
+#define ST_SENTINEL 0x0B0BCAFE
+
+SEC("tcx/ingress")
+int st_blinded_integrity(struct __sk_buff *skb)
+{
+ struct kasan_test_val *val;
+ __u32 key = 0;
+
+ val = bpf_map_lookup_elem(&test_map, &key);
+ if (!val)
+ return 0;
+
+ bpf_kfunc_kasan_poison(val, sizeof(struct kasan_test_val));
+ asm volatile ("*(u64 *)(%[val] + 8) = %[sentinel];"
+ :
+ : [val] "r" (val), [sentinel] "i" (ST_SENTINEL)
+ : "memory");
+ bpf_kfunc_kasan_unpoison(val, sizeof(struct kasan_test_val));
+
+ return 0;
+}
+
char LICENSE[] SEC("license") = "GPL";
--
2.53.0-Meta
next prev parent reply other threads:[~2026-08-25 0:13 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-21 22:39 [PATCH bpf-next v7 0/9] bpf: add support for KASAN checks in JITed programs Alexis Lothoré (eBPF Foundation)
2026-08-21 22:39 ` [PATCH bpf-next v7 1/9] bpf: mark instructions accessing program stack Alexis Lothoré (eBPF Foundation)
2026-08-21 22:54 ` sashiko-bot
2026-08-21 23:24 ` bot+bpf-ci
2026-08-23 22:33 ` Kumar Kartikeya Dwivedi
2026-08-21 22:39 ` [PATCH bpf-next v7 2/9] bpf: add BPF_JIT_KASAN for KASAN instrumentation of JITed programs Alexis Lothoré (eBPF Foundation)
2026-08-21 22:39 ` [PATCH bpf-next v7 3/9] bpf, x86: refactor BPF_ST management in do_jit Alexis Lothoré (eBPF Foundation)
2026-08-21 22:39 ` [PATCH bpf-next v7 4/9] bpf, x86: emit KASAN checks in x86 JITed programs Alexis Lothoré (eBPF Foundation)
2026-08-21 23:24 ` bot+bpf-ci
2026-08-21 23:33 ` sashiko-bot
2026-08-23 22:30 ` Kumar Kartikeya Dwivedi
2026-08-25 0:12 ` Ihor Solodrai [this message]
2026-08-25 0:26 ` Kumar Kartikeya Dwivedi
2026-08-25 7:03 ` Alexis Lothoré
2026-08-21 22:39 ` [PATCH bpf-next v7 5/9] bpf, x86: enable KASAN for JITed programs on x86 Alexis Lothoré (eBPF Foundation)
2026-08-21 22:55 ` sashiko-bot
2026-08-21 22:39 ` [PATCH bpf-next v7 6/9] selftests/bpf: make cmdline_contains stricter Alexis Lothoré (eBPF Foundation)
2026-08-21 22:39 ` [PATCH bpf-next v7 7/9] selftests/bpf: add helpers for KASAN in JIT testing Alexis Lothoré (eBPF Foundation)
2026-08-21 22:39 ` [PATCH bpf-next v7 8/9] selftests/bpf: move bpf_jit_harden helper into testing_helpers Alexis Lothoré (eBPF Foundation)
2026-08-21 23:13 ` bot+bpf-ci
2026-08-21 22:39 ` [PATCH bpf-next v7 9/9] selftests/bpf: add tests to validate KASAN on JIT programs Alexis Lothoré (eBPF Foundation)
2026-08-21 23:36 ` bot+bpf-ci
2026-08-23 22:40 ` Kumar Kartikeya Dwivedi
2026-08-23 22:53 ` Kumar Kartikeya Dwivedi
2026-08-23 22:53 ` [PATCH bpf-next v7 0/9] bpf: add support for KASAN checks in JITed programs Kumar Kartikeya Dwivedi
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=fa7cc4ad-6b8d-4bdf-8e2c-66d98ddef7df@linux.dev \
--to=ihor.solodrai@linux.dev \
--cc=alexis.lothore@bootlin.com \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=memxor@gmail.com \
--cc=sashiko-reviews@lists.linux.dev \
/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