mirror of
https://github.com/torvalds/linux.git
synced 2026-09-22 04:34:03 +02:00
selftest: Add tests for useful handling of LSM denials on SCM_RIGHTS
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> Link: https://patch.msgid.link/20260814172806.158954-1-jkoolstra@xs4all.nl Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
parent
fd8756fa14
commit
5d513ce19d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
CONFIG_AF_UNIX_OOB=y
|
||||
CONFIG_BPF=y
|
||||
CONFIG_BPF_EVENTS=y
|
||||
CONFIG_BPF_JIT=y
|
||||
CONFIG_BPF_LSM=y
|
||||
CONFIG_BPF_SYSCALL=y
|
||||
CONFIG_DEBUG_INFO_BTF=y
|
||||
CONFIG_SECURITY=y
|
||||
CONFIG_UNIX=y
|
||||
CONFIG_UNIX_DIAG=m
|
||||
CONFIG_USER_NS=y
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
292
tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
Normal file
292
tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
// 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);
|
||||
int ret;
|
||||
|
||||
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));
|
||||
|
||||
ret = sendmsg(sk, &msg, 0);
|
||||
if (ret != 1)
|
||||
return -1;
|
||||
|
||||
return 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;
|
||||
|
||||
ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER]));
|
||||
ASSERT_EQ(0, 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 (int i = 0; i < nr_slots; 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_EQ(0, 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]);
|
||||
for (int i = 1; i < nr_slots; i++) {
|
||||
ASSERT_GE(slots[i], 0);
|
||||
EXPECT_EQ(0, check_secret(slots[i], i));
|
||||
close(slots[i]);
|
||||
}
|
||||
}
|
||||
|
||||
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_EQ(0, 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_slots; 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_EQ(0, 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
|
||||
Loading…
Reference in New Issue
Block a user