Re: [PATCH net-next v6 4/4] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
Kuniyuki Iwashima <[email protected]> Tue, 4 Aug 2026 10:26:57 -0700
| Newsgroups | gmane.linux.file-systems,gmane.linux.network,gmane.linux.kernel |
|---|---|
| Message-ID | <CAAVpQUC7O1yiKQ+3FcGWbDkmcd-LeFc5tCC0vF7QgioVOXvb5Q@mail.gmail.com> |
On Sun, Aug 2, 2026 at 8:11=E2=80=AFAM Jori Koolstra <[email protected]> = wrote: > > 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 <[email protected]> > --- > .../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.c > > diff --git a/tools/testing/selftests/net/af_unix/.gitignore b/tools/testi= ng/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_connreset > diff --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 :=3D \ > 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 :=3D scm_rights_denial_lsm.bpf.o > + > include ../../lib.mk > +include ../bpf.mk > + > +$(OUTPUT)/scm_rights_denial_lsm: $(BPFOBJ) > +$(OUTPUT)/scm_rights_denial_lsm: CFLAGS +=3D -I$(SCRATCH_DIR)/include > +$(OUTPUT)/scm_rights_denial_lsm: LDLIBS +=3D -lelf -lz > diff --git a/tools/testing/selftests/net/af_unix/config b/tools/testing/s= elftests/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=3Dy > CONFIG_UNIX=3Dy > CONFIG_UNIX_DIAG=3Dm > CONFIG_USER_NS=3Dy > +CONFIG_BPF=3Dy > +CONFIG_BPF_SYSCALL=3Dy > +CONFIG_BPF_EVENTS=3Dy > +CONFIG_BPF_JIT=3Dy > +CONFIG_SECURITY=3Dy > +CONFIG_BPF_LSM=3Dy > +CONFIG_DEBUG_INFO_BTF=3Dy Please sort configs as CI complains. https://patchwork.kernel.org/project/netdevbpf/patch/20260802151212.3294591= [email protected]/ > diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bp= f.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") =3D "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 =3D bpf_get_current_pid_tgid() >> 32; > + __u64 ino =3D file->f_inode->i_ino; > + __u32 *owner; > + > + owner =3D bpf_map_lookup_elem(&denied_inodes, &ino); > + if (owner && *owner =3D=3D 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 =3D SOCK_STREAM, > +}; > + > +FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, dgram) > +{ > + .sock_type =3D SOCK_DGRAM, > +}; > + > +FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, seqpacket) > +{ > + .sock_type =3D SOCK_SEQPACKET, > +}; > + > +FIXTURE_SETUP(scm_rights_denial_bpf) > +{ > + struct bpf_program *prog; > + char lsms[256] =3D {}; > + int i, fd; > + > + if (geteuid() !=3D 0) > + SKIP(return, "requires root"); > + > + fd =3D 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=3D...,bpf= )"); > + > + self->obj =3D 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 =3D bpf_object__find_program_by_name(self->obj, "scm_rights_= deny"); > + ASSERT_NE(NULL, prog); > + > + self->link =3D bpf_program__attach_lsm(prog); > + ASSERT_NE(NULL, self->link); > + > + self->map_fd =3D 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 =3D 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] =3D 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] =3D st.st_ino; > + } > +} > + > +FIXTURE_TEARDOWN(scm_rights_denial_bpf) > +{ > + bpf_link__destroy(self->link); > + bpf_object__close(self->obj); > + > + for (int i =3D 0; i < NR_FILES; i++) { > + if (self->files[i] >=3D 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 =3D getpid(); > + > + return bpf_map_update_elem(map_fd, &ino, &tgid, BPF_ANY); > +} > + > +static int set_notrunc(int sk) > +{ > + int one =3D 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))] =3D {}; > + char data =3D 'x'; > + struct iovec iov =3D { > + .iov_base =3D &data, > + .iov_len =3D sizeof(data), > + }; > + struct msghdr msg =3D { > + .msg_iov =3D &iov, > + .msg_iovlen =3D 1, > + .msg_control =3D ctrl, > + .msg_controllen =3D CMSG_SPACE(n * sizeof(int)), > + }; > + struct cmsghdr *cmsg =3D CMSG_FIRSTHDR(&msg); > + > + cmsg->cmsg_level =3D SOL_SOCKET; > + cmsg->cmsg_type =3D SCM_RIGHTS; > + cmsg->cmsg_len =3D 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 =3D { > + .iov_base =3D &data, > + .iov_len =3D sizeof(data), > + }; > + struct msghdr msg =3D { > + .msg_iov =3D &iov, > + .msg_iovlen =3D 1, > + .msg_control =3D ctrl, > + .msg_controllen =3D sizeof(ctrl), > + }; > + struct cmsghdr *cmsg; > + > + if (recvmsg(sk, &msg, 0) < 0) > + return -1; > + > + *msg_flags =3D msg.msg_flags; > + > + cmsg =3D CMSG_FIRSTHDR(&msg); > + if (!cmsg) > + return 0; > + > + nr_slots =3D (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] =3D {}; > + > + 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 =3D recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags); > + > + ASSERT_EQ(NR_FILES, nr_slots); > + EXPECT_EQ(0, flags & MSG_CTRUNC); > + > + for (i =3D 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 =3D 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 =3D 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 =3D recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags); > + > + ASSERT_EQ(NR_FILES, nr_slots); > + EXPECT_EQ(0, flags & MSG_CTRUNC); > + > + for (i =3D 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 =3D 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 >