bpf-restrict-fsaccess: allow execution from overlayfs on signed dm-verity (#43223)

RestrictFileSystemAccess= currently denies execution from any overlayfs
  mount, even when every layer sits on a signed dm-verity device. The
enforcement hooks compare file->f_inode->i_sb->s_dev against the map of
  trusted devices and for a file on an overlay that's the overlay's own
  anonymous device, never the backing block device. So the fairly common
  image based setup where a signed DDI is the lower layer and a writable
  upper is stacked on top of it is out.

Kernel v7.2 grew the bpf_real_data_inode() kfunc which resolves a file
to
the inode hosting its data. Use it from the two enforcement hooks that
get
to see the union-level file. Execution through such an overlay then
works
  while anything whose data lives in an untrusted upper layer is still
  denied.

  All of it is gated on kernel support. The kfunc is a __weak __ksym and
every call sits behind a const volatile .rodata flag that PID1 only sets
when the kernel BTF advertises the function, so the object still loads
on
older kernels and keeps denying union filesystems exactly like before.
If
loading with resolution enabled fails anyway the loader retries with the
  flag off. A setup failure here is fatal to PID1 startup and I'd rather
  degrade than not boot.

  Two things worth pointing out. The mmap_file hook matters more than it
  looks like. binfmt_elf maps the PT_INTERP interpreter via vm_mmap() so
  ld.so is only ever checked there and never by bprm_check_security. And
  file_mprotect deliberately doesn't use the kfunc. It runs under
  mmap_write_lock where sleeping in d_real() would invert the documented
i_rwsem -> mmap_lock order, and it doesn't need it anyway since
ovl_mmap()
  has already installed the backing file into vma->vm_file by then.
  
   Known limitations:

    * With metacopy=on an untrusted upper layer can still own a trusted
      binary's mode, ownership and setuid bits since the resolution only
follows the data. Documented in the man page, just don't build that.

    * composefs with a separate data directory isn't covered. That needs
fs-verity digests and bpf_get_fsverity_digest() takes a struct file
      and gets the overlay inode, so it doesn't compose with this today.

    * erofs page cache sharing is denied. The inode comes from a pseudo
      filesystem whose s_dev is never in the verity map.

    * Multi-device filesystems like btrfs stay unsupported, as before.
    
    Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-08-11 14:31:25 +02:00
committed by GitHub
10 changed files with 354 additions and 61 deletions
+6
View File
@@ -60,6 +60,12 @@ CHANGES WITH 262:
allows a container to be started with systemd as PID1 without
installing any unit files.
* RestrictFileSystemAccess= now permits execution from overlayfs
mounts whose file data resides on a signed and verified
dm-verity-protected filesystem, while still denying files in a
writable upper layer. This requires kernel v7.2 or newer; on older
kernels execution from overlayfs mounts remains denied entirely.
Changes in systemd-firstboot:
* The "systemd.firstboot=" kernel command line option now accepts the
+6 -4
View File
@@ -553,10 +553,12 @@
shared libraries (<literal>mmap_file</literal>), and write-to-execute transitions such as JIT
compilation (<literal>file_mprotect</literal>).</para>
<para>Note that execution from overlayfs mounts is blocked even if the underlying layers reside on
signed dm-verity devices, because the BPF program sees the overlay filesystem's anonymous device
number rather than the underlying block device. Multi-device filesystems such as btrfs are similarly
unsupported.</para>
<para>Since v262, files on overlayfs mounts are resolved to the layer that hosts their data, so
execution from an overlay whose data resides on a signed dm-verity device is permitted, while files
in a writable upper layer are denied. This requires kernel v7.2 or newer; on older kernels execution from
overlayfs mounts is denied entirely. Note that only the file data is checked, so an untrusted upper
layer may still control a binary's ownership and setuid bits. Multi-device filesystems such as
btrfs are unsupported.</para>
<para>Note that, without further measures to secure the system, kexec can be used to circumvent this.</para>
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# bpftool was untangled in resolute; Debian ships it standalone everywhere
[TriggerMatch]
Distribution=debian
[TriggerMatch]
Distribution=ubuntu
Release=!jammy
Release=!noble
[Content]
Packages=bpftool
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# bpftool was untangled in resolute, needs linux-tools-generic in older releases
[TriggerMatch]
Distribution=ubuntu
Release=|jammy
Release=|noble
[Content]
Packages=linux-tools-generic
+99 -20
View File
@@ -18,6 +18,11 @@
* - bprm_check_security: blocks execve() from untrusted sources
* - mmap_file: blocks PROT_EXEC mmap from untrusted sources
* - file_mprotect: blocks W->X transitions from untrusted sources
*
* On kernels providing the bpf_real_data_inode() kfunc (v7.2+), files on
* union filesystems (overlayfs) are resolved to the layer hosting their
* data, so execution from overlays stacked on signed dm-verity devices
* works. Without the kfunc such files are denied, as before.
*/
/* If offsetof() is implemented via __builtin_offset() then it doesn't work on current compilers, since the
@@ -116,39 +121,100 @@ void BPF_PROG(restrict_fsaccess_bdev_free, struct block_device *bdev)
/* ---- Enforcement helpers ---- */
/* Check whether a file is from a trusted source.
* Returns 0 (allow) or -EPERM (deny). */
static __always_inline int check_trusted_file(struct file *file)
/* Set by PID1 between skeleton open and load (.rodata is read-only and
* frozen after load): true when the kernel provides the
* bpf_real_data_inode() kfunc (v7.2+). The verifier constant-folds the flag
* and removes the (then unresolvable) kfunc call on kernels without it. */
const volatile bool have_real_data_inode = false;
/* Returns the inode hosting a regular file's data: on union filesystems
* (overlayfs) the backing layer's inode rather than the union inode.
* Sleepable, hence only callable from lsm.s/ programs. Weak so that the
* object still loads on kernels that lack the kfunc. */
extern struct inode *bpf_real_data_inode(struct file *file) __weak __ksym;
/* Check whether a file's data lives on a trusted device.
* Returns 0 (allow) or -EPERM (deny).
*
* mnt_dev is the device of the filesystem the file was opened on, data_dev
* the device of the filesystem hosting its data; they differ only on union
* filesystems. The initramfs window compares mnt_dev: whether a file is on
* the initramfs is a question about the mounted filesystem — userspace
* recorded the mount's s_dev, which for a union-mount initrd is the union's
* anonymous device, not any backing device. The verity map is keyed by
* block device, so it is compared against data_dev. */
static __always_inline int check_trusted_dev(__u32 mnt_dev, __u32 data_dev)
{
__u32 s_dev;
__u8 *sig_valid;
BPF_CORE_READ_INTO(&s_dev, file, f_inode, i_sb, s_dev);
/* Check initramfs trust (active only during early boot) */
if (initramfs_s_dev != 0 && s_dev == initramfs_s_dev)
if (initramfs_s_dev != 0 && mnt_dev == initramfs_s_dev)
return 0;
/* Check verity device map */
sig_valid = bpf_map_lookup_elem(&verity_devices, &s_dev);
sig_valid = bpf_map_lookup_elem(&verity_devices, &data_dev);
if (sig_valid && *sig_valid)
return 0;
return -EPERM;
}
/* ---- Enforcement hooks ---- */
SEC("lsm/bprm_check_security")
int BPF_PROG(restrict_fsaccess_bprm_check, struct linux_binprm *bprm)
/* Check a file as the user sees it, resolving union filesystems to the
* layer hosting the data. May sleep — lsm.s/ programs only.
*
* The kfunc follows the data: a metacopy upper on an untrusted layer can
* still own the metadata (mode, ownership, setuid) of a binary whose data —
* and hence verdict — comes from the trusted lower device. */
static __always_inline int check_trusted_file(struct file *file)
{
struct file *file;
struct inode *inode;
__u32 mnt_dev, data_dev;
BPF_CORE_READ_INTO(&file, bprm, file);
return check_trusted_file(file);
BPF_CORE_READ_INTO(&mnt_dev, file, f_inode, i_sb, s_dev);
/* No kfunc: the mount device stands in, denying union filesystems. */
data_dev = mnt_dev;
if (have_real_data_inode) {
inode = bpf_real_data_inode(file);
if (!inode)
return -EPERM;
BPF_CORE_READ_INTO(&data_dev, inode, i_sb, s_dev);
}
return check_trusted_dev(mnt_dev, data_dev);
}
SEC("lsm/mmap_file")
/* Check vma->vm_file. By the time a vma exists, stacking filesystems have
* already installed the backing file into it (ovl_mmap() →
* backing_file_mmap() → vma_set_file()), so no union resolution is needed —
* and unlike re-resolving, vm_file names the file the pages actually come
* from even after a later copy-up. */
static __always_inline int check_trusted_vm_file(struct file *file)
{
__u32 s_dev;
BPF_CORE_READ_INTO(&s_dev, file, f_inode, i_sb, s_dev);
return check_trusted_dev(s_dev, s_dev);
}
/* ---- Enforcement hooks ---- */
SEC("lsm.s/bprm_check_security")
int BPF_PROG(restrict_fsaccess_bprm_check, struct linux_binprm *bprm)
{
/* Direct dereference (CO-RE-relocated via vmlinux.h's
* preserve_access_index) instead of bpf_probe_read(): only direct
* loads keep the verifier's trusted-pointer tracking. */
return check_trusted_file(bprm->file);
}
/* Covers more than plain mmap(): binfmt_elf maps the PT_INTERP interpreter
* via vm_mmap(), so ld.so is only ever checked here, not by
* bprm_check_security. */
SEC("lsm.s/mmap_file")
int BPF_PROG(restrict_fsaccess_mmap_file, struct file *file, unsigned long reqprot,
unsigned long prot, unsigned long flags)
{
@@ -156,13 +222,23 @@ int BPF_PROG(restrict_fsaccess_mmap_file, struct file *file, unsigned long reqpr
if (!(prot & PROT_EXEC))
return 0;
/* Anonymous executable mapping — no file backing, deny */
/* Anonymous executable mapping — no file backing, deny. The hook's
* file argument is __nullable since kernel v7.0 (94e948b7e684), so the
* verifier types it trusted-or-NULL and this check is mandatory:
* without it, handing file to bpf_real_data_inode() fails to load.
* Older kernels lack the annotation and drop the branch, but they lack
* the kfunc too, so it never sees a NULL file. */
if (!file)
return -EPERM;
return check_trusted_file(file);
}
/* Deliberately not sleepable: this hook runs under mmap_write_lock, where
* sleeping in d_real()/verity I/O could invert the documented i_rwsem →
* mmap_lock order (and bpf_lsm_file_mprotect is not in the kernel's
* sleepable_lsm_hooks set anyway). No resolution is needed here, see
* check_trusted_vm_file(). */
SEC("lsm/file_mprotect")
int BPF_PROG(restrict_fsaccess_file_mprotect, struct vm_area_struct *vma,
unsigned long reqprot, unsigned long prot)
@@ -180,12 +256,15 @@ int BPF_PROG(restrict_fsaccess_file_mprotect, struct vm_area_struct *vma,
if (vm_flags & VM_EXEC)
return 0;
/* Anonymous executable mapping — no file backing, deny */
BPF_CORE_READ_INTO(&file, vma, vm_file);
/* Anonymous executable mapping — no file backing, deny. Direct
* dereference, see restrict_fsaccess_bprm_check(). vm_file is in the
* verifier's BTF_TYPE_SAFE_TRUSTED_OR_NULL list, so this check is live
* and required. */
file = vma->vm_file;
if (!file)
return -EPERM;
return check_trusted_file(file);
return check_trusted_vm_file(file);
}
/* ---- PID1 ptrace protection ----
+88 -35
View File
@@ -109,60 +109,109 @@ static int get_root_s_dev(uint32_t *ret) {
return 0;
}
int bpf_restrict_fsaccess_prepare(struct restrict_fsaccess_bpf **ret) {
/* On failure sets *reterr_load_failed to true if the verifier rejected the object, which the caller may
* retry with resolution off, and to false for setup errors, which are fatal and already logged. */
static int restrict_fsaccess_load_variant(
bool compat,
bool have_real_data_inode,
struct restrict_fsaccess_bpf **ret,
bool *reterr_load_failed) {
_cleanup_(restrict_fsaccess_bpf_freep) struct restrict_fsaccess_bpf *obj = NULL;
int r;
assert(ret);
assert(reterr_load_failed);
/* Try the preferred version first — it reads the const void *value
* argument for defense-in-depth. On kernels before v6.16 (missing
* 1271a40eeafa) the verifier rejects loads from const void * context
* arguments, so we fall back to the _compat variant that only reads
* the size argument via raw ctx access. */
obj = restrict_fsaccess_bpf__open();
if (!obj)
if (!obj) {
*reterr_load_failed = false;
return log_error_errno(errno, "bpf-restrict-fsaccess: Failed to open BPF object: %m");
r = sym_bpf_map__set_max_entries(obj->maps.verity_devices, DMVERITY_DEVICES_MAX);
if (r < 0)
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to size hash table: %m");
r = sym_bpf_program__set_autoload(obj->progs.restrict_fsaccess_bdev_setintegrity_compat, false);
if (r < 0)
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to disable compat program: %m");
r = restrict_fsaccess_bpf__load(obj);
if (r >= 0) {
log_debug("bpf-restrict-fsaccess: Loaded with full const void * access.");
*ret = TAKE_PTR(obj);
return 0;
}
log_debug_errno(r, "bpf-restrict-fsaccess: Full version failed to load (%m), trying compat variant.");
obj = restrict_fsaccess_bpf_free(obj);
obj = restrict_fsaccess_bpf__open();
if (!obj)
return log_error_errno(errno, "bpf-restrict-fsaccess: Failed to reopen BPF object: %m");
r = sym_bpf_map__set_max_entries(obj->maps.verity_devices, DMVERITY_DEVICES_MAX);
if (r < 0)
if (r < 0) {
*reterr_load_failed = false;
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to size hash table: %m");
}
r = sym_bpf_program__set_autoload(obj->progs.restrict_fsaccess_bdev_setintegrity, false);
if (r < 0)
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to disable full program: %m");
r = sym_bpf_program__set_autoload(compat ? obj->progs.restrict_fsaccess_bdev_setintegrity
: obj->progs.restrict_fsaccess_bdev_setintegrity_compat, false);
if (r < 0) {
*reterr_load_failed = false;
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to disable %s bdev_setintegrity program: %m",
compat ? "full" : "compat");
}
/* The .rodata map is read-only and frozen once loaded — unlike the .bss globals, this flag can
* only be written between open and load. */
obj->rodata->have_real_data_inode = have_real_data_inode;
r = restrict_fsaccess_bpf__load(obj);
if (r < 0)
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to load BPF object (compat): %m");
if (r < 0) {
*reterr_load_failed = true;
return r;
}
log_debug("bpf-restrict-fsaccess: Loaded with compat bdev_setintegrity.");
*ret = TAKE_PTR(obj);
return 0;
}
int bpf_restrict_fsaccess_prepare(struct restrict_fsaccess_bpf **ret) {
static int cached_have_real_data_inode = -1;
int r;
assert(ret);
/* Whether the kernel provides the kfunc cannot change at runtime, and probing it parses the
* whole vmlinux BTF. */
if (cached_have_real_data_inode < 0)
cached_have_real_data_inode = bpf_kernel_has_kfunc("bpf_real_data_inode");
bool have_real_data_inode = cached_have_real_data_inode;
/* Try the preferred bdev_setintegrity variant first — it reads the const void *value argument
* for defense-in-depth. On kernels before v6.16 (missing 1271a40eeafa) the verifier rejects
* loads from const void * context arguments, so we fall back to the _compat variant that only
* reads the size argument via raw ctx access.
*
* Independently of that, union filesystem resolution is enabled when the kernel BTF advertises
* bpf_real_data_inode(). BTF only proves the function exists, not that it is registered as an
* LSM kfunc with the expected signature, so if loading with the resolution enabled fails, retry
* without it: files on union filesystems are then denied (fail closed) instead of the whole
* feature — and with it PID1 startup — failing. */
for (;;) {
bool load_failed;
r = restrict_fsaccess_load_variant(/* compat= */ false, have_real_data_inode, ret, &load_failed);
if (r >= 0) {
log_debug("bpf-restrict-fsaccess: Loaded with full const void * access%s.",
have_real_data_inode ? " and union filesystem resolution" : "");
return 0;
}
if (!load_failed)
return r;
log_debug_errno(r, "bpf-restrict-fsaccess: Full version failed to load (%m), trying compat variant.");
r = restrict_fsaccess_load_variant(/* compat= */ true, have_real_data_inode, ret, &load_failed);
if (r >= 0) {
log_debug("bpf-restrict-fsaccess: Loaded with compat bdev_setintegrity%s.",
have_real_data_inode ? " and union filesystem resolution" : "");
return 0;
}
if (!load_failed)
return r;
if (!have_real_data_inode)
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to load BPF object (compat): %m");
log_warning_errno(r, "bpf-restrict-fsaccess: Kernel BTF advertises bpf_real_data_inode() but loading with it failed, "
"retrying without union filesystem resolution: %m");
have_real_data_inode = false;
}
}
/* Partial deserialization (some FDs but not all) is fatal: continuing
* would leave enforcement incomplete. */
static int restrict_fsaccess_have_deserialized_fds(Manager *m) {
@@ -392,6 +441,10 @@ int bpf_restrict_fsaccess_setup(Manager *m) {
if (r < 0)
return r;
if (!obj->rodata->have_real_data_inode)
log_info("bpf-restrict-fsaccess: Kernel does not provide the bpf_real_data_inode() kfunc, "
"files on union filesystems are not resolved to their backing device and will be denied.");
/* If we're still in the initramfs, allow execution from it by recording
* its s_dev. After switch_root, PID1 re-execs and in_initrd() returns
* false — initramfs_s_dev stays at 0 (its default), closing the trust
+39 -2
View File
@@ -54,6 +54,9 @@ static int missing_bpf_token_create(int bpffs_fd, struct bpf_token_create_opts *
return -ENOSYS;
}
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(libbpf_set_print) = NULL;
DLSYM_PROTOTYPE(ring_buffer__epoll_fd) = NULL;
DLSYM_PROTOTYPE(ring_buffer__free) = NULL;
@@ -143,12 +146,18 @@ int dlopen_bpf(int log_level) {
"Neither libbpf.so.1 nor libbpf.so.0 are installed, cgroup BPF features disabled.");
/* 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+. Unresolved prototypes keep
* their initializers (NULL, or a fallback returning -ENOSYS for bpf_token_create). */
* 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). */
DLSYM_OPTIONAL(bpf_dl, bpf_create_map);
DLSYM_OPTIONAL(bpf_dl, bpf_map_create);
DLSYM_OPTIONAL(bpf_dl, bpf_object__next_map);
DLSYM_OPTIONAL(bpf_dl, bpf_token_create);
DLSYM_OPTIONAL(bpf_dl, btf__find_by_name_kind);
DLSYM_OPTIONAL(bpf_dl, btf__free);
DLSYM_OPTIONAL(bpf_dl, btf__load_vmlinux_btf);
/* We set the print helper unconditionally. Otherwise libbpf will emit not useful log messages. */
(void) sym_libbpf_set_print(bpf_print_func);
@@ -177,4 +186,32 @@ int bpf_get_error_translated(const void *ptr) {
return r;
}
}
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;
assert(name);
if (dlopen_bpf(LOG_DEBUG) < 0)
return false;
if (!sym_btf__load_vmlinux_btf || !sym_btf__find_by_name_kind || !sym_btf__free) {
log_debug("libbpf too old to probe the kernel BTF for functions, assuming %s() is unavailable.", name);
return false;
}
btf = sym_btf__load_vmlinux_btf();
r = bpf_get_error_translated(btf); /* libbpf < 1.0 returns error pointers, not NULL; btf__free() copes with both */
if (r != 0) {
log_debug_errno(r, "Failed to load vmlinux BTF, assuming kfunc %s() is unavailable: %m", name);
return false;
}
id = sym_btf__find_by_name_kind(btf, name, BTF_KIND_FUNC);
log_debug("Kernel BTF %s %s().", id > 0 ? "provides" : "lacks", name);
return id > 0;
}
#endif
+12
View File
@@ -12,6 +12,7 @@
#endif
#include <bpf/bpf.h> /* IWYU pragma: export */
#include <bpf/btf.h> /* IWYU pragma: export */
#include <bpf/libbpf.h> /* IWYU pragma: export */
#include "dlfcn-util.h"
@@ -21,6 +22,7 @@
* - bpf_map_create, bpf_object__next_map: available since libbpf 0.7
* - bpf_token_create: available since libbpf 1.5
* - bpf_create_map: removed in libbpf 1.0
* - btf__load_vmlinux_btf: available since libbpf 0.5
*/
DISABLE_WARNING_REDUNDANT_DECLS;
/* NOLINTBEGIN(readability-redundant-declaration) */
@@ -30,6 +32,7 @@ extern int bpf_create_map(enum bpf_map_type, int key_size, int value_size, int m
extern int bpf_map_create(enum bpf_map_type, const char *, __u32, __u32, __u32, const struct bpf_map_create_opts *);
extern struct bpf_map* bpf_object__next_map(const struct bpf_object *obj, const struct bpf_map *map);
extern int bpf_token_create(int bpffs_fd, struct bpf_token_create_opts *opts);
extern struct btf* btf__load_vmlinux_btf(void);
/* NOLINTEND(readability-redundant-declaration) */
REENABLE_WARNING;
@@ -65,6 +68,9 @@ extern DLSYM_PROTOTYPE(bpf_program__attach_lsm);
extern DLSYM_PROTOTYPE(bpf_program__name);
extern DLSYM_PROTOTYPE(bpf_program__set_autoload);
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(libbpf_set_print);
extern DLSYM_PROTOTYPE(ring_buffer__epoll_fd);
extern DLSYM_PROTOTYPE(ring_buffer__free);
@@ -92,6 +98,12 @@ static inline int compat_bpf_map_create(
* this helper instead of libbpf_get_error() to ensure some of the known ones are translated into errnos
* we understand. */
int bpf_get_error_translated(const void *ptr);
/* Probe the kernel's vmlinux BTF for a function of the given name, to check whether a BPF kfunc is
* available. This probes vmlinux BTF only, not module BTFs; and BTF advertising the function only proves
* it exists, not that it is registered as a kfunc for a given program type — callers must still handle
* program load failure. */
bool bpf_kernel_has_kfunc(const char *name);
#endif
int dlopen_bpf(int log_level) _dlopen_loader_;
+1
View File
@@ -137,6 +137,7 @@ static int do_attach(void) {
printf("VERITY_MAP_ID=%u\n", (unsigned) obj->bss->protected_map_id_verity);
printf("BSS_MAP_ID=%u\n", (unsigned) obj->bss->protected_map_id_bss);
printf("HAVE_REAL_DATA_INODE=%u\n", (unsigned) obj->rodata->have_real_data_inode);
/* Print comma-separated prog and link IDs for guard tests */
printf("PROG_IDS=\"");
@@ -79,6 +79,11 @@ at_exit() {
rm -rf /tmp/restrict-fsaccess-test
umount /tmp/restrict-fsaccess-baseline 2>/dev/null || true
rm -rf /tmp/restrict-fsaccess-baseline
# Clean up overlay test mounts
umount /tmp/restrict-fsaccess-overlay/merged 2>/dev/null || true
umount /tmp/restrict-fsaccess-overlay/rw 2>/dev/null || true
systemd-dissect --umount /tmp/restrict-fsaccess-overlay/lower 2>/dev/null || true
rm -rf /tmp/restrict-fsaccess-overlay
# Clean up background processes
[[ -n "${SLEEP_PID:-}" ]] && kill "$SLEEP_PID" 2>/dev/null || true
rm -rf /tmp/restrict-fsaccess-attach.out
@@ -195,6 +200,81 @@ else
echo "Verity keyring trust not available, skipping positive verity test"
fi
# ------ Test: overlayfs over a signed dm-verity lower layer ------
# Kernels with the bpf_real_data_inode() kfunc (v7.2+) resolve files on
# union filesystems to the layer hosting the data: execution through an
# overlay whose lower layer sits on a signed verity device is permitted,
# while files created in (or copied up to) an untrusted upper layer are
# denied. Without the kfunc, everything on the overlay is denied.
if command -v bpftool >/dev/null 2>&1; then
KERNEL_HAS_REAL_DATA_INODE=0
if bpftool btf dump file /sys/kernel/btf/vmlinux 2>/dev/null | grep "FUNC 'bpf_real_data_inode'" >/dev/null; then
KERNEL_HAS_REAL_DATA_INODE=1
fi
# The loader's BTF probe must agree with the kernel BTF
HAVE_RDI="$(sed -n 's/^HAVE_REAL_DATA_INODE=//p' /tmp/restrict-fsaccess-attach.out)"
if [[ "$HAVE_RDI" != "$KERNEL_HAS_REAL_DATA_INODE" ]]; then
echo "ERROR: Loader probed have_real_data_inode=$HAVE_RDI but kernel BTF says $KERNEL_HAS_REAL_DATA_INODE!" >&2
exit 1
fi
if machine_supports_verity_keyring; then
OVL=/tmp/restrict-fsaccess-overlay
mkdir -p "$OVL"/{lower,rw,merged}
systemd-dissect --mount "$MINIMAL.raw" "$OVL/lower"
mount -t tmpfs tmpfs "$OVL/rw"
mkdir -p "$OVL/rw/upper" "$OVL/rw/work"
# metacopy=off: a metadata-only copy-up would keep the data — and
# hence the verdict — on the lower layer, breaking the copy-up test
mount -t overlay overlay \
-o "lowerdir=$OVL/lower/usr,upperdir=$OVL/rw/upper,workdir=$OVL/rw/work,metacopy=off" \
"$OVL/merged"
if [[ "$KERNEL_HAS_REAL_DATA_INODE" == 1 ]]; then
# Resolution follows the data to the signed verity lower layer
"$OVL/merged/bin/bash" -c 'exit 0'
echo "Execution through overlay from signed verity lower: OK"
# A file created in the (tmpfs) upper layer is untrusted.
# Basename must stay "true" for multicall coreutils binaries —
# see the baseline comment above.
cp /usr/bin/true "$OVL/merged/true"
if "$OVL/merged/true" 2>/dev/null; then
echo "ERROR: Execution of upper-layer file should have been blocked!" >&2
exit 1
fi
echo "Execution from overlay upper layer blocked: OK"
# Copy-up moves the data to the untrusted upper layer
chmod 700 "$OVL/merged/bin/bash"
if "$OVL/merged/bin/bash" -c 'exit 0' 2>/dev/null; then
echo "ERROR: Execution of copied-up file should have been blocked!" >&2
exit 1
fi
echo "Execution of copied-up file blocked: OK"
else
# Without the kfunc all the program sees is the overlay's
# anonymous device: deny, even though the lower is trusted
if "$OVL/merged/bin/bash" -c 'exit 0' 2>/dev/null; then
echo "ERROR: Execution through overlay should have been blocked without bpf_real_data_inode!" >&2
exit 1
fi
echo "Execution through overlay blocked (no bpf_real_data_inode): OK"
fi
umount "$OVL/merged"
umount "$OVL/rw"
systemd-dissect --umount "$OVL/lower"
rm -rf "$OVL"
else
echo "Verity keyring trust not available, skipping overlay tests"
fi
else
echo "bpftool not available, skipping overlay tests"
fi
# ------ Test: Guard blocks non-PID1 from obtaining BPF object FDs by ID ------
if command -v bpftool >/dev/null 2>&1 && [[ -n "${VERITY_MAP_ID:-}" ]]; then