Add tests for LSM interactions (both bpf_token_capable and bpf_token_cmd LSM hooks) with BPF token in bpf() subsystem. Now child process passes back token FD for parent to be able to do tests with token originating in "wrong" userns. But we also create token in initns and check that token LSMs don't accidentally reject BPF operations when capable() checks pass without BPF token. Signed-off-by: Andrii Nakryiko <andrii@kernel.org> Signed-off-by: Alexei Starovoitov <ast@kernel.org> Link: https://lore.kernel.org/bpf/20240124022127.2379740-31-andrii@kernel.org
1053 lines
27 KiB
C
1053 lines
27 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/* Copyright (c) 2023 Meta Platforms, Inc. and affiliates. */
|
|
#define _GNU_SOURCE
|
|
#include <test_progs.h>
|
|
#include <bpf/btf.h>
|
|
#include "cap_helpers.h"
|
|
#include <fcntl.h>
|
|
#include <sched.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
#include <linux/filter.h>
|
|
#include <linux/unistd.h>
|
|
#include <linux/mount.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/un.h>
|
|
#include "priv_map.skel.h"
|
|
#include "priv_prog.skel.h"
|
|
#include "dummy_st_ops_success.skel.h"
|
|
#include "token_lsm.skel.h"
|
|
|
|
static inline int sys_mount(const char *dev_name, const char *dir_name,
|
|
const char *type, unsigned long flags,
|
|
const void *data)
|
|
{
|
|
return syscall(__NR_mount, dev_name, dir_name, type, flags, data);
|
|
}
|
|
|
|
static inline int sys_fsopen(const char *fsname, unsigned flags)
|
|
{
|
|
return syscall(__NR_fsopen, fsname, flags);
|
|
}
|
|
|
|
static inline int sys_fspick(int dfd, const char *path, unsigned flags)
|
|
{
|
|
return syscall(__NR_fspick, dfd, path, flags);
|
|
}
|
|
|
|
static inline int sys_fsconfig(int fs_fd, unsigned cmd, const char *key, const void *val, int aux)
|
|
{
|
|
return syscall(__NR_fsconfig, fs_fd, cmd, key, val, aux);
|
|
}
|
|
|
|
static inline int sys_fsmount(int fs_fd, unsigned flags, unsigned ms_flags)
|
|
{
|
|
return syscall(__NR_fsmount, fs_fd, flags, ms_flags);
|
|
}
|
|
|
|
static inline int sys_move_mount(int from_dfd, const char *from_path,
|
|
int to_dfd, const char *to_path,
|
|
unsigned flags)
|
|
{
|
|
return syscall(__NR_move_mount, from_dfd, from_path, to_dfd, to_path, flags);
|
|
}
|
|
|
|
static int drop_priv_caps(__u64 *old_caps)
|
|
{
|
|
return cap_disable_effective((1ULL << CAP_BPF) |
|
|
(1ULL << CAP_PERFMON) |
|
|
(1ULL << CAP_NET_ADMIN) |
|
|
(1ULL << CAP_SYS_ADMIN), old_caps);
|
|
}
|
|
|
|
static int restore_priv_caps(__u64 old_caps)
|
|
{
|
|
return cap_enable_effective(old_caps, NULL);
|
|
}
|
|
|
|
static int set_delegate_mask(int fs_fd, const char *key, __u64 mask, const char *mask_str)
|
|
{
|
|
char buf[32];
|
|
int err;
|
|
|
|
if (!mask_str) {
|
|
if (mask == ~0ULL) {
|
|
mask_str = "any";
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "0x%llx", (unsigned long long)mask);
|
|
mask_str = buf;
|
|
}
|
|
}
|
|
|
|
err = sys_fsconfig(fs_fd, FSCONFIG_SET_STRING, key,
|
|
mask_str, 0);
|
|
if (err < 0)
|
|
err = -errno;
|
|
return err;
|
|
}
|
|
|
|
#define zclose(fd) do { if (fd >= 0) close(fd); fd = -1; } while (0)
|
|
|
|
struct bpffs_opts {
|
|
__u64 cmds;
|
|
__u64 maps;
|
|
__u64 progs;
|
|
__u64 attachs;
|
|
const char *cmds_str;
|
|
const char *maps_str;
|
|
const char *progs_str;
|
|
const char *attachs_str;
|
|
};
|
|
|
|
static int create_bpffs_fd(void)
|
|
{
|
|
int fs_fd;
|
|
|
|
/* create VFS context */
|
|
fs_fd = sys_fsopen("bpf", 0);
|
|
ASSERT_GE(fs_fd, 0, "fs_fd");
|
|
|
|
return fs_fd;
|
|
}
|
|
|
|
static int materialize_bpffs_fd(int fs_fd, struct bpffs_opts *opts)
|
|
{
|
|
int mnt_fd, err;
|
|
|
|
/* set up token delegation mount options */
|
|
err = set_delegate_mask(fs_fd, "delegate_cmds", opts->cmds, opts->cmds_str);
|
|
if (!ASSERT_OK(err, "fs_cfg_cmds"))
|
|
return err;
|
|
err = set_delegate_mask(fs_fd, "delegate_maps", opts->maps, opts->maps_str);
|
|
if (!ASSERT_OK(err, "fs_cfg_maps"))
|
|
return err;
|
|
err = set_delegate_mask(fs_fd, "delegate_progs", opts->progs, opts->progs_str);
|
|
if (!ASSERT_OK(err, "fs_cfg_progs"))
|
|
return err;
|
|
err = set_delegate_mask(fs_fd, "delegate_attachs", opts->attachs, opts->attachs_str);
|
|
if (!ASSERT_OK(err, "fs_cfg_attachs"))
|
|
return err;
|
|
|
|
/* instantiate FS object */
|
|
err = sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
|
|
if (err < 0)
|
|
return -errno;
|
|
|
|
/* create O_PATH fd for detached mount */
|
|
mnt_fd = sys_fsmount(fs_fd, 0, 0);
|
|
if (err < 0)
|
|
return -errno;
|
|
|
|
return mnt_fd;
|
|
}
|
|
|
|
/* send FD over Unix domain (AF_UNIX) socket */
|
|
static int sendfd(int sockfd, int fd)
|
|
{
|
|
struct msghdr msg = {};
|
|
struct cmsghdr *cmsg;
|
|
int fds[1] = { fd }, err;
|
|
char iobuf[1];
|
|
struct iovec io = {
|
|
.iov_base = iobuf,
|
|
.iov_len = sizeof(iobuf),
|
|
};
|
|
union {
|
|
char buf[CMSG_SPACE(sizeof(fds))];
|
|
struct cmsghdr align;
|
|
} u;
|
|
|
|
msg.msg_iov = &io;
|
|
msg.msg_iovlen = 1;
|
|
msg.msg_control = u.buf;
|
|
msg.msg_controllen = sizeof(u.buf);
|
|
cmsg = CMSG_FIRSTHDR(&msg);
|
|
cmsg->cmsg_level = SOL_SOCKET;
|
|
cmsg->cmsg_type = SCM_RIGHTS;
|
|
cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
|
|
memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
|
|
|
|
err = sendmsg(sockfd, &msg, 0);
|
|
if (err < 0)
|
|
err = -errno;
|
|
if (!ASSERT_EQ(err, 1, "sendmsg"))
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* receive FD over Unix domain (AF_UNIX) socket */
|
|
static int recvfd(int sockfd, int *fd)
|
|
{
|
|
struct msghdr msg = {};
|
|
struct cmsghdr *cmsg;
|
|
int fds[1], err;
|
|
char iobuf[1];
|
|
struct iovec io = {
|
|
.iov_base = iobuf,
|
|
.iov_len = sizeof(iobuf),
|
|
};
|
|
union {
|
|
char buf[CMSG_SPACE(sizeof(fds))];
|
|
struct cmsghdr align;
|
|
} u;
|
|
|
|
msg.msg_iov = &io;
|
|
msg.msg_iovlen = 1;
|
|
msg.msg_control = u.buf;
|
|
msg.msg_controllen = sizeof(u.buf);
|
|
|
|
err = recvmsg(sockfd, &msg, 0);
|
|
if (err < 0)
|
|
err = -errno;
|
|
if (!ASSERT_EQ(err, 1, "recvmsg"))
|
|
return -EINVAL;
|
|
|
|
cmsg = CMSG_FIRSTHDR(&msg);
|
|
if (!ASSERT_OK_PTR(cmsg, "cmsg_null") ||
|
|
!ASSERT_EQ(cmsg->cmsg_len, CMSG_LEN(sizeof(fds)), "cmsg_len") ||
|
|
!ASSERT_EQ(cmsg->cmsg_level, SOL_SOCKET, "cmsg_level") ||
|
|
!ASSERT_EQ(cmsg->cmsg_type, SCM_RIGHTS, "cmsg_type"))
|
|
return -EINVAL;
|
|
|
|
memcpy(fds, CMSG_DATA(cmsg), sizeof(fds));
|
|
*fd = fds[0];
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t write_nointr(int fd, const void *buf, size_t count)
|
|
{
|
|
ssize_t ret;
|
|
|
|
do {
|
|
ret = write(fd, buf, count);
|
|
} while (ret < 0 && errno == EINTR);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int write_file(const char *path, const void *buf, size_t count)
|
|
{
|
|
int fd;
|
|
ssize_t ret;
|
|
|
|
fd = open(path, O_WRONLY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW);
|
|
if (fd < 0)
|
|
return -1;
|
|
|
|
ret = write_nointr(fd, buf, count);
|
|
close(fd);
|
|
if (ret < 0 || (size_t)ret != count)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int create_and_enter_userns(void)
|
|
{
|
|
uid_t uid;
|
|
gid_t gid;
|
|
char map[100];
|
|
|
|
uid = getuid();
|
|
gid = getgid();
|
|
|
|
if (unshare(CLONE_NEWUSER))
|
|
return -1;
|
|
|
|
if (write_file("/proc/self/setgroups", "deny", sizeof("deny") - 1) &&
|
|
errno != ENOENT)
|
|
return -1;
|
|
|
|
snprintf(map, sizeof(map), "0 %d 1", uid);
|
|
if (write_file("/proc/self/uid_map", map, strlen(map)))
|
|
return -1;
|
|
|
|
|
|
snprintf(map, sizeof(map), "0 %d 1", gid);
|
|
if (write_file("/proc/self/gid_map", map, strlen(map)))
|
|
return -1;
|
|
|
|
if (setgid(0))
|
|
return -1;
|
|
|
|
if (setuid(0))
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
typedef int (*child_callback_fn)(int bpffs_fd, struct token_lsm *lsm_skel);
|
|
|
|
static void child(int sock_fd, struct bpffs_opts *opts, child_callback_fn callback)
|
|
{
|
|
int mnt_fd = -1, fs_fd = -1, err = 0, bpffs_fd = -1, token_fd = -1;
|
|
struct token_lsm *lsm_skel = NULL;
|
|
|
|
/* load and attach LSM "policy" before we go into unpriv userns */
|
|
lsm_skel = token_lsm__open_and_load();
|
|
if (!ASSERT_OK_PTR(lsm_skel, "lsm_skel_load")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
lsm_skel->bss->my_pid = getpid();
|
|
err = token_lsm__attach(lsm_skel);
|
|
if (!ASSERT_OK(err, "lsm_skel_attach"))
|
|
goto cleanup;
|
|
|
|
/* setup userns with root mappings */
|
|
err = create_and_enter_userns();
|
|
if (!ASSERT_OK(err, "create_and_enter_userns"))
|
|
goto cleanup;
|
|
|
|
/* setup mountns to allow creating BPF FS (fsopen("bpf")) from unpriv process */
|
|
err = unshare(CLONE_NEWNS);
|
|
if (!ASSERT_OK(err, "create_mountns"))
|
|
goto cleanup;
|
|
|
|
err = sys_mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 0);
|
|
if (!ASSERT_OK(err, "remount_root"))
|
|
goto cleanup;
|
|
|
|
fs_fd = create_bpffs_fd();
|
|
if (!ASSERT_GE(fs_fd, 0, "create_bpffs_fd")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* ensure unprivileged child cannot set delegation options */
|
|
err = set_delegate_mask(fs_fd, "delegate_cmds", 0x1, NULL);
|
|
ASSERT_EQ(err, -EPERM, "delegate_cmd_eperm");
|
|
err = set_delegate_mask(fs_fd, "delegate_maps", 0x1, NULL);
|
|
ASSERT_EQ(err, -EPERM, "delegate_maps_eperm");
|
|
err = set_delegate_mask(fs_fd, "delegate_progs", 0x1, NULL);
|
|
ASSERT_EQ(err, -EPERM, "delegate_progs_eperm");
|
|
err = set_delegate_mask(fs_fd, "delegate_attachs", 0x1, NULL);
|
|
ASSERT_EQ(err, -EPERM, "delegate_attachs_eperm");
|
|
|
|
/* pass BPF FS context object to parent */
|
|
err = sendfd(sock_fd, fs_fd);
|
|
if (!ASSERT_OK(err, "send_fs_fd"))
|
|
goto cleanup;
|
|
zclose(fs_fd);
|
|
|
|
/* avoid mucking around with mount namespaces and mounting at
|
|
* well-known path, just get detach-mounted BPF FS fd back from parent
|
|
*/
|
|
err = recvfd(sock_fd, &mnt_fd);
|
|
if (!ASSERT_OK(err, "recv_mnt_fd"))
|
|
goto cleanup;
|
|
|
|
/* try to fspick() BPF FS and try to add some delegation options */
|
|
fs_fd = sys_fspick(mnt_fd, "", FSPICK_EMPTY_PATH);
|
|
if (!ASSERT_GE(fs_fd, 0, "bpffs_fspick")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* ensure unprivileged child cannot reconfigure to set delegation options */
|
|
err = set_delegate_mask(fs_fd, "delegate_cmds", 0, "any");
|
|
if (!ASSERT_EQ(err, -EPERM, "delegate_cmd_eperm_reconfig")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
err = set_delegate_mask(fs_fd, "delegate_maps", 0, "any");
|
|
if (!ASSERT_EQ(err, -EPERM, "delegate_maps_eperm_reconfig")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
err = set_delegate_mask(fs_fd, "delegate_progs", 0, "any");
|
|
if (!ASSERT_EQ(err, -EPERM, "delegate_progs_eperm_reconfig")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
err = set_delegate_mask(fs_fd, "delegate_attachs", 0, "any");
|
|
if (!ASSERT_EQ(err, -EPERM, "delegate_attachs_eperm_reconfig")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
zclose(fs_fd);
|
|
|
|
bpffs_fd = openat(mnt_fd, ".", 0, O_RDWR);
|
|
if (!ASSERT_GE(bpffs_fd, 0, "bpffs_open")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* create BPF token FD and pass it to parent for some extra checks */
|
|
token_fd = bpf_token_create(bpffs_fd, NULL);
|
|
if (!ASSERT_GT(token_fd, 0, "child_token_create")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
err = sendfd(sock_fd, token_fd);
|
|
if (!ASSERT_OK(err, "send_token_fd"))
|
|
goto cleanup;
|
|
zclose(token_fd);
|
|
|
|
/* do custom test logic with customly set up BPF FS instance */
|
|
err = callback(bpffs_fd, lsm_skel);
|
|
if (!ASSERT_OK(err, "test_callback"))
|
|
goto cleanup;
|
|
|
|
err = 0;
|
|
cleanup:
|
|
zclose(sock_fd);
|
|
zclose(mnt_fd);
|
|
zclose(fs_fd);
|
|
zclose(bpffs_fd);
|
|
zclose(token_fd);
|
|
|
|
lsm_skel->bss->my_pid = 0;
|
|
token_lsm__destroy(lsm_skel);
|
|
|
|
exit(-err);
|
|
}
|
|
|
|
static int wait_for_pid(pid_t pid)
|
|
{
|
|
int status, ret;
|
|
|
|
again:
|
|
ret = waitpid(pid, &status, 0);
|
|
if (ret == -1) {
|
|
if (errno == EINTR)
|
|
goto again;
|
|
|
|
return -1;
|
|
}
|
|
|
|
if (!WIFEXITED(status))
|
|
return -1;
|
|
|
|
return WEXITSTATUS(status);
|
|
}
|
|
|
|
static void parent(int child_pid, struct bpffs_opts *bpffs_opts, int sock_fd)
|
|
{
|
|
int fs_fd = -1, mnt_fd = -1, token_fd = -1, err;
|
|
|
|
err = recvfd(sock_fd, &fs_fd);
|
|
if (!ASSERT_OK(err, "recv_bpffs_fd"))
|
|
goto cleanup;
|
|
|
|
mnt_fd = materialize_bpffs_fd(fs_fd, bpffs_opts);
|
|
if (!ASSERT_GE(mnt_fd, 0, "materialize_bpffs_fd")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
zclose(fs_fd);
|
|
|
|
/* pass BPF FS context object to parent */
|
|
err = sendfd(sock_fd, mnt_fd);
|
|
if (!ASSERT_OK(err, "send_mnt_fd"))
|
|
goto cleanup;
|
|
zclose(mnt_fd);
|
|
|
|
/* receive BPF token FD back from child for some extra tests */
|
|
err = recvfd(sock_fd, &token_fd);
|
|
if (!ASSERT_OK(err, "recv_token_fd"))
|
|
goto cleanup;
|
|
|
|
err = wait_for_pid(child_pid);
|
|
ASSERT_OK(err, "waitpid_child");
|
|
|
|
cleanup:
|
|
zclose(sock_fd);
|
|
zclose(fs_fd);
|
|
zclose(mnt_fd);
|
|
zclose(token_fd);
|
|
|
|
if (child_pid > 0)
|
|
(void)kill(child_pid, SIGKILL);
|
|
}
|
|
|
|
static void subtest_userns(struct bpffs_opts *bpffs_opts,
|
|
child_callback_fn child_cb)
|
|
{
|
|
int sock_fds[2] = { -1, -1 };
|
|
int child_pid = 0, err;
|
|
|
|
err = socketpair(AF_UNIX, SOCK_STREAM, 0, sock_fds);
|
|
if (!ASSERT_OK(err, "socketpair"))
|
|
goto cleanup;
|
|
|
|
child_pid = fork();
|
|
if (!ASSERT_GE(child_pid, 0, "fork"))
|
|
goto cleanup;
|
|
|
|
if (child_pid == 0) {
|
|
zclose(sock_fds[0]);
|
|
return child(sock_fds[1], bpffs_opts, child_cb);
|
|
|
|
} else {
|
|
zclose(sock_fds[1]);
|
|
return parent(child_pid, bpffs_opts, sock_fds[0]);
|
|
}
|
|
|
|
cleanup:
|
|
zclose(sock_fds[0]);
|
|
zclose(sock_fds[1]);
|
|
if (child_pid > 0)
|
|
(void)kill(child_pid, SIGKILL);
|
|
}
|
|
|
|
static int userns_map_create(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
LIBBPF_OPTS(bpf_map_create_opts, map_opts);
|
|
int err, token_fd = -1, map_fd = -1;
|
|
__u64 old_caps = 0;
|
|
|
|
/* create BPF token from BPF FS mount */
|
|
token_fd = bpf_token_create(mnt_fd, NULL);
|
|
if (!ASSERT_GT(token_fd, 0, "token_create")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* while inside non-init userns, we need both a BPF token *and*
|
|
* CAP_BPF inside current userns to create privileged map; let's test
|
|
* that neither BPF token alone nor namespaced CAP_BPF is sufficient
|
|
*/
|
|
err = drop_priv_caps(&old_caps);
|
|
if (!ASSERT_OK(err, "drop_caps"))
|
|
goto cleanup;
|
|
|
|
/* no token, no CAP_BPF -> fail */
|
|
map_opts.map_flags = 0;
|
|
map_opts.token_fd = 0;
|
|
map_fd = bpf_map_create(BPF_MAP_TYPE_STACK, "wo_token_wo_bpf", 0, 8, 1, &map_opts);
|
|
if (!ASSERT_LT(map_fd, 0, "stack_map_wo_token_wo_cap_bpf_should_fail")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* token without CAP_BPF -> fail */
|
|
map_opts.map_flags = BPF_F_TOKEN_FD;
|
|
map_opts.token_fd = token_fd;
|
|
map_fd = bpf_map_create(BPF_MAP_TYPE_STACK, "w_token_wo_bpf", 0, 8, 1, &map_opts);
|
|
if (!ASSERT_LT(map_fd, 0, "stack_map_w_token_wo_cap_bpf_should_fail")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* get back effective local CAP_BPF (and CAP_SYS_ADMIN) */
|
|
err = restore_priv_caps(old_caps);
|
|
if (!ASSERT_OK(err, "restore_caps"))
|
|
goto cleanup;
|
|
|
|
/* CAP_BPF without token -> fail */
|
|
map_opts.map_flags = 0;
|
|
map_opts.token_fd = 0;
|
|
map_fd = bpf_map_create(BPF_MAP_TYPE_STACK, "wo_token_w_bpf", 0, 8, 1, &map_opts);
|
|
if (!ASSERT_LT(map_fd, 0, "stack_map_wo_token_w_cap_bpf_should_fail")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* finally, namespaced CAP_BPF + token -> success */
|
|
map_opts.map_flags = BPF_F_TOKEN_FD;
|
|
map_opts.token_fd = token_fd;
|
|
map_fd = bpf_map_create(BPF_MAP_TYPE_STACK, "w_token_w_bpf", 0, 8, 1, &map_opts);
|
|
if (!ASSERT_GT(map_fd, 0, "stack_map_w_token_w_cap_bpf")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
cleanup:
|
|
zclose(token_fd);
|
|
zclose(map_fd);
|
|
return err;
|
|
}
|
|
|
|
static int userns_btf_load(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
LIBBPF_OPTS(bpf_btf_load_opts, btf_opts);
|
|
int err, token_fd = -1, btf_fd = -1;
|
|
const void *raw_btf_data;
|
|
struct btf *btf = NULL;
|
|
__u32 raw_btf_size;
|
|
__u64 old_caps = 0;
|
|
|
|
/* create BPF token from BPF FS mount */
|
|
token_fd = bpf_token_create(mnt_fd, NULL);
|
|
if (!ASSERT_GT(token_fd, 0, "token_create")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* while inside non-init userns, we need both a BPF token *and*
|
|
* CAP_BPF inside current userns to create privileged map; let's test
|
|
* that neither BPF token alone nor namespaced CAP_BPF is sufficient
|
|
*/
|
|
err = drop_priv_caps(&old_caps);
|
|
if (!ASSERT_OK(err, "drop_caps"))
|
|
goto cleanup;
|
|
|
|
/* setup a trivial BTF data to load to the kernel */
|
|
btf = btf__new_empty();
|
|
if (!ASSERT_OK_PTR(btf, "empty_btf"))
|
|
goto cleanup;
|
|
|
|
ASSERT_GT(btf__add_int(btf, "int", 4, 0), 0, "int_type");
|
|
|
|
raw_btf_data = btf__raw_data(btf, &raw_btf_size);
|
|
if (!ASSERT_OK_PTR(raw_btf_data, "raw_btf_data"))
|
|
goto cleanup;
|
|
|
|
/* no token + no CAP_BPF -> failure */
|
|
btf_opts.btf_flags = 0;
|
|
btf_opts.token_fd = 0;
|
|
btf_fd = bpf_btf_load(raw_btf_data, raw_btf_size, &btf_opts);
|
|
if (!ASSERT_LT(btf_fd, 0, "no_token_no_cap_should_fail"))
|
|
goto cleanup;
|
|
|
|
/* token + no CAP_BPF -> failure */
|
|
btf_opts.btf_flags = BPF_F_TOKEN_FD;
|
|
btf_opts.token_fd = token_fd;
|
|
btf_fd = bpf_btf_load(raw_btf_data, raw_btf_size, &btf_opts);
|
|
if (!ASSERT_LT(btf_fd, 0, "token_no_cap_should_fail"))
|
|
goto cleanup;
|
|
|
|
/* get back effective local CAP_BPF (and CAP_SYS_ADMIN) */
|
|
err = restore_priv_caps(old_caps);
|
|
if (!ASSERT_OK(err, "restore_caps"))
|
|
goto cleanup;
|
|
|
|
/* token + CAP_BPF -> success */
|
|
btf_opts.btf_flags = BPF_F_TOKEN_FD;
|
|
btf_opts.token_fd = token_fd;
|
|
btf_fd = bpf_btf_load(raw_btf_data, raw_btf_size, &btf_opts);
|
|
if (!ASSERT_GT(btf_fd, 0, "token_and_cap_success"))
|
|
goto cleanup;
|
|
|
|
err = 0;
|
|
cleanup:
|
|
btf__free(btf);
|
|
zclose(btf_fd);
|
|
zclose(token_fd);
|
|
return err;
|
|
}
|
|
|
|
static int userns_prog_load(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
LIBBPF_OPTS(bpf_prog_load_opts, prog_opts);
|
|
int err, token_fd = -1, prog_fd = -1;
|
|
struct bpf_insn insns[] = {
|
|
/* bpf_jiffies64() requires CAP_BPF */
|
|
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_jiffies64),
|
|
/* bpf_get_current_task() requires CAP_PERFMON */
|
|
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_get_current_task),
|
|
/* r0 = 0; exit; */
|
|
BPF_MOV64_IMM(BPF_REG_0, 0),
|
|
BPF_EXIT_INSN(),
|
|
};
|
|
size_t insn_cnt = ARRAY_SIZE(insns);
|
|
__u64 old_caps = 0;
|
|
|
|
/* create BPF token from BPF FS mount */
|
|
token_fd = bpf_token_create(mnt_fd, NULL);
|
|
if (!ASSERT_GT(token_fd, 0, "token_create")) {
|
|
err = -EINVAL;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* validate we can successfully load BPF program with token; this
|
|
* being XDP program (CAP_NET_ADMIN) using bpf_jiffies64() (CAP_BPF)
|
|
* and bpf_get_current_task() (CAP_PERFMON) helpers validates we have
|
|
* BPF token wired properly in a bunch of places in the kernel
|
|
*/
|
|
prog_opts.prog_flags = BPF_F_TOKEN_FD;
|
|
prog_opts.token_fd = token_fd;
|
|
prog_opts.expected_attach_type = BPF_XDP;
|
|
prog_fd = bpf_prog_load(BPF_PROG_TYPE_XDP, "token_prog", "GPL",
|
|
insns, insn_cnt, &prog_opts);
|
|
if (!ASSERT_GT(prog_fd, 0, "prog_fd")) {
|
|
err = -EPERM;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* no token + caps -> failure */
|
|
prog_opts.prog_flags = 0;
|
|
prog_opts.token_fd = 0;
|
|
prog_fd = bpf_prog_load(BPF_PROG_TYPE_XDP, "token_prog", "GPL",
|
|
insns, insn_cnt, &prog_opts);
|
|
if (!ASSERT_EQ(prog_fd, -EPERM, "prog_fd_eperm")) {
|
|
err = -EPERM;
|
|
goto cleanup;
|
|
}
|
|
|
|
err = drop_priv_caps(&old_caps);
|
|
if (!ASSERT_OK(err, "drop_caps"))
|
|
goto cleanup;
|
|
|
|
/* no caps + token -> failure */
|
|
prog_opts.prog_flags = BPF_F_TOKEN_FD;
|
|
prog_opts.token_fd = token_fd;
|
|
prog_fd = bpf_prog_load(BPF_PROG_TYPE_XDP, "token_prog", "GPL",
|
|
insns, insn_cnt, &prog_opts);
|
|
if (!ASSERT_EQ(prog_fd, -EPERM, "prog_fd_eperm")) {
|
|
err = -EPERM;
|
|
goto cleanup;
|
|
}
|
|
|
|
/* no caps + no token -> definitely a failure */
|
|
prog_opts.prog_flags = 0;
|
|
prog_opts.token_fd = 0;
|
|
prog_fd = bpf_prog_load(BPF_PROG_TYPE_XDP, "token_prog", "GPL",
|
|
insns, insn_cnt, &prog_opts);
|
|
if (!ASSERT_EQ(prog_fd, -EPERM, "prog_fd_eperm")) {
|
|
err = -EPERM;
|
|
goto cleanup;
|
|
}
|
|
|
|
err = 0;
|
|
cleanup:
|
|
zclose(prog_fd);
|
|
zclose(token_fd);
|
|
return err;
|
|
}
|
|
|
|
static int userns_obj_priv_map(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
LIBBPF_OPTS(bpf_object_open_opts, opts);
|
|
char buf[256];
|
|
struct priv_map *skel;
|
|
int err;
|
|
|
|
skel = priv_map__open_and_load();
|
|
if (!ASSERT_ERR_PTR(skel, "obj_tokenless_load")) {
|
|
priv_map__destroy(skel);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* use bpf_token_path to provide BPF FS path */
|
|
snprintf(buf, sizeof(buf), "/proc/self/fd/%d", mnt_fd);
|
|
opts.bpf_token_path = buf;
|
|
skel = priv_map__open_opts(&opts);
|
|
if (!ASSERT_OK_PTR(skel, "obj_token_path_open"))
|
|
return -EINVAL;
|
|
|
|
err = priv_map__load(skel);
|
|
priv_map__destroy(skel);
|
|
if (!ASSERT_OK(err, "obj_token_path_load"))
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int userns_obj_priv_prog(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
LIBBPF_OPTS(bpf_object_open_opts, opts);
|
|
char buf[256];
|
|
struct priv_prog *skel;
|
|
int err;
|
|
|
|
skel = priv_prog__open_and_load();
|
|
if (!ASSERT_ERR_PTR(skel, "obj_tokenless_load")) {
|
|
priv_prog__destroy(skel);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* use bpf_token_path to provide BPF FS path */
|
|
snprintf(buf, sizeof(buf), "/proc/self/fd/%d", mnt_fd);
|
|
opts.bpf_token_path = buf;
|
|
skel = priv_prog__open_opts(&opts);
|
|
if (!ASSERT_OK_PTR(skel, "obj_token_path_open"))
|
|
return -EINVAL;
|
|
err = priv_prog__load(skel);
|
|
priv_prog__destroy(skel);
|
|
if (!ASSERT_OK(err, "obj_token_path_load"))
|
|
return -EINVAL;
|
|
|
|
/* provide BPF token, but reject bpf_token_capable() with LSM */
|
|
lsm_skel->bss->reject_capable = true;
|
|
lsm_skel->bss->reject_cmd = false;
|
|
skel = priv_prog__open_opts(&opts);
|
|
if (!ASSERT_OK_PTR(skel, "obj_token_lsm_reject_cap_open"))
|
|
return -EINVAL;
|
|
err = priv_prog__load(skel);
|
|
priv_prog__destroy(skel);
|
|
if (!ASSERT_ERR(err, "obj_token_lsm_reject_cap_load"))
|
|
return -EINVAL;
|
|
|
|
/* provide BPF token, but reject bpf_token_cmd() with LSM */
|
|
lsm_skel->bss->reject_capable = false;
|
|
lsm_skel->bss->reject_cmd = true;
|
|
skel = priv_prog__open_opts(&opts);
|
|
if (!ASSERT_OK_PTR(skel, "obj_token_lsm_reject_cmd_open"))
|
|
return -EINVAL;
|
|
err = priv_prog__load(skel);
|
|
priv_prog__destroy(skel);
|
|
if (!ASSERT_ERR(err, "obj_token_lsm_reject_cmd_load"))
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* this test is called with BPF FS that doesn't delegate BPF_BTF_LOAD command,
|
|
* which should cause struct_ops application to fail, as BTF won't be uploaded
|
|
* into the kernel, even if STRUCT_OPS programs themselves are allowed
|
|
*/
|
|
static int validate_struct_ops_load(int mnt_fd, bool expect_success)
|
|
{
|
|
LIBBPF_OPTS(bpf_object_open_opts, opts);
|
|
char buf[256];
|
|
struct dummy_st_ops_success *skel;
|
|
int err;
|
|
|
|
snprintf(buf, sizeof(buf), "/proc/self/fd/%d", mnt_fd);
|
|
opts.bpf_token_path = buf;
|
|
skel = dummy_st_ops_success__open_opts(&opts);
|
|
if (!ASSERT_OK_PTR(skel, "obj_token_path_open"))
|
|
return -EINVAL;
|
|
|
|
err = dummy_st_ops_success__load(skel);
|
|
dummy_st_ops_success__destroy(skel);
|
|
if (expect_success) {
|
|
if (!ASSERT_OK(err, "obj_token_path_load"))
|
|
return -EINVAL;
|
|
} else /* expect failure */ {
|
|
if (!ASSERT_ERR(err, "obj_token_path_load"))
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int userns_obj_priv_btf_fail(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
return validate_struct_ops_load(mnt_fd, false /* should fail */);
|
|
}
|
|
|
|
static int userns_obj_priv_btf_success(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
return validate_struct_ops_load(mnt_fd, true /* should succeed */);
|
|
}
|
|
|
|
#define TOKEN_ENVVAR "LIBBPF_BPF_TOKEN_PATH"
|
|
#define TOKEN_BPFFS_CUSTOM "/bpf-token-fs"
|
|
|
|
static int userns_obj_priv_implicit_token(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
LIBBPF_OPTS(bpf_object_open_opts, opts);
|
|
struct dummy_st_ops_success *skel;
|
|
int err;
|
|
|
|
/* before we mount BPF FS with token delegation, struct_ops skeleton
|
|
* should fail to load
|
|
*/
|
|
skel = dummy_st_ops_success__open_and_load();
|
|
if (!ASSERT_ERR_PTR(skel, "obj_tokenless_load")) {
|
|
dummy_st_ops_success__destroy(skel);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* mount custom BPF FS over /sys/fs/bpf so that libbpf can create BPF
|
|
* token automatically and implicitly
|
|
*/
|
|
err = sys_move_mount(mnt_fd, "", AT_FDCWD, "/sys/fs/bpf", MOVE_MOUNT_F_EMPTY_PATH);
|
|
if (!ASSERT_OK(err, "move_mount_bpffs"))
|
|
return -EINVAL;
|
|
|
|
/* disable implicit BPF token creation by setting
|
|
* LIBBPF_BPF_TOKEN_PATH envvar to empty value, load should fail
|
|
*/
|
|
err = setenv(TOKEN_ENVVAR, "", 1 /*overwrite*/);
|
|
if (!ASSERT_OK(err, "setenv_token_path"))
|
|
return -EINVAL;
|
|
skel = dummy_st_ops_success__open_and_load();
|
|
if (!ASSERT_ERR_PTR(skel, "obj_token_envvar_disabled_load")) {
|
|
unsetenv(TOKEN_ENVVAR);
|
|
dummy_st_ops_success__destroy(skel);
|
|
return -EINVAL;
|
|
}
|
|
unsetenv(TOKEN_ENVVAR);
|
|
|
|
/* now the same struct_ops skeleton should succeed thanks to libppf
|
|
* creating BPF token from /sys/fs/bpf mount point
|
|
*/
|
|
skel = dummy_st_ops_success__open_and_load();
|
|
if (!ASSERT_OK_PTR(skel, "obj_implicit_token_load"))
|
|
return -EINVAL;
|
|
|
|
dummy_st_ops_success__destroy(skel);
|
|
|
|
/* now disable implicit token through empty bpf_token_path, should fail */
|
|
opts.bpf_token_path = "";
|
|
skel = dummy_st_ops_success__open_opts(&opts);
|
|
if (!ASSERT_OK_PTR(skel, "obj_empty_token_path_open"))
|
|
return -EINVAL;
|
|
|
|
err = dummy_st_ops_success__load(skel);
|
|
dummy_st_ops_success__destroy(skel);
|
|
if (!ASSERT_ERR(err, "obj_empty_token_path_load"))
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int userns_obj_priv_implicit_token_envvar(int mnt_fd, struct token_lsm *lsm_skel)
|
|
{
|
|
LIBBPF_OPTS(bpf_object_open_opts, opts);
|
|
struct dummy_st_ops_success *skel;
|
|
int err;
|
|
|
|
/* before we mount BPF FS with token delegation, struct_ops skeleton
|
|
* should fail to load
|
|
*/
|
|
skel = dummy_st_ops_success__open_and_load();
|
|
if (!ASSERT_ERR_PTR(skel, "obj_tokenless_load")) {
|
|
dummy_st_ops_success__destroy(skel);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* mount custom BPF FS over custom location, so libbpf can't create
|
|
* BPF token implicitly, unless pointed to it through
|
|
* LIBBPF_BPF_TOKEN_PATH envvar
|
|
*/
|
|
rmdir(TOKEN_BPFFS_CUSTOM);
|
|
if (!ASSERT_OK(mkdir(TOKEN_BPFFS_CUSTOM, 0777), "mkdir_bpffs_custom"))
|
|
goto err_out;
|
|
err = sys_move_mount(mnt_fd, "", AT_FDCWD, TOKEN_BPFFS_CUSTOM, MOVE_MOUNT_F_EMPTY_PATH);
|
|
if (!ASSERT_OK(err, "move_mount_bpffs"))
|
|
goto err_out;
|
|
|
|
/* even though we have BPF FS with delegation, it's not at default
|
|
* /sys/fs/bpf location, so we still fail to load until envvar is set up
|
|
*/
|
|
skel = dummy_st_ops_success__open_and_load();
|
|
if (!ASSERT_ERR_PTR(skel, "obj_tokenless_load2")) {
|
|
dummy_st_ops_success__destroy(skel);
|
|
goto err_out;
|
|
}
|
|
|
|
err = setenv(TOKEN_ENVVAR, TOKEN_BPFFS_CUSTOM, 1 /*overwrite*/);
|
|
if (!ASSERT_OK(err, "setenv_token_path"))
|
|
goto err_out;
|
|
|
|
/* now the same struct_ops skeleton should succeed thanks to libppf
|
|
* creating BPF token from custom mount point
|
|
*/
|
|
skel = dummy_st_ops_success__open_and_load();
|
|
if (!ASSERT_OK_PTR(skel, "obj_implicit_token_load"))
|
|
goto err_out;
|
|
|
|
dummy_st_ops_success__destroy(skel);
|
|
|
|
/* now disable implicit token through empty bpf_token_path, envvar
|
|
* will be ignored, should fail
|
|
*/
|
|
opts.bpf_token_path = "";
|
|
skel = dummy_st_ops_success__open_opts(&opts);
|
|
if (!ASSERT_OK_PTR(skel, "obj_empty_token_path_open"))
|
|
goto err_out;
|
|
|
|
err = dummy_st_ops_success__load(skel);
|
|
dummy_st_ops_success__destroy(skel);
|
|
if (!ASSERT_ERR(err, "obj_empty_token_path_load"))
|
|
goto err_out;
|
|
|
|
rmdir(TOKEN_BPFFS_CUSTOM);
|
|
unsetenv(TOKEN_ENVVAR);
|
|
return 0;
|
|
err_out:
|
|
rmdir(TOKEN_BPFFS_CUSTOM);
|
|
unsetenv(TOKEN_ENVVAR);
|
|
return -EINVAL;
|
|
}
|
|
|
|
#define bit(n) (1ULL << (n))
|
|
|
|
void test_token(void)
|
|
{
|
|
if (test__start_subtest("map_token")) {
|
|
struct bpffs_opts opts = {
|
|
.cmds_str = "map_create",
|
|
.maps_str = "stack",
|
|
};
|
|
|
|
subtest_userns(&opts, userns_map_create);
|
|
}
|
|
if (test__start_subtest("btf_token")) {
|
|
struct bpffs_opts opts = {
|
|
.cmds = 1ULL << BPF_BTF_LOAD,
|
|
};
|
|
|
|
subtest_userns(&opts, userns_btf_load);
|
|
}
|
|
if (test__start_subtest("prog_token")) {
|
|
struct bpffs_opts opts = {
|
|
.cmds_str = "PROG_LOAD",
|
|
.progs_str = "XDP",
|
|
.attachs_str = "xdp",
|
|
};
|
|
|
|
subtest_userns(&opts, userns_prog_load);
|
|
}
|
|
if (test__start_subtest("obj_priv_map")) {
|
|
struct bpffs_opts opts = {
|
|
.cmds = bit(BPF_MAP_CREATE),
|
|
.maps = bit(BPF_MAP_TYPE_QUEUE),
|
|
};
|
|
|
|
subtest_userns(&opts, userns_obj_priv_map);
|
|
}
|
|
if (test__start_subtest("obj_priv_prog")) {
|
|
struct bpffs_opts opts = {
|
|
.cmds = bit(BPF_PROG_LOAD),
|
|
.progs = bit(BPF_PROG_TYPE_KPROBE),
|
|
.attachs = ~0ULL,
|
|
};
|
|
|
|
subtest_userns(&opts, userns_obj_priv_prog);
|
|
}
|
|
if (test__start_subtest("obj_priv_btf_fail")) {
|
|
struct bpffs_opts opts = {
|
|
/* disallow BTF loading */
|
|
.cmds = bit(BPF_MAP_CREATE) | bit(BPF_PROG_LOAD),
|
|
.maps = bit(BPF_MAP_TYPE_STRUCT_OPS),
|
|
.progs = bit(BPF_PROG_TYPE_STRUCT_OPS),
|
|
.attachs = ~0ULL,
|
|
};
|
|
|
|
subtest_userns(&opts, userns_obj_priv_btf_fail);
|
|
}
|
|
if (test__start_subtest("obj_priv_btf_success")) {
|
|
struct bpffs_opts opts = {
|
|
/* allow BTF loading */
|
|
.cmds = bit(BPF_BTF_LOAD) | bit(BPF_MAP_CREATE) | bit(BPF_PROG_LOAD),
|
|
.maps = bit(BPF_MAP_TYPE_STRUCT_OPS),
|
|
.progs = bit(BPF_PROG_TYPE_STRUCT_OPS),
|
|
.attachs = ~0ULL,
|
|
};
|
|
|
|
subtest_userns(&opts, userns_obj_priv_btf_success);
|
|
}
|
|
if (test__start_subtest("obj_priv_implicit_token")) {
|
|
struct bpffs_opts opts = {
|
|
/* allow BTF loading */
|
|
.cmds = bit(BPF_BTF_LOAD) | bit(BPF_MAP_CREATE) | bit(BPF_PROG_LOAD),
|
|
.maps = bit(BPF_MAP_TYPE_STRUCT_OPS),
|
|
.progs = bit(BPF_PROG_TYPE_STRUCT_OPS),
|
|
.attachs = ~0ULL,
|
|
};
|
|
|
|
subtest_userns(&opts, userns_obj_priv_implicit_token);
|
|
}
|
|
if (test__start_subtest("obj_priv_implicit_token_envvar")) {
|
|
struct bpffs_opts opts = {
|
|
/* allow BTF loading */
|
|
.cmds = bit(BPF_BTF_LOAD) | bit(BPF_MAP_CREATE) | bit(BPF_PROG_LOAD),
|
|
.maps = bit(BPF_MAP_TYPE_STRUCT_OPS),
|
|
.progs = bit(BPF_PROG_TYPE_STRUCT_OPS),
|
|
.attachs = ~0ULL,
|
|
};
|
|
|
|
subtest_userns(&opts, userns_obj_priv_implicit_token_envvar);
|
|
}
|
|
}
|