* [PATCH bpf v2 0/2] libbpf: Reject out-of-range linker relocation offsets
@ 2026-06-14 9:26 HyeongJun An
2026-06-14 9:26 ` [PATCH bpf v2 1/2] " HyeongJun An
2026-06-14 9:26 ` [PATCH bpf v2 2/2] selftests/bpf: Test linker rejects out-of-range relocation offset HyeongJun An
0 siblings, 2 replies; 5+ messages in thread
From: HyeongJun An @ 2026-06-14 9:26 UTC (permalink / raw)
To: Andrii Nakryiko, Alexei Starovoitov, Daniel Borkmann
Cc: Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Shuah Khan, bpf,
linux-kselftest, linux-kernel, HyeongJun An
The libbpf static linker validates relocation type, symbol index and
instruction alignment in linker_sanity_check_elf_relos(), but does not
check that the relocation offset is inside the relocated section. A
malformed BPF object processed by the static linker (e.g. via
"bpftool gen object") can therefore carry an out-of-range r_offset that
linker_append_elf_relos() then uses to index the section data, reading
and writing past the buffer.
The normal object-loading path already rejects such offsets (libbpf.c,
rel->r_offset >= scn_data->d_size); the static linker path is the
missing sibling. Patch 1 adds the same bound. Patch 2 adds a selftest
that builds a tiny object with an out-of-range relocation offset and
checks that the linker now rejects it, with a valid relocation as a
positive control.
Reproduced with ASAN: before patch 1 the out-of-range relocation is
accepted (and triggers a heap-buffer-overflow); after, it is rejected
with -EINVAL.
Changes in v2:
- selftest: set the generated ELF object's EI_DATA from the host byte
order instead of hardcoding little-endian, so it works on big-endian
hosts (e.g. s390x).
- selftest: add fallback definitions for EM_BPF and R_BPF_64_64 for
older system headers.
Patch 1 (the fix) is unchanged.
HyeongJun An (2):
libbpf: Reject out-of-range linker relocation offsets
selftests/bpf: Test linker rejects out-of-range relocation offset
tools/lib/bpf/linker.c | 6 +
.../selftests/bpf/prog_tests/libbpf_linker.c | 231 ++++++++++++++++++
2 files changed, 237 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/libbpf_linker.c
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH bpf v2 1/2] libbpf: Reject out-of-range linker relocation offsets
2026-06-14 9:26 [PATCH bpf v2 0/2] libbpf: Reject out-of-range linker relocation offsets HyeongJun An
@ 2026-06-14 9:26 ` HyeongJun An
2026-06-14 9:37 ` sashiko-bot
2026-06-15 0:05 ` Alexei Starovoitov
2026-06-14 9:26 ` [PATCH bpf v2 2/2] selftests/bpf: Test linker rejects out-of-range relocation offset HyeongJun An
1 sibling, 2 replies; 5+ messages in thread
From: HyeongJun An @ 2026-06-14 9:26 UTC (permalink / raw)
To: Andrii Nakryiko, Alexei Starovoitov, Daniel Borkmann
Cc: Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Shuah Khan, bpf,
linux-kselftest, linux-kernel, HyeongJun An, stable
The static linker sanity-checks relocation sections before appending them,
but for executable target sections it only verifies that r_offset is
BPF-instruction aligned. It does not verify that the offset is inside the
relocated section.
A malformed object can therefore pass an out-of-range offset through
linker_sanity_check_elf_relos(). When the relocation is against an
STT_SECTION symbol, linker_append_elf_relos() uses the unchecked offset to
find the instruction to adjust:
insn = dst_linked_sec->raw_data + dst_rel->r_offset;
and then reads insn->code and updates insn->imm.
This is reproducible with bpftool's static linker by crafting a BPF object
with a 16-byte executable section and a relocation in its .rel section
whose r_offset is 0x1000:
BUG: AddressSanitizer: heap-buffer-overflow in linker_append_elf_relos
READ of size 1
linker_append_elf_relos
bpf_linker_add_file
bpf_linker__add_file
do_object
Reject relocation offsets that are outside the relocated section before any
later use. This mirrors the normal object loading path, which already
rejects executable relocations whose r_offset is not inside the program
section.
Fixes: faf6ed321cf6 ("libbpf: Add BPF static linker APIs")
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5
Signed-off-by: HyeongJun An <sammiee5311@gmail.com>
---
tools/lib/bpf/linker.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/tools/lib/bpf/linker.c b/tools/lib/bpf/linker.c
index 78f92c39290a..3eb23da167d2 100644
--- a/tools/lib/bpf/linker.c
+++ b/tools/lib/bpf/linker.c
@@ -1048,6 +1048,12 @@ static int linker_sanity_check_elf_relos(struct src_obj *obj, struct src_sec *se
return -EINVAL;
}
+ if (relo->r_offset >= link_sec->shdr->sh_size) {
+ pr_warn("ELF relo #%d in section #%zu has invalid offset %zu in %s\n",
+ i, sec->sec_idx, (size_t)relo->r_offset, obj->filename);
+ return -EINVAL;
+ }
+
if (link_sec->shdr->sh_flags & SHF_EXECINSTR) {
if (relo->r_offset % sizeof(struct bpf_insn) != 0) {
pr_warn("ELF relo #%d in section #%zu points to missing symbol #%zu in %s\n",
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH bpf v2 2/2] selftests/bpf: Test linker rejects out-of-range relocation offset
2026-06-14 9:26 [PATCH bpf v2 0/2] libbpf: Reject out-of-range linker relocation offsets HyeongJun An
2026-06-14 9:26 ` [PATCH bpf v2 1/2] " HyeongJun An
@ 2026-06-14 9:26 ` HyeongJun An
1 sibling, 0 replies; 5+ messages in thread
From: HyeongJun An @ 2026-06-14 9:26 UTC (permalink / raw)
To: Andrii Nakryiko, Alexei Starovoitov, Daniel Borkmann
Cc: Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Shuah Khan, bpf,
linux-kselftest, linux-kernel, HyeongJun An
Add regression coverage for libbpf static linker relocation offset checks.
Build a minimal ET_REL/EM_BPF object in memory with a 16-byte executable
section and one relocation against that section. Check that a valid
relocation offset is accepted and an offset outside the relocated section
is rejected with -EINVAL.
Assisted-by: Codex:gpt-5
Signed-off-by: HyeongJun An <sammiee5311@gmail.com>
---
.../selftests/bpf/prog_tests/libbpf_linker.c | 231 ++++++++++++++++++
1 file changed, 231 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/libbpf_linker.c
diff --git a/tools/testing/selftests/bpf/prog_tests/libbpf_linker.c b/tools/testing/selftests/bpf/prog_tests/libbpf_linker.c
new file mode 100644
index 000000000000..a0a61b32fe2e
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/libbpf_linker.c
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <elf.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <bpf/libbpf.h>
+#include <linux/bpf.h>
+#include <test_progs.h>
+
+#ifndef EM_BPF
+#define EM_BPF 247
+#endif
+
+#ifndef R_BPF_64_64
+#define R_BPF_64_64 1
+#endif
+
+enum {
+ SEC_NULL,
+ SEC_TEXT,
+ SEC_REL_TEXT,
+ SEC_SYMTAB,
+ SEC_STRTAB,
+ SEC_SHSTRTAB,
+ SEC_CNT,
+};
+
+enum {
+ SHSTR_TEXT = 1,
+ SHSTR_REL_TEXT = SHSTR_TEXT + sizeof(".text"),
+ SHSTR_SYMTAB = SHSTR_REL_TEXT + sizeof(".rel.text"),
+ SHSTR_STRTAB = SHSTR_SYMTAB + sizeof(".symtab"),
+ SHSTR_SHSTRTAB = SHSTR_STRTAB + sizeof(".strtab"),
+};
+
+struct test_elf {
+ void *buf;
+ size_t sz;
+};
+
+static size_t elf_round_up(size_t value, size_t align)
+{
+ return (value + align - 1) / align * align;
+}
+
+static unsigned char elf_byteorder(void)
+{
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ return ELFDATA2LSB;
+#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+ return ELFDATA2MSB;
+#else
+#error "Unrecognized __BYTE_ORDER__"
+#endif
+}
+
+/*
+ * Build a minimal ET_REL object in memory. A normal .bpf.c source cannot
+ * produce a relocation whose offset points past the relocated section, so the
+ * test constructs the ELF directly and feeds it to bpf_linker__add_buf().
+ */
+static struct test_elf make_relo_obj(size_t relo_off)
+{
+ static const char shstrtab[] = "\0.text\0.rel.text\0.symtab\0.strtab\0.shstrtab";
+ static const char strtab[] = "\0";
+ struct bpf_insn insns[] = {
+ {
+ .code = BPF_ALU64 | BPF_MOV | BPF_K,
+ .dst_reg = BPF_REG_0,
+ .imm = 0,
+ },
+ {
+ .code = BPF_JMP | BPF_EXIT,
+ },
+ };
+ size_t off, text_off, rel_off, symtab_off, strtab_off, shstrtab_off, shdr_off;
+ struct test_elf obj = {};
+ Elf64_Shdr *shdr;
+ Elf64_Ehdr *ehdr;
+ Elf64_Sym *sym;
+ Elf64_Rel *rel;
+
+ off = sizeof(*ehdr);
+ text_off = elf_round_up(off, 8);
+ off = text_off + sizeof(insns);
+ rel_off = elf_round_up(off, 8);
+ off = rel_off + sizeof(*rel);
+ symtab_off = elf_round_up(off, 8);
+ off = symtab_off + 2 * sizeof(*sym);
+ strtab_off = off;
+ off = strtab_off + sizeof(strtab);
+ shstrtab_off = off;
+ off = shstrtab_off + sizeof(shstrtab);
+ shdr_off = elf_round_up(off, 8);
+ off = shdr_off + SEC_CNT * sizeof(*shdr);
+
+ obj.buf = calloc(1, off);
+ if (!obj.buf)
+ return obj;
+ obj.sz = off;
+
+ ehdr = obj.buf;
+ memcpy(ehdr->e_ident, ELFMAG, SELFMAG);
+ ehdr->e_ident[EI_CLASS] = ELFCLASS64;
+ ehdr->e_ident[EI_DATA] = elf_byteorder();
+ ehdr->e_ident[EI_VERSION] = EV_CURRENT;
+ ehdr->e_type = ET_REL;
+ ehdr->e_machine = EM_BPF;
+ ehdr->e_version = EV_CURRENT;
+ ehdr->e_ehsize = sizeof(*ehdr);
+ ehdr->e_shoff = shdr_off;
+ ehdr->e_shentsize = sizeof(*shdr);
+ ehdr->e_shnum = SEC_CNT;
+ ehdr->e_shstrndx = SEC_SHSTRTAB;
+
+ memcpy(obj.buf + text_off, insns, sizeof(insns));
+
+ rel = obj.buf + rel_off;
+ rel->r_offset = relo_off;
+ rel->r_info = ELF64_R_INFO(1, R_BPF_64_64);
+
+ sym = obj.buf + symtab_off;
+ sym[1].st_info = ELF64_ST_INFO(STB_LOCAL, STT_SECTION);
+ sym[1].st_shndx = SEC_TEXT;
+
+ memcpy(obj.buf + strtab_off, strtab, sizeof(strtab));
+ memcpy(obj.buf + shstrtab_off, shstrtab, sizeof(shstrtab));
+
+ shdr = obj.buf + shdr_off;
+ shdr[SEC_TEXT] = (Elf64_Shdr) {
+ .sh_name = SHSTR_TEXT,
+ .sh_type = SHT_PROGBITS,
+ .sh_flags = SHF_ALLOC | SHF_EXECINSTR,
+ .sh_offset = text_off,
+ .sh_size = sizeof(insns),
+ .sh_addralign = 8,
+ .sh_entsize = sizeof(struct bpf_insn),
+ };
+ shdr[SEC_REL_TEXT] = (Elf64_Shdr) {
+ .sh_name = SHSTR_REL_TEXT,
+ .sh_type = SHT_REL,
+ .sh_offset = rel_off,
+ .sh_size = sizeof(*rel),
+ .sh_link = SEC_SYMTAB,
+ .sh_info = SEC_TEXT,
+ .sh_addralign = 8,
+ .sh_entsize = sizeof(*rel),
+ };
+ shdr[SEC_SYMTAB] = (Elf64_Shdr) {
+ .sh_name = SHSTR_SYMTAB,
+ .sh_type = SHT_SYMTAB,
+ .sh_offset = symtab_off,
+ .sh_size = 2 * sizeof(*sym),
+ .sh_link = SEC_STRTAB,
+ .sh_info = 2,
+ .sh_addralign = 8,
+ .sh_entsize = sizeof(*sym),
+ };
+ shdr[SEC_STRTAB] = (Elf64_Shdr) {
+ .sh_name = SHSTR_STRTAB,
+ .sh_type = SHT_STRTAB,
+ .sh_offset = strtab_off,
+ .sh_size = sizeof(strtab),
+ .sh_addralign = 1,
+ };
+ shdr[SEC_SHSTRTAB] = (Elf64_Shdr) {
+ .sh_name = SHSTR_SHSTRTAB,
+ .sh_type = SHT_STRTAB,
+ .sh_offset = shstrtab_off,
+ .sh_size = sizeof(shstrtab),
+ .sh_addralign = 1,
+ };
+
+ return obj;
+}
+
+static int link_relo_obj(size_t relo_off)
+{
+ char path[] = "/tmp/libbpf_linker_relo_XXXXXX";
+ struct test_elf obj = {};
+ struct bpf_linker *linker;
+ int err, fd;
+
+ fd = mkstemp(path);
+ if (!ASSERT_OK_FD(fd, "mkstemp"))
+ return -errno;
+ close(fd);
+
+ linker = bpf_linker__new(path, NULL);
+ if (!ASSERT_OK_PTR(linker, "linker_new")) {
+ err = libbpf_get_error(linker);
+ goto out_unlink;
+ }
+
+ obj = make_relo_obj(relo_off);
+ if (!ASSERT_OK_PTR(obj.buf, "make_relo_obj")) {
+ err = -ENOMEM;
+ goto out_free_linker;
+ }
+
+ err = bpf_linker__add_buf(linker, obj.buf, obj.sz, NULL);
+ if (!err)
+ err = bpf_linker__finalize(linker);
+
+ free(obj.buf);
+out_free_linker:
+ bpf_linker__free(linker);
+out_unlink:
+ unlink(path);
+ return err;
+}
+
+static void test_valid_relo_offset(void)
+{
+ ASSERT_OK(link_relo_obj(0), "valid_relo_offset");
+}
+
+static void test_invalid_relo_offset(void)
+{
+ ASSERT_EQ(link_relo_obj(0x1000), -EINVAL, "invalid_relo_offset");
+}
+
+void test_libbpf_linker(void)
+{
+ if (test__start_subtest("valid_relo_offset"))
+ test_valid_relo_offset();
+ if (test__start_subtest("invalid_relo_offset"))
+ test_invalid_relo_offset();
+}
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH bpf v2 1/2] libbpf: Reject out-of-range linker relocation offsets
2026-06-14 9:26 ` [PATCH bpf v2 1/2] " HyeongJun An
@ 2026-06-14 9:37 ` sashiko-bot
2026-06-15 0:05 ` Alexei Starovoitov
1 sibling, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-06-14 9:37 UTC (permalink / raw)
To: HyeongJun An; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
Pre-existing issues:
- [High] Missing bounds check on st_shndx for STT_SECTION symbols bypassing validation with reserved indices causes an out-of-bounds array access.
- [High] Relocations against SHT_NOBITS sections marked with SHF_EXECINSTR lead to a NULL pointer dereference.
- [High] Integer overflow in symbol index validation leads to an out-of-bounds heap read on 32-bit platforms.
--
commit d6f111169154c451a99d6d0abcb52570ad19f6c1
Author: HyeongJun An <sammiee5311@gmail.com>
libbpf: Reject out-of-range linker relocation offsets
The static linker sanity-checks relocation sections before appending them,
but for executable target sections it only verifies that r_offset is
BPF-instruction aligned. It does not verify that the offset is inside the
relocated section.
> diff --git a/tools/lib/bpf/linker.c b/tools/lib/bpf/linker.c
> index 78f92c39290af..3eb23da167d29 100644
> --- a/tools/lib/bpf/linker.c
> +++ b/tools/lib/bpf/linker.c
> @@ -1048,6 +1048,12 @@ static int linker_sanity_check_elf_relos(struct src_obj *obj, struct src_sec *se
> return -EINVAL;
> }
>
[Severity: High]
This isn't a bug introduced by this patch, but can the symbol index validation
overflow on 32-bit platforms?
In linker_sanity_check_elf_relos(), the size check just above this line
multiplies by sizeof(Elf64_Sym):
if (!sym_idx || sym_idx * sizeof(Elf64_Sym) >= sym_sec->shdr->sh_size) {
If an exceptionally large sym_idx is provided (like 0x15555556), the
multiplication can overflow a 32-bit size_t, bypassing the bounds check.
Later in linker_append_elf_relos(), this unvalidated index is used to access
the obj->sym_map array:
dst_sym_idx = obj->sym_map[src_sym_idx];
Could this result in an out-of-bounds heap read?
> + if (relo->r_offset >= link_sec->shdr->sh_size) {
> + pr_warn("ELF relo #%d in section #%zu has invalid offset %zu in %s\n",
> + i, sec->sec_idx, (size_t)relo->r_offset, obj->filename);
> + return -EINVAL;
> + }
> +
> if (link_sec->shdr->sh_flags & SHF_EXECINSTR) {
> if (relo->r_offset % sizeof(struct bpf_insn) != 0) {
> pr_warn("ELF relo #%d in section #%zu points to missing symbol #%zu in %s\n",
[Severity: High]
This is a pre-existing issue, but could reserved indices for STT_SECTION
symbols bypass validation and cause an out-of-bounds array access?
In linker_sanity_check_elf_symtab(), the bounds check is skipped if the index
is reserved:
if (sym->st_shndx < SHN_LORESERVE && sym->st_shndx >= obj->sec_cnt) {
Later in linker_append_elf_relos(), relocations against STT_SECTION symbols
use this unchecked index directly:
struct src_sec *sec = &obj->secs[src_sym->st_shndx];
If an attacker provides an ELF with a reserved index like SHN_ABS, could this
read past the allocated obj->secs array and leak heap memory when sec->dst_off
is added to the instruction?
[Severity: High]
This is a pre-existing issue, but could relocations against SHT_NOBITS sections
lead to a NULL pointer dereference?
Currently, linker_sanity_check_elf() does not validate the SHF_EXECINSTR flag
for SHT_NOBITS sections. In extend_sec(), SHT_NOBITS sections skip allocating
dst->raw_data, leaving it as NULL.
When linker_append_elf_relos() processes a relocation for such a section,
it computes an address from raw_data:
insn = dst_linked_sec->raw_data + dst_rel->r_offset;
if (insn->code == (BPF_JMP | BPF_CALL))
Since raw_data is NULL, would this result in a direct NULL pointer dereference?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260614092616.165337-1-sammiee5311@gmail.com?part=1
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH bpf v2 1/2] libbpf: Reject out-of-range linker relocation offsets
2026-06-14 9:26 ` [PATCH bpf v2 1/2] " HyeongJun An
2026-06-14 9:37 ` sashiko-bot
@ 2026-06-15 0:05 ` Alexei Starovoitov
1 sibling, 0 replies; 5+ messages in thread
From: Alexei Starovoitov @ 2026-06-15 0:05 UTC (permalink / raw)
To: HyeongJun An, Andrii Nakryiko, Alexei Starovoitov,
Daniel Borkmann
Cc: Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Shuah Khan, bpf,
linux-kselftest, linux-kernel, stable
On Sun Jun 14, 2026 at 2:26 AM PDT, HyeongJun An wrote:
> The static linker sanity-checks relocation sections before appending them,
> but for executable target sections it only verifies that r_offset is
> BPF-instruction aligned. It does not verify that the offset is inside the
> relocated section.
>
> A malformed object can therefore pass an out-of-range offset through
> linker_sanity_check_elf_relos(). When the relocation is against an
> STT_SECTION symbol, linker_append_elf_relos() uses the unchecked offset to
> find the instruction to adjust:
>
> insn = dst_linked_sec->raw_data + dst_rel->r_offset;
>
> and then reads insn->code and updates insn->imm.
>
> This is reproducible with bpftool's static linker by crafting a BPF object
> with a 16-byte executable section and a relocation in its .rel section
> whose r_offset is 0x1000:
libbpf trusts ELF.
There are many way to crash libbpf and libelf, for that matter, with corrupted ELF.
Please don't send such hardening patches.
pw-bot: cr
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-06-15 0:05 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-14 9:26 [PATCH bpf v2 0/2] libbpf: Reject out-of-range linker relocation offsets HyeongJun An
2026-06-14 9:26 ` [PATCH bpf v2 1/2] " HyeongJun An
2026-06-14 9:37 ` sashiko-bot
2026-06-15 0:05 ` Alexei Starovoitov
2026-06-14 9:26 ` [PATCH bpf v2 2/2] selftests/bpf: Test linker rejects out-of-range relocation offset HyeongJun An
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.