mirror of
https://github.com/systemd/systemd.git
synced 2026-11-06 05:17:24 +00:00
nsresourced: add an inode-based BPF policy
The path policy rejects persistent OverlayFS uppers even for clients whose identity mappings record only stable IDs. Let's choose inode hooks when the kernel passes them mount idmaps (Linux 7.4 and newer), so we can check ownership on the actual upper filesystem for each operation. We keep the path policy for kernels without those signatures. Let's preserve the policy maps and retire the old path attachments only after the inode policy is active. We classify mapped IDs rather than the caller's IDs, since a stable ID can map into a transient range. PR #44620 restricts O_TMPFILE, RENAME_WHITEOUT and Btrfs subvolume creation outside the path creation hooks. Let's use the inode policy's ownership checks in those hooks too. We can't use inode_rename() for whiteouts, because it gets neither the rename flags nor the mount idmap. A link adds a name for an existing inode, so the inode policy checks the owner of that inode instead of the caller's fsuid. Otherwise a client can switch its fsuid to another mapped ID and link a file owned by its transient ID onto persistent storage. nsresourced reuses the program links it pinned earlier, so a restart after an upgrade keeps the old programs attached until the next reboot. Let's attach replacement programs before publishing their pins together, so a failed upgrade leaves the previous policy attached. The regression tests cover identity and idmapped OverlayFS uppers, unsafe work directories, unnamed files, whiteouts, links, ownership changes, POSIX queue lifetimes and failed pin replacement. Like nsresourced, the tests remove the map entries of a namespace when they're done with it. Otherwise a later namespace can reuse the inode number and match a stale entry.
This commit is contained in:
1 parent
4b10f0a649
commit
5163ed26cc
6 files changed
+645
-205
No files matched your search
@@ -94,6 +94,11 @@ REQUIREMENTS:
|
||||
✅ systemd utilizes several new kernel APIs, but will fall back gracefully
|
||||
when unavailable.
|
||||
|
||||
systemd-nsresourced uses inode LSM hooks when the kernel passes mount
|
||||
idmaps to them. On kernels without this support, it uses path hooks and
|
||||
refuses OverlayFS uppers that can record transient user or group IDs on
|
||||
file systems that outlive the user namespace.
|
||||
|
||||
Kernel Config Options:
|
||||
CONFIG_DEVTMPFS
|
||||
CONFIG_CGROUPS (it is OK to disable all controllers)
|
||||
|
||||
+178
-90
@@ -35,8 +35,7 @@ void* bpf_rdonly_cast(const void *, __u32) __ksym;
|
||||
* nsfs) from leaving objects owned by their transient UID/GID range behind on file systems that outlive the
|
||||
* namespace. Transient ranges are recycled, so any such object would later be owned by an unrelated client.
|
||||
*
|
||||
* This hooks into the various path-based LSM entrypoints that control inode creation as well as chown(), and
|
||||
* for each asks the only two questions that matter:
|
||||
* The inode and path variants check inode creation and ownership changes against two conditions:
|
||||
*
|
||||
* a) Does the file system die together with the user namespace? Superblocks record the user namespace they
|
||||
* were mounted in, so this is simply whether sb->s_user_ns sits at or below the provisioned namespace.
|
||||
@@ -71,7 +70,13 @@ void* bpf_rdonly_cast(const void *, __u32) __ksym;
|
||||
#define OVERLAYFS_SUPER_MAGIC 0x794c7630
|
||||
#define CGROUP2_SUPER_MAGIC 0x63677270
|
||||
|
||||
/* From include/uapi/linux/fs.h, for the same reason. */
|
||||
/* The kernel defines MQUEUE_MAGIC in ipc/mqueue.c rather than a UAPI header. */
|
||||
#define MQUEUE_MAGIC 0x19800202
|
||||
|
||||
#define ATTR_UID (1U << 1)
|
||||
#define ATTR_GID (1U << 2)
|
||||
|
||||
/* RENAME_WHITEOUT comes from include/uapi/linux/fs.h, which vmlinux.h does not provide either. */
|
||||
#define RENAME_WHITEOUT (1U << 2)
|
||||
|
||||
/* From include/uapi/asm-generic/fcntl.h. Only archs without BPF trampolines (no LSM programs) deviate. */
|
||||
@@ -244,8 +249,7 @@ static int uid_gid_map_translate(const struct uid_gid_map *map, uid_t id, bool u
|
||||
* the passed ID creates an object on the given mount. The gid flag selects the GID maps over the UID maps.
|
||||
* Stores UID_GID_INVALID if the ID does not map, in which case the kernel refuses the operation with
|
||||
* EOVERFLOW anyway, see fsuidgid_has_mapping(). Returns negative if a map could not be read. */
|
||||
static int mount_map_id(struct vfsmount *mnt, struct user_namespace *fs_userns, uid_t id, bool gid, uid_t *ret) {
|
||||
const struct mnt_idmap *idmap;
|
||||
static int mount_map_id(const struct mnt_idmap *idmap, struct user_namespace *fs_userns, uid_t id, bool gid, uid_t *ret) {
|
||||
const struct uid_gid_map *m;
|
||||
int r;
|
||||
|
||||
@@ -253,7 +257,6 @@ static int mount_map_id(struct vfsmount *mnt, struct user_namespace *fs_userns,
|
||||
if (!ret)
|
||||
return -EINVAL;
|
||||
|
||||
idmap = mnt->mnt_idmap;
|
||||
if (!idmap) {
|
||||
*ret = id;
|
||||
return 0;
|
||||
@@ -261,17 +264,18 @@ static int mount_map_id(struct vfsmount *mnt, struct user_namespace *fs_userns,
|
||||
|
||||
m = gid ? &idmap->gid_map : &idmap->uid_map;
|
||||
|
||||
/* Both nop_mnt_idmap and invalid_mnt_idmap carry empty maps. For the former that is an identity
|
||||
* mapping; for the latter the operation fails later on regardless, so treating it as identity only
|
||||
* ever makes us stricter. */
|
||||
if (m->nr_extents > 0) {
|
||||
r = uid_gid_map_translate(m, id, /* up= */ true, &id);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (id == UID_GID_INVALID) {
|
||||
*ret = UID_GID_INVALID;
|
||||
return 0;
|
||||
}
|
||||
/* nop_mnt_idmap bypasses the filesystem idmap in from_vfsuid()/from_vfsgid(). */
|
||||
if (m->nr_extents == 0) {
|
||||
*ret = id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
r = uid_gid_map_translate(m, id, /* up= */ true, &id);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (id == UID_GID_INVALID) {
|
||||
*ret = UID_GID_INVALID;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* initial_idmapping(): the initial user namespace is the only one without a parent. */
|
||||
@@ -287,59 +291,74 @@ static int mount_map_id(struct vfsmount *mnt, struct user_namespace *fs_userns,
|
||||
* namespace (every mount of it shares that one), and it cannot be idmapped. That's fine: DAC makes sure a
|
||||
* transient range only ever owns the cgroup delegated to it via AddControlGroupToUserNamespace(), which we
|
||||
* remove along with the client, best-effort (a cgroup that refuses to go away outlives its range). */
|
||||
static bool mount_is_exempt(struct vfsmount *v) {
|
||||
return v->mnt_sb->s_magic == CGROUP2_SUPER_MAGIC;
|
||||
static bool superblock_is_exempt(struct super_block *sb) {
|
||||
return sb->s_magic == CGROUP2_SUPER_MAGIC;
|
||||
}
|
||||
|
||||
/* Returns 1 if the calling task belongs to a user namespace provisioned by nsresourced and the file system
|
||||
* outlives that namespace, 0 if the policy doesn't apply, and negative on failure. */
|
||||
static int task_writes_outlive_userns(struct super_block *sb, const struct cred **ret_cred) {
|
||||
struct user_namespace *managed;
|
||||
const struct cred *cred;
|
||||
struct task_struct *task;
|
||||
int r;
|
||||
|
||||
task = (struct task_struct*) bpf_get_current_task_btf();
|
||||
cred = task->cred;
|
||||
if (!cred)
|
||||
return -EPERM;
|
||||
|
||||
/* User namespaces that were not provisioned by nsresourced can still write to the transient ranges
|
||||
* so that we don't break use cases like systemd-nspawn's --private-users=pick switch. */
|
||||
r = find_managed_userns(cred->user_ns, &managed);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
if (superblock_is_exempt(sb))
|
||||
return 0;
|
||||
|
||||
/* Does the file system die together with the user namespace? Then nothing it carries can outlive
|
||||
* the transient range, and we can allow this. */
|
||||
r = userns_is_below(managed, sb->s_user_ns);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0)
|
||||
return 0;
|
||||
|
||||
if (ret_cred)
|
||||
*ret_cred = cred;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Common tail of the inode creation hooks: would the object the calling task is about to create end up
|
||||
* owned by a transient ID on a file system that outlives the user namespace that ID was handed to? */
|
||||
static int validate_mount(struct vfsmount *v, int ret) {
|
||||
struct user_namespace *managed, *fs_userns;
|
||||
const struct cred *cred;
|
||||
struct task_struct *task;
|
||||
static int validate_creation(const struct mnt_idmap *idmap, struct super_block *sb, int ret) {
|
||||
const struct cred *cred = NULL;
|
||||
uid_t uid, gid;
|
||||
int r;
|
||||
|
||||
if (ret != 0) /* propagate earlier error */
|
||||
return ret;
|
||||
|
||||
/* Get user namespace from task */
|
||||
task = (struct task_struct*) bpf_get_current_task_btf();
|
||||
cred = task->cred;
|
||||
/* A POSIX queue retains its creator's ucounts, which hold a reference to the creator's user
|
||||
* namespace until the queue is evicted. Its IDs cannot be recycled while the queue exists. */
|
||||
if (sb->s_magic == MQUEUE_MAGIC)
|
||||
return 0;
|
||||
|
||||
r = task_writes_outlive_userns(sb, &cred);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
if (!cred)
|
||||
return -EPERM;
|
||||
|
||||
/* fsuid/fsgid are the UID/GID in the initial user namespace. Anything outside the transient ranges
|
||||
* is never recycled by us, hence there's nothing to protect. */
|
||||
if (!uid_is_transient(cred->fsuid.val) && !uid_is_transient((uid_t) cred->fsgid.val))
|
||||
return 0;
|
||||
|
||||
r = find_managed_userns(cred->user_ns, &managed);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) /* Not provisioned by nsresourced? Then this is none of our business. */
|
||||
return 0;
|
||||
|
||||
if (mount_is_exempt(v))
|
||||
return 0;
|
||||
|
||||
fs_userns = v->mnt_sb->s_user_ns;
|
||||
|
||||
/* Does the file system die together with the user namespace? Then nothing it carries can outlive
|
||||
* the transient range, and we can allow this. */
|
||||
r = userns_is_below(managed, fs_userns);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0)
|
||||
return 0;
|
||||
|
||||
/* Refuse if we cannot resolve where the IDs land: an ID we failed to translate is not an ID we know
|
||||
* to be harmless. */
|
||||
r = mount_map_id(v, fs_userns, cred->fsuid.val, /* gid= */ false, &uid);
|
||||
r = mount_map_id(idmap, sb->s_user_ns, cred->fsuid.val, /* gid= */ false, &uid);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = mount_map_id(v, fs_userns, (uid_t) cred->fsgid.val, /* gid= */ true, &gid);
|
||||
r = mount_map_id(idmap, sb->s_user_ns, (uid_t) cred->fsgid.val, /* gid= */ true, &gid);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -351,68 +370,89 @@ static int validate_mount(struct vfsmount *v, int ret) {
|
||||
return -EPERM;
|
||||
}
|
||||
|
||||
SEC("lsm/path_chown")
|
||||
int BPF_PROG(userns_restrict_path_chown, struct path *path, unsigned long long uid, unsigned long long gid, int ret) {
|
||||
struct user_namespace *managed;
|
||||
const struct cred *cred;
|
||||
struct task_struct *task;
|
||||
static int validate_chown(struct super_block *sb, uid_t uid, uid_t gid, int ret) {
|
||||
int r;
|
||||
|
||||
if (ret != 0) /* propagate earlier error */
|
||||
return ret;
|
||||
|
||||
/* Get user namespace from task */
|
||||
task = (struct task_struct*) bpf_get_current_task_btf();
|
||||
cred = task->cred;
|
||||
if (!cred)
|
||||
return -EPERM;
|
||||
|
||||
/* User namespaces that were not provisioned by nsresourced can still write to the transient ranges
|
||||
* so that we don't break use cases like systemd-nspawn's --private-users=pick switch. */
|
||||
r = find_managed_userns(cred->user_ns, &managed);
|
||||
if (r < 0)
|
||||
r = task_writes_outlive_userns(sb, /* ret_cred= */ NULL);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
return 0;
|
||||
|
||||
if (mount_is_exempt(path->mnt))
|
||||
return 0;
|
||||
|
||||
r = userns_is_below(managed, path->mnt->mnt_sb->s_user_ns);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0)
|
||||
return 0;
|
||||
|
||||
/* Unlike the creation hooks this one is handed the IDs with the mount's idmapping already applied,
|
||||
* i.e. the ones that end up on disk, hence we can look at them directly. See chown_common() in the
|
||||
* kernel sources. */
|
||||
if (uid_is_transient((uid_t) uid) || uid_is_transient((uid_t) gid))
|
||||
if (uid_is_transient(uid) || uid_is_transient(gid))
|
||||
return -EPERM;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A link adds a name for an existing inode, so we check the owner of that inode instead of the caller's
|
||||
* fsuid. Otherwise a client could create an O_TMPFILE as its transient ID, switch its fsuid to another ID,
|
||||
* and link the file onto a file system that outlives the user namespace. */
|
||||
static int validate_link(struct dentry *old_dentry, struct super_block *sb, int ret) {
|
||||
struct inode *inode;
|
||||
|
||||
if (ret != 0) /* propagate earlier error */
|
||||
return ret;
|
||||
|
||||
inode = old_dentry->d_inode;
|
||||
if (!inode)
|
||||
return -EPERM;
|
||||
|
||||
return validate_chown(sb, inode->i_uid.val, inode->i_gid.val, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/path_chown")
|
||||
int BPF_PROG(userns_restrict_path_chown, struct path *path, unsigned long long uid, unsigned long long gid, int ret) {
|
||||
/* chown_common() applies the mount idmap before calling the path hook. */
|
||||
return validate_chown(path->mnt->mnt_sb, (uid_t) uid, (uid_t) gid, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/path_mkdir")
|
||||
int BPF_PROG(userns_restrict_path_mkdir, struct path *dir, struct dentry *dentry, umode_t mode, int ret) {
|
||||
return validate_mount(dir->mnt, ret);
|
||||
return validate_creation(dir->mnt->mnt_idmap, dir->mnt->mnt_sb, ret);
|
||||
}
|
||||
|
||||
/* The mknod hook covers all named file creations, including regular files, in case the reader is looking for
|
||||
* a missing hook for open(). O_TMPFILE is handled by the file_open hook below. */
|
||||
SEC("lsm/path_mknod")
|
||||
int BPF_PROG(userns_restrict_path_mknod, const struct path *dir, struct dentry *dentry, umode_t mode, unsigned dev, int ret) {
|
||||
return validate_mount(dir->mnt, ret);
|
||||
return validate_creation(dir->mnt->mnt_idmap, dir->mnt->mnt_sb, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/path_symlink")
|
||||
int BPF_PROG(userns_restrict_path_symlink, const struct path *dir, struct dentry *dentry, const char *old_name, int ret) {
|
||||
return validate_mount(dir->mnt, ret);
|
||||
return validate_creation(dir->mnt->mnt_idmap, dir->mnt->mnt_sb, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/path_link")
|
||||
int BPF_PROG(userns_restrict_path_link, struct dentry *old_dentry, const struct path *new_dir, struct dentry *new_dentry, int ret) {
|
||||
return validate_mount(new_dir->mnt, ret);
|
||||
return validate_creation(new_dir->mnt->mnt_idmap, new_dir->mnt->mnt_sb, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/inode_create")
|
||||
int BPF_PROG(userns_restrict_inode_create, const struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, int ret) {
|
||||
return validate_creation(idmap, dir->i_sb, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/inode_mkdir")
|
||||
int BPF_PROG(userns_restrict_inode_mkdir, const struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, int ret) {
|
||||
return validate_creation(idmap, dir->i_sb, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/inode_mknod")
|
||||
int BPF_PROG(userns_restrict_inode_mknod, const struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev, int ret) {
|
||||
return validate_creation(idmap, dir->i_sb, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/inode_symlink")
|
||||
int BPF_PROG(userns_restrict_inode_symlink, const struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, const char *old_name, int ret) {
|
||||
return validate_creation(idmap, dir->i_sb, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/inode_link")
|
||||
int BPF_PROG(userns_restrict_inode_link, const struct mnt_idmap *idmap, struct dentry *old_dentry, struct inode *dir, struct dentry *new_dentry, int ret) {
|
||||
return validate_link(old_dentry, dir->i_sb, ret);
|
||||
}
|
||||
|
||||
/* A host process can reopen an O_TMPFILE through /proc/self/fd without keeping the creator's user
|
||||
@@ -422,17 +462,65 @@ int BPF_PROG(userns_restrict_file_open, struct file *file, int ret) {
|
||||
if (!(file->f_flags & __O_TMPFILE))
|
||||
return ret;
|
||||
|
||||
return validate_mount(file->f_path.mnt, ret);
|
||||
return validate_creation(file->f_path.mnt->mnt_idmap, file->f_path.mnt->mnt_sb, ret);
|
||||
}
|
||||
|
||||
/* RENAME_WHITEOUT puts a whiteout owned by the calling task in place of the source, past the mknod hook */
|
||||
/* The file system creates the whiteout of RENAME_WHITEOUT itself, owned by the caller's fsuid and fsgid,
|
||||
* without calling inode_mknod(). The inode_rename hook gets neither the rename flags nor the mount idmap,
|
||||
* so the inode policy uses the path_rename hook for whiteouts. */
|
||||
SEC("lsm/path_rename")
|
||||
int BPF_PROG(userns_restrict_path_rename, const struct path *old_dir, struct dentry *old_dentry,
|
||||
const struct path *new_dir, struct dentry *new_dentry, unsigned flags, int ret) {
|
||||
if (!(flags & RENAME_WHITEOUT))
|
||||
return ret;
|
||||
|
||||
return validate_mount(old_dir->mnt, ret);
|
||||
return validate_creation(old_dir->mnt->mnt_idmap, old_dir->mnt->mnt_sb, ret);
|
||||
}
|
||||
|
||||
/* Older vmlinux.h files lack the VFS ID fields in struct iattr. */
|
||||
struct iattr___local {
|
||||
unsigned ia_valid;
|
||||
struct {
|
||||
uid_t val;
|
||||
} ia_vfsuid, ia_vfsgid;
|
||||
} __attribute__((preserve_access_index));
|
||||
|
||||
SEC("lsm/inode_setattr")
|
||||
int BPF_PROG(userns_restrict_inode_setattr, const struct mnt_idmap *idmap, struct dentry *dentry, struct iattr___local *attr, int ret) {
|
||||
struct super_block *sb;
|
||||
uid_t uid = UID_GID_INVALID, gid = UID_GID_INVALID;
|
||||
int r;
|
||||
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
if (!(attr->ia_valid & (ATTR_UID|ATTR_GID)))
|
||||
return 0;
|
||||
|
||||
sb = dentry->d_sb;
|
||||
|
||||
/* We translate the IDs only for managed tasks. Otherwise every chown() on the system walks the
|
||||
* map extents, and a failed map read refuses chown() for tasks we don't manage. */
|
||||
r = task_writes_outlive_userns(sb, /* ret_cred= */ NULL);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
if (attr->ia_valid & ATTR_UID) {
|
||||
r = mount_map_id(idmap, sb->s_user_ns, attr->ia_vfsuid.val, /* gid= */ false, &uid);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (attr->ia_valid & ATTR_GID) {
|
||||
r = mount_map_id(idmap, sb->s_user_ns, (uid_t) attr->ia_vfsgid.val, /* gid= */ true, &gid);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (uid_is_transient(uid) || uid_is_transient(gid))
|
||||
return -EPERM;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Btrfs creates subvolume root inodes without calling the path or inode creation hooks. */
|
||||
@@ -443,7 +531,7 @@ static int validate_ioctl(struct file *file, unsigned cmd, int ret) {
|
||||
if (file->f_path.mnt->mnt_sb->s_magic != BTRFS_SUPER_MAGIC)
|
||||
return ret;
|
||||
|
||||
return validate_mount(file->f_path.mnt, ret);
|
||||
return validate_creation(file->f_path.mnt->mnt_idmap, file->f_path.mnt->mnt_sb, ret);
|
||||
}
|
||||
|
||||
SEC("lsm/file_ioctl")
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
#include <sched.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/eventfd.h>
|
||||
#include <sys/fsuid.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "bpf-link.h"
|
||||
#include "bpf-util.h"
|
||||
#include "btrfs-util.h"
|
||||
@@ -67,6 +69,8 @@ static int make_idmapped_tmpfs_fsmount(int userns_fd) {
|
||||
static struct userns_restrict_bpf *bpf_obj = NULL;
|
||||
STATIC_DESTRUCTOR_REGISTER(bpf_obj, userns_restrict_bpf_freep);
|
||||
|
||||
static bool inode_hooks = false;
|
||||
|
||||
static int intro(void) {
|
||||
int r;
|
||||
|
||||
@@ -77,6 +81,8 @@ static int intro(void) {
|
||||
return log_tests_skipped("Lacking privileges");
|
||||
ASSERT_OK(r);
|
||||
|
||||
inode_hooks = sym_bpf_program__fd(bpf_obj->progs.userns_restrict_inode_create) >= 0;
|
||||
|
||||
r = userns_restrict_attach(bpf_obj, /* pin= */ false);
|
||||
if (ERRNO_IS_NOT_SUPPORTED(r))
|
||||
return log_tests_skipped("LSM-BPF programs cannot be attached");
|
||||
@@ -85,7 +91,119 @@ static int intro(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static typeof(sym_bpf_link__pin) pin_without_failure;
|
||||
static unsigned pins_until_failure;
|
||||
|
||||
static int pin_with_failure(struct bpf_link *link, const char *path) {
|
||||
if (--pins_until_failure == 0)
|
||||
return -ENOMEM;
|
||||
|
||||
return pin_without_failure(link, path);
|
||||
}
|
||||
|
||||
static uint32_t link_id(struct bpf_link *link) {
|
||||
struct bpf_link_info info = {};
|
||||
uint32_t len = sizeof(info);
|
||||
|
||||
ASSERT_OK(sym_bpf_obj_get_info_by_fd(sym_bpf_link__fd(link), &info, &len));
|
||||
return info.id;
|
||||
}
|
||||
|
||||
static void check_pinned_links(const uint32_t *ids, bool replaced) {
|
||||
for (int i = 0; i < bpf_obj->skeleton->prog_cnt; i++) {
|
||||
struct bpf_prog_skeleton *ps = bpf_obj->skeleton->progs + i;
|
||||
_cleanup_(bpf_link_freep) struct bpf_link *link = NULL;
|
||||
_cleanup_free_ char *path = NULL;
|
||||
const char *name;
|
||||
|
||||
if (sym_bpf_program__fd(*ps->prog) < 0)
|
||||
continue;
|
||||
|
||||
name = ASSERT_NOT_NULL(startswith(ps->name, "userns_restrict_"));
|
||||
ASSERT_NOT_NULL(path = path_join("/sys/fs/bpf/systemd/userns-restrict-v2/programs", name));
|
||||
link = sym_bpf_link__open(path);
|
||||
ASSERT_OK(bpf_get_error_translated(link));
|
||||
if (replaced)
|
||||
ASSERT_NE(link_id(link), ids[i]);
|
||||
else
|
||||
ASSERT_EQ(link_id(link), ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_RET(userns_restrict_replace_pins) {
|
||||
_cleanup_(pidref_done_sigkill_wait) PidRef child = PIDREF_NULL;
|
||||
_cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *obj = NULL;
|
||||
_cleanup_free_ uint32_t *ids = NULL;
|
||||
struct stat st;
|
||||
int r;
|
||||
|
||||
r = ASSERT_OK(pidref_safe_fork("(test-bpf-pins)", FORK_DEATHSIG_SIGKILL, &child));
|
||||
if (r > 0)
|
||||
return ASSERT_OK(pidref_wait_for_terminate_and_check("(test-bpf-pins)", &child, WAIT_LOG));
|
||||
|
||||
/* A private bpffs prevents the test from replacing the daemon's pinned policy. */
|
||||
r = RET_NERRNO(unshare(CLONE_NEWNS));
|
||||
if (ERRNO_IS_NEG_PRIVILEGE(r))
|
||||
_exit(log_tests_skipped_errno(r, "Cannot create a mount namespace"));
|
||||
ASSERT_OK(r);
|
||||
ASSERT_OK_ERRNO(mount(/* source= */ NULL, "/", /* filesystemtype= */ NULL, MS_PRIVATE|MS_REC, /* data= */ NULL));
|
||||
ASSERT_OK_ERRNO(mount("bpf", "/sys/fs/bpf", "bpf", /* mountflags= */ 0, /* data= */ NULL));
|
||||
|
||||
ASSERT_OK(userns_restrict_install(/* pin= */ true, &obj));
|
||||
ASSERT_OK(userns_restrict_attach(obj, /* pin= */ true));
|
||||
|
||||
ASSERT_NOT_NULL(ids = new0(uint32_t, obj->skeleton->prog_cnt));
|
||||
for (int i = 0; i < obj->skeleton->prog_cnt; i++) {
|
||||
struct bpf_prog_skeleton *ps = obj->skeleton->progs + i;
|
||||
|
||||
if (*ps->link)
|
||||
ids[i] = link_id(*ps->link);
|
||||
}
|
||||
obj = userns_restrict_bpf_free(obj);
|
||||
|
||||
ASSERT_OK(userns_restrict_install(/* pin= */ true, &obj));
|
||||
pin_without_failure = sym_bpf_link__pin;
|
||||
sym_bpf_link__pin = pin_with_failure;
|
||||
pins_until_failure = 2;
|
||||
ASSERT_ERROR(userns_restrict_attach(obj, /* pin= */ true), ENOMEM);
|
||||
ASSERT_EQ(pins_until_failure, 0U);
|
||||
sym_bpf_link__pin = pin_without_failure;
|
||||
obj = userns_restrict_bpf_free(obj);
|
||||
|
||||
check_pinned_links(ids, /* replaced= */ false);
|
||||
ASSERT_ERROR_ERRNO(lstat("/sys/fs/bpf/systemd/userns-restrict-v2/programs-new", &st), ENOENT);
|
||||
|
||||
ASSERT_OK(userns_restrict_install(/* pin= */ true, &obj));
|
||||
ASSERT_OK(userns_restrict_attach(obj, /* pin= */ true));
|
||||
obj = userns_restrict_bpf_free(obj);
|
||||
check_pinned_links(ids, /* replaced= */ true);
|
||||
ASSERT_ERROR_ERRNO(lstat("/sys/fs/bpf/systemd/userns-restrict-v2/programs-new", &st), ENOENT);
|
||||
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* nsresourced removes the map entries of a user namespace when the namespace dies. The tests don't run
|
||||
* nsresourced, so they remove the entries themselves. Otherwise a later namespace can get the same inode
|
||||
* number, and the BPF programs treat it as managed. The setgroups() check then stops at that namespace
|
||||
* and never reaches a parent with a deny entry. */
|
||||
static void unregister_userns(uint64_t *inode) {
|
||||
assert(inode);
|
||||
|
||||
if (*inode != 0)
|
||||
(void) userns_restrict_reset_by_inode(bpf_obj, *inode);
|
||||
}
|
||||
|
||||
static uint64_t register_userns(int userns_fd) {
|
||||
struct stat st;
|
||||
|
||||
ASSERT_OK_ERRNO(fstat(userns_fd, &st));
|
||||
ASSERT_OK(userns_restrict_register_by_inode(bpf_obj, st.st_ino));
|
||||
|
||||
return st.st_ino;
|
||||
}
|
||||
|
||||
TEST(userns_restrict) {
|
||||
_cleanup_(unregister_userns) uint64_t registered = 0;
|
||||
_cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF,
|
||||
idmapped_tmpfs = -EBADF;
|
||||
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
|
||||
@@ -99,6 +217,10 @@ TEST(userns_restrict) {
|
||||
ASSERT_OK_ERRNO(chown(t, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
|
||||
|
||||
host_fd1 = ASSERT_OK(xopenat(AT_FDCWD, t, O_DIRECTORY));
|
||||
safe_close(ASSERT_OK(xopenat_full(host_fd1, "source", O_CREAT|O_EXCL|O_WRONLY, /* xopen_flags= */ 0, 0600)));
|
||||
ASSERT_OK_ERRNO(fchownat(host_fd1, "source", CONTAINER_UID_MIN, CONTAINER_UID_MIN, 0));
|
||||
ASSERT_OK_ERRNO(mkdirat(host_fd1, "empty", 0700));
|
||||
ASSERT_OK_ERRNO(fchownat(host_fd1, "empty", CONTAINER_UID_MIN, CONTAINER_UID_MIN, 0));
|
||||
host_tmpfs = ASSERT_OK(make_tmpfs_fsmount());
|
||||
|
||||
r = xopenat_full(host_fd1, ".", O_TMPFILE|O_RDWR, /* xopen_flags= */ 0, 0666);
|
||||
@@ -119,7 +241,7 @@ TEST(userns_restrict) {
|
||||
|
||||
idmapped_tmpfs = ASSERT_OK(make_idmapped_tmpfs_fsmount(userns_fd));
|
||||
|
||||
ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, userns_fd));
|
||||
registered = register_userns(userns_fd);
|
||||
|
||||
r = ASSERT_OK(pidref_safe_fork("(test)", FORK_DEATHSIG_SIGKILL, &pidref));
|
||||
if (r == 0) {
|
||||
@@ -138,27 +260,36 @@ TEST(userns_restrict) {
|
||||
/* xopen_flags= */ 0, 0666), EPERM);
|
||||
ASSERT_ERROR_ERRNO(mkdirat(host_fd1, "test2", 0666), EPERM);
|
||||
ASSERT_ERROR_ERRNO(mkdirat(host_tmpfs, "xxx2", 0666), EPERM);
|
||||
ASSERT_ERROR_ERRNO(symlinkat("target", host_fd1, "symlink"), EPERM);
|
||||
ASSERT_ERROR_ERRNO(mkfifoat(host_fd1, "fifo", 0600), EPERM);
|
||||
ASSERT_ERROR_ERRNO(linkat(host_fd1, "source", host_fd1, "link", 0), EPERM);
|
||||
|
||||
ASSERT_OK_ERRNO(renameat(host_fd1, "source", host_fd1, "renamed"));
|
||||
ASSERT_OK_ERRNO(unlinkat(host_fd1, "renamed", 0));
|
||||
ASSERT_OK_ERRNO(unlinkat(host_fd1, "empty", AT_REMOVEDIR));
|
||||
|
||||
ASSERT_OK_ERRNO(fchmod(host_fd1, 0700));
|
||||
|
||||
/* But this mount created locally should be fine */
|
||||
safe_close(ASSERT_OK(xopenat_full(private_tmpfs, "yyy", O_RDWR|O_CREAT,
|
||||
/* xopen_flags= */ 0, 0666)));
|
||||
ASSERT_OK_ERRNO(mkdirat(private_tmpfs, "yyy2", 0666));
|
||||
ASSERT_OK_ERRNO(symlinkat("yyy", private_tmpfs, "symlink"));
|
||||
ASSERT_OK_ERRNO(mkfifoat(private_tmpfs, "fifo", 0600));
|
||||
ASSERT_OK_ERRNO(linkat(private_tmpfs, "yyy", private_tmpfs, "link", 0));
|
||||
|
||||
/* And so should the idmapped host mount: it translates our transient UID to UID 0 before
|
||||
* anything reaches the disk */
|
||||
safe_close(ASSERT_OK(xopenat_full(idmapped_tmpfs, "zzz", O_RDWR|O_CREAT,
|
||||
/* xopen_flags= */ 0, 0666)));
|
||||
ASSERT_OK_ERRNO(mkdirat(idmapped_tmpfs, "zzz2", 0666));
|
||||
ASSERT_OK_ERRNO(symlinkat("zzz", idmapped_tmpfs, "symlink"));
|
||||
ASSERT_OK_ERRNO(mkfifoat(idmapped_tmpfs, "fifo", 0600));
|
||||
ASSERT_OK_ERRNO(linkat(idmapped_tmpfs, "zzz", idmapped_tmpfs, "link", 0));
|
||||
|
||||
/* chown() is subject to the same policy. Unlike the creation hooks it is handed the ID that
|
||||
* ends up on disk, so it needs no idmap arithmetic of its own. Setting a group the inode
|
||||
* already has still reaches the hook, before the change is applied. Giving the host
|
||||
* directory (which outlives us) our transient GID would leave something recyclable behind,
|
||||
* hence is refused... */
|
||||
/* Giving the host directory our transient GID would persist ownership after the namespace exits. */
|
||||
ASSERT_ERROR_ERRNO(fchownat(host_fd1, "", GID_INVALID, 0, AT_EMPTY_PATH), EPERM);
|
||||
|
||||
/* ...while the same chown() is fine on the file system that dies with us, and on the
|
||||
* idmapped mount where our transient GID is translated to 0 before it reaches the disk. */
|
||||
ASSERT_OK_ERRNO(fchownat(private_tmpfs, "yyy", GID_INVALID, 0, 0));
|
||||
ASSERT_OK_ERRNO(fchownat(idmapped_tmpfs, "zzz", GID_INVALID, 0, 0));
|
||||
|
||||
@@ -309,7 +440,107 @@ TEST(userns_restrict_btrfs) {
|
||||
ASSERT_OK(userns_restrict_reset_by_inode(bpf_obj, st.st_ino));
|
||||
}
|
||||
|
||||
TEST(userns_restrict_link_owner) {
|
||||
_cleanup_(unregister_userns) uint64_t registered = 0;
|
||||
_cleanup_close_ int userns_fd = -EBADF, host_tmpfs = -EBADF;
|
||||
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
|
||||
_cleanup_free_ char *idmap = NULL;
|
||||
int r;
|
||||
|
||||
if (!inode_hooks)
|
||||
return (void) log_tests_skipped("Inode LSM hooks with mount idmaps are not supported");
|
||||
|
||||
host_tmpfs = ASSERT_OK(make_tmpfs_fsmount());
|
||||
|
||||
/* The client links this file below. The file is owned by the client's transient ID, and its mode
|
||||
* keeps fs.protected_hardlinks=1 from refusing the link before the BPF-LSM runs. */
|
||||
safe_close(ASSERT_OK(xopenat_full(host_tmpfs, "file", O_CREAT|O_EXCL|O_WRONLY, /* xopen_flags= */ 0, 0666)));
|
||||
ASSERT_OK_ERRNO(fchmodat(host_tmpfs, "file", 0666, 0));
|
||||
ASSERT_OK_ERRNO(fchownat(host_tmpfs, "file", CONTAINER_UID_MIN, CONTAINER_UID_MIN, 0));
|
||||
|
||||
/* ID 1 maps to a foreign ID, which is not transient. The client switches its fsuid and fsgid to it
|
||||
* before it adds the link. */
|
||||
ASSERT_OK(asprintf(&idmap, "0 "UID_FMT" 1\n1 "UID_FMT" 1", CONTAINER_UID_MIN, FOREIGN_UID_MIN));
|
||||
userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ true));
|
||||
|
||||
registered = register_userns(userns_fd);
|
||||
|
||||
r = ASSERT_OK(pidref_safe_fork("(test-link)", FORK_DEATHSIG_SIGKILL, &pidref));
|
||||
if (r == 0) {
|
||||
ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF));
|
||||
|
||||
/* A linkable O_TMPFILE would be owned by our transient ID, and any task that gets the file
|
||||
* descriptor could link it. */
|
||||
ASSERT_ERROR(xopenat_full(host_tmpfs, ".", O_TMPFILE|O_RDWR, /* xopen_flags= */ 0, 0600), EPERM);
|
||||
|
||||
(void) setfsgid(1);
|
||||
(void) setfsuid(1);
|
||||
ASSERT_EQ(setfsuid(UID_INVALID), 1);
|
||||
|
||||
/* The file is owned by our transient ID, no matter which fsuid adds the link. */
|
||||
ASSERT_ERROR_ERRNO(linkat(host_tmpfs, "file", host_tmpfs, "link", 0), EPERM);
|
||||
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
ASSERT_OK(pidref_wait_for_terminate_and_check("(test-link)", &pidref, WAIT_LOG));
|
||||
}
|
||||
|
||||
TEST(userns_restrict_idmap_to_transient) {
|
||||
_cleanup_(unregister_userns) uint64_t registered = 0;
|
||||
_cleanup_close_ int userns_fd = -EBADF, idmap_fd = -EBADF, mntfd = -EBADF;
|
||||
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
|
||||
_cleanup_free_ char *idmap = NULL;
|
||||
int r;
|
||||
|
||||
userns_fd = ASSERT_OK(userns_acquire("0 0 1", "0 0 1", /* setgroups_deny= */ true));
|
||||
ASSERT_OK(asprintf(&idmap, UID_FMT" 0 1", CONTAINER_UID_MIN));
|
||||
idmap_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ true));
|
||||
|
||||
mntfd = ASSERT_OK(make_tmpfs_fsmount());
|
||||
ASSERT_OK_ERRNO(fchownat(mntfd, "", CONTAINER_UID_MIN, CONTAINER_UID_MIN, AT_EMPTY_PATH));
|
||||
ASSERT_OK_ERRNO(mount_setattr(mntfd, "", AT_EMPTY_PATH,
|
||||
&(struct mount_attr) {
|
||||
.attr_set = MOUNT_ATTR_IDMAP,
|
||||
.userns_fd = idmap_fd,
|
||||
}, sizeof(struct mount_attr)));
|
||||
|
||||
registered = register_userns(userns_fd);
|
||||
r = ASSERT_OK(pidref_safe_fork("(test-idmap)", FORK_DEATHSIG_SIGKILL, &pidref));
|
||||
if (r == 0) {
|
||||
ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF));
|
||||
ASSERT_ERROR(xopenat_full(mntfd, "file", O_CREAT|O_WRONLY, /* xopen_flags= */ 0, 0600), EPERM);
|
||||
ASSERT_ERROR_ERRNO(mkdirat(mntfd, "dir", 0700), EPERM);
|
||||
ASSERT_ERROR_ERRNO(fchownat(mntfd, "", UID_INVALID, 0, AT_EMPTY_PATH), EPERM);
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
ASSERT_OK(pidref_wait_for_terminate_and_check("(test-idmap)", &pidref, WAIT_LOG));
|
||||
}
|
||||
|
||||
static void assert_overlay_unwritable(const char *target, unsigned long flags, const char *options) {
|
||||
_cleanup_close_ int fd = -EBADF;
|
||||
|
||||
if (!inode_hooks) {
|
||||
ASSERT_ERROR_ERRNO(mount("overlay", target, "overlay", flags, options), EPERM);
|
||||
return;
|
||||
}
|
||||
|
||||
/* OverlayFS mounts read-only if an inode hook denies creating its work directory. */
|
||||
if (mount("overlay", target, "overlay", flags, options) < 0) {
|
||||
ASSERT_EQ(errno, EPERM);
|
||||
return;
|
||||
}
|
||||
|
||||
fd = ASSERT_OK(xopenat(AT_FDCWD, target, O_RDONLY|O_DIRECTORY));
|
||||
ASSERT_ERROR(xopenat_full(fd, "file", O_CREAT|O_RDWR, /* xopen_flags= */ 0, 0600), EROFS);
|
||||
|
||||
fd = safe_close(fd);
|
||||
ASSERT_OK_ERRNO(umount(target));
|
||||
}
|
||||
|
||||
TEST(userns_restrict_overlayfs) {
|
||||
_cleanup_(unregister_userns) uint64_t registered = 0;
|
||||
_cleanup_close_ int userns_fd = -EBADF, idmapped_upper = -EBADF, bad_userns_fd = -EBADF,
|
||||
bad_upper = -EBADF;
|
||||
_cleanup_(rm_rf_physical_and_freep) char *t = NULL, *th = NULL, *ti = NULL, *tb = NULL;
|
||||
@@ -318,21 +549,13 @@ TEST(userns_restrict_overlayfs) {
|
||||
*hmerged = NULL;
|
||||
int r;
|
||||
|
||||
/* overlay writes land on its upper layer via vfs_create(), which the path-based hooks never see, so
|
||||
* a transient-owned inode could persist there. The policy judges the upper at mount time. An upper
|
||||
* is allowed when it dies with the namespace (a tmpfs the client set up) or is idmapped to map the
|
||||
* transient range away; it is refused when it outlives the namespace and records the transient range
|
||||
* on disk (a plain init-owned upper, or one idmapped so the transient range still reaches its disk).
|
||||
* We also check that an overlay mounted read-only but with such an upper is refused, since it could
|
||||
* otherwise be remounted read-write afterwards without re-entering the hook. */
|
||||
|
||||
ASSERT_OK(mkdtemp_malloc(/* template= */ NULL, &t));
|
||||
ASSERT_OK_ERRNO(chown(t, CONTAINER_UID_MIN, CONTAINER_UID_MIN));
|
||||
|
||||
ASSERT_OK(asprintf(&idmap, "0 "UID_FMT" 1", CONTAINER_UID_MIN));
|
||||
userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ true));
|
||||
|
||||
ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, userns_fd));
|
||||
registered = register_userns(userns_fd);
|
||||
|
||||
/* Set up the layer dirs for the refuse case on the init-owned file system backing our temporary dir,
|
||||
* i.e. one that outlives the user namespace. This has to happen here as root: the managed client
|
||||
@@ -436,20 +659,18 @@ TEST(userns_restrict_overlayfs) {
|
||||
|
||||
ASSERT_OK_ERRNO(umount(merged));
|
||||
|
||||
/* The same writable overlay, but with its upper on the init-owned host file system that
|
||||
* outlives our user namespace: the policy must refuse it. Overlayfs sets the upper up via
|
||||
* vfs_mkdir()/vfs_create(), reaching only the inode hooks, so nothing blocks it before
|
||||
* sb_kern_mount runs, and the kernel itself permits the mount (verified without the BPF-LSM
|
||||
* loaded) — hence the EPERM here is sb_kern_mount refusing it. */
|
||||
_cleanup_free_ char *bad_opts = NULL;
|
||||
ASSERT_NOT_NULL(bad_opts = strjoin("lowerdir=", hlower, ",upperdir=", hupper,
|
||||
",workdir=", hwork));
|
||||
ASSERT_ERROR_ERRNO(mount("overlay", hmerged, "overlay", /* mountflags= */ 0, bad_opts), EPERM);
|
||||
assert_overlay_unwritable(hmerged, /* flags= */ 0, bad_opts);
|
||||
assert_overlay_unwritable(hmerged, MS_RDONLY, bad_opts);
|
||||
|
||||
/* The same persistent upper, but mounted read-only, must be refused too: it still has an
|
||||
* upper, so allowing it on the strength of the (mutable) read-only flag would let the client
|
||||
* remount it read-write afterwards, which does not re-enter this hook. */
|
||||
ASSERT_ERROR_ERRNO(mount("overlay", hmerged, "overlay", MS_RDONLY, bad_opts), EPERM);
|
||||
if (inode_hooks) {
|
||||
_cleanup_free_ char *p = NULL;
|
||||
|
||||
ASSERT_NOT_NULL(p = path_join(hwork, "work"));
|
||||
ASSERT_ERROR_ERRNO(access(p, F_OK), ENOENT);
|
||||
}
|
||||
|
||||
/* Then the idmapped upper: its superblock outlives us, but its idmapping maps our
|
||||
* transient range away, so nothing recyclable reaches its disk and the mount is allowed
|
||||
@@ -463,17 +684,16 @@ TEST(userns_restrict_overlayfs) {
|
||||
ASSERT_NOT_NULL(idmapped_opts = strjoin("lowerdir=", lower, ",upperdir=", iupper,
|
||||
",workdir=", iwork));
|
||||
ASSERT_OK_ERRNO(mount("overlay", merged, "overlay", /* mountflags= */ 0, idmapped_opts));
|
||||
safe_close(ASSERT_OK(xopenat_full(AT_FDCWD, f, O_RDWR|O_CREAT, /* xopen_flags= */ 0, 0600)));
|
||||
ASSERT_OK_ERRNO(fchownat(AT_FDCWD, f, UID_INVALID, 0, 0));
|
||||
ASSERT_OK_ERRNO(umount(merged));
|
||||
|
||||
/* Finally the idmapped upper whose idmapping does NOT map our transient range away (it
|
||||
* records it on disk unchanged): the mount reaches sb_kern_mount, whose idmap check must
|
||||
* refuse it. */
|
||||
_cleanup_free_ char *bupper = NULL, *bwork = NULL, *bad_idmapped_opts = NULL;
|
||||
ASSERT_NOT_NULL(bupper = path_join(tb, "upper"));
|
||||
ASSERT_NOT_NULL(bwork = path_join(tb, "work"));
|
||||
ASSERT_NOT_NULL(bad_idmapped_opts = strjoin("lowerdir=", lower, ",upperdir=", bupper,
|
||||
",workdir=", bwork));
|
||||
ASSERT_ERROR_ERRNO(mount("overlay", merged, "overlay", /* mountflags= */ 0, bad_idmapped_opts), EPERM);
|
||||
assert_overlay_unwritable(merged, /* flags= */ 0, bad_idmapped_opts);
|
||||
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
@@ -486,7 +706,64 @@ TEST(userns_restrict_overlayfs) {
|
||||
(void) umount(tb);
|
||||
}
|
||||
|
||||
TEST(userns_restrict_overlayfs_identity) {
|
||||
_cleanup_(unregister_userns) uint64_t registered = 0;
|
||||
_cleanup_close_ int userns_fd = -EBADF;
|
||||
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
|
||||
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
|
||||
_cleanup_free_ char *lower = NULL, *upper = NULL, *work = NULL, *merged = NULL, *options = NULL;
|
||||
int r;
|
||||
|
||||
if (!inode_hooks)
|
||||
return (void) log_tests_skipped("Inode LSM hooks with mount idmaps are not supported");
|
||||
|
||||
ASSERT_OK(mkdtemp_malloc(/* template= */ NULL, &t));
|
||||
ASSERT_NOT_NULL(lower = path_join(t, "lower"));
|
||||
ASSERT_NOT_NULL(upper = path_join(t, "upper"));
|
||||
ASSERT_NOT_NULL(work = path_join(t, "work"));
|
||||
ASSERT_NOT_NULL(merged = path_join(t, "merged"));
|
||||
ASSERT_OK_ERRNO(mkdir(lower, 0755));
|
||||
ASSERT_OK_ERRNO(mkdir(upper, 0755));
|
||||
ASSERT_OK_ERRNO(mkdir(work, 0755));
|
||||
ASSERT_OK_ERRNO(mkdir(merged, 0755));
|
||||
ASSERT_NOT_NULL(options = strjoin("lowerdir=", lower, ",upperdir=", upper, ",workdir=", work));
|
||||
|
||||
userns_fd = ASSERT_OK(userns_acquire("0 0 1", "0 0 1", /* setgroups_deny= */ true));
|
||||
registered = register_userns(userns_fd);
|
||||
|
||||
r = ASSERT_OK(pidref_safe_fork("(test-ovl-id)", FORK_DEATHSIG_SIGKILL, &pidref));
|
||||
if (r == 0) {
|
||||
_cleanup_close_ int fd = -EBADF;
|
||||
|
||||
ASSERT_OK(namespace_enter(-EBADF, -EBADF, -EBADF, userns_fd, -EBADF));
|
||||
ASSERT_OK_ERRNO(unshare(CLONE_NEWNS));
|
||||
ASSERT_OK_ERRNO(mount(/* source= */ NULL, "/", /* filesystemtype= */ NULL, MS_SLAVE|MS_REC, /* data= */ NULL));
|
||||
|
||||
r = mount("overlay", merged, "overlay", /* mountflags= */ 0, options);
|
||||
if (r < 0 && errno != EPERM) {
|
||||
log_tests_skipped_errno(errno, "OverlayFS upper is not supported");
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
ASSERT_OK_ERRNO(r);
|
||||
fd = ASSERT_OK(xopenat(AT_FDCWD, merged, O_RDONLY|O_DIRECTORY));
|
||||
safe_close(ASSERT_OK(xopenat_full(fd, "file", O_CREAT|O_RDWR, /* xopen_flags= */ 0, 0600)));
|
||||
ASSERT_OK_ERRNO(mkdirat(fd, "dir", 0700));
|
||||
ASSERT_OK_ERRNO(symlinkat("file", fd, "symlink"));
|
||||
ASSERT_OK_ERRNO(mkfifoat(fd, "fifo", 0600));
|
||||
ASSERT_OK_ERRNO(linkat(fd, "file", fd, "link", 0));
|
||||
ASSERT_OK_ERRNO(fchownat(fd, "file", 0, 0, 0));
|
||||
|
||||
fd = safe_close(fd);
|
||||
ASSERT_OK_ERRNO(umount(merged));
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
ASSERT_OK(pidref_wait_for_terminate_and_check("(test-ovl-id)", &pidref, WAIT_LOG));
|
||||
}
|
||||
|
||||
TEST(userns_restrict_cgroupfs) {
|
||||
_cleanup_(unregister_userns) uint64_t registered = 0;
|
||||
_cleanup_close_ int userns_fd = -EBADF, undelegated_fd = -EBADF, delegated_fd = -EBADF;
|
||||
_cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
|
||||
_cleanup_free_ char *cg = NULL, *undelegated = NULL, *delegated = NULL, *idmap = NULL;
|
||||
@@ -523,7 +800,7 @@ TEST(userns_restrict_cgroupfs) {
|
||||
ASSERT_OK(asprintf(&idmap, "0 "UID_FMT" 1", CONTAINER_UID_MIN));
|
||||
userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ true));
|
||||
|
||||
ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, userns_fd));
|
||||
registered = register_userns(userns_fd);
|
||||
|
||||
r = ASSERT_OK(pidref_safe_fork("(test-cgroup)", FORK_DEATHSIG_SIGKILL, &pidref));
|
||||
if (r == 0) {
|
||||
@@ -567,6 +844,7 @@ static void write_child_mappings(PidRef *child, int parent_userns_fd) {
|
||||
}
|
||||
|
||||
TEST(setgroups_deny) {
|
||||
_cleanup_(unregister_userns) uint64_t registered_deny = 0, registered_allow = 0;
|
||||
_cleanup_close_ int deny_userns_fd = -EBADF, allow_userns_fd = -EBADF,
|
||||
afd = -EBADF, bfd = -EBADF;
|
||||
int r;
|
||||
@@ -579,13 +857,13 @@ TEST(setgroups_deny) {
|
||||
* denial specifically. */
|
||||
deny_userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ false));
|
||||
|
||||
ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, deny_userns_fd));
|
||||
registered_deny = register_userns(deny_userns_fd);
|
||||
ASSERT_OK(userns_restrict_setgroups_deny_by_fd(bpf_obj, deny_userns_fd));
|
||||
|
||||
/* Create a userns that is managed but does NOT have setgroups() denied */
|
||||
allow_userns_fd = ASSERT_OK(userns_acquire(idmap, idmap, /* setgroups_deny= */ false));
|
||||
|
||||
ASSERT_OK(userns_restrict_register_by_fd(bpf_obj, allow_userns_fd));
|
||||
registered_allow = register_userns(allow_userns_fd);
|
||||
|
||||
afd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
|
||||
bfd = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
|
||||
@@ -710,8 +988,7 @@ TEST_RET(userns_mqueue_lifetime) {
|
||||
inode = (unsigned) userns_st.st_ino;
|
||||
ASSERT_NE(inode, 0U);
|
||||
|
||||
/* The setgroups map enables free notifications without imposing inode creation restrictions. */
|
||||
ASSERT_OK(userns_restrict_setgroups_deny_by_fd(bpf_obj, userns_fd));
|
||||
ASSERT_EQ(register_userns(userns_fd), userns_st.st_ino);
|
||||
|
||||
r = ASSERT_OK(pidref_safe_fork("(mqueue-creator)", FORK_DEATHSIG_SIGKILL, &creator));
|
||||
if (r == 0) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -8,7 +10,6 @@
|
||||
#endif
|
||||
|
||||
#include "bpf-util.h"
|
||||
#include "bpf-link.h"
|
||||
#include "log.h"
|
||||
#include "lsm-util.h"
|
||||
#include "mkdir.h"
|
||||
@@ -31,6 +32,86 @@
|
||||
* single unshare(). They'd stay attached next to ours, hence get rid of them. */
|
||||
#define OBSOLETE_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict"
|
||||
|
||||
#if HAVE_VMLINUX_H
|
||||
static bool inode_hooks_supported(void) {
|
||||
_cleanup_(sym_btf__freep) struct btf *btf = NULL;
|
||||
int idmap_id;
|
||||
|
||||
if (!sym_btf__load_vmlinux_btf || !sym_btf__find_by_name_kind ||
|
||||
!sym_btf__type_by_id || !sym_btf__free)
|
||||
return false;
|
||||
|
||||
btf = sym_btf__load_vmlinux_btf();
|
||||
if (bpf_get_error_translated(btf) < 0)
|
||||
return false;
|
||||
|
||||
idmap_id = sym_btf__find_by_name_kind(btf, "mnt_idmap", BTF_KIND_STRUCT);
|
||||
if (idmap_id <= 0)
|
||||
return false;
|
||||
|
||||
FOREACH_STRING(name,
|
||||
"bpf_lsm_inode_create",
|
||||
"bpf_lsm_inode_link",
|
||||
"bpf_lsm_inode_symlink",
|
||||
"bpf_lsm_inode_mkdir",
|
||||
"bpf_lsm_inode_mknod",
|
||||
"bpf_lsm_inode_setattr") {
|
||||
const struct btf_type *type;
|
||||
int id;
|
||||
|
||||
id = sym_btf__find_by_name_kind(btf, name, BTF_KIND_FUNC);
|
||||
if (id <= 0)
|
||||
return false;
|
||||
|
||||
type = sym_btf__type_by_id(btf, id);
|
||||
if (!type)
|
||||
return false;
|
||||
|
||||
type = sym_btf__type_by_id(btf, type->type);
|
||||
if (!type || !btf_is_func_proto(type) || btf_vlen(type) == 0)
|
||||
return false;
|
||||
|
||||
type = sym_btf__type_by_id(btf, btf_params(type)[0].type);
|
||||
if (!type || !btf_is_ptr(type))
|
||||
return false;
|
||||
|
||||
/* Kernel 7.4 passes the idmap to the hooks as a const pointer. BTF records the const as a
|
||||
* separate type between the pointer and struct mnt_idmap. Without skipping it, we would
|
||||
* fall back to the path hooks on 7.4. */
|
||||
id = type->type;
|
||||
for (;;) {
|
||||
type = sym_btf__type_by_id(btf, id);
|
||||
if (!type)
|
||||
return false;
|
||||
if (!btf_is_mod(type))
|
||||
break;
|
||||
id = type->type;
|
||||
}
|
||||
|
||||
if (id != idmap_id)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool program_enabled(const char *name, bool inode_hooks) {
|
||||
if (streq(name, "userns_restrict_file_ioctl_compat"))
|
||||
return bpf_kernel_has_kfunc("bpf_lsm_file_ioctl_compat");
|
||||
|
||||
if (streq(name, "userns_restrict_path_rename"))
|
||||
return true;
|
||||
|
||||
if (startswith(name, "userns_restrict_inode_"))
|
||||
return inode_hooks;
|
||||
|
||||
if (startswith(name, "userns_restrict_path_") || streq(name, "userns_restrict_sb_kern_mount"))
|
||||
return !inode_hooks;
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj) {
|
||||
#if HAVE_VMLINUX_H
|
||||
userns_restrict_bpf__destroy(obj); /* this call is fine with NULL */
|
||||
@@ -69,11 +150,16 @@ int userns_restrict_install(
|
||||
if (!obj)
|
||||
return log_error_errno(errno, "Failed to open userns_restrict BPF object: %m");
|
||||
|
||||
/* Kernels without the compat ioctl hook cannot load a program attached to it. */
|
||||
r = sym_bpf_program__set_autoload(obj->progs.userns_restrict_file_ioctl_compat,
|
||||
bpf_kernel_has_kfunc("bpf_lsm_file_ioctl_compat"));
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to select compat ioctl BPF program: %m");
|
||||
bool inode_hooks = inode_hooks_supported();
|
||||
log_debug("Using %s hooks to restrict user namespace writes.", inode_hooks ? "inode" : "path");
|
||||
|
||||
for (int i = 0; i < obj->skeleton->prog_cnt; i++) {
|
||||
struct bpf_prog_skeleton *ps = obj->skeleton->progs + i;
|
||||
|
||||
r = sym_bpf_program__set_autoload(*ps->prog, program_enabled(ps->name, inode_hooks));
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to select user namespace restriction BPF program: %m");
|
||||
}
|
||||
|
||||
if (pin) {
|
||||
struct bpf_map *map;
|
||||
@@ -86,8 +172,8 @@ int userns_restrict_install(
|
||||
log_warning_errno(r, "Failed to create '%s', ignoring: %m", d);
|
||||
}
|
||||
|
||||
map = sym_bpf_object__next_map(obj->obj, /* map= */ NULL);
|
||||
while (map) {
|
||||
for (map = sym_bpf_object__next_map(obj->obj, /* map= */ NULL); map;
|
||||
map = sym_bpf_object__next_map(obj->obj, map)) {
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
|
||||
fn = path_join(MAP_LINK_PREFIX, sym_bpf_map__name(map));
|
||||
@@ -97,8 +183,6 @@ int userns_restrict_install(
|
||||
r = sym_bpf_map__set_pin_path(map, fn);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to set pin path to '%s': %m", fn);
|
||||
|
||||
map = sym_bpf_object__next_map(obj->obj, map);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,35 +219,52 @@ int userns_restrict_install(
|
||||
#endif
|
||||
}
|
||||
|
||||
#if HAVE_VMLINUX_H
|
||||
static char** unpin_links_and_free(char **paths) {
|
||||
STRV_FOREACH(p, paths)
|
||||
if (unlink(*p) < 0 && errno != ENOENT)
|
||||
log_debug_errno(errno, "Failed to unpin '%s', ignoring: %m", *p);
|
||||
|
||||
return strv_free(paths);
|
||||
}
|
||||
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(char**, unpin_links_and_free);
|
||||
#endif
|
||||
|
||||
int userns_restrict_attach(struct userns_restrict_bpf *obj, bool pin) {
|
||||
|
||||
#if HAVE_VMLINUX_H
|
||||
_cleanup_(rm_rf_physical_and_freep) char *staging = NULL;
|
||||
int r;
|
||||
|
||||
assert(obj);
|
||||
|
||||
/* If a program fails to attach, the programs attached before it stay attached, because their links
|
||||
* are pinned. The caller then drops its BPF state and no longer reaps dead namespaces from the maps.
|
||||
* Remove the pins created here, so that destroying the object detaches the programs. */
|
||||
_cleanup_(unpin_links_and_freep) char **pinned = NULL;
|
||||
/* Attach every program before we touch the pins. If a program fails to attach, the caller destroys
|
||||
* the object, which detaches the programs attached here. The programs that a previous run pinned
|
||||
* then stay attached and keep enforcing the policy. */
|
||||
for (int i = 0; i < obj->skeleton->prog_cnt; i++) {
|
||||
struct bpf_prog_skeleton *ps = obj->skeleton->progs + i;
|
||||
struct bpf_link *link;
|
||||
|
||||
if (sym_bpf_program__fd(*ps->prog) < 0)
|
||||
continue;
|
||||
|
||||
link = sym_bpf_program__attach(*ps->prog);
|
||||
r = bpf_get_error_translated(link);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to attach LSM BPF program %s: %m", ps->name);
|
||||
|
||||
*ps->link = link;
|
||||
log_debug("userns-restrict BPF-LSM program %s now attached.", ps->name);
|
||||
}
|
||||
|
||||
if (!pin)
|
||||
return 0;
|
||||
|
||||
staging = strdup(PROGRAM_LINK_PREFIX "-new");
|
||||
if (!staging)
|
||||
return log_oom();
|
||||
|
||||
/* A previous run may have exited before removing its staging directory. The active directory
|
||||
* still pins the complete policy, so removing the staging directory cannot remove protection. */
|
||||
r = rm_rf(staging, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to remove previous LSM attachment staging directory: %m");
|
||||
|
||||
if (mkdir(staging, 0755) < 0)
|
||||
return log_error_errno(errno, "Failed to create LSM attachment staging directory: %m");
|
||||
|
||||
for (int i = 0; i < obj->skeleton->prog_cnt; i++) {
|
||||
_cleanup_(bpf_link_freep) struct bpf_link *link = NULL;
|
||||
struct bpf_prog_skeleton *ps = obj->skeleton->progs + i;
|
||||
_cleanup_free_ char *fn = NULL;
|
||||
bool linked = false;
|
||||
const char *e;
|
||||
|
||||
if (sym_bpf_program__fd(*ps->prog) < 0)
|
||||
@@ -172,55 +273,21 @@ int userns_restrict_attach(struct userns_restrict_bpf *obj, bool pin) {
|
||||
e = startswith(ps->name, "userns_restrict_");
|
||||
assert(e);
|
||||
|
||||
if (pin) {
|
||||
fn = path_join(PROGRAM_LINK_PREFIX, e);
|
||||
if (!fn)
|
||||
return log_oom();
|
||||
fn = path_join(staging, e);
|
||||
if (!fn)
|
||||
return log_oom();
|
||||
|
||||
link = sym_bpf_link__open(fn);
|
||||
r = bpf_get_error_translated(link);
|
||||
if (r < 0) {
|
||||
if (r != -ENOENT)
|
||||
return log_error_errno(r, "Unable to open pinned program link: %m");
|
||||
link = NULL;
|
||||
} else {
|
||||
linked = true;
|
||||
log_debug("userns-restrict BPF-LSM program %s already attached.", ps->name);
|
||||
}
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
link = sym_bpf_program__attach(*ps->prog);
|
||||
r = bpf_get_error_translated(link);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to attach LSM BPF program: %m");
|
||||
|
||||
log_info("userns-restrict BPF-LSM program %s now attached.", ps->name);
|
||||
}
|
||||
|
||||
if (pin && !linked) {
|
||||
assert(fn);
|
||||
|
||||
r = sym_bpf_link__pin(link, fn);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to pin LSM attachment: %m");
|
||||
|
||||
if (strv_extend(&pinned, fn) < 0) {
|
||||
(void) unlink(fn);
|
||||
return log_oom();
|
||||
}
|
||||
}
|
||||
|
||||
*ps->link = TAKE_PTR(link);
|
||||
r = sym_bpf_link__pin(*ps->link, fn);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to pin LSM attachment '%s': %m", fn);
|
||||
}
|
||||
|
||||
pinned = strv_free(pinned);
|
||||
/* Replacing individual pins could remove protection if a later pin fails and the caller destroys
|
||||
* the object. The exchange preserves every old pin until the complete replacement is pinned. */
|
||||
if (renameat2(AT_FDCWD, staging, AT_FDCWD, PROGRAM_LINK_PREFIX, RENAME_EXCHANGE) < 0)
|
||||
return log_error_errno(errno, "Failed to replace pinned LSM attachments: %m");
|
||||
|
||||
/* Only now that our own programs enforce the policy, drop the ones the previous version left
|
||||
* pinned. Removing their links detaches them, so doing this any earlier would hand the namespaces
|
||||
* that predate an upgrade a stretch with no policy attached at all. */
|
||||
if (pin)
|
||||
(void) rm_rf(OBSOLETE_LINK_PREFIX, REMOVE_PHYSICAL);
|
||||
(void) rm_rf(OBSOLETE_LINK_PREFIX, REMOVE_PHYSICAL);
|
||||
|
||||
return 0;
|
||||
#else
|
||||
|
||||
@@ -59,6 +59,7 @@ DLSYM_PROTOTYPE(bpf_token_create) = missing_bpf_token_create;
|
||||
DLSYM_PROTOTYPE(btf__find_by_name_kind) = NULL;
|
||||
DLSYM_PROTOTYPE(btf__free) = NULL;
|
||||
DLSYM_PROTOTYPE(btf__load_vmlinux_btf) = NULL;
|
||||
DLSYM_PROTOTYPE(btf__type_by_id) = NULL;
|
||||
DLSYM_PROTOTYPE(libbpf_set_print) = NULL;
|
||||
DLSYM_PROTOTYPE(ring_buffer__epoll_fd) = NULL;
|
||||
DLSYM_PROTOTYPE(ring_buffer__free) = NULL;
|
||||
@@ -151,10 +152,10 @@ int dlopen_bpf(int log_level) {
|
||||
|
||||
/* Version-specific symbols: bpf_create_map exists only in libbpf < 1.0; bpf_map_create and
|
||||
* bpf_object__next_map only in 0.7+. bpf_token_create only in 1.5+. btf__load_vmlinux_btf only in
|
||||
* 0.5+; its two btf__* companions are older, but keep the whole probe trio optional so a missing
|
||||
* symbol degrades bpf_kernel_has_kfunc() instead of failing dlopen_bpf() entirely. Unresolved
|
||||
* prototypes keep their initializers (NULL, or a fallback returning -ENOSYS for
|
||||
* bpf_token_create). */
|
||||
* 0.5+. The other btf__* symbols are older, but we keep all of them optional. Callers that inspect
|
||||
* the kernel BTF, such as bpf_kernel_has_kfunc() and nsresourced, then report the feature as
|
||||
* missing instead of failing dlopen_bpf() entirely. Unresolved prototypes keep their initializers
|
||||
* (NULL, or a fallback returning -ENOSYS for bpf_token_create). */
|
||||
DLSYM_OPTIONAL(bpf_dl, bpf_create_map);
|
||||
DLSYM_OPTIONAL(bpf_dl, bpf_map_create);
|
||||
DLSYM_OPTIONAL(bpf_dl, bpf_object__next_map);
|
||||
@@ -162,6 +163,7 @@ int dlopen_bpf(int log_level) {
|
||||
DLSYM_OPTIONAL(bpf_dl, btf__find_by_name_kind);
|
||||
DLSYM_OPTIONAL(bpf_dl, btf__free);
|
||||
DLSYM_OPTIONAL(bpf_dl, btf__load_vmlinux_btf);
|
||||
DLSYM_OPTIONAL(bpf_dl, btf__type_by_id);
|
||||
|
||||
/* We set the print helper unconditionally. Otherwise libbpf will emit not useful log messages. */
|
||||
(void) sym_libbpf_set_print(bpf_print_func);
|
||||
@@ -191,8 +193,6 @@ int bpf_get_error_translated(const void *ptr) {
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct btf *, sym_btf__free, NULL);
|
||||
|
||||
bool bpf_kernel_has_kfunc(const char *name) {
|
||||
_cleanup_(sym_btf__freep) struct btf *btf = NULL;
|
||||
int id, r;
|
||||
|
||||
@@ -73,6 +73,9 @@ extern DLSYM_PROTOTYPE(bpf_token_create);
|
||||
extern DLSYM_PROTOTYPE(btf__find_by_name_kind);
|
||||
extern DLSYM_PROTOTYPE(btf__free);
|
||||
extern DLSYM_PROTOTYPE(btf__load_vmlinux_btf);
|
||||
extern DLSYM_PROTOTYPE(btf__type_by_id);
|
||||
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct btf*, sym_btf__free, NULL);
|
||||
extern DLSYM_PROTOTYPE(libbpf_set_print);
|
||||
extern DLSYM_PROTOTYPE(ring_buffer__epoll_fd);
|
||||
extern DLSYM_PROTOTYPE(ring_buffer__free);
|
||||
|
||||
Reference in new issue
Block a user