Re: [PATCH net-next v6 4/4] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS

Kuniyuki Iwashima <[email protected]>
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 AM 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/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_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 := \
>         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 -lz
> diff --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/[email protected]/


> 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
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.