From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from linux.microsoft.com (linux.microsoft.com [13.77.154.182]) by smtp.subspace.kernel.org (Postfix) with ESMTP id 920A818DB1E for ; Fri, 5 Dec 2025 03:33:31 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=13.77.154.182 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1764905614; cv=none; b=brL/S960JyE6JjAjwgM/sXmwQ2SmdIV30fXDoJqQxGf8WXLc/U4PkKsZY8VztSEs0ncyDljcXZUL8bAGJsZX5xYol+inh78vKudEEEyeDRdA8zpB0cygTfvtGDZHtoMBmJ6WLnq2X8llLu6Qah3M6NWstVL/S/ptPV9RmC3Fyfc= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1764905614; c=relaxed/simple; bh=P0W4kLX4PD+990vGLQKBfD3rdyhq6rqkHffBWfo5f/U=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=m1JUT6b+sN63Ya64jQOaB9R7y/Q5U2vlU/mnnW1SWrGYWSVidVNOzCl9r/dvkabJ5a1dlAZEqViJ6rxyfs7A5t4KZbxTO5llHf+pTt3hKhg/xTRoAV3llxjMVr4gP/gsykaSgVrutyuvlp35J6xCnU+gKWsC2Yle6zM0QGqEMhI= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.microsoft.com; spf=pass smtp.mailfrom=linux.microsoft.com; dkim=pass (1024-bit key) header.d=linux.microsoft.com header.i=@linux.microsoft.com header.b=UQlvJj5E; arc=none smtp.client-ip=13.77.154.182 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.microsoft.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.microsoft.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.microsoft.com header.i=@linux.microsoft.com header.b="UQlvJj5E" Received: from ericsu-dev1 (unknown [131.107.1.202]) by linux.microsoft.com (Postfix) with ESMTPSA id 377C22123265; Thu, 4 Dec 2025 19:33:29 -0800 (PST) DKIM-Filter: OpenDKIM Filter v2.11.0 linux.microsoft.com 377C22123265 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.microsoft.com; s=default; t=1764905609; bh=i6Qm0deeg2zTHBKk7t3W1Lc8uV3qWIVCeMA8FM+WeUc=; h=From:To:Cc:Subject:Date:From; b=UQlvJj5EACW6XXSIE+eI4WkwPqyQlDydaP/kogJZiWgud3KNT+duknIeZKxgsb0/A BCCIn2Zi2swsPjKO6Hap87l80+JgvKq8t6S8in2IUfSDsWwINcwSTlAsHocNkQDWIY 0Sq3CblbFM+1eYSoDr+prBvJYoOJCYnrncopO/io= From: Eric Suen 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 Message-ID: <20251205033328.1644-1-ericsu@linux.microsoft.com> X-Mailer: git-send-email 2.52.0.windows.1 Precedence: bulk X-Mailing-List: selinux@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit 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 --- 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 #include #include +#include #include #include #include @@ -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 +#include +#include +#include +#include +#include +#include +#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 \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 +#include +#include +#include +#include +#include +#include +#include +#include +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 +#include +#include +#include +#include + +#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