From: Eric Suen <ericsu@linux.microsoft.com>
To: selinux@vger.kernel.org
Cc: paul@paul-moore.com, stephen.smalley.work@gmail.com,
omosnace@redhat.com, danieldurning.work@gmail.com,
ericsu@linux.microsoft.com
Subject: [PATCH testsuite v7] tests/bpf: Add tests for SELinux BPF token access control
Date: Thu, 4 Dec 2025 19:33:28 -0800 [thread overview]
Message-ID: <20251205033328.1644-1-ericsu@linux.microsoft.com> (raw)
This patch adds new tests to verify the SELinux support for BPF token
access control, as introduced in the corresponding kernel patch:
https://lore.kernel.org/selinux/20250816201420.197-1-ericsu@linux.microsoft.com/
Note that new tests require changes in libsepol which is covered in
https://lore.kernel.org/selinux/20250808183506.665-1-ericsu@linux.microsoft.com/
Four new tests are added to cover both positive and negative scenarios,
ensuring that the SELinux policy enforcement on BPF token usage behaves
as expected.
- Successful map_create and prog_load when SELinux permissions are
granted.
- Enforcement of SELinux policy restrictions when access is denied.
For testing purposes, you can update the base policy by manually
modifying your base module and tweaking /usr/share/selinux/devel as
follows:
sudo semodule -c -E base
sudo sed -i.orig "s/map_create/map_create map_create_as/" base.cil
sudo sed -i "s/prog_load/prog_load prog_load_as/" base.cil
sudo semodule -i base.cil
echo "(policycap bpf_token_perms)" > bpf_token_perms.cil
sudo semodule -i bpf_token_perms.cil
sudo cp /usr/share/selinux/devel/include/support/all_perms.spt \
/usr/share/selinux/devel/include/support/all_perms.spt.orig
sudo sed -i "s/map_create/map_create map_create_as/" \
/usr/share/selinux/devel/include/support/all_perms.spt
sudo sed -i "s/prog_load/prog_load prog_load_as/" \
/usr/share/selinux/devel/include/support/all_perms.spt
When finished testing, you can semodule -r base bpf_token_perms to
undo the two module changes and restore your all_perms.spt file from
the saved .orig file.
Signed-off-by: Eric Suen <ericsu@linux.microsoft.com>
---
Changes in v2:
- Removed allow rule for 'kernel_t' in test_bpf.te which was added due
to a bug in the kernel
- Cleaned up other unnecessary rules in test_bpf.te
- Added token_test.c which was missing from previous patch
Changes in v3:
- Added original license in 'token_test.c'
- Updated patch description to
- replace 'base.sil' with 'base.cil'
- Remove extra quotation mark in 'sudo 'sed -i "s/"map_create'
Changes in v4:
- Updated 'token_test.c' to write error messages only when DEBUG
is defined
Changes in v5:
- Created test_bpf_token.te which gets loaded when required bpf
permissions (i.e. map_create_as, prog_load_as) are available, and
policy capability (i.e. bpf_token_perms) is defined
- Added condition in tests/bpf/test to run new tests when policy
capability bpf_token_perms is defined
Changes in v6:
- Updated commit description per review feedback
- Update comment in policy/Makefile
- Fixed missing new line in tests/bpf/Makefile
Changes in v7:
- Added new tests to verify cross domain scenarios in selinux_bpf_token_capable
policy/Makefile | 7 +
policy/test_bpf_token.te | 74 ++++++
tests/bpf/Makefile | 11 +-
tests/bpf/bpf_common.h | 13 ++
tests/bpf/bpf_test.c | 84 +++++--
tests/bpf/test | 45 ++++
tests/bpf/token_child_helper.c | 95 ++++++++
tests/bpf/token_test.c | 410 +++++++++++++++++++++++++++++++++
tests/bpf/token_test_common.c | 181 +++++++++++++++
tests/bpf/token_test_common.h | 96 ++++++++
10 files changed, 997 insertions(+), 19 deletions(-)
create mode 100644 policy/test_bpf_token.te
create mode 100644 tests/bpf/token_child_helper.c
create mode 100644 tests/bpf/token_test.c
create mode 100644 tests/bpf/token_test_common.c
create mode 100644 tests/bpf/token_test_common.h
diff --git a/policy/Makefile b/policy/Makefile
index ffd774d..915555c 100644
--- a/policy/Makefile
+++ b/policy/Makefile
@@ -105,6 +105,13 @@ ifeq ($(shell grep -q bpf $(POLDEV)/include/support/all_perms.spt && echo true),
TARGETS += test_bpf.te test_fdreceive_bpf.te test_binder_bpf.te
endif
+# bpf token test dependencies: bpf permission, bpf_token capability
+ifeq ($(shell [ -f /sys/fs/selinux/class/bpf/perms/map_create_as ] && [ -f /sys/fs/selinux/class/bpf/perms/prog_load_as ] && echo true),true)
+ifeq ($(shell [ -f $(SELINUXFS)/policy_capabilities/bpf_token_perms ] && grep -q 1 $(SELINUXFS)/policy_capabilities/bpf_token_perms && echo true),true)
+TARGETS += test_bpf_token.te
+endif
+endif
+
ifeq ($(shell grep -q all_key_perms $(POLDEV)/include/support/all_perms.spt && echo true),true)
TARGETS += test_keys.te test_watchkey.te
endif
diff --git a/policy/test_bpf_token.te b/policy/test_bpf_token.te
new file mode 100644
index 0000000..bc7a777
--- /dev/null
+++ b/policy/test_bpf_token.te
@@ -0,0 +1,74 @@
+########################################
+#
+# Policy for testing BPF map create and program load with BPF token
+
+################### Allow map_create_as and prog_load_as ###################
+fs_list_bpf_dirs(test_bpf_t)
+allow test_bpf_t bpf_t:filesystem mount;
+allow test_bpf_t root_t:dir mounton;
+allow test_bpf_t self:bpf { map_create_as prog_load_as };
+allow test_bpf_t self:cap2_userns { bpf perfmon };
+allow test_bpf_t self:cap_userns { net_admin setgid setuid sys_admin };
+allow test_bpf_t self:user_namespace create;
+
+############################ Deny map_create_as ############################
+type test_bpf_deny_map_create_as_t;
+testsuite_domain_type(test_bpf_deny_map_create_as_t)
+typeattribute test_bpf_deny_map_create_as_t bpfdomain;
+allow test_bpf_deny_map_create_as_t self:process { setrlimit };
+allow test_bpf_deny_map_create_as_t self:capability { sys_resource sys_admin };
+
+fs_list_bpf_dirs(test_bpf_deny_map_create_as_t)
+allow test_bpf_deny_map_create_as_t bpf_t:filesystem mount;
+allow test_bpf_deny_map_create_as_t root_t:dir mounton;
+allow test_bpf_deny_map_create_as_t self:bpf { map_create map_read map_write prog_load prog_load_as };
+allow test_bpf_deny_map_create_as_t self:cap2_userns { bpf };
+allow test_bpf_deny_map_create_as_t self:cap_userns { setgid setuid sys_admin };
+allow test_bpf_deny_map_create_as_t self:user_namespace create;
+
+############################ Deny prog_load_as #############################
+type test_bpf_deny_prog_load_as_t;
+testsuite_domain_type(test_bpf_deny_prog_load_as_t)
+typeattribute test_bpf_deny_prog_load_as_t bpfdomain;
+allow test_bpf_deny_prog_load_as_t self:process { setrlimit };
+allow test_bpf_deny_prog_load_as_t self:capability { sys_resource sys_admin };
+
+fs_list_bpf_dirs(test_bpf_deny_prog_load_as_t)
+allow test_bpf_deny_prog_load_as_t bpf_t:filesystem mount;
+allow test_bpf_deny_prog_load_as_t root_t:dir mounton;
+allow test_bpf_deny_prog_load_as_t self:bpf { map_create map_create_as map_read map_write prog_load };
+allow test_bpf_deny_prog_load_as_t self:cap2_userns { bpf perfmon };
+allow test_bpf_deny_prog_load_as_t self:cap_userns { net_admin setgid setuid sys_admin };
+allow test_bpf_deny_prog_load_as_t self:user_namespace create;
+
+################### Test BPF Token Capability Check ########################
+type test_bpf_deny_helper_exec_t;
+files_type(test_bpf_deny_helper_exec_t)
+
+# Automatic transition when executing test_bpf_deny_helper_exec_t files
+domain_entry_file(test_bpf_deny_token_cap_t, test_bpf_deny_helper_exec_t)
+domtrans_pattern(test_bpf_t, test_bpf_deny_helper_exec_t, test_bpf_deny_token_cap_t)
+
+allow test_bpf_t test_bpf_deny_token_cap_t:fd use;
+allow test_bpf_deny_token_cap_t test_bpf_t:unix_stream_socket { read write getattr sendto };
+allow test_bpf_t test_bpf_deny_token_cap_t:unix_stream_socket { read write getattr recvfrom };
+
+######################## Deny Token Capability #############################
+type test_bpf_deny_token_cap_t;
+testsuite_domain_type(test_bpf_deny_token_cap_t)
+typeattribute test_bpf_deny_token_cap_t bpfdomain;
+allow test_bpf_deny_token_cap_t self:process { setrlimit };
+allow test_bpf_deny_token_cap_t self:capability { sys_resource sys_admin };
+
+fs_list_bpf_dirs(test_bpf_deny_token_cap_t)
+allow test_bpf_deny_token_cap_t bpf_t:filesystem mount;
+allow test_bpf_deny_token_cap_t root_t:dir mounton;
+allow test_bpf_deny_token_cap_t test_bpf_t:bpf { map_create_as };
+allow test_bpf_deny_token_cap_t self:cap_userns { setgid setuid sys_admin };
+allow test_bpf_deny_token_cap_t self:cap2_userns { bpf perfmon };
+allow test_bpf_deny_token_cap_t self:user_namespace create;
+
+# CRITICAL: Do NOT grant capability permissions over test_bpf_t (the grantor)
+# allow test_bpf_deny_token_cap_t test_bpf_t:cap2_userns bpf;
+# Omitting this rule above causes avc_has_perm in selinux_bpf_token_capable
+# to fail which is the purpose of 'test_bpf_token_cap_cross_failure'
diff --git a/tests/bpf/Makefile b/tests/bpf/Makefile
index 1ae8ce9..a24182b 100644
--- a/tests/bpf/Makefile
+++ b/tests/bpf/Makefile
@@ -1,5 +1,6 @@
-TARGETS = bpf_test
-DEPS = bpf_common.c bpf_common.h
+TARGETS = bpf_test token_child_helper
+BPF_TEST_SRCS = bpf_test.c bpf_common.c token_test.c token_test_common.c
+TOKEN_CHILD_HELPER_SRCS = token_child_helper.c bpf_common.c token_test_common.c
LDLIBS += -lselinux -lbpf
# export so that BPF_ENABLED entries get built correctly on local build
@@ -14,4 +15,8 @@ clean:
rm -f $(TARGETS) test_sock flag *_flag
@set -e; for i in $(BPF_ENABLED); do $(MAKE) -C $$i clean ; done
-$(TARGETS): $(DEPS)
+bpf_test: $(BPF_TEST_SRCS)
+ $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
+
+token_child_helper: $(TOKEN_CHILD_HELPER_SRCS)
+ $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
diff --git a/tests/bpf/bpf_common.h b/tests/bpf/bpf_common.h
index 44ac28f..cae36e5 100644
--- a/tests/bpf/bpf_common.h
+++ b/tests/bpf/bpf_common.h
@@ -4,6 +4,7 @@
#include <stdlib.h>
#include <errno.h>
#include <stdbool.h>
+#include <libgen.h>
#include <selinux/selinux.h>
#include <bpf/bpf.h>
#include <linux/bpf.h>
@@ -12,6 +13,10 @@
extern int create_bpf_map(void);
extern int create_bpf_prog(void);
extern void bpf_setrlimit(void);
+extern int test_bpf_map_create(void);
+extern int test_bpf_prog_load(void);
+extern int test_bpf_token_cap_cross_success(void);
+extern int test_bpf_token_cap_cross_failure(void);
/* edited eBPF instruction library */
/* Short form of mov, dst_reg = imm32 */
@@ -32,3 +37,11 @@ extern void bpf_setrlimit(void);
.off = 0, \
.imm = 0 })
+/* Raw code statement block */
+#define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM) \
+ ((struct bpf_insn) { \
+ .code = CODE, \
+ .dst_reg = DST, \
+ .src_reg = SRC, \
+ .off = OFF, \
+ .imm = IMM })
diff --git a/tests/bpf/bpf_test.c b/tests/bpf/bpf_test.c
index 3c6a29c..c97a10d 100644
--- a/tests/bpf/bpf_test.c
+++ b/tests/bpf/bpf_test.c
@@ -1,28 +1,42 @@
#include "bpf_common.h"
+#define write_verbose(verbose, fmt, ...) \
+ do { \
+ if (verbose) \
+ printf(fmt "\n", ##__VA_ARGS__); \
+ } while (0)
+
static void usage(char *progname)
{
fprintf(stderr,
- "usage: %s -m|-p [-v]\n"
+ "usage: %s -m|-p|-c|-l|-s|-f [-v]\n"
"Where:\n\t"
"-m Create BPF map fd\n\t"
"-p Create BPF prog fd\n\t"
+ "-c Test BPF token map create\n\t"
+ "-l Test BPF token program load\n\t"
+ "-s Test BPF token cross-domain SUCCESS (exec-based)\n\t"
+ "-f Test BPF token cross-domain FAILURE (exec-based)\n\t"
"-v Print information.\n", progname);
exit(-1);
}
int main(int argc, char *argv[])
{
- int opt, result, fd;
- bool verbose = false;
+ int opt, result, ret;
+ bool verbose = false, is_fd = true;
char *context;
enum {
MAP_FD = 1,
- PROG_FD
+ PROG_FD,
+ MAP_CREATE,
+ PROG_LOAD,
+ TOKEN_CROSS_DOMAIN_SUCCESS,
+ TOKEN_CROSS_DOMAIN_FAILURE,
} bpf_fd_type;
- while ((opt = getopt(argc, argv, "mpv")) != -1) {
+ while ((opt = getopt(argc, argv, "mpclvsf")) != -1) {
switch (opt) {
case 'm':
bpf_fd_type = MAP_FD;
@@ -30,6 +44,18 @@ int main(int argc, char *argv[])
case 'p':
bpf_fd_type = PROG_FD;
break;
+ case 'c':
+ bpf_fd_type = MAP_CREATE;
+ break;
+ case 'l':
+ bpf_fd_type = PROG_LOAD;
+ break;
+ case 's':
+ bpf_fd_type = TOKEN_CROSS_DOMAIN_SUCCESS;
+ break;
+ case 'f':
+ bpf_fd_type = TOKEN_CROSS_DOMAIN_FAILURE;
+ break;
case 'v':
verbose = true;
break;
@@ -44,8 +70,10 @@ int main(int argc, char *argv[])
exit(-1);
}
- if (verbose)
- printf("Process context:\n\t%s\n", context);
+ write_verbose(verbose, "Process context:\n\n%s", context);
+
+ /* Set environment variable for child helper to find itself */
+ setenv("TEST_BASEDIR", dirname(strdup(argv[0])), 1);
free(context);
@@ -54,24 +82,48 @@ int main(int argc, char *argv[])
switch (bpf_fd_type) {
case MAP_FD:
- if (verbose)
- printf("Creating BPF map\n");
+ write_verbose(verbose, "Creating BPF map");
- fd = create_bpf_map();
+ ret = create_bpf_map();
break;
case PROG_FD:
- if (verbose)
- printf("Creating BPF prog\n");
+ write_verbose(verbose, "Creating BPF prog");
+
+ ret = create_bpf_prog();
+ break;
+ case MAP_CREATE:
+ is_fd = false;
+ write_verbose(verbose, "Testing BPF map create");
- fd = create_bpf_prog();
+ ret = test_bpf_map_create();
+ break;
+ case PROG_LOAD:
+ is_fd = false;
+ write_verbose(verbose, "Testing BPF prog load");
+
+ ret = test_bpf_prog_load();
+ break;
+ case TOKEN_CROSS_DOMAIN_SUCCESS:
+ is_fd = false;
+ write_verbose(verbose, "Testing BPF token cross-domain SUCCESS (exec)");
+
+ ret = test_bpf_token_cap_cross_success();
+ break;
+ case TOKEN_CROSS_DOMAIN_FAILURE:
+ is_fd = false;
+ write_verbose(verbose, "Testing BPF token cross-domain FAILURE (exec)");
+
+ ret = test_bpf_token_cap_cross_failure();
break;
default:
usage(argv[0]);
}
- if (fd < 0)
- return fd;
+ if (ret < 0)
+ return ret;
+
+ if (is_fd)
+ close(ret);
- close(fd);
return 0;
}
diff --git a/tests/bpf/test b/tests/bpf/test
index a3fd856..7119916 100755
--- a/tests/bpf/test
+++ b/tests/bpf/test
@@ -6,12 +6,25 @@ BEGIN {
$basedir =~ s|(.*)/[^/]*|$1|;
$fdr_basedir = "$basedir/../fdreceive";
$binder_basedir = "$basedir/../binder";
+ $test_bpf_token = 0;
$test_bpf_count = 7;
$test_fdreceive_count = 4;
$test_count = $test_bpf_count + $test_fdreceive_count;
+ if (
+ system(
+"grep -q 1 /sys/fs/selinux/policy_capabilities/bpf_token_perms 2> /dev/null"
+ ) == 0
+ )
+ {
+ $test_bpf_token = 1;
+
+ $test_bpf_token_count = 6;
+ $test_count += $test_bpf_token_count;
+ }
+
# allow info to be shown during tests
$v = $ARGV[0];
if ($v) {
@@ -92,6 +105,38 @@ $result =
system "runcon -t test_bpf_deny_prog_run_t $basedir/bpf_test -p $v 2>&1";
ok($result);
+if ($test_bpf_token) {
+
+ # BPF token - BPF_MAP_CREATE_AS, BPF_PROG_LOAD_AS
+ $result = system "runcon -t test_bpf_t $basedir/bpf_test -c $v";
+ ok( $result eq 0 );
+
+ $result = system "runcon -t test_bpf_t $basedir/bpf_test -l $v";
+ ok( $result eq 0 );
+
+ # BPF token - deny BPF_MAP_CREATE_AS
+ $result =
+ system
+ "runcon -t test_bpf_deny_map_create_as_t $basedir/bpf_test -c $v 2>&1";
+ ok($result);
+
+ # BPF token - deny BPF_PROG_LOAD_AS
+ $result =
+ system
+ "runcon -t test_bpf_deny_prog_load_as_t $basedir/bpf_test -l $v 2>&1";
+ ok($result);
+
+ # BPF token cross-domain capability check - SUCCESS case
+ system("chcon -t test_file_t $basedir/token_child_helper");
+ $result = system "runcon -t test_bpf_t $basedir/bpf_test -s $v";
+ ok( $result eq 0 );
+
+ # BPF token cross-domain capability check - FAILURE case
+ system("chcon -t test_bpf_deny_helper_exec_t $basedir/token_child_helper");
+ $result = system "runcon -t test_bpf_t $basedir/bpf_test -f $v 2>&1";
+ ok($result);
+}
+
#
################ BPF Tests for fdreceive #######################
#
diff --git a/tests/bpf/token_child_helper.c b/tests/bpf/token_child_helper.c
new file mode 100644
index 0000000..dfa82b4
--- /dev/null
+++ b/tests/bpf/token_child_helper.c
@@ -0,0 +1,95 @@
+#include "bpf_common.h"
+#include <sys/socket.h>
+#include <linux/unistd.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <linux/mount.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "token_test_common.h"
+
+int main(int argc, char *argv[])
+{
+ LIBBPF_OPTS(bpf_map_create_opts, map_opts);
+ int sock_fd, token_fd = -1, map_fd = -1, fs_fd = -1, mnt_fd = -1, bpffs_fd = -1;
+ int err;
+
+ if (argc != 2) {
+ fprintf(stderr, "Usage: %s <socket_fd>\n", argv[0]);
+ return 1;
+ }
+
+ sock_fd = atoi(argv[1]);
+
+ /* Create user namespace - required for bpf_token_create() */
+ err = create_and_enter_userns();
+ if (!ASSERT_OK(err, "token_child_helper/create_and_enter_userns"))
+ goto cleanup;
+
+ /* Create mount namespace for isolation */
+ err = unshare(CLONE_NEWNS);
+ if (!ASSERT_OK(err, "token_child_helper/create_mountns"))
+ goto cleanup;
+
+ err = sys_mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 0);
+ if (!ASSERT_OK(err, "token_child_helper/remount_root"))
+ goto cleanup;
+
+ /* Create bpffs fd and send to parent for configuration */
+ fs_fd = create_bpffs_fd();
+ if (!ASSERT_GT(fs_fd, 0, "token_child_helper/create_bpffs_fd")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+ err = sendfd(sock_fd, fs_fd);
+ if (!ASSERT_OK(err, "token_child_helper/send_fs_fd"))
+ goto cleanup;
+ zclose(fs_fd);
+
+ err = recvfd(sock_fd, &mnt_fd);
+ if (!ASSERT_OK(err, "token_child_helper/recv_mnt_fd"))
+ goto cleanup;
+
+ /* Open bpffs from mnt_fd */
+ bpffs_fd = openat(mnt_fd, ".", 0, O_RDWR);
+ if (!ASSERT_GT(bpffs_fd, 0, "token_child_helper/bpffs_open")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+ token_fd = bpf_token_create(bpffs_fd, NULL);
+ if (!ASSERT_GT(token_fd, 0, "token_child_helper/bpf_token_create")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+ /* Try to use the token - this will trigger selinux_bpf_token_capable()
+ * Kernel will check: avc_has_perm(current_sid, grantor_sid, cap2_userns, bpf)
+ */
+ map_opts.map_flags = BPF_F_TOKEN_FD;
+ map_opts.token_fd = token_fd;
+ map_fd = bpf_map_create(BPF_MAP_TYPE_STACK, "cross_domain_map", 0, 8, 1,
+ &map_opts);
+ if (!ASSERT_GT(map_fd, 0, "token_child_helper/bpf_map_create")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+ err = 0;
+
+cleanup:
+ zclose(fs_fd);
+ zclose(mnt_fd);
+ zclose(bpffs_fd);
+ zclose(map_fd);
+ zclose(token_fd);
+ zclose(sock_fd);
+
+ if (err)
+ fprintf(stderr, "BPF token cross-domain capability check failed: %s\n",
+ strerror(errno));
+
+ return err;
+}
+
diff --git a/tests/bpf/token_test.c b/tests/bpf/token_test.c
new file mode 100644
index 0000000..0d0986b
--- /dev/null
+++ b/tests/bpf/token_test.c
@@ -0,0 +1,410 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Code derived from: linux/source/tools/testing/selftests/bpf/prog_tests/token.c
+ * Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
+ */
+
+#include "bpf_common.h"
+#include <signal.h>
+#include <linux/mount.h>
+#include <linux/unistd.h>
+#include <sys/wait.h>
+#include <sys/socket.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <bpf/btf.h>
+#include <selinux/selinux.h>
+#include "token_test_common.h"
+
+#define bit(n) (1ULL << (n))
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+struct bpffs_opts {
+ __u64 cmds;
+ __u64 maps;
+ __u64 progs;
+ __u64 attachs;
+ const char *cmds_str;
+ const char *maps_str;
+ const char *progs_str;
+ const char *attachs_str;
+};
+
+typedef int (*child_callback_fn)(int bpffs_fd);
+
+static int set_delegate_mask(int fs_fd, const char *key, __u64 mask,
+ const char *mask_str)
+{
+ char buf[32];
+ int err;
+
+ if (!mask_str) {
+ if (mask == ~0ULL)
+ mask_str = "any";
+ else {
+ snprintf(buf, sizeof(buf), "0x%llx", (unsigned long long)mask);
+ mask_str = buf;
+ }
+ }
+
+ err = sys_fsconfig(fs_fd, FSCONFIG_SET_STRING, key,
+ mask_str, 0);
+ if (err < 0)
+ err = -errno;
+ return err;
+}
+
+static int materialize_bpffs_fd(int fs_fd, struct bpffs_opts *opts)
+{
+ int mnt_fd, err;
+
+ /* set up token delegation mount options */
+ err = set_delegate_mask(fs_fd, "delegate_cmds", opts->cmds, opts->cmds_str);
+ if (!ASSERT_OK(err, "fs_cfg_cmd"))
+ return err;
+ err = set_delegate_mask(fs_fd, "delegate_maps", opts->maps, opts->maps_str);
+ if (!ASSERT_OK(err, "fs_cfg_maps"))
+ return err;
+ err = set_delegate_mask(fs_fd, "delegate_progs", opts->progs, opts->progs_str);
+ if (!ASSERT_OK(err, "fs_cfg_progs"))
+ return err;
+ err = set_delegate_mask(fs_fd, "delegate_attachs", opts->attachs,
+ opts->attachs_str);
+ if (!ASSERT_OK(err, "fs_cfg_attachs"))
+ return err;
+
+ /* instantiate FS object */
+ err = sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
+ if (err < 0)
+ return -errno;
+
+ /* create O_PATH fd for detached mount */
+ mnt_fd = sys_fsmount(fs_fd, 0, 0);
+ if (mnt_fd < 0)
+ return -errno;
+
+ return mnt_fd;
+}
+
+static int wait_for_pid(pid_t pid)
+{
+ int status, ret;
+
+again:
+ ret = waitpid(pid, &status, 0);
+ if (ret == -1) {
+ if (errno == EINTR)
+ goto again;
+
+ return -1;
+ }
+
+ if (!WIFEXITED(status))
+ return -1;
+
+ return WEXITSTATUS(status);
+}
+
+static int child(int sock_fd, struct bpffs_opts *bpffs_opts,
+ child_callback_fn callback)
+{
+ int mnt_fd = -1, fs_fd = -1, err = 0, bpffs_fd = -1, token_fd = -1;
+
+ err = create_and_enter_userns();
+ if (!ASSERT_OK(err, "create_and_enter_userns"))
+ goto cleanup;
+
+ err = unshare(CLONE_NEWNS);
+ if (!ASSERT_OK(err, "create_mountns"))
+ goto cleanup;
+
+ err = sys_mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 0);
+ if (!ASSERT_OK(err, "remount_root"))
+ goto cleanup;
+
+ fs_fd = create_bpffs_fd();
+ if (!ASSERT_GE(fs_fd, 0, "create_bpffs_fd")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+ err = sendfd(sock_fd, fs_fd);
+ if (!ASSERT_OK(err, "send_fs_fd"))
+ goto cleanup;
+ zclose(fs_fd);
+
+ err = recvfd(sock_fd, &mnt_fd);
+ if (!ASSERT_OK(err, "recv_mnt_fd"))
+ goto cleanup;
+
+ bpffs_fd = openat(mnt_fd, ".", 0, O_RDWR);
+ if (!ASSERT_GE(bpffs_fd, 0, "bpffs_open")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+ err = callback(bpffs_fd);
+ if (!ASSERT_OK(err, "test_callback"))
+ goto cleanup;
+
+ err = 0;
+
+cleanup:
+ zclose(sock_fd);
+ zclose(mnt_fd);
+ zclose(fs_fd);
+ zclose(bpffs_fd);
+ zclose(token_fd);
+
+ exit(-err);
+}
+
+static int parent(int child_pid, struct bpffs_opts *bpffs_opts, int sock_fd)
+{
+ int fs_fd = -1, mnt_fd = -1, token_fd = -1, err;
+
+ err = recvfd(sock_fd, &fs_fd);
+ if (!ASSERT_OK(err, "recv_bpffs_fd"))
+ goto cleanup;
+
+ mnt_fd = materialize_bpffs_fd(fs_fd, bpffs_opts);
+ if (!ASSERT_GE(mnt_fd, 0, "materialize_bpffs_fd")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+ zclose(fs_fd);
+
+ err = sendfd(sock_fd, mnt_fd);
+ if (!ASSERT_OK(err, "send_mnt_fd"))
+ goto cleanup;
+ zclose(mnt_fd);
+
+ err = wait_for_pid(child_pid);
+ if (!ASSERT_OK(err, "waitpid_child")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+cleanup:
+ zclose(sock_fd);
+ zclose(fs_fd);
+ zclose(mnt_fd);
+ zclose(token_fd);
+
+ if (child_pid > 0)
+ (void)kill(child_pid, SIGKILL);
+
+ return err;
+}
+
+static int subtest(struct bpffs_opts *bpffs_opts, child_callback_fn child_cb)
+{
+ int sock_fds[2] = { -1, -1 };
+ int child_pid = 0, err;
+
+ err = socketpair(AF_UNIX, SOCK_STREAM, 0, sock_fds);
+ if (!ASSERT_OK(err, "socketpair"))
+ goto cleanup;
+
+ child_pid = fork();
+ if (!ASSERT_GE(child_pid, 0, "fork"))
+ goto cleanup;
+
+ if (child_pid == 0) {
+ zclose(sock_fds[0]);
+ return child(sock_fds[1], bpffs_opts, child_cb);
+ } else {
+ zclose(sock_fds[1]);
+ return parent(child_pid, bpffs_opts, sock_fds[0]);
+ }
+
+cleanup:
+ zclose(sock_fds[0]);
+ zclose(sock_fds[1]);
+ if (child_pid > 0)
+ (void)kill(child_pid, SIGKILL);
+
+ return -err;
+}
+
+static int userns_map_create(int mnt_fd)
+{
+ LIBBPF_OPTS(bpf_map_create_opts, map_opts);
+ int err = 0, token_fd = -1, map_fd = -1;
+
+ /* create BPF token from BPF FS mount */
+ token_fd = bpf_token_create(mnt_fd, NULL);
+ if (!ASSERT_GT(token_fd, 0, "userns_map_create/token_create")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+ map_opts.map_flags = BPF_F_TOKEN_FD;
+ map_opts.token_fd = token_fd;
+ map_fd = bpf_map_create(BPF_MAP_TYPE_STACK, "userns_map_create", 0, 8, 1,
+ &map_opts);
+ if (!ASSERT_GT(map_fd, 0, "userns_map_create/bpf_map_create")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+cleanup:
+ zclose(token_fd);
+ zclose(map_fd);
+
+ if (err)
+ fprintf(stderr, "Failed to create BPF map with BPF token enabled: %s\n",
+ strerror(errno));
+
+ return err;
+}
+
+static int userns_prog_load(int mnt_fd)
+{
+ LIBBPF_OPTS(bpf_prog_load_opts, prog_opts);
+ int err, token_fd = -1, prog_fd = -1;
+ struct bpf_insn insns[] = {
+ /* bpf_jiffies64() requires CAP_BPF */
+ BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_jiffies64),
+ /* bpf_get_current_task() requires CAP_PERFMON */
+ BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_get_current_task),
+ /* r0 = 0; exit; */
+ BPF_MOV64_IMM(BPF_REG_0, 0),
+ BPF_EXIT_INSN(),
+ };
+ size_t insn_cnt = ARRAY_SIZE(insns);
+
+ token_fd = bpf_token_create(mnt_fd, NULL);
+ if (!ASSERT_GT(token_fd, 0, "userns_prog_load/token_create")) {
+ err = -EINVAL;
+ goto cleanup;
+ }
+
+ prog_opts.prog_flags = BPF_F_TOKEN_FD;
+ prog_opts.token_fd = token_fd;
+ prog_opts.expected_attach_type = BPF_XDP;
+ prog_fd = bpf_prog_load(BPF_PROG_TYPE_XDP, "token_prog", "GPL",
+ insns, insn_cnt, &prog_opts);
+ if (!ASSERT_GT(prog_fd, 0, "userns_prog_load/bpf_prog_load")) {
+ err = -EPERM;
+ goto cleanup;
+ }
+
+ err = 0;
+
+cleanup:
+ zclose(prog_fd);
+ zclose(token_fd);
+
+ if (err)
+ fprintf(stderr, "Failed to load BPF prog with token enabled: %s\n",
+ strerror(errno));
+
+ return err;
+}
+
+int test_bpf_map_create(void)
+{
+ struct bpffs_opts opts = {
+ .cmds_str = "map_create",
+ .maps_str = "stack"
+ };
+
+ return subtest(&opts, userns_map_create);
+}
+
+int test_bpf_prog_load(void)
+{
+ struct bpffs_opts opts = {
+ .cmds_str = "prog_load",
+ .progs_str = "XDP",
+ .attachs_str = "xdp",
+ };
+
+ return subtest(&opts, userns_prog_load);
+}
+
+/* Child helper that execs token_child_helper
+ * SELinux domain transition is automatic, controlled by file label:
+ * - test_file_t: no transition, stays in test_bpf_t
+ * - test_bpf_deny_helper_exec_t: auto-transitions to test_bpf_deny_token_cap_t
+ */
+static int child_exec_helper(int sock_fd, const char *helper_path)
+{
+ char sock_fd_str[32];
+
+ /* Pass socket FD as argument */
+ snprintf(sock_fd_str, sizeof(sock_fd_str), "%d", sock_fd);
+
+ /* Exec the helper program - transition determined by file label */
+ execl(helper_path, helper_path, sock_fd_str, NULL);
+
+ /* If we get here, exec failed */
+ fprintf(stderr, "exec failed: %s\n", strerror(errno));
+ _exit(1);
+}
+
+/* Cross-domain test with automatic SELinux transition based on file label */
+int test_bpf_token_cap_cross_domain_exec(void)
+{
+ struct bpffs_opts opts = {
+ .cmds_str = "map_create",
+ .maps_str = "stack"
+ };
+ int sock_fds[2] = {-1, -1};
+ int child_pid = 0, err;
+ char helper_path[256];
+ char *basedir;
+
+ /* Find the helper program */
+ basedir = getenv("TEST_BASEDIR");
+ if (!basedir)
+ basedir = ".";
+ snprintf(helper_path, sizeof(helper_path), "%s/token_child_helper", basedir);
+
+ /* Parent stays in init_user_ns to call materialize_bpffs_fd()
+ * Child will create its own user namespace after exec
+ */
+
+ err = socketpair(AF_UNIX, SOCK_STREAM, 0, sock_fds);
+ if (!ASSERT_OK(err, "socketpair"))
+ goto cleanup;
+
+ child_pid = fork();
+ if (!ASSERT_GE(child_pid, 0, "fork"))
+ goto cleanup;
+
+ if (child_pid == 0) {
+ zclose(sock_fds[0]);
+ /* Child process - exec helper, transition based on file label */
+ child_exec_helper(sock_fds[1], helper_path);
+ }
+
+ /* Parent process - materializes bpffs and sends mnt_fd to child */
+ zclose(sock_fds[1]);
+ return parent(child_pid, &opts, sock_fds[0]);
+
+cleanup:
+ zclose(sock_fds[0]);
+ zclose(sock_fds[1]);
+ if (child_pid > 0)
+ (void)kill(child_pid, SIGKILL);
+
+ return -err;
+}
+
+/* Wrapper for cross-domain success test
+ * Test expects token_child_helper to be labeled test_file_t (no transition)
+ */
+int test_bpf_token_cap_cross_success(void)
+{
+ return test_bpf_token_cap_cross_domain_exec();
+}
+
+/* Wrapper for cross-domain failure test
+ * Test expects token_child_helper to be labeled test_bpf_deny_helper_exec_t (auto-transition)
+ */
+int test_bpf_token_cap_cross_failure(void)
+{
+ return test_bpf_token_cap_cross_domain_exec();
+}
diff --git a/tests/bpf/token_test_common.c b/tests/bpf/token_test_common.c
new file mode 100644
index 0000000..201fb43
--- /dev/null
+++ b/tests/bpf/token_test_common.c
@@ -0,0 +1,181 @@
+#include "token_test_common.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <linux/unistd.h>
+
+inline int sys_fsopen(const char *fsname, unsigned int flags)
+{
+ return syscall(__NR_fsopen, fsname, flags);
+}
+
+inline int sys_mount(const char *dev_name, const char *dir_name,
+ const char *type, unsigned long flags,
+ const void *data)
+{
+ return syscall(__NR_mount, dev_name, dir_name, type, flags, data);
+}
+
+inline int sys_fsconfig(int fs_fd, unsigned int cmd, const char *key,
+ const void *val, int aux)
+{
+ return syscall(__NR_fsconfig, fs_fd, cmd, key, val, aux);
+}
+
+inline int sys_fsmount(int fs_fd, unsigned int flags,
+ unsigned int ms_flags)
+{
+ return syscall(__NR_fsmount, fs_fd, flags, ms_flags);
+}
+
+static ssize_t write_nointr(int fd, const void *buf, size_t count)
+{
+ ssize_t ret;
+
+ do {
+ ret = write(fd, buf, count);
+ } while (ret < 0 && errno == EINTR);
+
+ return ret;
+}
+
+int write_file(const char *path, const void *buf, size_t count)
+{
+ int fd;
+ ssize_t ret;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW);
+ if (fd < 0)
+ return -1;
+
+ ret = write_nointr(fd, buf, count);
+ close(fd);
+ if (ret < 0 || (size_t)ret != count)
+ return -1;
+
+ return 0;
+}
+
+int create_bpffs_fd(void)
+{
+ int fs_fd;
+
+ /* create VFS context */
+ fs_fd = sys_fsopen("bpf", 0);
+ ASSERT_GE(fs_fd, 0, "fs_fd");
+
+ return fs_fd;
+}
+
+int create_and_enter_userns(void)
+{
+ uid_t uid;
+ gid_t gid;
+ char map[100];
+
+ uid = getuid();
+ gid = getgid();
+
+ if (unshare(CLONE_NEWUSER))
+ return -1;
+
+ if (write_file("/proc/self/setgroups", "deny", sizeof("deny") - 1) &&
+ errno != ENOENT)
+ return -1;
+
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map, strlen(map)))
+ return -1;
+
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map, strlen(map)))
+ return -1;
+
+ if (setgid(0))
+ return -1;
+
+ if (setuid(0))
+ return -1;
+
+ return 0;
+}
+
+int sendfd(int sockfd, int fd)
+{
+ struct msghdr msg = {};
+ struct cmsghdr *cmsg;
+ int fds[1] = {fd}, err;
+ char iobuf[1];
+ struct iovec io = {
+ .iov_base = iobuf,
+ .iov_len = sizeof(iobuf),
+ };
+ union {
+ char buf[CMSG_SPACE(sizeof(fds))];
+ struct cmsghdr align;
+ } u;
+
+ msg.msg_iov = &io;
+ msg.msg_iovlen = 1;
+ msg.msg_control = u.buf;
+ msg.msg_controllen = sizeof(u.buf);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
+ memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
+
+ err = sendmsg(sockfd, &msg, 0);
+ if (err < 0)
+ err = -errno;
+ if (!ASSERT_EQ(err, 1, "sendmsg"))
+ return -EINVAL;
+
+ return 0;
+}
+
+int recvfd(int sockfd, int *fd)
+{
+ struct msghdr msg = {};
+ struct cmsghdr *cmsg;
+ int fds[1], err;
+ char iobuf[1];
+ struct iovec io = {
+ .iov_base = iobuf,
+ .iov_len = sizeof(iobuf),
+ };
+ union {
+ char buf[CMSG_SPACE(sizeof(fds))];
+ struct cmsghdr align;
+ } u;
+
+ msg.msg_iov = &io;
+ msg.msg_iovlen = 1;
+ msg.msg_control = u.buf;
+ msg.msg_controllen = sizeof(u.buf);
+
+ err = recvmsg(sockfd, &msg, 0);
+ if (err < 0)
+ err = -errno;
+ if (!ASSERT_EQ(err, 1, "recvmsg"))
+ return -EINVAL;
+
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (!ASSERT_OK_PTR(cmsg, "cmsg_null") ||
+ !ASSERT_EQ(cmsg->cmsg_len, CMSG_LEN(sizeof(fds)), "cmsg_len") ||
+ !ASSERT_EQ(cmsg->cmsg_level, SOL_SOCKET, "cmsg_level") ||
+ !ASSERT_EQ(cmsg->cmsg_type, SCM_RIGHTS, "cmsg_type"))
+ return -EINVAL;
+
+ memcpy(fds, CMSG_DATA(cmsg), sizeof(fds));
+ *fd = fds[0];
+
+ return 0;
+}
diff --git a/tests/bpf/token_test_common.h b/tests/bpf/token_test_common.h
new file mode 100644
index 0000000..16845eb
--- /dev/null
+++ b/tests/bpf/token_test_common.h
@@ -0,0 +1,96 @@
+#ifndef TOKEN_TEST_COMMON_H
+#define TOKEN_TEST_COMMON_H
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <errno.h>
+#include <bpf/libbpf.h>
+
+#ifdef DEBUG
+#define _CHECK(condition, format...) ({ \
+ int __ret = !!(condition); \
+ int __save_errno = errno; \
+ if (__ret) { \
+ fprintf(stderr, ##format); \
+ } \
+ errno = __save_errno; \
+ __ret; \
+})
+#else
+#define _CHECK(condition, format...) ({ \
+ int __ret = !!(condition); \
+ __ret; \
+})
+#endif
+
+#define ASSERT_OK(res, name) ({ \
+ long long ___res = (res); \
+ bool ___ok = ___res == 0; \
+ _CHECK(!___ok, \
+ "%s failed. unexpected error: %lld (errno %d)\n", \
+ name, ___res, errno); \
+ ___ok; \
+})
+
+#define ASSERT_GT(actual, expected, name) ({ \
+ typeof(actual) ___act = (actual); \
+ typeof(expected) ___exp = (expected); \
+ bool ___ok = ___act > ___exp; \
+ _CHECK(!___ok, \
+ "unexpected %s: actual %lld <= expected %lld (errno %d)\n", \
+ (name), (long long)(___act), (long long)(___exp), errno); \
+ ___ok; \
+})
+
+
+#define ASSERT_GE(actual, expected, name) ({ \
+ typeof(actual) ___act = (actual); \
+ typeof(expected) ___exp = (expected); \
+ bool ___ok = ___act >= ___exp; \
+ _CHECK(!___ok, \
+ "unexpected %s: actual %lld < expected %lld (errno %d)\n", \
+ (name), (long long)(___act), (long long)(___exp), errno); \
+ ___ok; \
+})
+
+
+#define ASSERT_EQ(actual, expected, name) ({ \
+ typeof(actual) ___act = (actual); \
+ typeof(expected) ___exp = (expected); \
+ bool ___ok = ___act == ___exp; \
+ _CHECK(!___ok, \
+ "unexpected %s: actual %lld != expected %lld (errno %d)\n", \
+ (name), (long long)(___act), (long long)(___exp), errno); \
+ ___ok; \
+})
+
+
+#define ASSERT_OK_PTR(ptr, name) ({ \
+ const void *___res = (ptr); \
+ int ___err = libbpf_get_error(___res); \
+ bool ___ok = ___err == 0; \
+ _CHECK(!___ok, \
+ "%s unexpected error: %d\n", name, ___err); \
+ ___ok; \
+})
+
+
+#define zclose(fd) do { if (fd >= 0) close(fd); fd = -1; } while (0)
+
+int sys_fsopen(const char *fsname, unsigned int flags);
+int sys_mount(const char *dev_name, const char *dir_name,
+ const char *type, unsigned long flags,
+ const void *data);
+int sys_fsconfig(int fs_fd, unsigned int cmd, const char *key,
+ const void *val, int aux);
+int sys_fsmount(int fs_fd, unsigned int flags,
+ unsigned int ms_flags);
+int write_file(const char *path, const void *buf, size_t count);
+int create_bpffs_fd(void);
+int create_and_enter_userns(void);
+int sendfd(int sockfd, int fd);
+int recvfd(int sockfd, int *fd);
+
+#endif /* TOKEN_TEST_COMMON_H */
+
--
2.50.1
next reply other threads:[~2025-12-05 3:33 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-05 3:33 Eric Suen [this message]
2026-02-09 18:01 ` [PATCH testsuite v7] tests/bpf: Add tests for SELinux BPF token access control Daniel Durning
2026-02-09 20:43 ` Stephen Smalley
2026-02-11 9:40 ` Paul Moore
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20251205033328.1644-1-ericsu@linux.microsoft.com \
--to=ericsu@linux.microsoft.com \
--cc=danieldurning.work@gmail.com \
--cc=omosnace@redhat.com \
--cc=paul@paul-moore.com \
--cc=selinux@vger.kernel.org \
--cc=stephen.smalley.work@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is 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.