Re: [PATCH net-next v6 4/4] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
From: Kuniyuki Iwashima <kuniyu@google.com>
Date: 2026-08-04 17:27:09
Also in:
linux-fsdevel, lkml
On Sun, Aug 2, 2026 at 8:11 AM Jori Koolstra [off-list ref] wrote:
quoted hunk ↗ jump to hunk
Tests SCM_RIGHTS fd passing on a socket with the new socket option SO_RIGHTS_NOTRUNC turned on. To hook into the security_file_receive() call, BPF is used. The BPF program shares a hashmap with userspace that lists the inos to be blocked (of the receiver tgid). Signed-off-by: Jori Koolstra <jkoolstra@xs4all.nl> --- .../testing/selftests/net/af_unix/.gitignore | 2 + tools/testing/selftests/net/af_unix/Makefile | 8 + tools/testing/selftests/net/af_unix/config | 7 + .../net/af_unix/scm_rights_denial_lsm.bpf.c | 36 +++ .../net/af_unix/scm_rights_denial_lsm.c | 285 ++++++++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c create mode 100644 tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.cdiff --git a/tools/testing/selftests/net/af_unix/.gitignore b/tools/testing/selftests/net/af_unix/.gitignore index 973176644103..954f0958dd03 100644 --- a/tools/testing/selftests/net/af_unix/.gitignore +++ b/tools/testing/selftests/net/af_unix/.gitignore@@ -3,6 +3,8 @@ msg_oob scm_inq scm_pidfd scm_rights +scm_rights_denial_lsm +scm_rights_denial_lsm.bpf.o so_peek_off unix_connect unix_connresetdiff --git a/tools/testing/selftests/net/af_unix/Makefile b/tools/testing/selftests/net/af_unix/Makefile index 57d159803a3a..a66f10fb0c23 100644 --- a/tools/testing/selftests/net/af_unix/Makefile +++ b/tools/testing/selftests/net/af_unix/Makefile@@ -11,10 +11,18 @@ TEST_GEN_PROGS := \ scm_inq \ scm_pidfd \ scm_rights \ + scm_rights_denial_lsm \ so_peek_off \ unix_connect \ unix_connreset \ unix_listen \ # end of TEST_GEN_PROGS +TEST_GEN_FILES := scm_rights_denial_lsm.bpf.o + include ../../lib.mk +include ../bpf.mk + +$(OUTPUT)/scm_rights_denial_lsm: $(BPFOBJ) +$(OUTPUT)/scm_rights_denial_lsm: CFLAGS += -I$(SCRATCH_DIR)/include +$(OUTPUT)/scm_rights_denial_lsm: LDLIBS += -lelf -lzdiff --git a/tools/testing/selftests/net/af_unix/config b/tools/testing/selftests/net/af_unix/config index 41dbb03c747e..468f7a0bb64e 100644 --- a/tools/testing/selftests/net/af_unix/config +++ b/tools/testing/selftests/net/af_unix/config@@ -2,3 +2,10 @@ CONFIG_AF_UNIX_OOB=y CONFIG_UNIX=y CONFIG_UNIX_DIAG=m CONFIG_USER_NS=y +CONFIG_BPF=y +CONFIG_BPF_SYSCALL=y +CONFIG_BPF_EVENTS=y +CONFIG_BPF_JIT=y +CONFIG_SECURITY=y +CONFIG_BPF_LSM=y +CONFIG_DEBUG_INFO_BTF=y
Please sort configs as CI complains. https://patchwork.kernel.org/project/netdevbpf/patch/20260802151212.3294591-5-jkoolstra@xs4all.nl/
quoted hunk ↗ jump to hunk
diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c new file mode 100644 index 000000000000..4f2414465bfd --- /dev/null +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c@@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <linux/bpf.h> +#include <linux/errno.h> +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> + +char _license[] SEC("license") = "GPL"; + +struct inode { + unsigned long i_ino; +} __attribute__((preserve_access_index)); + +struct file { + struct inode *f_inode; +} __attribute__((preserve_access_index)); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 16); + __type(key, __u64); /* inode number */ + __type(value, __u32); /* tgid of the receiver being tested */ +} denied_inodes SEC(".maps"); + +SEC("lsm/file_receive") +int BPF_PROG(scm_rights_deny, struct file *file) +{ + __u32 tgid = bpf_get_current_pid_tgid() >> 32; + __u64 ino = file->f_inode->i_ino; + __u32 *owner; + + owner = bpf_map_lookup_elem(&denied_inodes, &ino); + if (owner && *owner == tgid) + return -EPERM; + + return 0; +}diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c new file mode 100644 index 000000000000..941b7decf798 --- /dev/null +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c@@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <bpf/bpf.h> +#include <bpf/libbpf.h> + +#include "kselftest_harness.h" + +#ifndef SO_RIGHTS_NOTRUNC +#define SO_RIGHTS_NOTRUNC 85 +#endif + +#define NR_FILES 2 + +/* Per-file content, so a received fd can be matched to the file sent */ +#define SECRET(n) "secret %d", (n) + +/* Indices into the socketpair */ +#define SK_SENDER 0 +#define SK_RECEIVER 1 + +FIXTURE(scm_rights_denial_bpf) +{ + struct bpf_object *obj; + struct bpf_link *link; + int map_fd; + int sk[2]; + int files[NR_FILES]; + __u64 inos[NR_FILES]; + char paths[NR_FILES][64]; +}; + +FIXTURE_VARIANT(scm_rights_denial_bpf) +{ + int sock_type; +}; + +FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, stream) +{ + .sock_type = SOCK_STREAM, +}; + +FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, dgram) +{ + .sock_type = SOCK_DGRAM, +}; + +FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, seqpacket) +{ + .sock_type = SOCK_SEQPACKET, +}; + +FIXTURE_SETUP(scm_rights_denial_bpf) +{ + struct bpf_program *prog; + char lsms[256] = {}; + int i, fd; + + if (geteuid() != 0) + SKIP(return, "requires root"); + + fd = open("/sys/kernel/security/lsm", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_LT(0, read(fd, lsms, sizeof(lsms) - 1)); + close(fd); + + if (!strstr(lsms, "bpf")) + SKIP(return, "BPF LSM not active (boot with lsm=...,bpf)"); + + self->obj = bpf_object__open_file("scm_rights_denial_lsm.bpf.o", NULL); + ASSERT_NE(NULL, self->obj); + ASSERT_EQ(0, bpf_object__load(self->obj)); + + prog = bpf_object__find_program_by_name(self->obj, "scm_rights_deny"); + ASSERT_NE(NULL, prog); + + self->link = bpf_program__attach_lsm(prog); + ASSERT_NE(NULL, self->link); + + self->map_fd = bpf_object__find_map_fd_by_name(self->obj, + "denied_inodes"); + ASSERT_GE(self->map_fd, 0); + + ASSERT_EQ(0, socketpair(AF_UNIX, variant->sock_type, 0, self->sk)); + + for (i = 0; i < NR_FILES; i++) { + struct stat st; + + snprintf(self->paths[i], sizeof(self->paths[i]), + "/tmp/scm_rights_denial_bpf.%d.XXXXXX", i); + self->files[i] = mkstemp(self->paths[i]); + ASSERT_GE(self->files[i], 0); + + ASSERT_LT(0, dprintf(self->files[i], SECRET(i))); + + ASSERT_EQ(0, fstat(self->files[i], &st)); + self->inos[i] = st.st_ino; + } +} + +FIXTURE_TEARDOWN(scm_rights_denial_bpf) +{ + bpf_link__destroy(self->link); + bpf_object__close(self->obj); + + for (int i = 0; i < NR_FILES; i++) { + if (self->files[i] >= 0) { + close(self->files[i]); + unlink(self->paths[i]); + } + } + + close(self->sk[SK_SENDER]); + close(self->sk[SK_RECEIVER]); +} + +static int deny_inode(int map_fd, __u64 ino) +{ + __u32 tgid = getpid(); + + return bpf_map_update_elem(map_fd, &ino, &tgid, BPF_ANY); +} + +static int set_notrunc(int sk) +{ + int one = 1; + + return setsockopt(sk, SOL_SOCKET, SO_RIGHTS_NOTRUNC, + &one, sizeof(one)); +} + +static int send_fds(int sk, int *fds, int n) +{ + char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))] = {}; + char data = 'x'; + struct iovec iov = { + .iov_base = &data, + .iov_len = sizeof(data), + }; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = ctrl, + .msg_controllen = CMSG_SPACE(n * sizeof(int)), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(n * sizeof(int)); + memcpy(CMSG_DATA(cmsg), fds, n * sizeof(int)); + + return sendmsg(sk, &msg, 0);
ASSERT_EQ(1, ret) and return 0/-1 explicitly, and..
+}
+
+static int recv_fd_slots(int sk, int *slots, int *msg_flags)
+{
+ int nr_slots;
+ char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))];
+ char data;
+ struct iovec iov = {
+ .iov_base = &data,
+ .iov_len = sizeof(data),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = ctrl,
+ .msg_controllen = sizeof(ctrl),
+ };
+ struct cmsghdr *cmsg;
+
+ if (recvmsg(sk, &msg, 0) < 0)
+ return -1;
+
+ *msg_flags = msg.msg_flags;
+
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (!cmsg)
+ return 0;
+
+ nr_slots = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
+ memcpy(slots, CMSG_DATA(cmsg), nr_slots * sizeof(int));
+
+ return nr_slots;
+}
+
+/* Prove a received fd works by reading back the file's content. */
+static int check_secret(int fd, int idx)
+{
+ char want[32], got[32] = {};
+
+ snprintf(want, sizeof(want), SECRET(idx));
+ if (pread(fd, got, sizeof(got) - 1, 0) < 0)
+ return -1;
+
+ return strcmp(want, got);
+}
+
+TEST_F(scm_rights_denial_bpf, all_allowed)
+{
+ int slots[NR_FILES], nr_slots, flags, i;
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));then here we can use ASSERT_EQ(0, ..)
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+
+ for (i = 0; i < NR_FILES; i++) {
+ ASSERT_GE(slots[i], 0);
+ EXPECT_EQ(0, check_secret(slots[i], i));
+ close(slots[i]);
+ }
+}
+
+TEST_F(scm_rights_denial_bpf, first_denied)
+{
+ int slots[NR_FILES], nr_slots, flags;
+
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[0]));
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+ EXPECT_EQ(-EPERM, slots[0]);
+
+ ASSERT_GE(slots[1], 0);
+ EXPECT_EQ(0, check_secret(slots[1], 1));Check secrets from 1 to NR_FILES just in case.
+ close(slots[1]);
+}
+
+TEST_F(scm_rights_denial_bpf, all_denied)
+{
+ int slots[NR_FILES], nr_slots, flags, i;
+
+ for (i = 0; i < NR_FILES; i++)
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[i]));
+
+ ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(NR_FILES, nr_slots);
+ EXPECT_EQ(0, flags & MSG_CTRUNC);
+
+ for (i = 0; i < NR_FILES; i++)
+ EXPECT_EQ(-EPERM, slots[i]);
+}
+
+TEST_F(scm_rights_denial_bpf, denied_without_notrunc)
+{
+ int slots[NR_FILES], nr_slots, flags;
+
+ /*
+ * Baseline behaviour without SO_RIGHTS_NOTRUNC: the fd array is
+ * truncated at the first denied fd and MSG_CTRUNC is set.
+ */
+ ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[1]));
+
+ ASSERT_NE(-1, send_fds(self->sk[SK_SENDER], self->files, NR_FILES));
+ nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags);
+
+ ASSERT_EQ(1, nr_slots);
+ EXPECT_NE(0, flags & MSG_CTRUNC);
+
+ ASSERT_GE(slots[0], 0);
+ EXPECT_EQ(0, check_secret(slots[0], 0));
+ close(slots[0]);
+}
+
+TEST_HARNESS_MAIN
--
2.55.0