* [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed in allocated sections
@ 2026-09-25 22:46 Andrii Nakryiko
2026-09-25 22:46 ` [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs " Andrii Nakryiko
2026-09-25 23:35 ` [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed " bot+bpf-ci
0 siblings, 2 replies; 7+ messages in thread
From: Andrii Nakryiko @ 2026-09-25 22:46 UTC (permalink / raw)
To: bpf; +Cc: andrii, kernel-team
Clang puts an extern variable that ends up with a section, an extern
map in .maps or an arena global in .addr_space.<N>, into the BTF
DATASEC of that section, next to the object's own definitions and
with offset 0. Unlike .kconfig and .ksyms, that section is a real
allocated ELF section whenever the same object also defines a
variable in it. The static linker handles such externs incorrectly in
two ways.
The output symbol of an unresolved extern gets the destination
section's index instead of staying SHN_UNDEF. For ephemeral sections
sec_idx is 0, so this only shows up for allocated sections, where the
extern turns into a size-zero NOTYPE definition. Linking the output
again then fails with "conflicting non-weak symbol" against a strong
definition of that variable, or, against a weak one, keeps the bogus
symbol and resolves every reference to offset 0 of the section.
When the extern is resolved by a later object in the same link, the
DATASEC entry appended for the extern keeps its placeholder offset,
as linker_append_btf() only updates the size of an existing entry.
The ELF symbol and the BTF secinfo then disagree and skeleton
generation fails with:
Error: Something is wrong for .addr_space.1's variable #1: need offset 0, already at 8.
Keep unresolved externs SHN_UNDEF in the output symbol table, and
once an extern is resolved take the offset of its DATASEC entry from
the ELF symbol, which already accounts for weak vs. strong resolution.
Signed-off-by: Andrii Nakryiko <andrii@kernel.org>
---
tools/lib/bpf/linker.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/tools/lib/bpf/linker.c b/tools/lib/bpf/linker.c
index 53f64a1a1f25..0eaf98b093f0 100644
--- a/tools/lib/bpf/linker.c
+++ b/tools/lib/bpf/linker.c
@@ -2159,7 +2159,10 @@ static int linker_append_elf_sym(struct bpf_linker *linker, struct src_obj *obj,
dst_sym->st_name = name_off;
dst_sym->st_info = sym->st_info;
dst_sym->st_other = sym->st_other;
- dst_sym->st_shndx = dst_sec ? dst_sec->sec_idx : sym->st_shndx;
+ if (sym_is_extern)
+ dst_sym->st_shndx = SHN_UNDEF;
+ else
+ dst_sym->st_shndx = dst_sec ? dst_sec->sec_idx : sym->st_shndx;
dst_sym->st_value = (src_sec ? src_sec->dst_off : 0) + sym->st_value;
dst_sym->st_size = sym->st_size;
@@ -2592,6 +2595,10 @@ static int linker_append_btf(struct bpf_linker *linker, struct src_obj *obj)
continue;
dst_var = &dst_sec->sec_vars[glob_sym->var_idx];
+ if (!glob_sym->is_extern) {
+ Elf64_Sym *sym = get_sym_by_idx(linker, glob_sym->sym_idx);
+ dst_var->offset = sym->st_value;
+ }
/* Because underlying BTF type might have
* changed, so might its size have changed, so
* re-calculate and update it in sec_var.
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs in allocated sections
2026-09-25 22:46 [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed in allocated sections Andrii Nakryiko
@ 2026-09-25 22:46 ` Andrii Nakryiko
2026-09-25 22:58 ` sashiko-bot
2026-09-25 23:35 ` bot+bpf-ci
2026-09-25 23:35 ` [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed " bot+bpf-ci
1 sibling, 2 replies; 7+ messages in thread
From: Andrii Nakryiko @ 2026-09-25 22:46 UTC (permalink / raw)
To: bpf; +Cc: andrii, kernel-team
Add two objects that define an arena global each and declare the
other one's as extern, linked into a single skeleton with the
extern-declaring object first.
The skeleton build relinks the linked object twice more, which fixes
up DATASEC offsets from the ELF symbol table, so the test also drives
the libbpf linker directly: it links linked_arena1.bpf.o on its own,
links the result with linked_arena2.bpf.o, checks that no variable in
the .addr_space.1 DATASEC is left extern, and loads the object. The
same is done with linked_maps1/2.bpf.o, which have an extern map in
.maps.
Signed-off-by: Andrii Nakryiko <andrii@kernel.org>
---
tools/testing/selftests/bpf/Makefile.skel | 3 +-
.../selftests/bpf/prog_tests/linked_externs.c | 109 ++++++++++++++++++
.../selftests/bpf/progs/linked_arena1.c | 22 ++++
.../selftests/bpf/progs/linked_arena2.c | 18 +++
4 files changed, 151 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/linked_externs.c
create mode 100644 tools/testing/selftests/bpf/progs/linked_arena1.c
create mode 100644 tools/testing/selftests/bpf/progs/linked_arena2.c
diff --git a/tools/testing/selftests/bpf/Makefile.skel b/tools/testing/selftests/bpf/Makefile.skel
index 3d92cdca62ed..2e22bb901bf3 100644
--- a/tools/testing/selftests/bpf/Makefile.skel
+++ b/tools/testing/selftests/bpf/Makefile.skel
@@ -23,7 +23,7 @@ BPF_OBJS := $(patsubst %.c,$(RDIR)/%.bpf.o,$(BPF_SRCS))
SKEL_BLACKLIST := btf__% test_pinning_invalid.c test_sk_assign.c
LINKED_SKELS := test_static_linked.skel.h linked_funcs.skel.h \
- linked_vars.skel.h linked_maps.skel.h \
+ linked_vars.skel.h linked_maps.skel.h linked_arena.skel.h \
test_subskeleton.skel.h test_subskeleton_lib.skel.h \
test_usdt.skel.h tracing_multi.skel.h \
tracing_multi_module.skel.h \
@@ -46,6 +46,7 @@ test_static_linked.skel.h-deps := test_static_linked1.bpf.o test_static_linked2.
linked_funcs.skel.h-deps := linked_funcs1.bpf.o linked_funcs2.bpf.o
linked_vars.skel.h-deps := linked_vars1.bpf.o linked_vars2.bpf.o
linked_maps.skel.h-deps := linked_maps1.bpf.o linked_maps2.bpf.o
+linked_arena.skel.h-deps := linked_arena1.bpf.o linked_arena2.bpf.o
# In the subskeleton case, we want the test_subskeleton_lib.subskel.h file
# but that's created as a side-effect of the skel.h generation.
test_subskeleton.skel.h-deps := test_subskeleton_lib2.bpf.o test_subskeleton_lib.bpf.o test_subskeleton.bpf.o
diff --git a/tools/testing/selftests/bpf/prog_tests/linked_externs.c b/tools/testing/selftests/bpf/prog_tests/linked_externs.c
new file mode 100644
index 000000000000..29358c82dc35
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/linked_externs.c
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include <bpf/btf.h>
+#include "linked_arena.skel.h"
+
+static void test_skel(void)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ struct linked_arena *skel;
+ int err;
+
+ skel = linked_arena__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel_open_and_load"))
+ return;
+
+ ASSERT_EQ(skel->arena->a_val, 1, "a_val_init");
+ ASSERT_EQ(skel->arena->b_val, 2, "b_val_init");
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.sum1), &opts);
+ ASSERT_OK(err, "sum1_run");
+ ASSERT_EQ(opts.retval, 1 + 2, "sum1_retval");
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.bump2), &opts);
+ ASSERT_OK(err, "bump2_run");
+ ASSERT_EQ(opts.retval, 11 + 22, "bump2_retval");
+
+ ASSERT_EQ(skel->arena->a_val, 11, "a_val");
+ ASSERT_EQ(skel->arena->b_val, 22, "b_val");
+
+ linked_arena__destroy(skel);
+}
+
+static int link_objs(const char *out, const char *in1, const char *in2)
+{
+ struct bpf_linker *linker;
+ int err;
+
+ linker = bpf_linker__new(out, NULL);
+ if (!ASSERT_OK_PTR(linker, "linker_new"))
+ return -EINVAL;
+
+ err = bpf_linker__add_file(linker, in1, NULL);
+ if (!ASSERT_OK(err, in1))
+ goto cleanup;
+ if (in2) {
+ err = bpf_linker__add_file(linker, in2, NULL);
+ if (!ASSERT_OK(err, in2))
+ goto cleanup;
+ }
+ err = bpf_linker__finalize(linker);
+ ASSERT_OK(err, "finalize");
+
+cleanup:
+ bpf_linker__free(linker);
+ return err;
+}
+
+/* link in1 on its own first, then link the result with in2 */
+static void test_relink(const char *in1, const char *in2, const char *sec_name)
+{
+ char out1[] = "/tmp/linked_externs.XXXXXX", out2[] = "/tmp/linked_externs.XXXXXX";
+ const struct btf_var_secinfo *vi;
+ struct bpf_object *obj = NULL;
+ const struct btf_type *t;
+ struct btf *btf;
+ int i, id, n;
+
+ close(mkstemp(out1));
+ close(mkstemp(out2));
+
+ if (!ASSERT_OK(link_objs(out1, in1, NULL), "link_stage1") ||
+ !ASSERT_OK(link_objs(out2, out1, in2), "link_stage2"))
+ goto cleanup;
+
+ obj = bpf_object__open_file(out2, NULL);
+ if (!ASSERT_OK_PTR(obj, "obj_open"))
+ goto cleanup;
+
+ btf = bpf_object__btf(obj);
+ id = btf__find_by_name_kind(btf, sec_name, BTF_KIND_DATASEC);
+ if (!ASSERT_GT(id, 0, "find_datasec"))
+ goto cleanup;
+
+ t = btf__type_by_id(btf, id);
+ vi = btf_var_secinfos(t);
+ n = btf_vlen(t);
+ for (i = 0; i < n; i++) {
+ t = btf__type_by_id(btf, vi[i].type);
+ ASSERT_NEQ(btf_var(t)->linkage, BTF_VAR_GLOBAL_EXTERN, "var_resolved");
+ }
+
+ ASSERT_OK(bpf_object__load(obj), "obj_load");
+
+cleanup:
+ bpf_object__close(obj);
+ unlink(out1);
+ unlink(out2);
+}
+
+void test_linked_externs(void)
+{
+ if (test__start_subtest("skel_arena"))
+ test_skel();
+ if (test__start_subtest("relink_arena"))
+ test_relink("linked_arena1.bpf.o", "linked_arena2.bpf.o", ".addr_space.1");
+ if (test__start_subtest("relink_maps"))
+ test_relink("linked_maps1.bpf.o", "linked_maps2.bpf.o", ".maps");
+}
diff --git a/tools/testing/selftests/bpf/progs/linked_arena1.c b/tools/testing/selftests/bpf/progs/linked_arena1.c
new file mode 100644
index 000000000000..3e8788dec1d9
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/linked_arena1.c
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_arena_common.h"
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 1); /* number of pages */
+} arena SEC(".maps");
+
+long __arena a_val = 1;
+extern long __arena b_val; /* defined in linked_arena2.c */
+
+SEC("syscall")
+int sum1(void *ctx)
+{
+ return a_val + b_val;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/linked_arena2.c b/tools/testing/selftests/bpf/progs/linked_arena2.c
new file mode 100644
index 000000000000..d0bcdf18a4b6
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/linked_arena2.c
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_arena_common.h"
+
+long __arena b_val = 2;
+extern long __arena a_val; /* defined in linked_arena1.c */
+
+SEC("syscall")
+int bump2(void *ctx)
+{
+ a_val += 10;
+ b_val += 20;
+ return a_val + b_val;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs in allocated sections
2026-09-25 22:46 ` [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs " Andrii Nakryiko
@ 2026-09-25 22:58 ` sashiko-bot
2026-09-25 23:32 ` Andrii Nakryiko
2026-09-25 23:35 ` bot+bpf-ci
1 sibling, 1 reply; 7+ messages in thread
From: sashiko-bot @ 2026-09-25 22:58 UTC (permalink / raw)
To: Andrii Nakryiko; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] bpf: TOCTOU and missing error check for mkstemp in test_relink()
- [Medium] bpf: Missing NULL check for bpf_object__btf() in test_relink()
--
commit 2e7da3dcbd552604f0f6c052dc8cda7a3628ffb8
Author: Andrii Nakryiko <andrii@kernel.org>
selftests/bpf: Add linked_externs test for externs in allocated sections
This commit adds two objects that define an arena global each and declare the
other one's as extern, linking them into a single skeleton. It also introduces
a test to drive the libbpf linker directly to verify the relinking process.
> diff --git a/tools/testing/selftests/bpf/prog_tests/linked_externs.c b/tools/testing/selftests/bpf/prog_tests/linked_externs.c
> new file mode 100644
> index 0000000000000..29358c82dc356
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/linked_externs.c
[ ... ]
> +/* link in1 on its own first, then link the result with in2 */
> +static void test_relink(const char *in1, const char *in2, const char *sec_name)
> +{
> + char out1[] = "/tmp/linked_externs.XXXXXX", out2[] = "/tmp/linked_externs.XXXXXX";
> + const struct btf_var_secinfo *vi;
> + struct bpf_object *obj = NULL;
> + const struct btf_type *t;
> + struct btf *btf;
> + int i, id, n;
> +
> + close(mkstemp(out1));
> + close(mkstemp(out2));
[Severity: Medium]
Could this result in a Time-Of-Check to Time-Of-Use (TOCTOU) race condition
and silently ignore failures?
mkstemp() can fail, returning -1 and leaving the template unchanged. Because
the return value is discarded, could concurrent test runs end up using the
identical fallback filename "/tmp/linked_externs.XXXXXX" and corrupt each
other's output?
Additionally, even on success, does immediately closing the temporary file and
reopening it by name in a world-writable directory (/tmp) introduce a classic
TOCTOU race condition?
> +
> + if (!ASSERT_OK(link_objs(out1, in1, NULL), "link_stage1") ||
> + !ASSERT_OK(link_objs(out2, out1, in2), "link_stage2"))
> + goto cleanup;
> +
> + obj = bpf_object__open_file(out2, NULL);
> + if (!ASSERT_OK_PTR(obj, "obj_open"))
> + goto cleanup;
> +
> + btf = bpf_object__btf(obj);
> + id = btf__find_by_name_kind(btf, sec_name, BTF_KIND_DATASEC);
[Severity: Medium]
Does this code dereference a potentially NULL pointer?
bpf_object__btf(obj) can return NULL if the loaded BPF object lacks BTF
information. The returned btf pointer is then passed to
btf__find_by_name_kind(), which dereferences it internally (btf->start_id).
Could this lead to a segmentation fault on malformed objects instead of a
graceful test failure (e.g., via ASSERT_OK_PTR())?
> + if (!ASSERT_GT(id, 0, "find_datasec"))
> + goto cleanup;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260925224631.1490488-1-andrii@kernel.org?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs in allocated sections
2026-09-25 22:58 ` sashiko-bot
@ 2026-09-25 23:32 ` Andrii Nakryiko
0 siblings, 0 replies; 7+ messages in thread
From: Andrii Nakryiko @ 2026-09-25 23:32 UTC (permalink / raw)
To: sashiko-reviews; +Cc: Andrii Nakryiko, bpf
On Fri, Sep 25, 2026 at 3:58 PM <sashiko-bot@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Medium] bpf: TOCTOU and missing error check for mkstemp in test_relink()
> - [Medium] bpf: Missing NULL check for bpf_object__btf() in test_relink()
> --
>
> commit 2e7da3dcbd552604f0f6c052dc8cda7a3628ffb8
> Author: Andrii Nakryiko <andrii@kernel.org>
>
> selftests/bpf: Add linked_externs test for externs in allocated sections
>
> This commit adds two objects that define an arena global each and declare the
> other one's as extern, linking them into a single skeleton. It also introduces
> a test to drive the libbpf linker directly to verify the relinking process.
>
> > diff --git a/tools/testing/selftests/bpf/prog_tests/linked_externs.c b/tools/testing/selftests/bpf/prog_tests/linked_externs.c
> > new file mode 100644
> > index 0000000000000..29358c82dc356
> > --- /dev/null
> > +++ b/tools/testing/selftests/bpf/prog_tests/linked_externs.c
> [ ... ]
> > +/* link in1 on its own first, then link the result with in2 */
> > +static void test_relink(const char *in1, const char *in2, const char *sec_name)
> > +{
> > + char out1[] = "/tmp/linked_externs.XXXXXX", out2[] = "/tmp/linked_externs.XXXXXX";
> > + const struct btf_var_secinfo *vi;
> > + struct bpf_object *obj = NULL;
> > + const struct btf_type *t;
> > + struct btf *btf;
> > + int i, id, n;
> > +
> > + close(mkstemp(out1));
> > + close(mkstemp(out2));
>
> [Severity: Medium]
> Could this result in a Time-Of-Check to Time-Of-Use (TOCTOU) race condition
> and silently ignore failures?
>
> mkstemp() can fail, returning -1 and leaving the template unchanged. Because
> the return value is discarded, could concurrent test runs end up using the
> identical fallback filename "/tmp/linked_externs.XXXXXX" and corrupt each
> other's output?
>
> Additionally, even on success, does immediately closing the temporary file and
> reopening it by name in a world-writable directory (/tmp) introduce a classic
> TOCTOU race condition?
classic no less, but I think we can survive that possibility just fine.
>
> > +
> > + if (!ASSERT_OK(link_objs(out1, in1, NULL), "link_stage1") ||
> > + !ASSERT_OK(link_objs(out2, out1, in2), "link_stage2"))
> > + goto cleanup;
> > +
> > + obj = bpf_object__open_file(out2, NULL);
> > + if (!ASSERT_OK_PTR(obj, "obj_open"))
> > + goto cleanup;
> > +
> > + btf = bpf_object__btf(obj);
> > + id = btf__find_by_name_kind(btf, sec_name, BTF_KIND_DATASEC);
>
> [Severity: Medium]
> Does this code dereference a potentially NULL pointer?
>
> bpf_object__btf(obj) can return NULL if the loaded BPF object lacks BTF
except it won't lack BTF, so yes it's fine
> information. The returned btf pointer is then passed to
> btf__find_by_name_kind(), which dereferences it internally (btf->start_id).
>
> Could this lead to a segmentation fault on malformed objects instead of a
> graceful test failure (e.g., via ASSERT_OK_PTR())?
>
> > + if (!ASSERT_GT(id, 0, "find_datasec"))
> > + goto cleanup;
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260925224631.1490488-1-andrii@kernel.org?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed in allocated sections
2026-09-25 22:46 [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed in allocated sections Andrii Nakryiko
2026-09-25 22:46 ` [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs " Andrii Nakryiko
@ 2026-09-25 23:35 ` bot+bpf-ci
2026-09-25 23:46 ` Andrii Nakryiko
1 sibling, 1 reply; 7+ messages in thread
From: bot+bpf-ci @ 2026-09-25 23:35 UTC (permalink / raw)
To: andrii, bpf
Cc: andrii, kernel-team, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2720 bytes --]
> diff --git a/tools/lib/bpf/linker.c b/tools/lib/bpf/linker.c
> index 53f64a1a1f25..0eaf98b093f0 100644
> --- a/tools/lib/bpf/linker.c
> +++ b/tools/lib/bpf/linker.c
> @@ -2159,7 +2159,10 @@ static int linker_append_elf_sym(struct bpf_linker *linker, struct src_obj *obj,
> dst_sym->st_name = name_off;
> dst_sym->st_info = sym->st_info;
> dst_sym->st_other = sym->st_other;
> - dst_sym->st_shndx = dst_sec ? dst_sec->sec_idx : sym->st_shndx;
> + if (sym_is_extern)
> + dst_sym->st_shndx = SHN_UNDEF;
> + else
> + dst_sym->st_shndx = dst_sec ? dst_sec->sec_idx : sym->st_shndx;
> dst_sym->st_value = (src_sec ? src_sec->dst_off : 0) + sym->st_value;
> dst_sym->st_size = sym->st_size;
Does this code leave st_value non-zero for unresolved externs in
allocated sections?
For an extern placed in an allocated section (.maps, .addr_space.N),
src_sec is the object's real section. The extend_sec() call sets
src_sec->dst_off = dst_align_sz, which is non-zero whenever an earlier
object in the same link already contributed to that section.
So for unresolved externs, the output symbol would be:
{st_shndx = SHN_UNDEF, st_value = dst_off (> 0), NOTYPE, GLOBAL}
Won't linker_sanity_check_elf_symtab() reject this when the output is
linked again?
linker.c:linker_sanity_check_elf_symtab() {
...
if (sym->st_shndx == 0) {
if (sym_type != STT_NOTYPE || sym_bind == STB_LOCAL
|| sym->st_value != 0 || sym->st_size != 0) {
pr_warn("ELF sym #%d is invalid extern symbol in %s\n", ...);
return -EINVAL;
This check requires st_value == 0 for SHN_UNDEF symbols.
For example, if stage 1 links c.bpf.o (defines "long __arena c_val = 3;")
followed by linked_arena1.bpf.o (defines a_val and has "extern long __arena
b_val;"), linked_arena1's .addr_space.1 lands at dst_off 8. The output
would have b_val as UNDEF with st_value 8. Stage 2 links that output with
linked_arena2.bpf.o, and bpf_linker__add_file() fails in
linker_sanity_check_elf() with "ELF sym is invalid extern symbol".
The same issue affects extern maps when another object with map
definitions comes first. The outcome depends on input order. With a
single object in stage 1, dst_off is always 0, so the issue only shows up
with multiple objects.
Would setting st_value to 0 for externs address this? For example:
dst_sym->st_value = sym_is_extern ? 0 : (src_sec ? src_sec->dst_off : 0) + sym->st_value;
This would match what Clang emits and pass the sanity check.
[ ... ]
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/36199505247
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs in allocated sections
2026-09-25 22:46 ` [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs " Andrii Nakryiko
2026-09-25 22:58 ` sashiko-bot
@ 2026-09-25 23:35 ` bot+bpf-ci
1 sibling, 0 replies; 7+ messages in thread
From: bot+bpf-ci @ 2026-09-25 23:35 UTC (permalink / raw)
To: andrii, bpf
Cc: andrii, kernel-team, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 7011 bytes --]
> diff --git a/tools/testing/selftests/bpf/Makefile.skel b/tools/testing/selftests/bpf/Makefile.skel
> index 3d92cdca62ed..2e22bb901bf3 100644
> --- a/tools/testing/selftests/bpf/Makefile.skel
> +++ b/tools/testing/selftests/bpf/Makefile.skel
> @@ -23,7 +23,7 @@ BPF_OBJS := $(patsubst %.c,$(RDIR)/%.bpf.o,$(BPF_SRCS))
> SKEL_BLACKLIST := btf__% test_pinning_invalid.c test_sk_assign.c
>
> LINKED_SKELS := test_static_linked.skel.h linked_funcs.skel.h \
> - linked_vars.skel.h linked_maps.skel.h \
> + linked_vars.skel.h linked_maps.skel.h linked_arena.skel.h \
> test_subskeleton.skel.h test_subskeleton_lib.skel.h \
> test_usdt.skel.h tracing_multi.skel.h \
> tracing_multi_module.skel.h \
> @@ -46,6 +46,7 @@ test_static_linked.skel.h-deps := test_static_linked1.bpf.o test_static_linked2.
> linked_funcs.skel.h-deps := linked_funcs1.bpf.o linked_funcs2.bpf.o
> linked_vars.skel.h-deps := linked_vars1.bpf.o linked_vars2.bpf.o
> linked_maps.skel.h-deps := linked_maps1.bpf.o linked_maps2.bpf.o
> +linked_arena.skel.h-deps := linked_arena1.bpf.o linked_arena2.bpf.o
> # In the subskeleton case, we want the test_subskeleton_lib.subskel.h file
> # but that's created as a side-effect of the skel.h generation.
> test_subskeleton.skel.h-deps := test_subskeleton_lib2.bpf.o test_subskeleton_lib.bpf.o test_subskeleton.bpf.o
> diff --git a/tools/testing/selftests/bpf/prog_tests/linked_externs.c b/tools/testing/selftests/bpf/prog_tests/linked_externs.c
> new file mode 100644
> index 000000000000..29358c82dc35
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/linked_externs.c
> @@ -0,0 +1,109 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
> +#include <test_progs.h>
> +#include <bpf/btf.h>
> +#include "linked_arena.skel.h"
> +
> +static void test_skel(void)
> +{
> + LIBBPF_OPTS(bpf_test_run_opts, opts);
> + struct linked_arena *skel;
> + int err;
> +
> + skel = linked_arena__open_and_load();
> + if (!ASSERT_OK_PTR(skel, "skel_open_and_load"))
> + return;
> +
> + ASSERT_EQ(skel->arena->a_val, 1, "a_val_init");
> + ASSERT_EQ(skel->arena->b_val, 2, "b_val_init");
Will this break the test_progs-bpf_gcc build?
linked_arena1.c and linked_arena2.c declare their globals and externs with
a bare __arena:
linked_arena1.c: long __arena a_val = 1;
linked_arena1.c: extern long __arena b_val;
linked_arena2.c: long __arena b_val = 2;
linked_arena2.c: extern long __arena a_val;
bpf_arena_common.h only maps __arena to an address space when the compiler
supports it:
#if defined(__BPF_FEATURE_ADDR_SPACE_CAST) && !defined(BPF_ARENA_FORCE_ASM)
#define __arena __attribute__((address_space(1))) __attribute__((btf_type_tag("arena")))
#define __arena_global __attribute__((address_space(1)))
#else
#define __arena __attribute__((btf_type_tag("arena")))
#define __arena_global SEC(".addr_space.1")
bpf-gcc does not define __BPF_FEATURE_ADDR_SPACE_CAST. Clang versions
without address-space-cast support do not define it either. With those
compilers, a_val and b_val are emitted into .data instead of .addr_space.1.
The arena map then has no initial value (init_arena_map_data is never
reached), so bpftool's is_skel_data() is false for the arena map. The
generated linked_arena.skel.h therefore has no 'arena' data member, and
the variables show up under skel->data instead.
test_skel() above dereferences that member unconditionally, so every
test_progs flavour compiles all of prog_tests/*.c against that flavour's
own skeletons. test_progs-bpf_gcc is added whenever bpf-gcc is on PATH
(Makefile: BPF_GCC ?= $(shell command -v bpf-gcc;) / TEST_GEN_PROGS +=
test_progs-bpf_gcc), and .github/workflows/gcc-bpf.yml builds it in CI. So
the flavour's linked_externs.test.o fails to compile, and
DENYLIST.test_progs-bpf_gcc cannot mask a compile failure.
The existing arena selftests avoid this. arena_atomics.c uses
__arena_global, which falls back to SEC(".addr_space.1"). arena_list.c
uses an explicit guard with a skip flag:
#ifdef __BPF_FEATURE_ADDR_SPACE_CAST
long __arena arena_sum;
int __arena test_val = 1;
...
#else
long arena_sum SEC(".addr_space.1");
int test_val SEC(".addr_space.1");
#endif
prog_tests/arena_list.c then calls test__skip() when skel->bss->skip is
set.
Should linked_arena1.c and linked_arena2.c follow the same pattern? That
would mean placing the definitions and externs in .addr_space.1
unconditionally (for example via __arena_global, or a guarded
SEC(".addr_space.1") fallback), and adding a skip flag that test_skel()
checks, because the program bodies still need address-space-cast support to
run correctly.
> +
> + err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.sum1), &opts);
> + ASSERT_OK(err, "sum1_run");
> + ASSERT_EQ(opts.retval, 1 + 2, "sum1_retval");
> +
> + err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.bump2), &opts);
> + ASSERT_OK(err, "bump2_run");
> + ASSERT_EQ(opts.retval, 11 + 22, "bump2_retval");
> +
> + ASSERT_EQ(skel->arena->a_val, 11, "a_val");
> + ASSERT_EQ(skel->arena->b_val, 22, "b_val");
> +
> + linked_arena__destroy(skel);
> +}
[ ... ]
> diff --git a/tools/testing/selftests/bpf/progs/linked_arena1.c b/tools/testing/selftests/bpf/progs/linked_arena1.c
> new file mode 100644
> index 000000000000..3e8788dec1d9
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/linked_arena1.c
> @@ -0,0 +1,22 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
> +#include <vmlinux.h>
> +#include <bpf/bpf_helpers.h>
> +#include "bpf_arena_common.h"
> +
> +struct {
> + __uint(type, BPF_MAP_TYPE_ARENA);
> + __uint(map_flags, BPF_F_MMAPABLE);
> + __uint(max_entries, 1); /* number of pages */
> +} arena SEC(".maps");
> +
> +long __arena a_val = 1;
> +extern long __arena b_val; /* defined in linked_arena2.c */
> +
> +SEC("syscall")
> +int sum1(void *ctx)
> +{
> + return a_val + b_val;
> +}
> +
> +char _license[] SEC("license") = "GPL";
> diff --git a/tools/testing/selftests/bpf/progs/linked_arena2.c b/tools/testing/selftests/bpf/progs/linked_arena2.c
> new file mode 100644
> index 000000000000..d0bcdf18a4b6
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/linked_arena2.c
> @@ -0,0 +1,18 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
> +#include <vmlinux.h>
> +#include <bpf/bpf_helpers.h>
> +#include "bpf_arena_common.h"
> +
> +long __arena b_val = 2;
> +extern long __arena a_val; /* defined in linked_arena1.c */
> +
> +SEC("syscall")
> +int bump2(void *ctx)
> +{
> + a_val += 10;
> + b_val += 20;
> + return a_val + b_val;
> +}
> +
> +char _license[] SEC("license") = "GPL";
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/36199505247
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed in allocated sections
2026-09-25 23:35 ` [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed " bot+bpf-ci
@ 2026-09-25 23:46 ` Andrii Nakryiko
0 siblings, 0 replies; 7+ messages in thread
From: Andrii Nakryiko @ 2026-09-25 23:46 UTC (permalink / raw)
To: bot+bpf-ci
Cc: andrii, bpf, kernel-team, ast, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
On Fri, Sep 25, 2026 at 4:35 PM <bot+bpf-ci@kernel.org> wrote:
>
> > diff --git a/tools/lib/bpf/linker.c b/tools/lib/bpf/linker.c
> > index 53f64a1a1f25..0eaf98b093f0 100644
> > --- a/tools/lib/bpf/linker.c
> > +++ b/tools/lib/bpf/linker.c
> > @@ -2159,7 +2159,10 @@ static int linker_append_elf_sym(struct bpf_linker *linker, struct src_obj *obj,
> > dst_sym->st_name = name_off;
> > dst_sym->st_info = sym->st_info;
> > dst_sym->st_other = sym->st_other;
> > - dst_sym->st_shndx = dst_sec ? dst_sec->sec_idx : sym->st_shndx;
> > + if (sym_is_extern)
> > + dst_sym->st_shndx = SHN_UNDEF;
> > + else
> > + dst_sym->st_shndx = dst_sec ? dst_sec->sec_idx : sym->st_shndx;
> > dst_sym->st_value = (src_sec ? src_sec->dst_off : 0) + sym->st_value;
> > dst_sym->st_size = sym->st_size;
>
> Does this code leave st_value non-zero for unresolved externs in
> allocated sections?
>
> For an extern placed in an allocated section (.maps, .addr_space.N),
> src_sec is the object's real section. The extend_sec() call sets
> src_sec->dst_off = dst_align_sz, which is non-zero whenever an earlier
> object in the same link already contributed to that section.
regardless of whether that's a problem or not, setting st_value to
zero for SHN_UNDEF is not a bad idea, will update
>
> So for unresolved externs, the output symbol would be:
>
> {st_shndx = SHN_UNDEF, st_value = dst_off (> 0), NOTYPE, GLOBAL}
>
> Won't linker_sanity_check_elf_symtab() reject this when the output is
> linked again?
>
> linker.c:linker_sanity_check_elf_symtab() {
> ...
> if (sym->st_shndx == 0) {
> if (sym_type != STT_NOTYPE || sym_bind == STB_LOCAL
> || sym->st_value != 0 || sym->st_size != 0) {
> pr_warn("ELF sym #%d is invalid extern symbol in %s\n", ...);
> return -EINVAL;
>
> This check requires st_value == 0 for SHN_UNDEF symbols.
>
> For example, if stage 1 links c.bpf.o (defines "long __arena c_val = 3;")
> followed by linked_arena1.bpf.o (defines a_val and has "extern long __arena
> b_val;"), linked_arena1's .addr_space.1 lands at dst_off 8. The output
> would have b_val as UNDEF with st_value 8. Stage 2 links that output with
> linked_arena2.bpf.o, and bpf_linker__add_file() fails in
> linker_sanity_check_elf() with "ELF sym is invalid extern symbol".
>
> The same issue affects extern maps when another object with map
> definitions comes first. The outcome depends on input order. With a
> single object in stage 1, dst_off is always 0, so the issue only shows up
> with multiple objects.
>
> Would setting st_value to 0 for externs address this? For example:
>
> dst_sym->st_value = sym_is_extern ? 0 : (src_sec ? src_sec->dst_off : 0) + sym->st_value;
>
> This would match what Clang emits and pass the sanity check.
>
> [ ... ]
>
>
> ---
> AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
> See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
>
> CI run summary: https://github.com/kernel-patches/bpf/actions/runs/36199505247
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-25 23:47 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-25 22:46 [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed in allocated sections Andrii Nakryiko
2026-09-25 22:46 ` [PATCH bpf-next 2/2] selftests/bpf: Add linked_externs test for externs " Andrii Nakryiko
2026-09-25 22:58 ` sashiko-bot
2026-09-25 23:32 ` Andrii Nakryiko
2026-09-25 23:35 ` bot+bpf-ci
2026-09-25 23:35 ` [PATCH bpf-next 1/2] libbpf: Fix static linking of externs placed " bot+bpf-ci
2026-09-25 23:46 ` Andrii Nakryiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox