[PATCH bpf-next 5/5] selftests/bpf: add loader_load_fd tests
From: Thiébaud Weksteen
Date: Wed Aug 12 2026 - 20:28:14 EST
Add user-space selftests for BPF_LOADER_LOAD_FD command. The test ELF is
generated based on the existing light skeleton generator. An awk script
extracts the loader program and map. In the future, it is possible to
add an extra option to `bpftool gen skeleton` to output the ELF file
directly.
Signed-off-by: Thiébaud Weksteen <tweek@xxxxxxxxxx>
---
tools/testing/selftests/bpf/Makefile | 8 +-
tools/testing/selftests/bpf/loader_setup.sh | 68 ++++
.../selftests/bpf/prog_tests/loader_load_fd.c | 361 ++++++++++++++++++
.../testing/selftests/bpf/progs/test_loader.c | 21 +
4 files changed, 456 insertions(+), 2 deletions(-)
create mode 100755 tools/testing/selftests/bpf/loader_setup.sh
create mode 100644 tools/testing/selftests/bpf/prog_tests/loader_load_fd.c
create mode 100644 tools/testing/selftests/bpf/progs/test_loader.c
diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index d3655a706482..6d881584abbc 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -525,7 +525,7 @@ LINKED_SKELS := test_static_linked.skel.h linked_funcs.skel.h \
LSKELS := fexit_sleep.c trace_printk.c trace_vprintk.c map_ptr_kern.c \
core_kern.c core_kern_overflow.c test_ringbuf.c \
test_ringbuf_n.c test_ringbuf_map_key.c test_ringbuf_write.c \
- test_ringbuf_overwrite.c
+ test_ringbuf_overwrite.c test_loader.c
LSKELS_SIGNED := fentry_test.c fexit_test.c atomics.c
@@ -577,7 +577,8 @@ TRUNNER_EXTRA_OBJS := $$(patsubst %.c,$$(TRUNNER_OUTPUT)/%.o, \
$$(filter %.c,$(TRUNNER_EXTRA_SOURCES)))
TRUNNER_LIB_OBJS := $$(patsubst %.c,$$(TRUNNER_OUTPUT)/%.o, \
$$(filter %.c,$(TRUNNER_LIB_SOURCES)))
-TRUNNER_EXTRA_HDRS := $$(filter %.h,$(TRUNNER_EXTRA_SOURCES))
+TRUNNER_EXTRA_HDRS := $$(filter %.h,$(TRUNNER_EXTRA_SOURCES)) $$(TRUNNER_OUTPUT)/test_loader_processed.lskel.h
+
TRUNNER_TESTS_HDR := $(TRUNNER_TESTS_DIR)/tests.h
TRUNNER_BPF_SRCS := $$(notdir $$(wildcard $(TRUNNER_BPF_PROGS_DIR)/*.c))
TRUNNER_BPF_OBJS := $$(patsubst %.c,$$(TRUNNER_OUTPUT)/%.bpf.o, $$(TRUNNER_BPF_SRCS))
@@ -663,6 +664,9 @@ $(TRUNNER_BPF_LSKELS): %.lskel.h: %.bpf.o $(BPFTOOL) | $(TRUNNER_OUTPUT)
}) && \
rm -f $$(<:.o=.llinked1.o) $$(<:.o=.llinked2.o) $$(<:.o=.llinked3.o)
+$$(TRUNNER_OUTPUT)/test_loader_processed.lskel.h: $$(TRUNNER_OUTPUT)/test_loader.lskel.h loader_setup.sh
+ $$(Q)./loader_setup.sh $$< $$@
+
$(TRUNNER_BPF_LSKELS_SIGNED): %.lskel.h: %.bpf.o $(BPFTOOL) | $(TRUNNER_OUTPUT)
$(Q)$(if $(PERMISSIVE),if [ ! -f $$< ]; then \
$$(RM) $$@; \
diff --git a/tools/testing/selftests/bpf/loader_setup.sh b/tools/testing/selftests/bpf/loader_setup.sh
new file mode 100755
index 000000000000..f86651b0718f
--- /dev/null
+++ b/tools/testing/selftests/bpf/loader_setup.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Google LLC
+#
+# loader_setup.sh - Light skeleton data extraction helper for selftests/bpf
+#
+# This script parses an autogenerated light skeleton header (.lskel.h) produced
+# by bpftool ('bpftool gen skeleton -L'). It extracts the embedded loader data
+# ('opts_data') and loader BPF instructions ('opts_insn') string literals and
+# generates a C header file containing:
+# - loader_test_opts_data[]: Data for the '__loader.map' section.
+# - loader_test_opts_data_sz: Size of opts_data.
+# - loader_test_opts_insn[]: Instructions for the '__loader.prog' section.
+# - loader_test_opts_insn_sz: Size of opts_insn.
+#
+# These extracted sections are used by the 'loader_load_fd' selftest to populate
+# an in-memory ELF object file for kernel 'BPF_LOADER_LOAD_FD' testing.
+#
+# Usage:
+# loader_setup.sh <input_lskel.h> <output_data.h>
+
+INPUT="$1"
+OUTPUT="$2"
+
+if [ -z "$INPUT" ] || [ -z "$OUTPUT" ]; then
+ echo "Usage: $0 <input_lskel.h> <output_data.h>" >&2
+ exit 1
+fi
+
+awk -v input="$INPUT" '
+BEGIN {
+ print "/* Generated from " input " */"
+ in_data = 0
+ in_insn = 0
+}
+/opts_data\[\]/ {
+ in_data = 1
+ sub(/^.*opts_data\[\][^="]*=\s*"/, "")
+ printf "static const char test_loader_opts_data[] __attribute__((__aligned__(8))) = \""
+}
+in_data {
+ if (index($0, "\";") > 0) {
+ sub(/";.*/, "")
+ print $0 "\";"
+ print "static const size_t test_loader_opts_data_sz = " \
+ "sizeof(test_loader_opts_data) - 1;\n"
+ in_data = 0
+ } else {
+ print $0
+ }
+}
+/opts_insn\[\]/ {
+ in_insn = 1
+ sub(/^.*opts_insn\[\][^="]*=\s*"/, "")
+ printf "static const char test_loader_opts_insn[] __attribute__((__aligned__(8))) = \""
+}
+in_insn {
+ if (index($0, "\";") > 0) {
+ sub(/";.*/, "")
+ print $0 "\";"
+ print "static const size_t test_loader_opts_insn_sz = " \
+ "sizeof(test_loader_opts_insn) - 1;"
+ in_insn = 0
+ } else {
+ print $0
+ }
+}
+' "$INPUT" > "$OUTPUT"
diff --git a/tools/testing/selftests/bpf/prog_tests/loader_load_fd.c b/tools/testing/selftests/bpf/prog_tests/loader_load_fd.c
new file mode 100644
index 000000000000..ab971662dd04
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/loader_load_fd.c
@@ -0,0 +1,361 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Google LLC */
+
+#define _GNU_SOURCE
+#include <test_progs.h>
+#include <sys/syscall.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include <stdint.h>
+#include <libelf.h>
+#include <gelf.h>
+#include <linux/bpf.h>
+
+#include "bpf/skel_internal.h"
+#include "test_loader_processed.lskel.h"
+
+/* Test helper to create an in-memory ELF */
+static int create_loader_elf(const void *insns, size_t insns_sz,
+ const void *map_data, size_t map_data_sz,
+ const char *license, size_t license_sz,
+ bool omit_prog, bool omit_map, bool omit_license)
+{
+ char shstrtab[] = "\0__loader.prog\0__loader.map\0license\0.shstrtab";
+ size_t prog_off = 1;
+ size_t map_off = prog_off + strlen("__loader.prog") + 1;
+ size_t lic_off = map_off + strlen("__loader.map") + 1;
+ size_t shstr_off = lic_off + strlen("license") + 1;
+ size_t shstrtab_sz = sizeof(shstrtab);
+ Elf_Data *shstr_data, *data;
+ Elf64_Shdr *shstr_shdr, *shdr;
+ Elf_Scn *shstr_scn, *scn;
+ Elf64_Ehdr *ehdr;
+ Elf *elf;
+ int fd;
+
+ fd = memfd_create("loader_elf", 0);
+ if (!ASSERT_GE(fd, 0, "memfd_create"))
+ return -1;
+
+ elf_version(EV_CURRENT);
+ elf = elf_begin(fd, ELF_C_WRITE, NULL);
+ if (!ASSERT_OK_PTR(elf, "elf_begin")) {
+ close(fd);
+ return -1;
+ }
+
+ ehdr = elf64_newehdr(elf);
+ if (!ASSERT_OK_PTR(ehdr, "elf64_newehdr"))
+ goto err;
+
+ ehdr->e_ident[EI_MAG0] = ELFMAG0;
+ ehdr->e_ident[EI_MAG1] = ELFMAG1;
+ ehdr->e_ident[EI_MAG2] = ELFMAG2;
+ ehdr->e_ident[EI_MAG3] = ELFMAG3;
+ ehdr->e_ident[EI_CLASS] = ELFCLASS64;
+ ehdr->e_ident[EI_DATA] = ELFDATA2LSB;
+ ehdr->e_ident[EI_VERSION] = EV_CURRENT;
+ ehdr->e_machine = EM_BPF;
+ ehdr->e_type = ET_REL;
+ ehdr->e_version = EV_CURRENT;
+
+ shstr_scn = elf_newscn(elf);
+ shstr_shdr = elf64_getshdr(shstr_scn);
+ shstr_shdr->sh_name = shstr_off;
+ shstr_shdr->sh_type = SHT_STRTAB;
+ shstr_shdr->sh_flags = 0;
+
+ shstr_data = elf_newdata(shstr_scn);
+ shstr_data->d_buf = shstrtab;
+ shstr_data->d_size = shstrtab_sz;
+ shstr_data->d_type = ELF_T_BYTE;
+ shstr_data->d_align = 1;
+
+ ehdr->e_shstrndx = elf_ndxscn(shstr_scn);
+
+ if (!omit_prog && insns && insns_sz > 0) {
+ scn = elf_newscn(elf);
+ shdr = elf64_getshdr(scn);
+ shdr->sh_name = prog_off;
+ shdr->sh_type = SHT_PROGBITS;
+ shdr->sh_flags = SHF_ALLOC | SHF_EXECINSTR;
+
+ data = elf_newdata(scn);
+ data->d_buf = (void *)insns;
+ data->d_size = insns_sz;
+ data->d_type = ELF_T_BYTE;
+ data->d_align = 8;
+ }
+
+ if (!omit_map && map_data && map_data_sz > 0) {
+ scn = elf_newscn(elf);
+ shdr = elf64_getshdr(scn);
+ shdr->sh_name = map_off;
+ shdr->sh_type = SHT_PROGBITS;
+ shdr->sh_flags = SHF_ALLOC;
+
+ data = elf_newdata(scn);
+ data->d_buf = (void *)map_data;
+ data->d_size = map_data_sz;
+ data->d_type = ELF_T_BYTE;
+ data->d_align = 8;
+ }
+
+ if (!omit_license && license && license_sz > 0) {
+ scn = elf_newscn(elf);
+ shdr = elf64_getshdr(scn);
+ shdr->sh_name = lic_off;
+ shdr->sh_type = SHT_PROGBITS;
+ shdr->sh_flags = 0;
+
+ data = elf_newdata(scn);
+ data->d_buf = (void *)license;
+ data->d_size = license_sz;
+ data->d_type = ELF_T_BYTE;
+ data->d_align = 1;
+ }
+
+ if (elf_update(elf, ELF_C_WRITE) < 0)
+ goto err;
+
+ elf_end(elf);
+ lseek(fd, 0, SEEK_SET);
+ return fd;
+
+err:
+ elf_end(elf);
+ close(fd);
+ return -1;
+}
+
+static int sys_bpf_loader_load_fd(int loader_fd, void *ctx, __u32 ctx_size)
+{
+ union bpf_attr attr;
+
+ memset(&attr, 0, sizeof(attr));
+ attr.load_fd.loader_fd = loader_fd;
+ attr.load_fd.ctx = ptr_to_u64(ctx);
+ attr.load_fd.ctx_size = ctx_size;
+
+ return syscall(__NR_bpf, BPF_LOADER_LOAD_FD, &attr, sizeof(attr));
+}
+
+static void test_loader_load_fd_invalid_fd(void)
+{
+ struct bpf_loader_ctx ctx = {};
+ int err;
+
+ err = sys_bpf_loader_load_fd(-1, &ctx, sizeof(ctx));
+ ASSERT_EQ(err, -1, "invalid fd sys_bpf return");
+ ASSERT_EQ(errno, EINVAL, "invalid fd errno");
+}
+
+static void test_loader_load_fd_oversized_ctx(void)
+{
+ struct bpf_insn insns[] = {
+ BPF_MOV64_IMM(BPF_REG_0, 0),
+ BPF_EXIT_INSN(),
+ };
+ char map_data[] = "data";
+ char license[] = "GPL";
+ struct bpf_loader_ctx ctx = {};
+ int elf_fd, err;
+
+ elf_fd = create_loader_elf(insns, sizeof(insns),
+ map_data, sizeof(map_data),
+ license, sizeof(license),
+ false, false, false);
+
+ if (!ASSERT_GE(elf_fd, 0, "create_loader_elf"))
+ return;
+
+ err = sys_bpf_loader_load_fd(elf_fd, &ctx, 65536U);
+ ASSERT_EQ(err, -1, "oversized ctx sys_bpf return");
+ ASSERT_EQ(errno, EINVAL, "oversized ctx errno");
+
+ close(elf_fd);
+}
+
+static void test_loader_load_fd_invalid_elf(void)
+{
+ char garbage[] = "not_an_elf_file_content";
+ struct bpf_loader_ctx ctx = {};
+ int fd, err;
+
+ fd = memfd_create("garbage_file", 0);
+ if (!ASSERT_GE(fd, 0, "memfd_create"))
+ return;
+
+ if (!ASSERT_EQ(write(fd, garbage, sizeof(garbage)), sizeof(garbage), "write garbage")) {
+ close(fd);
+ return;
+ }
+ lseek(fd, 0, SEEK_SET);
+
+ err = sys_bpf_loader_load_fd(fd, &ctx, sizeof(ctx));
+ ASSERT_EQ(err, -1, "invalid elf sys_bpf return");
+ ASSERT_EQ(errno, ENOEXEC, "invalid elf errno");
+
+ close(fd);
+}
+
+static void test_loader_load_fd_missing_prog_sec(void)
+{
+ struct bpf_insn insns[] = {
+ BPF_MOV64_IMM(BPF_REG_0, 0),
+ BPF_EXIT_INSN(),
+ };
+ char map_data[] = "data";
+ char license[] = "GPL";
+ struct bpf_loader_ctx ctx = {};
+ int elf_fd, err;
+
+ elf_fd = create_loader_elf(insns, sizeof(insns),
+ map_data, sizeof(map_data),
+ license, sizeof(license),
+ true, false, false);
+ if (!ASSERT_GE(elf_fd, 0, "create_loader_elf"))
+ return;
+
+ err = sys_bpf_loader_load_fd(elf_fd, &ctx, sizeof(ctx));
+ ASSERT_EQ(err, -1, "missing prog sec sys_bpf return");
+ ASSERT_EQ(errno, EINVAL, "missing prog sec errno");
+
+ close(elf_fd);
+}
+
+static void test_loader_load_fd_missing_map_sec(void)
+{
+ struct bpf_insn insns[] = {
+ BPF_MOV64_IMM(BPF_REG_0, 0),
+ BPF_EXIT_INSN(),
+ };
+ char map_data[] = "data";
+ char license[] = "GPL";
+ struct bpf_loader_ctx ctx = {};
+ int elf_fd, err;
+
+ elf_fd = create_loader_elf(insns, sizeof(insns),
+ map_data, sizeof(map_data),
+ license, sizeof(license),
+ false, true, false);
+ if (!ASSERT_GE(elf_fd, 0, "create_loader_elf"))
+ return;
+
+ err = sys_bpf_loader_load_fd(elf_fd, &ctx, sizeof(ctx));
+ ASSERT_EQ(err, -1, "missing map sec sys_bpf return");
+ ASSERT_EQ(errno, EINVAL, "missing map sec errno");
+
+ close(elf_fd);
+}
+
+static void test_loader_load_fd_missing_license_sec(void)
+{
+ struct bpf_insn insns[] = {
+ BPF_MOV64_IMM(BPF_REG_0, 0),
+ BPF_EXIT_INSN(),
+ };
+ char map_data[] = "data";
+ char license[] = "GPL";
+ struct bpf_loader_ctx ctx = {};
+ int elf_fd, err;
+
+ elf_fd = create_loader_elf(insns, sizeof(insns),
+ map_data, sizeof(map_data),
+ license, sizeof(license),
+ false, false, true);
+ if (!ASSERT_GE(elf_fd, 0, "create_loader_elf"))
+ return;
+
+ err = sys_bpf_loader_load_fd(elf_fd, &ctx, sizeof(ctx));
+ ASSERT_EQ(err, -1, "missing license sec sys_bpf return");
+ ASSERT_EQ(errno, EINVAL, "missing license sec errno");
+
+ close(elf_fd);
+}
+
+static void test_loader_load_fd_loader_failure(void)
+{
+ struct bpf_insn insns[] = {
+ BPF_MOV64_IMM(BPF_REG_0, -EPERM),
+ BPF_EXIT_INSN(),
+ };
+ char map_data[] = "data";
+ char license[] = "GPL";
+ struct bpf_loader_ctx ctx = {};
+ int elf_fd, err;
+
+ elf_fd = create_loader_elf(insns, sizeof(insns),
+ map_data, sizeof(map_data),
+ license, sizeof(license),
+ false, false, false);
+ if (!ASSERT_GE(elf_fd, 0, "create_loader_elf"))
+ return;
+
+ err = sys_bpf_loader_load_fd(elf_fd, &ctx, sizeof(ctx));
+ ASSERT_EQ(err, -1, "loader failure sys_bpf return");
+ ASSERT_EQ(errno, EPERM, "loader failure errno");
+
+ close(elf_fd);
+}
+
+struct test_loader_lskel {
+ struct bpf_loader_ctx ctx;
+ struct {
+ struct bpf_map_desc test_map;
+ } maps;
+ struct {
+ struct bpf_prog_desc probe;
+ } progs;
+ struct {
+ int probe_fd;
+ } links;
+};
+
+static void test_loader_load_fd_lskel(void)
+{
+ struct test_loader_lskel skel = {};
+ int elf_fd, err;
+
+ skel.ctx.sz = (char *)&skel.links - (char *)&skel;
+
+ /* Build fake ELF using extracted opts_insn (__loader.prog) and opts_data (__loader.map) */
+ elf_fd = create_loader_elf(test_loader_opts_insn, test_loader_opts_insn_sz,
+ test_loader_opts_data, test_loader_opts_data_sz,
+ "GPL", sizeof("GPL"),
+ false, false, false);
+ if (!ASSERT_GE(elf_fd, 0, "create_loader_elf_lskel"))
+ return;
+
+ err = sys_bpf_loader_load_fd(elf_fd, &skel.ctx, skel.ctx.sz);
+ ASSERT_OK(err, "sys_bpf_loader_load_fd lskel");
+
+ ASSERT_GT(skel.progs.probe.prog_fd, 0, "test_loader probe prog_fd > 0");
+ ASSERT_GT(skel.maps.test_map.map_fd, 0, "test_loader test_map map_fd > 0");
+
+ if (skel.progs.probe.prog_fd > 0)
+ close(skel.progs.probe.prog_fd);
+ if (skel.maps.test_map.map_fd > 0)
+ close(skel.maps.test_map.map_fd);
+ close(elf_fd);
+}
+
+void test_loader_load_fd(void)
+{
+ if (test__start_subtest("invalid_fd"))
+ test_loader_load_fd_invalid_fd();
+ if (test__start_subtest("oversized_ctx"))
+ test_loader_load_fd_oversized_ctx();
+ if (test__start_subtest("invalid_elf"))
+ test_loader_load_fd_invalid_elf();
+ if (test__start_subtest("missing_prog_sec"))
+ test_loader_load_fd_missing_prog_sec();
+ if (test__start_subtest("missing_map_sec"))
+ test_loader_load_fd_missing_map_sec();
+ if (test__start_subtest("missing_license_sec"))
+ test_loader_load_fd_missing_license_sec();
+ if (test__start_subtest("loader_failure"))
+ test_loader_load_fd_loader_failure();
+ if (test__start_subtest("lskel"))
+ test_loader_load_fd_lskel();
+}
diff --git a/tools/testing/selftests/bpf/progs/test_loader.c b/tools/testing/selftests/bpf/progs/test_loader.c
new file mode 100644
index 000000000000..515e15ca042e
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_loader.c
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 4);
+ __type(key, __u32);
+ __type(value, __u64);
+} test_map SEC(".maps");
+
+SEC("socket")
+int probe(void *ctx)
+{
+ __u32 key = 0;
+ __u64 *val = bpf_map_lookup_elem(&test_map, &key);
+
+ return val ? (int)*val : 0;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.55.0.691.gc56d675ccc-goog