Netdev List
 help / color / mirror / Atom feed
From: Christian Brauner <brauner@kernel.org>
To: Jori Koolstra <jkoolstra@xs4all.nl>
Cc: Christian Brauner <brauner@kernel.org>,
	 Aleksa Sarai <cyphar@cyphar.com>,
	Kuniyuki Iwashima <kuniyu@google.com>,
	 "David S . Miller" <davem@davemloft.net>,
	 Eric Dumazet <edumazet@google.com>,
	Jakub Kicinski <kuba@kernel.org>,
	 Paolo Abeni <pabeni@redhat.com>, Simon Horman <horms@kernel.org>,
	 netdev@vger.kernel.org, linux-fsdevel@vger.kernel.org,
	 linux-kernel@vger.kernel.org
Subject: Re: [PATCH net-next v4 3/3] selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
Date: Tue, 07 Jul 2026 13:02:05 +0200	[thread overview]
Message-ID: <20260707-sinnieren-versichern-heuballen-e86897d216c3@brauner> (raw)
In-Reply-To: <20260705123826.3818443-4-jkoolstra@xs4all.nl>

> 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>
>
> diff --git a/tools/testing/selftests/net/af_unix/.gitignore b/tools/testing/selftests/net/af_unix/.gitignore
> index 240b26740c9e..5034482f8864 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 4c0375e28bbe..594cd26ec398 100644
> --- a/tools/testing/selftests/net/af_unix/Makefile
> +++ b/tools/testing/selftests/net/af_unix/Makefile
> @@ -11,9 +11,17 @@ TEST_GEN_PROGS := \
>  	scm_inq \
>  	scm_pidfd \
>  	scm_rights \
> +	scm_rights_denial_lsm \
>  	so_peek_off \
>  	unix_connect \
>  	unix_connreset \
>  # 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/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..b8656de86efe
> --- /dev/null
> +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
> @@ -0,0 +1,263 @@
> +// 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_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_LE(0, fd);
> +	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_LE(0, self->map_fd);
> +
> +	ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 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_LE(0, self->files[i]);
> +
> +		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);
> +}
> +
> +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));
> +	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_LE(0, slots[i]);

Why do you assert less-or-equal? You want a valid fd so you should
expect >= 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_LE(0, slots[1]);
> +	EXPECT_EQ(0, check_secret(slots[1], 1));
> +	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_LE(0, slots[0]);

Why do you expect less-equal than zero? Don't you need ASSERT_GE()
because you want slots[0] to be a valid file desscriptor?

And even the other way around... Zero is a valid file descriptor so on
failure you must expect ASSERT_LT()?

-- 
Christian Brauner <brauner@kernel.org>

      parent reply	other threads:[~2026-07-07 11:02 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-05 12:38 [PATCH net-next v4 0/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
2026-07-05 12:38 ` [PATCH net-next v4 1/3] net: scm: move scm_detach_fds() from common path to scm_recv_unix() Jori Koolstra
2026-07-06 18:20   ` Kuniyuki Iwashima
2026-07-05 12:38 ` [PATCH net-next v4 2/3] net: af_unix: useful handling of LSM denials on SCM_RIGHTS Jori Koolstra
2026-07-07 11:02   ` Christian Brauner
2026-07-05 12:38 ` [PATCH net-next v4 3/3] selftest: Add tests for " Jori Koolstra
2026-07-06 23:17   ` Kuniyuki Iwashima
2026-07-07 11:02   ` Christian Brauner [this message]

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=20260707-sinnieren-versichern-heuballen-e86897d216c3@brauner \
    --to=brauner@kernel.org \
    --cc=cyphar@cyphar.com \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=horms@kernel.org \
    --cc=jkoolstra@xs4all.nl \
    --cc=kuba@kernel.org \
    --cc=kuniyu@google.com \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox