summaryrefslogtreecommitdiffstats
path: root/lib/libxdp/tests
diff options
context:
space:
mode:
Diffstat (limited to 'lib/libxdp/tests')
-rw-r--r--lib/libxdp/tests/.gitignore4
-rw-r--r--lib/libxdp/tests/Makefile80
-rw-r--r--lib/libxdp/tests/check_kern_compat.c10
-rw-r--r--lib/libxdp/tests/test-libxdp.sh99
-rw-r--r--lib/libxdp/tests/test_dispatcher_versions.c300
-rwxr-xr-xlib/libxdp/tests/test_runner.sh118
-rw-r--r--lib/libxdp/tests/test_utils.h49
-rw-r--r--lib/libxdp/tests/test_xdp_frags.c339
-rw-r--r--lib/libxdp/tests/test_xsk_refcnt.c304
-rw-r--r--lib/libxdp/tests/xdp_dispatcher_v1.c43
-rw-r--r--lib/libxdp/tests/xdp_dispatcher_v1.h16
-rw-r--r--lib/libxdp/tests/xdp_pass.c11
12 files changed, 1373 insertions, 0 deletions
diff --git a/lib/libxdp/tests/.gitignore b/lib/libxdp/tests/.gitignore
new file mode 100644
index 0000000..cc3a114
--- /dev/null
+++ b/lib/libxdp/tests/.gitignore
@@ -0,0 +1,4 @@
+test_xsk_refcnt
+check_kern_compat
+test_xdp_frags
+test_dispatcher_versions
diff --git a/lib/libxdp/tests/Makefile b/lib/libxdp/tests/Makefile
new file mode 100644
index 0000000..3c22901
--- /dev/null
+++ b/lib/libxdp/tests/Makefile
@@ -0,0 +1,80 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+
+USER_TARGETS := test_xsk_refcnt check_kern_compat test_xdp_frags test_dispatcher_versions
+BPF_TARGETS := xdp_dispatcher_v1 xdp_pass
+USER_LIBS := -lpthread
+
+EXTRA_DEPS += xdp_dispatcher_v1.h
+EXTRA_USER_DEPS += test_utils.h
+
+TEST_FILE := ./test-libxdp.sh
+TEST_RUNNER := ./test_runner.sh
+
+USER_C := ${USER_TARGETS:=.c}
+USER_OBJ := ${USER_C:.c=.o}
+BPF_OBJS := $(BPF_TARGETS:=.o)
+
+LIB_DIR := ../..
+LDLIBS += $(USER_LIBS)
+
+include $(LIB_DIR)/defines.mk
+
+LDFLAGS+=-L$(LIBXDP_DIR)
+ifeq ($(DYNAMIC_LIBXDP),1)
+ LDLIBS:=-lxdp $(LDLIBS)
+ OBJECT_LIBXDP:=$(LIBXDP_DIR)/libxdp.so.$(LIBXDP_VERSION)
+else
+ LDLIBS:=-l:libxdp.a $(LDLIBS)
+ OBJECT_LIBXDP:=$(LIBXDP_DIR)/libxdp.a
+endif
+
+# Detect submodule libbpf source file changes
+ifeq ($(SYSTEM_LIBBPF),n)
+ LIBBPF_SOURCES := $(wildcard $(LIBBPF_DIR)/src/*.[ch])
+endif
+
+LIBXDP_SOURCES := $(wildcard $(LIBXDP_DIR)/*.[ch] $(LIBXDP_DIR)/*.in)
+
+CFLAGS += -I$(HEADER_DIR)
+
+BPF_HEADERS := $(wildcard $(HEADER_DIR)/bpf/*.h) $(wildcard $(HEADER_DIR)/xdp/*.h)
+
+all: $(USER_TARGETS) $(BPF_OBJS)
+
+.PHONY: clean
+clean::
+ $(Q)rm -f $(USER_TARGETS) $(USER_OBJ)
+
+$(OBJECT_LIBBPF): $(LIBBPF_SOURCES)
+ $(Q)$(MAKE) -C $(LIB_DIR) libbpf
+
+$(OBJECT_LIBXDP): $(LIBXDP_SOURCES)
+ $(Q)$(MAKE) -C $(LIBXDP_DIR)
+
+# Create expansions for dependencies
+LIB_H := ${LIB_OBJS:.o=.h}
+
+# Detect if any of common obj changed and create dependency on .h-files
+$(LIB_OBJS): %.o: %.c %.h $(LIB_H)
+ $(Q)$(MAKE) -C $(dir $@) $(notdir $@)
+
+ALL_EXEC_TARGETS=$(USER_TARGETS)
+$(ALL_EXEC_TARGETS): %: %.c $(OBJECT_LIBBPF) $(OBJECT_LIBXDP) $(LIBMK) $(LIB_OBJS) $(EXTRA_DEPS) $(EXTRA_USER_DEPS)
+ $(QUIET_CC)$(CC) -Wall $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ $(LIB_OBJS) \
+ $< $(LDLIBS)
+
+$(BPF_OBJS): %.o: %.c $(BPF_HEADERS) $(LIBMK) $(EXTRA_DEPS)
+ $(QUIET_CLANG)$(CLANG) -S \
+ -target $(BPF_TARGET) \
+ -D __BPF_TRACING__ \
+ $(BPF_CFLAGS) \
+ -Wall \
+ -Wno-unused-value \
+ -Wno-pointer-sign \
+ -Wno-compare-distinct-pointer-types \
+ -Werror \
+ -O2 -emit-llvm -c -g -o ${@:.o=.ll} $<
+ $(QUIET_LLC)$(LLC) -march=$(BPF_TARGET) -filetype=obj -o $@ ${@:.o=.ll}
+
+run: all
+ $(Q)env CC="$(CC)" CFLAGS="$(CFLAGS) $(LDFLAGS)" CPPFLAGS="$(CPPFLAGS)" LDLIBS="$(LDLIBS)" V=$(V) $(TEST_RUNNER) $(TEST_FILE) $(RUN_TESTS)
diff --git a/lib/libxdp/tests/check_kern_compat.c b/lib/libxdp/tests/check_kern_compat.c
new file mode 100644
index 0000000..8fb8991
--- /dev/null
+++ b/lib/libxdp/tests/check_kern_compat.c
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#include "test_utils.h"
+#include "../libxdp_internal.h"
+
+int main(__unused int argc, __unused char** argv)
+{
+ silence_libbpf_logging();
+ return libxdp_check_kern_compat();
+}
diff --git a/lib/libxdp/tests/test-libxdp.sh b/lib/libxdp/tests/test-libxdp.sh
new file mode 100644
index 0000000..90fc44c
--- /dev/null
+++ b/lib/libxdp/tests/test-libxdp.sh
@@ -0,0 +1,99 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+
+ALL_TESTS="test_link_so test_link_a test_old_dispatcher test_xdp_frags test_xsk_prog_refcnt_bpffs test_xsk_prog_refcnt_legacy"
+
+TESTS_DIR=$(dirname "${BASH_SOURCE[0]}")
+
+test_link_so()
+{
+ TMPDIR=$(mktemp --tmpdir -d libxdp-test.XXXXXX)
+ cat >$TMPDIR/libxdptest.c <<EOF
+#include <xdp/libxdp.h>
+int main(int argc, char **argv) {
+ (void) argc; (void) argv;
+ (void) xdp_program__open_file("filename", "section_name", NULL);
+ return 0;
+}
+EOF
+ $CC -o $TMPDIR/libxdptest $TMPDIR/libxdptest.c $CFLAGS $CPPFLAGS -lxdp $LDLIBS 2>&1
+ retval=$?
+ rm -rf "$TMPDIR"
+ return $retval
+}
+
+test_link_a()
+{
+ TMPDIR=$(mktemp --tmpdir -d libxdp-test.XXXXXX)
+ cat >$TMPDIR/libxdptest.c <<EOF
+#include <xdp/libxdp.h>
+int main(int argc, char **argv) {
+ (void) argc; (void) argv;
+ (void) xdp_program__open_file("filename", "section_name", NULL);
+ return 0;
+}
+EOF
+ $CC -o $TMPDIR/libxdptest $TMPDIR/libxdptest.c $CFLAGS $CPPFLAGS -l:libxdp.a $LDLIBS 2>&1
+ retval=$?
+ rm -rf "$TMPDIR"
+ return $retval
+}
+
+test_refcnt_once()
+{
+ # We need multiple queues for this test
+ NUM_QUEUES_REQUIRED=3
+ ip link add xsk_veth0 numrxqueues $NUM_QUEUES_REQUIRED type veth peer name xsk_veth1
+ check_run $TESTS_DIR/test_xsk_refcnt xsk_veth0 2>&1
+ ip link delete xsk_veth0
+}
+
+check_mount_bpffs()
+{
+ mount | grep -q /sys/fs/bpf || mount -t bpf bpf /sys/fs/bpf/ || echo "Unable to mount /sys/fs/bpf"
+ mount | grep -q /sys/fs/bpf
+}
+
+check_unmount_bpffs()
+{
+ mount | grep -q /sys/fs/bpf && umount /sys/fs/bpf/ || echo "Unable to unmount /sys/fs/bpf"
+ ! mount | grep -q /sys/fs/bpf
+}
+
+test_xsk_prog_refcnt_bpffs()
+{
+ check_mount_bpffs && test_refcnt_once "$@"
+}
+
+test_xsk_prog_refcnt_legacy()
+{
+ check_unmount_bpffs && test_refcnt_once "$@"
+}
+
+test_xdp_frags()
+{
+ skip_if_missing_libxdp_compat
+
+ check_mount_bpffs || return 1
+ ip link add xdp_veth_big0 mtu 5000 type veth peer name xdp_veth_big1 mtu 5000
+ ip link add xdp_veth_small0 type veth peer name xdp_veth_small1
+ check_run $TESTS_DIR/test_xdp_frags xdp_veth_big0 xdp_veth_small0 2>&1
+ ip link delete xdp_veth_big0
+ ip link delete xdp_veth_small0
+}
+
+test_old_dispatcher()
+{
+ skip_if_missing_libxdp_compat
+
+ check_mount_bpffs || return 1
+ ip link add xdp_veth0 type veth peer name xdp_veth1
+ check_run $TESTS_DIR/test_dispatcher_versions xdp_veth0
+ ip link delete xdp_veth0
+}
+
+cleanup_tests()
+{
+ ip link del dev xdp_veth_big0 >/dev/null 2>&1
+ ip link del dev xdp_veth_small0 >/dev/null 2>&1
+ ip link del dev xsk_veth0 >/dev/null 2>&1
+}
diff --git a/lib/libxdp/tests/test_dispatcher_versions.c b/lib/libxdp/tests/test_dispatcher_versions.c
new file mode 100644
index 0000000..14a8ba8
--- /dev/null
+++ b/lib/libxdp/tests/test_dispatcher_versions.c
@@ -0,0 +1,300 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <linux/err.h>
+#include <net/if.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "test_utils.h"
+#include "../libxdp_internal.h"
+#include "xdp_dispatcher_v1.h"
+
+#include <xdp/libxdp.h>
+#include <bpf/libbpf.h>
+#include <bpf/btf.h>
+
+#ifndef PATH_MAX
+#define PATH_MAX 4096
+#endif
+
+#define BPFFS_DIR "/sys/fs/bpf/xdp"
+
+#define PROG_RUN_PRIO 42
+#define PROG_CHAIN_CALL_ACTIONS (1 << XDP_DROP)
+
+int get_prog_id(int prog_fd)
+{
+ struct bpf_prog_info info = {};
+ __u32 len = sizeof(info);
+ int err;
+
+ err = bpf_obj_get_info_by_fd(prog_fd, &info, &len);
+ if (err)
+ return -errno;
+
+ return info.id;
+}
+
+int load_dispatcher_v1(int ifindex)
+{
+ struct xdp_dispatcher_config_v1 dispatcher_config = {};
+ struct bpf_object *obj_dispatcher, *obj_prog = NULL;
+ DECLARE_LIBBPF_OPTS(bpf_link_create_opts, opts);
+ struct bpf_program *dispatcher_prog, *xdp_prog;
+ int ret, btf_id, lfd = -1, dispatcher_id;
+ char pin_path[PATH_MAX], buf[PATH_MAX];
+ const char *attach_func = "prog0";
+ struct bpf_map *map;
+
+ if (!ifindex)
+ return -ENOENT;
+
+ obj_dispatcher = bpf_object__open("xdp_dispatcher_v1.o");
+ if (!obj_dispatcher)
+ return -errno;
+
+ btf_id = btf__find_by_name_kind(bpf_object__btf(obj_dispatcher),
+ attach_func, BTF_KIND_FUNC);
+ if (btf_id <= 0) {
+ ret = -ENOENT;
+ goto out;
+ }
+ opts.target_btf_id = btf_id;
+
+ map = bpf_object__next_map(obj_dispatcher, NULL);
+ if (!map) {
+ ret = -ENOENT;
+ goto out;
+ }
+
+ dispatcher_prog = bpf_object__find_program_by_name(obj_dispatcher,
+ "xdp_dispatcher");
+ if (!dispatcher_prog) {
+ ret = -errno;
+ goto out;
+ }
+
+ dispatcher_config.num_progs_enabled = 1;
+ dispatcher_config.chain_call_actions[0] = PROG_CHAIN_CALL_ACTIONS;
+ dispatcher_config.run_prios[0] = PROG_RUN_PRIO;
+
+ ret = bpf_map__set_initial_value(map, &dispatcher_config,
+ sizeof(dispatcher_config));
+ if (ret)
+ goto out;
+
+
+ ret = bpf_object__load(obj_dispatcher);
+ if (ret)
+ goto out;
+
+ dispatcher_id = get_prog_id(bpf_program__fd(dispatcher_prog));
+ if (dispatcher_id < 0) {
+ ret = dispatcher_id;
+ goto out;
+ }
+
+ obj_prog = bpf_object__open("xdp_pass.o");
+ if (!obj_prog) {
+ ret = -errno;
+ goto out;
+ }
+
+ xdp_prog = bpf_object__find_program_by_name(obj_prog, "xdp_pass");
+ if (!xdp_prog) {
+ ret = -errno;
+ goto out;
+ }
+
+ ret = bpf_program__set_attach_target(xdp_prog,
+ bpf_program__fd(dispatcher_prog),
+ attach_func);
+ if (ret)
+ goto out;
+
+ bpf_program__set_type(xdp_prog, BPF_PROG_TYPE_EXT);
+ bpf_program__set_expected_attach_type(xdp_prog, 0);
+
+ ret = bpf_object__load(obj_prog);
+ if (ret)
+ goto out;
+
+ lfd = bpf_link_create(bpf_program__fd(xdp_prog),
+ bpf_program__fd(dispatcher_prog), 0, &opts);
+ if (lfd < 0) {
+ ret = -errno;
+ goto out;
+ }
+
+ ret = try_snprintf(pin_path, sizeof(pin_path), "%s/dispatch-%d-%d",
+ BPFFS_DIR, ifindex, dispatcher_id);
+ if (ret)
+ goto out;
+
+ ret = mkdir(BPFFS_DIR, S_IRWXU);
+ if (ret && errno != EEXIST) {
+ ret = -errno;
+ printf("mkdir err (%s): %s\n", BPFFS_DIR, strerror(-ret));
+ goto out;
+ }
+
+ ret = mkdir(pin_path, S_IRWXU);
+ if (ret) {
+ ret = -errno;
+ printf("mkdir err (%s): %s\n", pin_path, strerror(-ret));
+ goto out;
+ }
+
+ ret = try_snprintf(buf, sizeof(buf), "%s/prog0-link", pin_path);
+ if (ret)
+ goto err_unpin;
+
+ ret = bpf_obj_pin(lfd, buf);
+ if (ret)
+ goto err_unpin;
+
+ ret = try_snprintf(buf, sizeof(buf), "%s/prog0-prog", pin_path);
+ if (ret)
+ goto err_unpin;
+
+ ret = bpf_obj_pin(bpf_program__fd(xdp_prog), buf);
+ if (ret)
+ goto err_unpin;
+
+ ret = xdp_attach_fd(bpf_program__fd(dispatcher_prog), -1, ifindex,
+ XDP_MODE_NATIVE);
+ if (ret)
+ goto err_unpin;
+
+out:
+ if (lfd >= 0)
+ close(lfd);
+ bpf_object__close(obj_dispatcher);
+ bpf_object__close(obj_prog);
+ return ret;
+
+err_unpin:
+ if (!try_snprintf(buf, sizeof(buf), "%s/prog0-link", pin_path))
+ unlink(buf);
+ if (!try_snprintf(buf, sizeof(buf), "%s/prog0-prog", pin_path))
+ unlink(buf);
+ rmdir(pin_path);
+ goto out;
+}
+
+int check_old_dispatcher(int ifindex)
+{
+ struct xdp_multiprog *mp = NULL;
+ struct xdp_program *xdp_prog;
+ char buf[100];
+ int ret;
+
+ ret = load_dispatcher_v1(ifindex);
+ if (ret)
+ goto out;
+
+ mp = xdp_multiprog__get_from_ifindex(ifindex);
+ ret = libxdp_get_error(mp);
+ if (ret)
+ goto out;
+
+ if (xdp_multiprog__is_legacy(mp)) {
+ printf("Got unexpected legacy multiprog\n");
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (xdp_multiprog__program_count(mp) != 1) {
+ printf("Expected 1 attached program, got %d\n",
+ xdp_multiprog__program_count(mp));
+ ret = -EINVAL;
+ goto out;
+ }
+
+ xdp_prog = xdp_multiprog__next_prog(NULL, mp);
+ if (!xdp_prog) {
+ ret = -errno;
+ goto out;
+ }
+
+ if (strcmp(xdp_program__name(xdp_prog), "xdp_pass")) {
+ printf("Expected xdp_pass program, got %s\n",
+ xdp_program__name(xdp_prog));
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (xdp_program__run_prio(xdp_prog) != PROG_RUN_PRIO) {
+ printf("Expected run prio %d got %d\n", PROG_RUN_PRIO,
+ xdp_program__run_prio(xdp_prog));
+ ret = -EINVAL;
+ goto out;
+ }
+
+ ret = xdp_program__print_chain_call_actions(xdp_prog, buf, sizeof(buf));
+ if (ret)
+ goto out;
+
+ if (strcmp(buf, "XDP_DROP")) {
+ printf("Expected actions XDP_PASS, got %s\n", buf);
+ ret = -EINVAL;
+ goto out;
+ }
+
+ xdp_prog = xdp_program__open_file("xdp_pass.o", "xdp", NULL);
+ ret = libxdp_get_error(xdp_prog);
+ if (ret)
+ goto out;
+
+ ret = xdp_program__attach(xdp_prog, ifindex, XDP_MODE_NATIVE, 0);
+ xdp_program__close(xdp_prog);
+ if (!ret) {
+ printf("Shouldn't have been able to attach a new program to ifindex!\n");
+ ret = -EINVAL;
+ goto out;
+ }
+ ret = 0;
+
+out:
+ if (mp)
+ xdp_multiprog__detach(mp);
+ xdp_multiprog__close(mp);
+ return ret;
+}
+
+static void usage(char *progname)
+{
+ fprintf(stderr, "Usage: %s <ifname>\n", progname);
+ exit(EXIT_FAILURE);
+}
+
+int main(int argc, char **argv)
+{
+ int ifindex, ret;
+ char *envval;
+
+ envval = secure_getenv("VERBOSE_TESTS");
+
+ silence_libbpf_logging();
+ if (envval && envval[0] == '1')
+ verbose_libxdp_logging();
+ else
+ silence_libxdp_logging();
+
+ if (argc != 2)
+ usage(argv[0]);
+
+ ifindex = if_nametoindex(argv[1]);
+
+ ret = check_old_dispatcher(ifindex);
+
+ return ret;
+}
diff --git a/lib/libxdp/tests/test_runner.sh b/lib/libxdp/tests/test_runner.sh
new file mode 100755
index 0000000..eb043a1
--- /dev/null
+++ b/lib/libxdp/tests/test_runner.sh
@@ -0,0 +1,118 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Script to setup and manage tests for xdp-tools.
+# Based on the test-env script from xdp-tutorial.
+#
+# Author: Toke Høiland-Jørgensen (toke@redhat.com)
+# Date: 26 May 2020
+# Copyright (c) 2020 Red Hat
+
+set -o errexit
+set -o nounset
+umask 077
+
+TEST_PROG_DIR="${TEST_PROG_DIR:-$(dirname "${BASH_SOURCE[0]}")}"
+ALL_TESTS=""
+VERBOSE_TESTS=${V:-0}
+
+export VERBOSE_TESTS
+
+# Odd return value for skipping, as only 0-255 is valid.
+SKIPPED_TEST=249
+
+skip_if_missing_libxdp_compat()
+{
+ if ! $TEST_PROG_DIR/check_kern_compat; then
+ exit "$SKIPPED_TEST"
+ fi
+}
+
+is_func()
+{
+ type "$1" 2>/dev/null | grep -q 'is a function'
+}
+
+check_run()
+{
+ local ret
+
+ [ "$VERBOSE_TESTS" -eq "1" ] && echo "$@"
+ "$@"
+ ret=$?
+ if [ "$ret" -ne "0" ]; then
+ exit $ret
+ fi
+}
+
+exec_test()
+{
+ local testn="$1"
+ local output
+ local ret
+
+ printf " %-30s" "[$testn]"
+ if ! is_func "$testn"; then
+ echo "INVALID"
+ return 1
+ fi
+
+ output=$($testn 2>&1)
+ ret=$?
+ if [ "$ret" -eq "0" ]; then
+ echo "PASS"
+ elif [ "$ret" -eq "$SKIPPED_TEST" ]; then
+ echo "SKIPPED"
+ ret=0
+ else
+ echo "FAIL"
+ fi
+ if [ "$ret" -ne "0" ] || [ "$VERBOSE_TESTS" -eq "1" ]; then
+ echo "$output" | sed 's/^/\t/'
+ fi
+ return $ret
+}
+
+run_tests()
+{
+ local TESTS="$*"
+ local ret=0
+ [ -z "$TESTS" ] && TESTS="$ALL_TESTS"
+
+ echo " Running tests from $TEST_DEFINITIONS"
+
+ for testn in $TESTS; do
+ exec_test $testn || ret=1
+ if is_func cleanup_tests; then
+ cleanup_tests || true
+ fi
+ done
+
+ return $ret
+}
+
+usage()
+{
+ echo "Usage: $0 <test_definition_file> [test names]" >&2
+ exit 1
+}
+
+if [ "$EUID" -ne "0" ]; then
+ if command -v sudo >/dev/null 2>&1; then
+ exec sudo env CC="$CC" CFLAGS="$CFLAGS" CPPFLAGS="$CPPFLAGS" LDLIBS="$LDLIBS" V=${VERBOSE_TESTS} "$0" "$@"
+ else
+ die "Tests must be run as root"
+ fi
+else
+ if [ "${DID_UNSHARE:-0}" -ne "1" ]; then
+ echo "Executing tests in separate net- and mount namespaces" >&2
+ exec env DID_UNSHARE=1 unshare -n -m "$0" "$@"
+ fi
+fi
+
+TEST_DEFINITIONS="${1:-}"
+[ -f "$TEST_DEFINITIONS" ] || usage
+source "$TEST_DEFINITIONS"
+
+shift
+run_tests "$@"
diff --git a/lib/libxdp/tests/test_utils.h b/lib/libxdp/tests/test_utils.h
new file mode 100644
index 0000000..1642c12
--- /dev/null
+++ b/lib/libxdp/tests/test_utils.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __TEST_UTILS_H
+#define __TEST_UTILS_H
+
+#include <bpf/libbpf.h>
+#include <xdp/libxdp.h>
+
+#define __unused __attribute__((unused))
+
+static int libbpf_silent_func(__unused enum libbpf_print_level level,
+ __unused const char *format,
+ __unused va_list args)
+{
+ return 0;
+}
+
+static inline void silence_libbpf_logging(void)
+{
+ libbpf_set_print(libbpf_silent_func);
+}
+
+static int libxdp_silent_func(__unused enum libxdp_print_level level,
+ __unused const char *format,
+ __unused va_list args)
+{
+ return 0;
+}
+
+static int libxdp_verbose_func(__unused enum libxdp_print_level level,
+ __unused const char *format,
+ __unused va_list args)
+{
+ fprintf(stderr, " ");
+ vfprintf(stderr, format, args);
+ return 0;
+}
+
+static inline void silence_libxdp_logging(void)
+{
+ libxdp_set_print(libxdp_silent_func);
+}
+
+static inline void verbose_libxdp_logging(void)
+{
+ libxdp_set_print(libxdp_verbose_func);
+}
+
+#endif
diff --git a/lib/libxdp/tests/test_xdp_frags.c b/lib/libxdp/tests/test_xdp_frags.c
new file mode 100644
index 0000000..d70e802
--- /dev/null
+++ b/lib/libxdp/tests/test_xdp_frags.c
@@ -0,0 +1,339 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <linux/err.h>
+#include <net/if.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <unistd.h>
+
+#include "test_utils.h"
+
+#include <xdp/libxdp.h>
+#include <bpf/libbpf.h>
+
+# define ARRAY_SIZE(_x) (sizeof(_x) / sizeof((_x)[0]))
+
+static bool kern_compat;
+
+
+static struct xdp_program *load_prog(void)
+{
+ DECLARE_LIBXDP_OPTS(xdp_program_opts, opts,
+ .prog_name = "xdp_pass",
+ .find_filename = "xdp-dispatcher.o",
+ );
+ return xdp_program__create(&opts);
+}
+
+static int check_attached_progs(int ifindex, int count, bool frags)
+{
+ struct xdp_multiprog *mp;
+ int ret;
+
+ /* If the kernel does not support frags, we always expect
+ * frags support to be disabled on a returned dispatcher
+ */
+ if (!kern_compat)
+ frags = false;
+
+ mp = xdp_multiprog__get_from_ifindex(ifindex);
+ ret = libxdp_get_error(mp);
+ if (ret) {
+ fprintf(stderr, "Couldn't get multiprog on ifindex %d: %s\n",
+ ifindex, strerror(-ret));
+ return ret;
+ }
+
+ ret = -EINVAL;
+
+ if (xdp_multiprog__is_legacy(mp)) {
+ fprintf(stderr, "Found legacy prog on ifindex %d\n", ifindex);
+ goto out;
+ }
+
+ if (xdp_multiprog__program_count(mp) != count) {
+ fprintf(stderr, "Expected %d programs loaded on ifindex %d, found %d\n",
+ count, ifindex, xdp_multiprog__program_count(mp));
+ goto out;
+ }
+
+ if (xdp_multiprog__xdp_frags_support(mp) != frags) {
+ fprintf(stderr,
+ "Multiprog on ifindex %d %s frags, expected %s\n",
+ ifindex,
+ xdp_multiprog__xdp_frags_support(mp) ?
+ "supports" :
+ "does not support",
+ frags ? "support" : "no support");
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ xdp_multiprog__close(mp);
+ return ret;
+}
+
+static void print_test_result(const char *func, int ret)
+{
+ fflush(stderr);
+ fprintf(stderr, "%s:\t%s\n", func, ret ? "FAILED" : "PASSED");
+ fflush(stdout);
+}
+
+static int load_attach_prog(struct xdp_program **prog, int ifindex, bool frags)
+{
+ int ret;
+
+ *prog = load_prog();
+ if (!*prog) {
+ ret = -errno;
+ fprintf(stderr, "Couldn't load program: %s\n", strerror(-ret));
+ return ret;
+ }
+
+ ret = xdp_program__set_xdp_frags_support(*prog, frags);
+ if (ret)
+ return ret;
+
+ return xdp_program__attach(*prog, ifindex, XDP_MODE_NATIVE, 0);
+}
+
+static int _check_load(int ifindex, bool frags, bool should_succeed)
+{
+ struct xdp_program *prog = NULL;
+ bool attached;
+ int ret;
+
+ ret = load_attach_prog(&prog, ifindex, frags);
+ attached = !ret;
+
+ if (attached != should_succeed) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (should_succeed)
+ ret = check_attached_progs(ifindex, 1, frags);
+ else
+ ret = 0;
+
+out:
+ if (attached)
+ xdp_program__detach(prog, ifindex, XDP_MODE_NATIVE, 0);
+ xdp_program__close(prog);
+ return ret;
+}
+
+static int check_load_frags(int ifindex_bigmtu, int ifindex_smallmtu)
+{
+ int ret = _check_load(ifindex_smallmtu, true, true);
+ if (!ret && ifindex_bigmtu)
+ _check_load(ifindex_bigmtu, true, true);
+ print_test_result(__func__, ret);
+ return ret;
+}
+
+static int check_load_nofrags_success(int ifindex)
+{
+ int ret = _check_load(ifindex, false, true);
+ print_test_result(__func__, ret);
+ return ret;
+}
+
+static int check_load_nofrags_fail(int ifindex)
+{
+ int ret = _check_load(ifindex, false, false);
+ print_test_result(__func__, ret);
+ return ret;
+}
+static int check_load_frags_multi(int ifindex)
+{
+ struct xdp_program *prog1 = NULL, *prog2 = NULL;
+ int ret;
+
+ ret = load_attach_prog(&prog1, ifindex, true);
+ if (ret)
+ goto out;
+
+ ret = load_attach_prog(&prog2, ifindex, true);
+ if (ret)
+ goto out_prog1;
+
+ ret = check_attached_progs(ifindex, 2, true);
+
+ xdp_program__detach(prog2, ifindex, XDP_MODE_NATIVE, 0);
+out_prog1:
+ xdp_program__detach(prog1, ifindex, XDP_MODE_NATIVE, 0);
+out:
+ xdp_program__close(prog2);
+ xdp_program__close(prog1);
+ print_test_result(__func__, ret);
+ return ret;
+}
+
+static int check_load_mix_small(int ifindex)
+{
+ struct xdp_program *prog1 = NULL, *prog2 = NULL;
+ int ret;
+
+ ret = load_attach_prog(&prog1, ifindex, true);
+ if (ret)
+ goto out;
+
+ /* First program attached, dispatcher supports frags */
+ ret = check_attached_progs(ifindex, 1, true);
+ if (ret)
+ goto out;
+
+ ret = load_attach_prog(&prog2, ifindex, false);
+ if (ret)
+ goto out_prog1;
+
+ /* Mixed program attachment, dispatcher should not support frags */
+ ret = check_attached_progs(ifindex, 2, false);
+
+ ret = xdp_program__detach(prog2, ifindex, XDP_MODE_NATIVE, 0) || ret;
+ if (ret)
+ goto out_prog1;
+
+ /* Second program removed, back to frags-only */
+ ret = check_attached_progs(ifindex, 1, true) || ret;
+
+out_prog1:
+ xdp_program__detach(prog1, ifindex, XDP_MODE_NATIVE, 0);
+
+out:
+ xdp_program__close(prog2);
+ xdp_program__close(prog1);
+ print_test_result(__func__, ret);
+ return ret;
+}
+
+static int check_load_mix_big(int ifindex)
+{
+ struct xdp_program *prog1 = NULL, *prog2 = NULL;
+ int ret;
+
+ ret = load_attach_prog(&prog1, ifindex, true);
+ if (ret)
+ goto out;
+
+ /* First program attached, dispatcher supports frags */
+ ret = check_attached_progs(ifindex, 1, true);
+ if (ret)
+ goto out;
+
+ /* Second non-frags program should fail on big-MTU device */
+ ret = load_attach_prog(&prog2, ifindex, false);
+ if (!ret) {
+ xdp_program__detach(prog2, ifindex, XDP_MODE_NATIVE, 0);
+ ret = -EINVAL;
+ goto out_prog1;
+ }
+
+ /* Still only a single program loaded, with frags support */
+ ret = check_attached_progs(ifindex, 1, true);
+
+out_prog1:
+ xdp_program__detach(prog1, ifindex, XDP_MODE_NATIVE, 0);
+
+out:
+ xdp_program__close(prog2);
+ xdp_program__close(prog1);
+ print_test_result(__func__, ret);
+ return ret;
+}
+
+
+static bool check_frags_compat(void)
+{
+ struct xdp_program *test_prog;
+ struct bpf_program *prog;
+ struct bpf_object *obj;
+ bool ret = false;
+ int err;
+
+ test_prog = load_prog();
+ if (!test_prog)
+ return false;
+
+ obj = xdp_program__bpf_obj(test_prog);
+ if (!obj)
+ goto out;
+
+ prog = bpf_object__find_program_by_name(obj, "xdp_pass");
+ if (!prog)
+ goto out;
+
+ bpf_program__set_flags(prog, BPF_F_XDP_HAS_FRAGS);
+ err = bpf_object__load(obj);
+ if (!err) {
+ printf("Kernel supports XDP programs with frags\n");
+ ret = true;
+ } else {
+ printf("Kernel DOES NOT support XDP programs with frags\n");
+ }
+ fflush(stdout);
+
+out:
+ xdp_program__close(test_prog);
+ return ret;
+}
+
+static void usage(char *progname)
+{
+ fprintf(stderr, "Usage: %s <ifname_bigmtu> <ifname_smallmtu>\n", progname);
+ exit(EXIT_FAILURE);
+}
+
+int main(int argc, char **argv)
+{
+ struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
+ int ifindex_bigmtu, ifindex_smallmtu, ret;
+ char *envval;
+
+ envval = secure_getenv("VERBOSE_TESTS");
+
+ silence_libbpf_logging();
+ if (envval && envval[0] == '1')
+ verbose_libxdp_logging();
+ else
+ silence_libxdp_logging();
+
+ kern_compat = check_frags_compat();
+
+ if (argc != 3)
+ usage(argv[0]);
+
+ ifindex_bigmtu = if_nametoindex(argv[1]);
+ ifindex_smallmtu = if_nametoindex(argv[2]);
+ if (!ifindex_bigmtu || !ifindex_smallmtu) {
+ fprintf(stderr, "Interface '%s' or '%s' not found.\n", argv[1], argv[2]);
+ usage(argv[0]);
+ }
+
+ if (setrlimit(RLIMIT_MEMLOCK, &r)) {
+ fprintf(stderr, "ERROR: setrlimit(RLIMIT_MEMLOCK) \"%s\"\n",
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ ret = check_load_frags(kern_compat ? ifindex_bigmtu : 0, ifindex_smallmtu);
+ ret = check_load_nofrags_success(ifindex_smallmtu) || ret;
+ if (kern_compat) {
+ ret = check_load_nofrags_fail(ifindex_bigmtu) || ret;
+ ret = check_load_frags_multi(ifindex_bigmtu) || ret;
+ ret = check_load_mix_big(ifindex_bigmtu) || ret;
+ }
+ ret = check_load_mix_small(ifindex_smallmtu) || ret;
+
+ return ret;
+}
diff --git a/lib/libxdp/tests/test_xsk_refcnt.c b/lib/libxdp/tests/test_xsk_refcnt.c
new file mode 100644
index 0000000..bdd22da
--- /dev/null
+++ b/lib/libxdp/tests/test_xsk_refcnt.c
@@ -0,0 +1,304 @@
+// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
+
+#include <errno.h>
+#include <linux/err.h>
+#include <net/if.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <unistd.h>
+
+#include "test_utils.h"
+
+#include <xdp/libxdp.h>
+#include <xdp/xsk.h>
+
+typedef __u64 u64;
+typedef __u32 u32;
+typedef __u16 u16;
+typedef __u8 u8;
+
+#define MAX_EVENTS 10
+#define MAX_NUM_QUEUES 4
+#define TEST_NAME_LENGTH 128
+
+struct xsk_umem_info {
+ struct xsk_ring_prod fq;
+ struct xsk_ring_cons cq;
+ struct xsk_umem *umem;
+ void *buffer;
+};
+
+struct xsk_socket_info {
+ struct xsk_ring_cons rx;
+ struct xsk_umem_info *umem;
+ struct xsk_socket *xsk;
+};
+
+/* Event holds socket operations that are run concurrently
+ * and in theory can produce a race condition
+ */
+struct xsk_test_event {
+ u32 num_create;
+ u32 num_delete;
+ u32 create_qids[MAX_NUM_QUEUES]; /* QIDs for sockets being created in this event */
+ u32 delete_qids[MAX_NUM_QUEUES]; /* QIDs for sockets being deleted in this event */
+};
+
+struct xsk_test {
+ char name[TEST_NAME_LENGTH];
+ u32 num_events;
+ struct xsk_test_event events[MAX_EVENTS];
+};
+
+/* Tests that use less queues must come first,
+ * so we can run all possible tests on VMs with
+ * small number of CPUs
+ */
+static struct xsk_test all_tests[] = {
+ { "Single socket created and deleted",
+ .num_events = 2,
+ .events = {{ .num_create = 1, .create_qids = {0} },
+ { .num_delete = 1, .delete_qids = {0} }
+ }},
+ { "2 sockets, created and deleted sequentially",
+ .num_events = 4,
+ .events = {{ .num_create = 1, .create_qids = {0} },
+ { .num_create = 1, .create_qids = {1} },
+ { .num_delete = 1, .delete_qids = {0} },
+ { .num_delete = 1, .delete_qids = {1} }
+ }},
+ { "2 sockets, created sequentially and deleted asynchronously",
+ .num_events = 3,
+ .events = {{ .num_create = 1, .create_qids = {0} },
+ { .num_create = 1, .create_qids = {1} },
+ { .num_delete = 2, .delete_qids = {0, 1} }
+ }},
+ { "2 sockets, asynchronously delete and create",
+ .num_events = 3,
+ .events = {{ .num_create = 1, .create_qids = {0} },
+ { .num_create = 1, .create_qids = {1},
+ .num_delete = 1, .delete_qids = {0} },
+ { .num_delete = 1, .delete_qids = {1} }
+ }},
+ { "3 sockets, created and deleted sequentially",
+ .num_events = 6,
+ .events = {{ .num_create = 1, .create_qids = {0} },
+ { .num_create = 1, .create_qids = {1} },
+ { .num_create = 1, .create_qids = {2} },
+ { .num_delete = 1, .delete_qids = {1} },
+ { .num_delete = 1, .delete_qids = {2} },
+ { .num_delete = 1, .delete_qids = {0} }
+ }},
+};
+
+# define ARRAY_SIZE(_x) (sizeof(_x) / sizeof((_x)[0]))
+
+static const char *opt_if;
+static const u8 num_tests = ARRAY_SIZE(all_tests);
+
+static struct xsk_socket_info *xsks[MAX_NUM_QUEUES];
+
+#define FRAME_SIZE 64
+#define NUM_FRAMES (XSK_RING_CONS__DEFAULT_NUM_DESCS * 2)
+
+static struct xsk_umem_info *xsk_configure_umem(void *buffer, u64 size)
+{
+ struct xsk_umem_info *umem;
+ int ret;
+
+ umem = calloc(1, sizeof(*umem));
+ if (!umem)
+ exit(EXIT_FAILURE);
+
+ ret = xsk_umem__create(&umem->umem, buffer, size, &umem->fq, &umem->cq,
+ NULL);
+ if (ret)
+ exit(ret);
+
+ umem->buffer = buffer;
+ return umem;
+}
+
+static struct xsk_socket_info *xsk_configure_socket(struct xsk_umem_info *umem,
+ unsigned int qid)
+{
+ struct xsk_socket_config cfg = {};
+ struct xsk_socket_info *xsk;
+ struct xsk_ring_cons *rxr;
+
+ xsk = calloc(1, sizeof(*xsk));
+ if (!xsk)
+ exit(EXIT_FAILURE);
+
+ xsk->umem = umem;
+ cfg.rx_size = XSK_RING_CONS__DEFAULT_NUM_DESCS;
+
+ rxr = &xsk->rx;
+ xsk_socket__create(&xsk->xsk, opt_if, qid, umem->umem,
+ rxr, NULL, &cfg);
+
+ return xsk;
+}
+
+static void *create_socket(void *args)
+{
+ struct xsk_umem_info *umem;
+ u32 qid = *(u32 *)args;
+ void *buffs;
+
+ if (posix_memalign(&buffs,
+ getpagesize(), /* PAGE_SIZE aligned */
+ NUM_FRAMES * FRAME_SIZE)) {
+ fprintf(stderr, "ERROR: Can't allocate buffer memory \"%s\"\n",
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ umem = xsk_configure_umem(buffs, NUM_FRAMES * FRAME_SIZE);
+ xsks[qid] = xsk_configure_socket(umem, qid);
+
+ return NULL;
+}
+
+static void *delete_socket(void *args)
+{
+ u32 qid = *(u32 *)args;
+ struct xsk_umem *umem;
+ void *buff;
+
+ buff = xsks[qid]->umem->buffer;
+ umem = xsks[qid]->umem->umem;
+ xsk_socket__delete(xsks[qid]->xsk);
+ free(buff);
+ (void)xsk_umem__delete(umem);
+
+ return NULL;
+}
+
+static bool xsk_prog_attached(void)
+{
+ char xsk_prog_name[] = "xsk_def_prog";
+ int ifindex = if_nametoindex(opt_if);
+ struct xdp_program *xsk_prog;
+ struct xdp_multiprog *mp;
+ bool answer = false;
+
+ mp = xdp_multiprog__get_from_ifindex(ifindex);
+ if (IS_ERR_OR_NULL(mp))
+ return false;
+
+ xsk_prog = xdp_multiprog__is_legacy(mp) ? xdp_multiprog__main_prog(mp) :
+ xdp_multiprog__next_prog(NULL, mp);
+
+ if (IS_ERR_OR_NULL(xsk_prog))
+ goto free_mp;
+
+ answer = !strncmp(xsk_prog_name, xdp_program__name(xsk_prog),
+ sizeof(xsk_prog_name));
+free_mp:
+ xdp_multiprog__close(mp);
+ return answer;
+}
+
+static void update_reference_refcnt(struct xsk_test_event *event, int *refcnt)
+{
+ *refcnt += event->num_create;
+ *refcnt -= event->num_delete;
+}
+
+static bool check_run_event(struct xsk_test_event *event, int *refcnt)
+{
+ pthread_t threads[MAX_NUM_QUEUES];
+ bool prog_attached, prog_needed;
+ u8 thread_num = 0, i;
+ int ret;
+
+ update_reference_refcnt(event, refcnt);
+
+ for (i = 0; i < event->num_create; i++) {
+ ret = pthread_create(&threads[thread_num++], NULL,
+ &create_socket, &event->create_qids[i]);
+ if (ret)
+ exit(ret);
+ }
+
+ for (i = 0; i < event->num_delete; i++) {
+ ret = pthread_create(&threads[thread_num++], NULL,
+ &delete_socket, &event->delete_qids[i]);
+ if (ret)
+ exit(ret);
+ }
+
+ for (i = 0; i < thread_num; i++)
+ pthread_join(threads[i], NULL);
+
+ prog_attached = xsk_prog_attached();
+ prog_needed = *refcnt > 0;
+
+ if (prog_needed != prog_attached) {
+ printf("Program is referenced by %d sockets, but is %s attached\n",
+ *refcnt, prog_attached ? "still" : "not");
+ return false;
+ }
+
+ return true;
+}
+
+static bool check_run_test(struct xsk_test *test)
+{
+ bool test_ok = false;
+ int refcnt = 0;
+ u8 i = 0;
+
+ for (i = 0; i < test->num_events; i++) {
+ if (!check_run_event(&test->events[i], &refcnt)) {
+ printf("Event %u failed\n", i);
+ goto print_result;
+ }
+ }
+
+ /* Do not let tests interfere with each other */
+ sleep(1);
+
+ test_ok = true;
+
+print_result:
+ printf("%s: %s\n", test->name, test_ok ? "PASSED" : "FAILED");
+ return test_ok;
+}
+
+static int read_args(int argc, char **argv)
+{
+ if (argc != 2)
+ return -1;
+
+ opt_if = argv[1];
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
+ u8 i = 0;
+
+ if (read_args(argc, argv))
+ return -1;
+
+ if (setrlimit(RLIMIT_MEMLOCK, &r)) {
+ fprintf(stderr, "ERROR: setrlimit(RLIMIT_MEMLOCK) \"%s\"\n",
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ silence_libbpf_logging();
+
+ for (i = 0; i < num_tests; i++) {
+ if (!check_run_test(&all_tests[i]))
+ exit(EXIT_FAILURE);
+ }
+
+ return 0;
+}
diff --git a/lib/libxdp/tests/xdp_dispatcher_v1.c b/lib/libxdp/tests/xdp_dispatcher_v1.c
new file mode 100644
index 0000000..00bb426
--- /dev/null
+++ b/lib/libxdp/tests/xdp_dispatcher_v1.c
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_endian.h>
+
+#include "xdp_dispatcher_v1.h"
+
+#define XDP_METADATA_SECTION "xdp_metadata"
+#define XDP_DISPATCHER_VERSION_V1 1
+#define XDP_DISPATCHER_RETVAL 31
+
+
+static volatile const struct xdp_dispatcher_config_v1 conf = {};
+
+__attribute__ ((noinline))
+int prog0(struct xdp_md *ctx) {
+ volatile int ret = XDP_DISPATCHER_RETVAL;
+
+ if (!ctx)
+ return XDP_ABORTED;
+ return ret;
+}
+__attribute__ ((noinline))
+
+SEC("xdp")
+int xdp_dispatcher(struct xdp_md *ctx)
+{
+ __u8 num_progs_enabled = conf.num_progs_enabled;
+ int ret;
+
+ if (num_progs_enabled < 1)
+ goto out;
+ ret = prog0(ctx);
+ if (!((1U << ret) & conf.chain_call_actions[0]))
+ return ret;
+
+out:
+ return XDP_PASS;
+}
+
+char _license[] SEC("license") = "GPL";
+__uint(dispatcher_version, XDP_DISPATCHER_VERSION_V1) SEC(XDP_METADATA_SECTION);
diff --git a/lib/libxdp/tests/xdp_dispatcher_v1.h b/lib/libxdp/tests/xdp_dispatcher_v1.h
new file mode 100644
index 0000000..55dac37
--- /dev/null
+++ b/lib/libxdp/tests/xdp_dispatcher_v1.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __XDP_DISPATCHER_V1_H
+#define __XDP_DISPATCHER_V1_H
+
+#ifndef MAX_DISPATCHER_ACTIONS
+#define MAX_DISPATCHER_ACTIONS 10
+#endif
+
+struct xdp_dispatcher_config_v1 {
+ __u8 num_progs_enabled;
+ __u32 chain_call_actions[MAX_DISPATCHER_ACTIONS];
+ __u32 run_prios[MAX_DISPATCHER_ACTIONS];
+};
+
+#endif
diff --git a/lib/libxdp/tests/xdp_pass.c b/lib/libxdp/tests/xdp_pass.c
new file mode 100644
index 0000000..6b61a00
--- /dev/null
+++ b/lib/libxdp/tests/xdp_pass.c
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+
+SEC("xdp")
+int xdp_pass(struct xdp_md *ctx)
+{
+ return XDP_PASS;
+}
+
+char _license[] SEC("license") = "GPL";