diff --git a/README b/README index 6a151a49e9..3811abfe06 100644 --- a/README +++ b/README @@ -35,7 +35,7 @@ REQUIREMENTS: Linux kernel >= 4.17 for cgroup-bpf socket address hooks Linux kernel >= 5.3 for bounded-loops in BPF program Linux kernel >= 5.4 for signed Verity images support - Linux kernel >= 5.7 for BPF links + Linux kernel >= 5.7 for BPF links and the BPF LSM hook Kernel Config Options: CONFIG_DEVTMPFS @@ -119,6 +119,13 @@ REQUIREMENTS: Required for signed Verity images support: CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG + Required for RestrictFileSystems= in service units: + CONFIG_BPF + CONFIG_BPF_SYSCALL + CONFIG_BPF_LSM + CONFIG_DEBUG_INFO_BTF + CONFIG_LSM="...,bpf" or kernel booted with lsm="...,bpf". + We recommend to turn off Real-Time group scheduling in the kernel when using systemd. RT group scheduling effectively makes RT scheduling unavailable for most userspace, since it diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index bede222670..0e1b5da333 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -2837,6 +2837,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly t RestrictNamespaces = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (bas) RestrictFileSystems = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(ssbt) BindPaths = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(ssbt) BindReadOnlyPaths = [...]; @@ -3362,6 +3364,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -3966,6 +3970,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -4677,6 +4683,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly t RestrictNamespaces = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (bas) RestrictFileSystems = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(ssbt) BindPaths = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(ssbt) BindReadOnlyPaths = [...]; @@ -5228,6 +5236,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + @@ -5828,6 +5838,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + @@ -6436,6 +6448,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly t RestrictNamespaces = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (bas) RestrictFileSystems = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(ssbt) BindPaths = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(ssbt) BindReadOnlyPaths = [...]; @@ -6915,6 +6929,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + @@ -7433,6 +7449,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + @@ -8162,6 +8180,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly t RestrictNamespaces = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (bas) RestrictFileSystems = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(ssbt) BindPaths = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(ssbt) BindReadOnlyPaths = [...]; @@ -8627,6 +8647,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + @@ -9131,6 +9153,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index fb872c4990..87e1536986 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -88,6 +88,12 @@ syscall-filter SET… + + systemd-analyze + OPTIONS + filesystems + SET… + systemd-analyze OPTIONS @@ -410,6 +416,14 @@ Conditions succeeded. SET must include the @ prefix. + + <command>systemd-analyze filesystems <optional><replaceable>SET</replaceable>...</optional></command> + + This command will list filesystems in the specified filesystem set + SET, or all known sets if no sets are specified. Argument + SET must include the @ prefix. + + <command>systemd-analyze calendar <replaceable>EXPRESSION</replaceable>...</command> diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 3cef36d3c3..2b7129eb40 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -1842,6 +1842,100 @@ BindReadOnlyPaths=/var/lib/systemd logging. + + RestrictFileSystems= + + Restricts the set of filesystems processes of this unit can open files on. Takes a space-separated + list of filesystem names. Any filesystem listed is made accessible to the unit's processes, access to filesystem + types not listed is prohibited (allow-listing). If the first character of the list is ~, the + effect is inverted: access to the filesystems listed is prohibited (deny-listing). If the empty string is assigned, + access to filesystems is not restricted. + + If you specify both types of this option (i.e. allow-listing and deny-listing), the first encountered will take + precedence and will dictate the default action (allow access to the filesystem or deny it). Then the next occurrences + of this option will add or delete the listed filesystems from the set of the restricted filesystems, depending on its + type and the default action. + + Example: if a unit has the following, + RestrictFileSystems=ext4 tmpfs +RestrictFileSystems=ext2 ext4 + then access to ext4, tmpfs, and ext2 is allowed + and access to other filesystems is denied. + + Example: if a unit has the following, + RestrictFileSystems=ext4 tmpfs +RestrictFileSystems=~ext4 + then only access tmpfs is allowed. + + Example: if a unit has the following, + RestrictFileSystems=~ext4 tmpfs +RestrictFileSystems=ext4 + then only access to tmpfs is denied. + + As the number of possible filesystems is large, predefined sets of filesystems are provided. A set + starts with @ character, followed by name of the set. + + + Currently predefined filesystem sets + + + + + + + Set + Description + + + + + @basic-api + Basic filesystem API. + + + @auxiliary-api + Auxiliary filesystem API. + + + @common-block + Common block device filesystems. + + + @historical-block + Historical block device filesystems. + + + @network + Well-known network filesystems. + + + @privileged-api + Privileged filesystem API. + + + @temporary + Temporary filesystems: tmpfs, ramfs. + + + @known + All known filesystems defined by the kernel. This list is defined statically in systemd based on a kernel + version that was available when this systemd version was released. It will become progressively more + out-of-date as the kernel is updated. + + + +
+ + Use + systemd-analyze1's + filesystems command to retrieve a list of filesystems defined on the local + system. + + Note that this setting might not be supported on some systems (for example if the LSM eBPF hook is + not enabled in the underlying kernel or if not using the unified control group hierarchy). In that case this setting + has no effect.
+
+ RestrictNamespaces= @@ -3761,6 +3855,11 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy EXIT_CREDENTIALS Failed to set up unit's credentials. See LoadCredential= and SetCredential= above. + + 245 + EXIT_BPF + Failed to apply BPF restrictions. See RestrictFileSystems= above. + diff --git a/mkosi.default.d/arch/mkosi.arch b/mkosi.default.d/arch/mkosi.arch index f584d2d126..19bc45a8d7 100644 --- a/mkosi.default.d/arch/mkosi.arch +++ b/mkosi.default.d/arch/mkosi.arch @@ -12,6 +12,7 @@ Distribution=arch BuildPackages= acl bzip2 + clang cryptsetup curl dbus @@ -26,6 +27,7 @@ BuildPackages= inetutils iptables kmod + libbpf libcap libgcrypt libidn2 @@ -34,6 +36,7 @@ BuildPackages= libutil-linux libxkbcommon libxslt + llvm lz4 meson pam @@ -48,6 +51,7 @@ BuildPackages= Packages= gdb + libbpf libidn2 nano qrencode diff --git a/mkosi.default.d/debian/mkosi.debian b/mkosi.default.d/debian/mkosi.debian index 9bd504bf57..2be71f3c7f 100644 --- a/mkosi.default.d/debian/mkosi.debian +++ b/mkosi.default.d/debian/mkosi.debian @@ -10,9 +10,11 @@ Release=unstable [Packages] BuildPackages= acl + clang docbook-xml docbook-xsl gcc + g++ gettext git gnu-efi @@ -20,6 +22,7 @@ BuildPackages= libacl1-dev libaudit-dev libblkid-dev + libbpf-dev libbz2-dev libcap-dev libcryptsetup-dev @@ -46,6 +49,7 @@ BuildPackages= libtss2-dev libxkbcommon-dev libzstd-dev + llvm meson pkg-config python3 @@ -59,6 +63,7 @@ BuildPackages= Packages= gdb + libbpf0 libfdisk1 libfido2-1 libidn2-0 diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 1410880be1..05b4afda54 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -28,6 +28,7 @@ #include "exit-status.h" #include "fd-util.h" #include "fileio.h" +#include "filesystems.h" #include "format-table.h" #include "glob-util.h" #include "hashmap.h" @@ -46,6 +47,7 @@ #endif #include "sort-util.h" #include "special.h" +#include "stat-util.h" #include "string-table.h" #include "strv.h" #include "strxcpyx.h" @@ -1622,6 +1624,9 @@ static int load_kernel_syscalls(Set **ret) { static void syscall_set_remove(Set *s, const SyscallFilterSet *set) { const char *syscall; + if (!set) + return; + NULSTR_FOREACH(syscall, set->value) { if (syscall[0] == '@') continue; @@ -1743,6 +1748,172 @@ static int dump_syscall_filters(int argc, char *argv[], void *userdata) { } #endif +static int load_available_kernel_filesystems(Set **ret) { + _cleanup_set_free_ Set *filesystems = NULL; + int r; + char *t; + + assert(ret); + + /* Let's read the available filesystems */ + + r = read_virtual_file("/proc/filesystems", SIZE_MAX, &t, NULL); + if (r < 0) + return r; + + for (int i = 0;;) { + _cleanup_free_ char *line = NULL; + const char *p; + + r = string_extract_line(t, i++, &line); + if (r < 0) + return log_oom(); + if (r == 0) + break; + + if (!line) + line = t; + + p = strchr(line, '\t'); + if (!p) + continue; + + p += strspn(p, WHITESPACE); + + r = set_put_strdup(&filesystems, p); + if (r < 0) + return log_error_errno(r, "Failed to add filesystem to list: %m"); + } + + *ret = TAKE_PTR(filesystems); + return 0; +} + +static void filesystem_set_remove(Set *s, const FilesystemSet *set) { + const char *filesystem; + + NULSTR_FOREACH(filesystem, set->value) { + if (filesystem[0] == '@') + continue; + + free(set_remove(s, filesystem)); + } +} + +static void dump_filesystem(const FilesystemSet *set) { + const char *filesystem; + + if (!set) + return; + + printf("%s%s%s\n" + " # %s\n", + ansi_highlight(), + set->name, + ansi_normal(), + set->help); + + NULSTR_FOREACH(filesystem, set->value) + printf(" %s%s%s\n", filesystem[0] == '@' ? ansi_underline() : "", filesystem, ansi_normal()); +} + +static int dump_filesystems(int argc, char *argv[], void *userdata) { + bool first = true; + +#if ! HAVE_LIBBPF + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Not compiled with libbpf support, sorry."); +#endif + + (void) pager_open(arg_pager_flags); + + if (strv_isempty(strv_skip(argv, 1))) { + _cleanup_set_free_ Set *kernel = NULL, *known = NULL; + const char *fs; + int k; + + NULSTR_FOREACH(fs, filesystem_sets[FILESYSTEM_SET_KNOWN].value) + if (set_put_strdup(&known, fs) < 0) + return log_oom(); + + k = load_available_kernel_filesystems(&kernel); + + for (FilesystemGroups i = 0; i < _FILESYSTEM_SET_MAX; i++) { + const FilesystemSet *set = filesystem_sets + i; + if (!first) + puts(""); + + dump_filesystem(set); + filesystem_set_remove(kernel, set); + if (i != FILESYSTEM_SET_KNOWN) + filesystem_set_remove(known, set); + first = false; + } + + if (!set_isempty(known)) { + _cleanup_free_ char **l = NULL; + char **filesystem; + + printf("\n" + "# %sUngrouped filesystems%s (known but not included in any of the groups except @known):\n", + ansi_highlight(), ansi_normal()); + + l = set_get_strv(known); + if (!l) + return log_oom(); + + strv_sort(l); + + STRV_FOREACH(filesystem, l) + printf("# %s\n", *filesystem); + } + + if (k < 0) { + fputc('\n', stdout); + fflush(stdout); + log_notice_errno(k, "# Not showing unlisted filesystems, couldn't retrieve kernel filesystem list: %m"); + } else if (!set_isempty(kernel)) { + _cleanup_free_ char **l = NULL; + char **filesystem; + + printf("\n" + "# %sUnlisted filesystems%s (available to the local kernel, but not included in any of the groups listed above):\n", + ansi_highlight(), ansi_normal()); + + l = set_get_strv(kernel); + if (!l) + return log_oom(); + + strv_sort(l); + + STRV_FOREACH(filesystem, l) + printf("# %s\n", *filesystem); + } + } else { + char **name; + + STRV_FOREACH(name, strv_skip(argv, 1)) { + const FilesystemSet *set; + + if (!first) + puts(""); + + set = filesystem_set_find(*name); + if (!set) { + /* make sure the error appears below normal output */ + fflush(stdout); + + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "Filesystem set \"%s\" not found.", *name); + } + + dump_filesystem(set); + first = false; + } + } + + return 0; +} + static void parsing_hint(const char *p, bool calendar, bool timestamp, bool timespan) { if (calendar && calendar_spec_from_string(p, NULL) >= 0) log_notice("Hint: this expression is a valid calendar specification. " @@ -2169,6 +2340,7 @@ static int help(int argc, char *argv[], void *userdata) { " capability [CAP...] List capability definitions\n" " syscall-filter [NAME...] Print list of syscalls in seccomp\n" " filter\n" + " filesystems [NAME...] Print list of filesystems\n" " condition CONDITION... Evaluate conditions and asserts\n" " verify FILE... Check unit files for correctness\n" " calendar SPEC... Validate repetitive calendar time\n" @@ -2503,6 +2675,7 @@ static int run(int argc, char *argv[]) { { "exit-status", VERB_ANY, VERB_ANY, 0, dump_exit_status }, { "syscall-filter", VERB_ANY, VERB_ANY, 0, dump_syscall_filters }, { "capability", VERB_ANY, VERB_ANY, 0, dump_capabilities }, + { "filesystems", VERB_ANY, VERB_ANY, 0, dump_filesystems }, { "condition", VERB_ANY, VERB_ANY, 0, do_condition }, { "verify", 2, VERB_ANY, 0, do_verify }, { "calendar", 2, VERB_ANY, 0, test_calendar }, diff --git a/src/basic/cgroup-util.c b/src/basic/cgroup-util.c index 9ba9f375b0..221157d57a 100644 --- a/src/basic/cgroup-util.c +++ b/src/basic/cgroup-util.c @@ -1367,6 +1367,29 @@ int cg_pid_get_machine_name(pid_t pid, char **machine) { return cg_path_get_machine_name(cgroup, machine); } +int cg_path_get_cgroupid(const char *path, uint64_t *ret) { + int mnt_id = -1; + + assert(path); + assert(ret); + + union { + struct file_handle f_handle; + uint8_t space[offsetof(struct file_handle, f_handle) + sizeof(uint64_t)]; + } buf = { + .f_handle.handle_bytes = sizeof(uint64_t), + }; + + /* This is cgroupfs so we know the size of the handle, thus no need to loop around like + * name_to_handle_at_loop() does in mountpoint-util.c */ + if (name_to_handle_at(AT_FDCWD, path, &buf.f_handle, &mnt_id, 0) < 0) + return -errno; + + *ret = *(uint64_t *) buf.f_handle.f_handle; + + return 0; +} + int cg_path_get_session(const char *path, char **session) { _cleanup_free_ char *unit = NULL; char *start, *end; diff --git a/src/basic/cgroup-util.h b/src/basic/cgroup-util.h index 90ccd2c032..43801ee0f4 100644 --- a/src/basic/cgroup-util.h +++ b/src/basic/cgroup-util.h @@ -33,6 +33,9 @@ typedef enum CGroupController { CGROUP_CONTROLLER_BPF_FOREIGN, CGROUP_CONTROLLER_BPF_SOCKET_BIND, CGROUP_CONTROLLER_BPF_RESTRICT_NETWORK_INTERFACES, + /* The BPF hook implementing RestrictFileSystems= is not defined here. + * It's applied as late as possible in exec_child() so we don't block + * our own unit setup code. */ _CGROUP_CONTROLLER_MAX, _CGROUP_CONTROLLER_INVALID = -EINVAL, @@ -236,6 +239,7 @@ int cg_is_empty_recursive(const char *controller, const char *path); int cg_get_root_path(char **path); +int cg_path_get_cgroupid(const char *path, uint64_t *ret); int cg_path_get_session(const char *path, char **session); int cg_path_get_owner_uid(const char *path, uid_t *uid); int cg_path_get_unit(const char *path, char **unit); diff --git a/src/basic/check-filesystems.sh b/src/basic/check-filesystems.sh new file mode 100755 index 0000000000..026574652e --- /dev/null +++ b/src/basic/check-filesystems.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eu +set -o pipefail + +cpp="$1" +filesystems_gperf="$2" +shift 2 + +includes="" +for i in "$@"; do + includes="$includes -include $i" +done + +error=false + +# shellcheck disable=SC2086 +for fs in $($cpp -dM $includes - /dev/null; then + # STACK_END_MAGIC doesn't refer to a filesystem + # mtd_inode was removed in 2015 + # futexfs was removed in 2018 + if [[ "$fs" =~ ^(STACK_END_MAGIC|MTD_INODE_FS_MAGIC|FUTEXFS_SUPER_MAGIC)$ ]]; then + continue + fi + echo "Filesystem found in kernel header but not in $(basename "$filesystems_gperf"): $fs"; + error=true + fi +done + +if $error; then + exit 1 +fi diff --git a/src/basic/filesystems-gperf.gperf b/src/basic/filesystems-gperf.gperf new file mode 100644 index 0000000000..f31c9de192 --- /dev/null +++ b/src/basic/filesystems-gperf.gperf @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +%{ +#include + +#include "filesystems.h" +#include "missing_magic.h" +#include "stat-util.h" + +struct FilesystemMagic { + const char *name; + statfs_f_type_t magic[FILESYSTEM_MAGIC_MAX]; +}; +%} +struct FilesystemMagic; +%language=ANSI-C +%define hash-function-name filesystems_gperf_hash +%define lookup-function-name filesystems_gperf_lookup +%define slot-name name +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +apparmorfs, {AAFS_MAGIC} +adfs, {ADFS_SUPER_MAGIC} +affs, {AFFS_SUPER_MAGIC} +afs, {AFS_FS_MAGIC, AFS_SUPER_MAGIC} +anon_inodefs, {ANON_INODE_FS_MAGIC} +autofs, {AUTOFS_SUPER_MAGIC} +balloon-kvm, {BALLOON_KVM_MAGIC} +bdev, {BDEVFS_MAGIC} +binder, {BINDERFS_SUPER_MAGIC} +binfmt_misc, {BINFMTFS_MAGIC} +bpf, {BPF_FS_MAGIC} +btrfs, {BTRFS_SUPER_MAGIC} +btrfs_test_fs, {BTRFS_TEST_MAGIC} +ceph, {CEPH_SUPER_MAGIC} +cgroup2, {CGROUP2_SUPER_MAGIC} +cgroup, {CGROUP_SUPER_MAGIC} +cifs, {CIFS_MAGIC_NUMBER} +coda, {CODA_SUPER_MAGIC} +configfs, {CONFIGFS_MAGIC} +cramfs, {CRAMFS_MAGIC} +dax, {DAXFS_MAGIC} +debugfs, {DEBUGFS_MAGIC} +devmem, {DEVMEM_MAGIC} +devpts, {DEVPTS_SUPER_MAGIC} +dmabuf, {DMA_BUF_MAGIC} +ecryptfs, {ECRYPTFS_SUPER_MAGIC} +efivarfs, {EFIVARFS_MAGIC} +efs, {EFS_SUPER_MAGIC} +erofs, {EROFS_SUPER_MAGIC_V1} +ext2, {EXT2_SUPER_MAGIC} +ext3, {EXT3_SUPER_MAGIC} +ext4, {EXT4_SUPER_MAGIC} +exfat, {EXFAT_SUPER_MAGIC} +f2fs, {F2FS_SUPER_MAGIC} +fuseblk, {FUSE_SUPER_MAGIC} +fuse, {FUSE_SUPER_MAGIC} +fusectl, {FUSE_CTL_SUPER_MAGIC} +gfs, {GFS2_MAGIC} +gfs2, {GFS2_MAGIC} +hostfs, {HOSTFS_SUPER_MAGIC} +hpfs, {HPFS_SUPER_MAGIC} +hugetlbfs, {HUGETLBFS_MAGIC} +iso9660, {ISOFS_SUPER_MAGIC} +jffs2, {JFFS2_SUPER_MAGIC} +minix, {MINIX_SUPER_MAGIC, MINIX_SUPER_MAGIC2, MINIX2_SUPER_MAGIC, MINIX2_SUPER_MAGIC2, MINIX3_SUPER_MAGIC} +mqueue, {MQUEUE_MAGIC} +msdos, {MSDOS_SUPER_MAGIC} +ncp, {NCP_SUPER_MAGIC} +ncpfs, {NCP_SUPER_MAGIC} +nfs, {NFS_SUPER_MAGIC} +nfs4, {NFS_SUPER_MAGIC} +nilfs2, {NILFS_SUPER_MAGIC} +nsfs, {NSFS_MAGIC} +ocfs2, {OCFS2_SUPER_MAGIC} +openpromfs, {OPENPROM_SUPER_MAGIC} +orangefs, {ORANGEFS_DEVREQ_MAGIC} +overlay, {OVERLAYFS_SUPER_MAGIC} +pipefs, {PIPEFS_MAGIC} +ppc-cmm, {PPC_CMM_MAGIC} +proc, {PROC_SUPER_MAGIC} +pstore, {PSTOREFS_MAGIC} +pvfs2, {ORANGEFS_DEVREQ_MAGIC} +qnx4, {QNX4_SUPER_MAGIC} +qnx6, {QNX6_SUPER_MAGIC} +ramfs, {RAMFS_MAGIC} +resctrl, {RDTGROUP_SUPER_MAGIC} +reiserfs, {REISERFS_SUPER_MAGIC} +secretmem, {SECRETMEM_MAGIC} +securityfs, {SECURITYFS_MAGIC} +selinuxfs, {SELINUX_MAGIC} +shiftfs, {SHIFTFS_MAGIC} +smackfs, {SMACK_MAGIC} +smb3, {SMB_SUPER_MAGIC} +smbfs, {SMB_SUPER_MAGIC} +sockfs, {SOCKFS_MAGIC} +squashfs, {SQUASHFS_MAGIC} +sysfs, {SYSFS_MAGIC} +tmpfs, {TMPFS_MAGIC} +tracefs, {TRACEFS_MAGIC} +udf, {UDF_SUPER_MAGIC} +usbdevfs, {USBDEVICE_SUPER_MAGIC} +vboxsf, {VBOXSF_SUPER_MAGIC} +vfat, {MSDOS_SUPER_MAGIC} +v9fs, {V9FS_MAGIC} +xenfs, {XENFS_SUPER_MAGIC} +xfs, {XFS_SUPER_MAGIC} +z3fold, {Z3FOLD_MAGIC} +zonefs, {ZONEFS_MAGIC} +zsmalloc, {ZSMALLOC_MAGIC} diff --git a/src/basic/filesystems.c b/src/basic/filesystems.c new file mode 100644 index 0000000000..9fccea2018 --- /dev/null +++ b/src/basic/filesystems.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "filesystems-gperf.h" + +int fs_type_from_string(const char *name, const statfs_f_type_t **ret) { + const struct FilesystemMagic *fs_magic; + + assert(name); + assert(ret); + + fs_magic = filesystems_gperf_lookup(name, strlen(name)); + if (!fs_magic) + return -EINVAL; + + *ret = fs_magic->magic; + + return 0; +} + +int fs_in_group(const struct statfs *s, FilesystemGroups fs_group) { + const char *fs; + int r; + + NULSTR_FOREACH(fs, filesystem_sets[fs_group].value) { + const statfs_f_type_t *magic; + + r = fs_type_from_string(fs, &magic); + if (r == 0) { + for (size_t i = 0; i < FILESYSTEM_MAGIC_MAX; i++) { + if (magic[i] == 0) + break; + + if (is_fs_type(s, magic[i])) + return true; + } + } + } + + return false; +} + +const FilesystemSet filesystem_sets[_FILESYSTEM_SET_MAX] = { + [FILESYSTEM_SET_BASIC_API] = { + .name = "@basic-api", + .help = "Basic filesystem API", + .value = + "cgroup\0" + "cgroup2\0" + "devpts\0" + "mqueue\0" + "proc\0" + "sysfs\0" + }, + [FILESYSTEM_SET_AUXILIARY_API] = { + .name = "@auxiliary-api", + .help = "Auxiliary filesystem API", + .value = + "configfs\0" + "efivarfs\0" + "fusectl\0" + "hugetlbfs\0" + "securityfs\0" + }, + [FILESYSTEM_SET_COMMON_BLOCK] = { + .name = "@common-block", + .help = "Common block device filesystems", + .value = + "btrfs\0" + "ext4\0" + "vfat\0" + "xfs\0" + }, + [FILESYSTEM_SET_HISTORICAL_BLOCK] = { + .name = "@historical-block", + .help = "Historical block device filesystems", + .value = + "ext2\0" + "ext3\0" + "minix\0" + }, + [FILESYSTEM_SET_NETWORK] = { + .name = "@network", + .help = "Well-known network filesystems", + .value = + "afs\0" + "cifs\0" + "gfs\0" + "gfs2\0" + "ncpfs\0" + "ncp\0" + "nfs\0" + "nfs4\0" + "ocfs2\0" + "pvfs2\0" + "smb3\0" + "smbfs\0" + }, + [FILESYSTEM_SET_PRIVILEGED_API] = { + .name = "@privileged-api", + .help = "Privileged filesystem API", + .value = + "bpf\0" + "debugfs\0" + "pstore\0" + "tracefs\0" + }, + [FILESYSTEM_SET_TEMPORARY] = { + .name = "@temporary", + .help = "Temporary filesystems", + .value = + "ramfs\0" + "tmpfs\0" + }, + [FILESYSTEM_SET_KNOWN] = { + .name = "@known", + .help = "All known filesystems declared in the kernel", + .value = +#include "filesystem-list.h" + }, +}; + +const FilesystemSet *filesystem_set_find(const char *name) { + if (isempty(name) || name[0] != '@') + return NULL; + + for (FilesystemGroups i = 0; i < _FILESYSTEM_SET_MAX; i++) + if (streq(filesystem_sets[i].name, name)) + return filesystem_sets + i; + + return NULL; +} diff --git a/src/basic/filesystems.h b/src/basic/filesystems.h new file mode 100644 index 0000000000..a0964476af --- /dev/null +++ b/src/basic/filesystems.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "nulstr-util.h" +#include "stat-util.h" +#include "string-util.h" + +#define FILESYSTEM_MAGIC_MAX 10 + +typedef enum FilesystemGroups { + /* Please leave BASIC_API first and KNOWN last, but sort the rest alphabetically */ + FILESYSTEM_SET_BASIC_API, + FILESYSTEM_SET_AUXILIARY_API, + FILESYSTEM_SET_COMMON_BLOCK, + FILESYSTEM_SET_HISTORICAL_BLOCK, + FILESYSTEM_SET_NETWORK, + FILESYSTEM_SET_PRIVILEGED_API, + FILESYSTEM_SET_TEMPORARY, + FILESYSTEM_SET_KNOWN, + _FILESYSTEM_SET_MAX, + _FILESYSTEM_SET_INVALID = -EINVAL, +} FilesystemGroups; + +typedef struct FilesystemSet { + const char *name; + const char *help; + const char *value; +} FilesystemSet; + +extern const FilesystemSet filesystem_sets[]; + +const FilesystemSet *filesystem_set_find(const char *name); + +int fs_type_from_string(const char *name, const statfs_f_type_t **ret); +int fs_in_group(const struct statfs *s, enum FilesystemGroups fs_group); + +/* gperf prototypes */ +const struct FilesystemMagic* filesystems_gperf_lookup(const char *key, GPERF_LEN_TYPE length); diff --git a/src/basic/generate-filesystem-list.py b/src/basic/generate-filesystem-list.py new file mode 100755 index 0000000000..8271b3fbeb --- /dev/null +++ b/src/basic/generate-filesystem-list.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1-or-later + +import sys + +keywords_section = False + +for line in open(sys.argv[1]): + if keywords_section: + print('"{}\\0"'.format(line.split(',')[0].strip())) + elif line.startswith('%%'): + keywords_section = True diff --git a/src/basic/meson.build b/src/basic/meson.build index ade45bdfc2..3bc5196fa0 100644 --- a/src/basic/meson.build +++ b/src/basic/meson.build @@ -54,6 +54,8 @@ basic_sources = files(''' fd-util.h fileio.c fileio.h + filesystems.c + filesystems.h format-util.c format-util.h fs-util.c @@ -373,6 +375,34 @@ run_target( ############################################################ +filesystem_includes = ['linux/magic.h', + 'linux/gfs2_ondisk.h'] + +check_filesystems = find_program('check-filesystems.sh') +r = run_command([check_filesystems, cpp, 'filesystems-gperf.gperf'] + filesystem_includes) +if r.returncode() != 0 + error('found unknown filesystem(s) defined in kernel headers:\n\n' + r.stdout()) + r.stdout() +endif + +filesystems_gperf_h = custom_target( + 'filesystems-gperf.h', + input : 'filesystems-gperf.gperf', + output : 'filesystems-gperf.h', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + +generate_filesystem_list = find_program('generate-filesystem-list.py') +fname = 'filesystem-list.h' +filesystem_list_h = custom_target( + fname, + input : 'filesystems-gperf.gperf', + output : fname, + command : [generate_filesystem_list, + '@INPUT@'], + capture : true) + +basic_sources += [filesystem_list_h, filesystems_gperf_h] + libbasic = static_library( 'basic', basic_sources, diff --git a/src/basic/missing_fs.h b/src/basic/missing_fs.h index 79c1620e83..cc43d7c277 100644 --- a/src/basic/missing_fs.h +++ b/src/basic/missing_fs.h @@ -52,11 +52,6 @@ #define EXT4_IOC_RESIZE_FS _IOW('f', 16, __u64) #endif -/* Not exposed yet. Defined at fs/cifs/cifsglob.h */ -#ifndef CIFS_MAGIC_NUMBER -#define CIFS_MAGIC_NUMBER 0xFF534D42 -#endif - /* linux/nsfs.h */ #ifndef NS_GET_NSTYPE /* d95fa3c76a66b6d76b1e109ea505c55e66360f3c (4.11) */ #define NS_GET_NSTYPE _IO(0xb7, 0x3) diff --git a/src/basic/missing_magic.h b/src/basic/missing_magic.h index e7466cb6bc..1f50e565a2 100644 --- a/src/basic/missing_magic.h +++ b/src/basic/missing_magic.h @@ -37,3 +37,138 @@ #ifndef XFS_SB_MAGIC #define XFS_SB_MAGIC 0x58465342 #endif + +/* Not exposed yet. Defined at fs/cifs/cifsglob.h */ +#ifndef CIFS_MAGIC_NUMBER +#define CIFS_MAGIC_NUMBER 0xFF534D42 +#endif + +/* 257f871993474e2bde6c497b54022c362cf398e1 (4.5) */ +#ifndef OVERLAYFS_SUPER_MAGIC +#define OVERLAYFS_SUPER_MAGIC 0x794c7630 +#endif + +/* 2a28900be20640fcd1e548b1e3bad79e8221fcf9 (4.7) */ +#ifndef UDF_SUPER_MAGIC +#define UDF_SUPER_MAGIC 0x15013346 +#endif + +/* b1123ea6d3b3da25af5c8a9d843bd07ab63213f4 (4.8)*/ +#ifndef BALLOON_KVM_MAGIC +#define BALLOON_KVM_MAGIC 0x13661366 +#endif + +/* 48b4800a1c6af2cdda344ea4e2c843dcc1f6afc9 (4.8) */ +#ifndef ZSMALLOC_MAGIC +#define ZSMALLOC_MAGIC 0x58295829 +#endif + +/* 3bc52c45bac26bf7ed1dc8d287ad1aeaed1250b6 (4.9) */ +#ifndef DAXFS_MAGIC +#define DAXFS_MAGIC 0x64646178 +#endif + +/* 5ff193fbde20df5d80fec367cea3e7856c057320 (4.10) */ +#ifndef RDTGROUP_SUPER_MAGIC +#define RDTGROUP_SUPER_MAGIC 0x7655821 +#endif + +/* a481f4d917835cad86701fc0d1e620c74bb5cd5f (4.13) */ +#ifndef AAFS_MAGIC +#define AAFS_MAGIC 0x5a3c69f0 +#endif + +/* f044c8847bb61eff5e1e95b6f6bb950e7f4a73a4 (4.15) */ +#ifndef AFS_FS_MAGIC +#define AFS_FS_MAGIC 0x6b414653 +#endif + +/* dddde68b8f06dd83486124b8d245e7bfb15c185d (4.20) */ +#ifndef XFS_SUPER_MAGIC +#define XFS_SUPER_MAGIC 0x58465342 +#endif + +/* 3ad20fe393b31025bebfc2d76964561f65df48aa (5.0) */ +#ifndef BINDERFS_SUPER_MAGIC +#define BINDERFS_SUPER_MAGIC 0x6c6f6f70 +#endif + +/* ed63bb1d1f8469586006a9ca63c42344401aa2ab (5.3) */ +#ifndef DMA_BUF_MAGIC +#define DMA_BUF_MAGIC 0x444d4142 +#endif + +/* ea8157ab2ae5e914dd427e5cfab533b6da3819cd (5.3) */ +#ifndef Z3FOLD_MAGIC +#define Z3FOLD_MAGIC 0x33 +#endif + +/* 47e4937a4a7ca4184fd282791dfee76c6799966a (5.4) */ +#ifndef EROFS_SUPER_MAGIC_V1 +#define EROFS_SUPER_MAGIC_V1 0xe0f5e1e2 +#endif + +/* fe030c9b85e6783bc52fe86449c0a4b8aa16c753 (5.5) */ +#ifndef PPC_CMM_MAGIC +#define PPC_CMM_MAGIC 0xc7571590 +#endif + +/* 8dcc1a9d90c10fa4143e5c17821082e5e60e46a1 (5.6) */ +#ifndef ZONEFS_MAGIC +#define ZONEFS_MAGIC 0x5a4f4653 +#endif + +/* 3234ac664a870e6ea69ae3a57d824cd7edbeacc5 (5.8) */ +#ifndef DEVMEM_MAGIC +#define DEVMEM_MAGIC 0x454d444d +#endif + +/* Not in mainline but included in Ubuntu */ +#ifndef SHIFTFS_MAGIC +#define SHIFTFS_MAGIC 0x6a656a62 +#endif + +/* 1507f51255c9ff07d75909a84e7c0d7f3c4b2f49 (5.14) */ +#ifndef SECRETMEM_MAGIC +#define SECRETMEM_MAGIC 0x5345434d +#endif + +/* Not exposed yet. Defined at fs/fuse/inode.c */ +#ifndef FUSE_SUPER_MAGIC +#define FUSE_SUPER_MAGIC 0x65735546 +#endif + +/* Not exposed yet. Defined at fs/fuse/control.c */ +#ifndef FUSE_CTL_SUPER_MAGIC +#define FUSE_CTL_SUPER_MAGIC 0x65735543 +#endif + +/* Not exposed yet. Defined at fs/ceph/super.h */ +#ifndef CEPH_SUPER_MAGIC +#define CEPH_SUPER_MAGIC 0x00c36400 +#endif + +/* Not exposed yet. Defined at fs/orangefs/orangefs-kernel.h */ +#ifndef ORANGEFS_DEVREQ_MAGIC +#define ORANGEFS_DEVREQ_MAGIC 0x20030529 +#endif + +/* linux/gfs2_ondisk.h */ +#ifndef GFS2_MAGIC +#define GFS2_MAGIC 0x01161970 +#endif + +/* Not exposed yet. Defined at fs/configfs/mount.c */ +#ifndef CONFIGFS_MAGIC +#define CONFIGFS_MAGIC 0x62656570 +#endif + +/* Not exposed yet. Defined at fs/vboxsf/super.c */ +#ifndef VBOXSF_SUPER_MAGIC +#define VBOXSF_SUPER_MAGIC 0x786f4256 +#endif + +/* Not exposed yet. Defined at fs/exfat/exfat_fs.h */ +#ifndef EXFAT_SUPER_MAGIC +#define EXFAT_SUPER_MAGIC 0x2011BAB0UL +#endif diff --git a/src/basic/mountpoint-util.c b/src/basic/mountpoint-util.c index f07534267f..783f94b2f7 100644 --- a/src/basic/mountpoint-util.c +++ b/src/basic/mountpoint-util.c @@ -8,11 +8,13 @@ #include "chase-symlinks.h" #include "fd-util.h" #include "fileio.h" +#include "filesystems.h" #include "fs-util.h" #include "missing_stat.h" #include "missing_syscall.h" #include "mkdir.h" #include "mountpoint-util.h" +#include "nulstr-util.h" #include "parse-util.h" #include "path-util.h" #include "stat-util.h" @@ -366,48 +368,34 @@ bool fstype_is_network(const char *fstype) { if (x) fstype = x; + if (nulstr_contains(filesystem_sets[FILESYSTEM_SET_NETWORK].value, fstype)) + return true; + + /* Filesystems not present in the internal database */ return STR_IN_SET(fstype, - "afs", "ceph", - "cifs", - "smb3", - "smbfs", - "sshfs", - "ncpfs", - "ncp", - "nfs", - "nfs4", - "gfs", - "gfs2", + "davfs", "glusterfs", - "pvfs2", /* OrangeFS */ - "ocfs2", "lustre", - "davfs"); + "sshfs"); } bool fstype_is_api_vfs(const char *fstype) { + const FilesystemSet *fs; + + FOREACH_POINTER(fs, + filesystem_sets + FILESYSTEM_SET_BASIC_API, + filesystem_sets + FILESYSTEM_SET_AUXILIARY_API, + filesystem_sets + FILESYSTEM_SET_PRIVILEGED_API, + filesystem_sets + FILESYSTEM_SET_TEMPORARY) + if (nulstr_contains(fs->value, fstype)) + return true; + + /* Filesystems not present in the internal database */ return STR_IN_SET(fstype, "autofs", - "bpf", - "cgroup", - "cgroup2", - "configfs", "cpuset", - "debugfs", - "devpts", - "devtmpfs", - "efivarfs", - "fusectl", - "hugetlbfs", - "mqueue", - "proc", - "pstore", - "ramfs", - "securityfs", - "sysfs", - "tmpfs", - "tracefs"); + "devtmpfs"); } bool fstype_is_blockdev_backed(const char *fstype) { diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index 45864e9e62..0004255bdb 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -13,10 +13,13 @@ #include "errno-util.h" #include "fd-util.h" #include "fileio.h" +#include "filesystems.h" +#include "fs-util.h" #include "macro.h" #include "missing_fs.h" #include "missing_magic.h" #include "missing_syscall.h" +#include "nulstr-util.h" #include "parse-util.h" #include "stat-util.h" #include "string-util.h" @@ -198,19 +201,11 @@ int path_is_fs_type(const char *path, statfs_f_type_t magic_value) { } bool is_temporary_fs(const struct statfs *s) { - return is_fs_type(s, TMPFS_MAGIC) || - is_fs_type(s, RAMFS_MAGIC); + return fs_in_group(s, FILESYSTEM_SET_TEMPORARY); } bool is_network_fs(const struct statfs *s) { - return is_fs_type(s, CIFS_MAGIC_NUMBER) || - is_fs_type(s, CODA_SUPER_MAGIC) || - is_fs_type(s, NCP_SUPER_MAGIC) || - is_fs_type(s, NFS_SUPER_MAGIC) || - is_fs_type(s, SMB_SUPER_MAGIC) || - is_fs_type(s, V9FS_MAGIC) || - is_fs_type(s, AFS_SUPER_MAGIC) || - is_fs_type(s, OCFS2_SUPER_MAGIC); + return fs_in_group(s, FILESYSTEM_SET_NETWORK); } int fd_is_temporary_fs(int fd) { diff --git a/src/core/bpf-lsm.c b/src/core/bpf-lsm.c new file mode 100644 index 0000000000..46c2446849 --- /dev/null +++ b/src/core/bpf-lsm.c @@ -0,0 +1,381 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "alloc-util.h" +#include "bpf-lsm.h" +#include "cgroup-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "filesystems.h" +#include "log.h" +#include "manager.h" +#include "mkdir.h" +#include "nulstr-util.h" +#include "stat-util.h" +#include "strv.h" + +#if BPF_FRAMEWORK +/* libbpf, clang and llc compile time dependencies are satisfied */ +#include "bpf-dlopen.h" +#include "bpf-link.h" +#include "bpf/restrict_fs/restrict-fs-skel.h" + +#define CGROUP_HASH_SIZE_MAX 2048 + +static struct restrict_fs_bpf *restrict_fs_bpf_free(struct restrict_fs_bpf *obj) { + /* restrict_fs_bpf__destroy handles object == NULL case */ + (void) restrict_fs_bpf__destroy(obj); + + return NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct restrict_fs_bpf *, restrict_fs_bpf_free); + +static bool bpf_can_link_lsm_program(struct bpf_program *prog) { + _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; + + assert(prog); + + link = sym_bpf_program__attach_lsm(prog); + if (!link) + return -ENOMEM; + + return 1; +} + +static int prepare_restrict_fs_bpf(struct restrict_fs_bpf **ret_obj) { + struct restrict_fs_bpf *obj = 0; + _cleanup_close_ int inner_map_fd = -1; + int r; + + assert(ret_obj); + + obj = restrict_fs_bpf__open(); + if (!obj) + return log_error_errno(errno, "Failed to open BPF object: %m"); + + /* TODO Maybe choose a number based on runtime information? */ + r = sym_bpf_map__resize(obj->maps.cgroup_hash, CGROUP_HASH_SIZE_MAX); + if (r != 0) + return log_error_errno(r, + "Failed to resize BPF map '%s': %m", + sym_bpf_map__name(obj->maps.cgroup_hash)); + + /* Dummy map to satisfy the verifier */ + inner_map_fd = sym_bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint32_t), 128, 0); + if (inner_map_fd < 0) + return log_error_errno(errno, "Failed to create BPF map: %m"); + + r = sym_bpf_map__set_inner_map_fd(obj->maps.cgroup_hash, inner_map_fd); + if (r < 0) + return log_error_errno(r, "Failed to set inner map fd: %m"); + + r = restrict_fs_bpf__load(obj); + if (r) + return log_error_errno(r, "Failed to load BPF object"); + + *ret_obj = TAKE_PTR(obj); + + return 0; +} + +static int mac_bpf_use(void) { + _cleanup_free_ char *lsm_list = NULL; + static int cached_use = -1; + int r; + + if (cached_use >= 0) + return cached_use; + + cached_use = 0; + + r = read_one_line_file("/sys/kernel/security/lsm", &lsm_list); + if (r < 0) { + if (errno != ENOENT) + log_debug_errno(r, "Failed to read /sys/kernel/security/lsm, ignoring: %m"); + + return 0; + } + + const char *p = lsm_list; + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", 0); + if (r == 0) + break; + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_debug_errno(r, "Failed to parse /sys/kernel/security/lsm, ignoring: %m"); + return 0; + } + + if (streq(word, "bpf")) { + cached_use = 1; + break; + } + } + + return cached_use; +} + +int lsm_bpf_supported(void) { + _cleanup_(restrict_fs_bpf_freep) struct restrict_fs_bpf *obj = NULL; + static int supported = -1; + int r; + + if (supported >= 0) + return supported; + + r = dlopen_bpf(); + if (r < 0) { + log_info_errno(r, "Failed to open libbpf, LSM BPF is not supported: %m"); + return supported = 0; + } + + r = cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER); + if (r < 0) { + log_warning_errno(r, "Can't determine whether the unified hierarchy is used: %m"); + return supported = 0; + } + + if (r == 0) { + log_info_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Not running with unified cgroup hierarchy, LSM BPF is not supported"); + return supported = 0; + } + + r = mac_bpf_use(); + if (r < 0) { + log_warning_errno(r, "Can't determine whether the BPF LSM module is used: %m"); + return supported = 0; + } + + if (r == 0) { + log_info_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "BPF LSM hook not enabled in the kernel, LSM BPF not supported"); + return supported = 0; + } + + r = prepare_restrict_fs_bpf(&obj); + if (r < 0) + return supported = 0; + + r = bpf_can_link_lsm_program(obj->progs.restrict_filesystems); + if (r < 0) { + log_warning_errno(r, "Failed to link BPF program. Assuming BPF is not available: %m"); + return supported = 0; + } + + return supported = 1; +} + +int lsm_bpf_setup(Manager *m) { + struct restrict_fs_bpf *obj = NULL; + _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; + int r; + + assert(m); + + r = prepare_restrict_fs_bpf(&obj); + if (r < 0) + return r; + + m->restrict_fs = obj; + + link = sym_bpf_program__attach_lsm(m->restrict_fs->progs.restrict_filesystems); + r = sym_libbpf_get_error(link); + if (r != 0) + return log_error_errno(r, "Failed to link '%s' LSM BPF program: %m", + sym_bpf_program__name(m->restrict_fs->progs.restrict_filesystems)); + + log_info("LSM BPF program attached"); + + m->restrict_fs->links.restrict_filesystems = TAKE_PTR(link); + + return 0; +} + +int lsm_bpf_unit_restrict_filesystems(Unit *u, const Set *filesystems, bool allow_list) { + int inner_map_fd = -1, outer_map_fd = -1; + uint32_t dummy_value = 1, zero = 0; + const char *fs; + const statfs_f_type_t *magic; + int r; + + assert(filesystems); + assert(u); + + inner_map_fd = sym_bpf_create_map( + BPF_MAP_TYPE_HASH, + sizeof(uint32_t), + sizeof(uint32_t), + 128, /* Should be enough for all filesystem types */ + 0); + if (inner_map_fd < 0) + return log_unit_error_errno(u, errno, "Failed to create inner LSM map: %m"); + + outer_map_fd = sym_bpf_map__fd(u->manager->restrict_fs->maps.cgroup_hash); + if (outer_map_fd < 0) + return log_unit_error_errno(u, errno, "Failed to get BPF map fd: %m"); + + if (sym_bpf_map_update_elem(outer_map_fd, &u->cgroup_id, &inner_map_fd, BPF_ANY) != 0) + return log_unit_error_errno(u, errno, "Error populating LSM BPF map: %m"); + + uint32_t allow = allow_list; + + /* Use key 0 to store whether this is an allow list or a deny list */ + if (sym_bpf_map_update_elem(inner_map_fd, &zero, &allow, BPF_ANY) != 0) + return log_unit_error_errno(u, errno, "Error initializing BPF map: %m"); + + SET_FOREACH(fs, filesystems) { + r = fs_type_from_string(fs, &magic); + if (r < 0) { + log_unit_warning(u, "Invalid filesystem name '%s', ignoring.", fs); + continue; + } + + log_unit_debug(u, "Restricting filesystem access to '%s'", fs); + + for (int i = 0; i < FILESYSTEM_MAGIC_MAX; i++) { + if (magic[i] == 0) + break; + + if (sym_bpf_map_update_elem(inner_map_fd, &magic[i], &dummy_value, BPF_ANY) != 0) { + r = log_unit_error_errno(u, errno, "Failed to update BPF map: %m"); + + if (sym_bpf_map_delete_elem(outer_map_fd, &u->cgroup_id) != 0) + log_unit_debug_errno(u, errno, "Failed to delete cgroup entry from LSM BPF map: %m"); + + return r; + } + } + } + + return 0; +} + +int lsm_bpf_cleanup(const Unit *u) { + int fd = -1; + + assert(u); + assert(u->manager); + + if (!lsm_bpf_supported()) + return 0; + + if (!u->manager->restrict_fs) + return 0; + + fd = sym_bpf_map__fd(u->manager->restrict_fs->maps.cgroup_hash); + if (fd < 0) + return log_unit_error_errno(u, errno, "Failed to get BPF map fd: %m"); + + if (sym_bpf_map_delete_elem(fd, &u->cgroup_id) != 0) + return log_unit_debug_errno(u, errno, "Failed to delete cgroup entry from LSM BPF map: %m"); + + return 0; +} + +int lsm_bpf_map_restrict_fs_fd(Unit *unit) { + assert(unit); + assert(unit->manager); + + if (!unit->manager->restrict_fs) + return -ENOMEDIUM; + + return sym_bpf_map__fd(unit->manager->restrict_fs->maps.cgroup_hash); +} + +void lsm_bpf_destroy(struct restrict_fs_bpf *prog) { + restrict_fs_bpf__destroy(prog); +} +#else /* ! BPF_FRAMEWORK */ +int lsm_bpf_supported(void) { + return 0; +} + +int lsm_bpf_setup(Manager *m) { + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Failed to set up LSM BPF: %m"); +} + +int lsm_bpf_unit_restrict_filesystems(Unit *u, const Set *filesystems, const bool allow_list) { + return log_unit_debug_errno(u, SYNTHETIC_ERRNO(EOPNOTSUPP), "Failed to restrict filesystems using LSM BPF: %m"); +} + +int lsm_bpf_cleanup(const Unit *u) { + return 0; +} + +int lsm_bpf_map_restrict_fs_fd(Unit *unit) { + return -ENOMEDIUM; +} + +void lsm_bpf_destroy(struct restrict_fs_bpf *prog) { + return; +} +#endif + +int lsm_bpf_parse_filesystem( + const char *name, + Set **filesystems, + FilesystemParseFlags flags, + const char *unit, + const char *filename, + unsigned line) { + int r; + + assert(name); + assert(filesystems); + + if (name[0] == '@') { + const FilesystemSet *set; + const char *i; + + set = filesystem_set_find(name); + if (!set) { + log_syntax(unit, flags & FILESYSTEM_PARSE_LOG ? LOG_WARNING : LOG_DEBUG, filename, line, 0, + "Unknown filesystem group, ignoring: %s", name); + return 0; + } + + NULSTR_FOREACH(i, set->value) { + /* Call ourselves again, for the group to parse. Note that we downgrade logging here (i.e. take + * away the FILESYSTEM_PARSE_LOG flag) since any issues in the group table are our own problem, + * not a problem in user configuration data and we shouldn't pretend otherwise by complaining + * about them. */ + r = lsm_bpf_parse_filesystem(i, filesystems, flags &~ FILESYSTEM_PARSE_LOG, unit, filename, line); + if (r < 0) + return r; + } + } else { + /* If we previously wanted to forbid access to a filesystem and now + * we want to allow it, then remove it from the list. */ + if (!(flags & FILESYSTEM_PARSE_INVERT) == !!(flags & FILESYSTEM_PARSE_ALLOW_LIST)) { + r = set_put_strdup(filesystems, name); + if (r < 0) + switch (r) { + case -ENOMEM: + return flags & FILESYSTEM_PARSE_LOG ? log_oom() : -ENOMEM; + case -EEXIST: + /* Alredy in set, ignore */ + break; + default: + return r; + } + } else + free(set_remove(*filesystems, name)); + } + + return 0; +} diff --git a/src/core/bpf-lsm.h b/src/core/bpf-lsm.h new file mode 100644 index 0000000000..8bd58a29e5 --- /dev/null +++ b/src/core/bpf-lsm.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "hashmap.h" + +typedef enum FilesystemParseFlags { + FILESYSTEM_PARSE_INVERT = 1 << 0, + FILESYSTEM_PARSE_ALLOW_LIST = 1 << 1, + FILESYSTEM_PARSE_LOG = 1 << 2, +} FilesystemParseFlags; + +typedef struct Unit Unit; +typedef struct Manager Manager; + +typedef struct restrict_fs_bpf restrict_fs_bpf; + +int lsm_bpf_supported(void); +int lsm_bpf_setup(Manager *m); +int lsm_bpf_unit_restrict_filesystems(Unit *u, const Set *filesystems, bool allow_list); +int lsm_bpf_cleanup(const Unit *u); +int lsm_bpf_map_restrict_fs_fd(Unit *u); +void lsm_bpf_destroy(struct restrict_fs_bpf *prog); +int lsm_bpf_parse_filesystem(const char *name, + Set **filesystems, + FilesystemParseFlags flags, + const char *unit, + const char *filename, + unsigned line); diff --git a/src/core/bpf/restrict_fs/meson.build b/src/core/bpf/restrict_fs/meson.build new file mode 100644 index 0000000000..7a21f5276e --- /dev/null +++ b/src/core/bpf/restrict_fs/meson.build @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +if conf.get('BPF_FRAMEWORK') == 1 + restrict_fs_skel_h = custom_target( + 'restrict-fs-skel.h', + input : 'restrict-fs.bpf.c', + output : 'restrict-fs-skel.h', + command : [build_bpf_skel_py, + '--clang_exec', clang.path(), + '--llvm_strip_exec', llvm_strip.path(), + '--bpftool_exec', bpftool.path(), + '--arch', host_machine.cpu_family(), + '@INPUT@', '@OUTPUT@']) +endif diff --git a/src/core/bpf/restrict_fs/restrict-fs.bpf.c b/src/core/bpf/restrict_fs/restrict-fs.bpf.c new file mode 100644 index 0000000000..cdc0613a01 --- /dev/null +++ b/src/core/bpf/restrict_fs/restrict-fs.bpf.c @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +struct super_block { + long unsigned int s_magic; +} __attribute__((preserve_access_index)); + +struct inode { + struct super_block *i_sb; +} __attribute__((preserve_access_index)); + +struct file { + struct inode *f_inode; +} __attribute__((preserve_access_index)); + +/* + * max_entries is set from user space with the bpf_map__resize helper. + * */ +struct { + __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS); + __type(key, uint64_t); /* cgroup ID */ + __type(value, uint32_t); /* fs magic set */ +} cgroup_hash SEC(".maps"); + +SEC("lsm/file_open") +int BPF_PROG(restrict_filesystems, struct file *file, int ret) +{ + unsigned long magic_number; + uint64_t cgroup_id; + uint32_t *value, *magic_map, zero = 0, *is_allow; + + /* ret is the return value from the previous BPF program or 0 if it's + * the first hook */ + if (ret != 0) + return ret; + + BPF_CORE_READ_INTO(&magic_number, file, f_inode, i_sb, s_magic); + + cgroup_id = bpf_get_current_cgroup_id(); + + magic_map = bpf_map_lookup_elem(&cgroup_hash, &cgroup_id); + if (!magic_map) + return 0; + + is_allow = bpf_map_lookup_elem(magic_map, &zero); + if (!is_allow) + /* Malformed map, it doesn't include whether it's an allow list + * or a deny list. Allow. */ + return 0; + + if (*is_allow) { + /* Allow-list: Allow access only if magic_number present in inner map */ + if (!bpf_map_lookup_elem(magic_map, &magic_number)) + return -EPERM; + } else { + /* Deny-list: Allow access only if magic_number is not present in inner map */ + if (bpf_map_lookup_elem(magic_map, &magic_number)) + return -EPERM; + } + + return 0; +} + +static const char _license[] SEC("license") = "GPL"; diff --git a/src/core/cgroup.c b/src/core/cgroup.c index a5770c0332..2b15310191 100644 --- a/src/core/cgroup.c +++ b/src/core/cgroup.c @@ -37,6 +37,12 @@ #include "string-util.h" #include "virt.h" +#if BPF_FRAMEWORK +#include "bpf-dlopen.h" +#include "bpf-link.h" +#include "bpf/restrict_fs/restrict-fs-skel.h" +#endif + #define CGROUP_CPU_QUOTA_DEFAULT_PERIOD_USEC ((usec_t) 100 * USEC_PER_MSEC) /* Returns the log level to use when cgroup attribute writes fail. When an attribute is missing or we have access @@ -2102,6 +2108,8 @@ static int unit_update_cgroup( bool created, is_root_slice; CGroupMask migrate_mask = 0; + _cleanup_free_ char *cgroup_full_path = NULL; + uint64_t cgroup_id = 0; int r; assert(u); @@ -2120,6 +2128,18 @@ static int unit_update_cgroup( return log_unit_error_errno(u, r, "Failed to create cgroup %s: %m", empty_to_root(u->cgroup_path)); created = r; + if (cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER) > 0) { + r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, u->cgroup_path, NULL, &cgroup_full_path); + if (r == 0) { + r = cg_path_get_cgroupid(cgroup_full_path, &cgroup_id); + if (r < 0) + log_unit_warning_errno(u, r, "Failed to get cgroup ID on cgroup %s, ignoring: %m", cgroup_full_path); + } else + log_unit_warning_errno(u, r, "Failed to get full cgroup path on cgroup %s, ignoring: %m", empty_to_root(u->cgroup_path)); + + u->cgroup_id = cgroup_id; + } + /* Start watching it */ (void) unit_watch_cgroup(u); (void) unit_watch_cgroup_memory(u); @@ -2722,6 +2742,10 @@ void unit_prune_cgroup(Unit *u) { (void) unit_get_cpu_usage(u, NULL); /* Cache the last CPU usage value before we destroy the cgroup */ +#if BPF_FRAMEWORK + (void) lsm_bpf_cleanup(u); /* Remove cgroup from the global LSM BPF map */ +#endif + is_root_slice = unit_has_name(u, SPECIAL_ROOT_SLICE); r = cg_trim_everywhere(u->manager->cgroup_supported, u->cgroup_path, !is_root_slice); diff --git a/src/core/cgroup.h b/src/core/cgroup.h index 8795f2724e..4413eeaaa0 100644 --- a/src/core/cgroup.h +++ b/src/core/cgroup.h @@ -3,6 +3,7 @@ #include +#include "bpf-lsm.h" #include "cgroup-util.h" #include "cpu-set-util.h" #include "list.h" diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 488de1242a..87d6aefc84 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -37,6 +37,7 @@ #endif #include "securebits-util.h" #include "specifier.h" +#include "stat-util.h" #include "strv.h" #include "syslog-util.h" #include "unit-printf.h" @@ -681,6 +682,46 @@ static int property_get_input_data( return sd_bus_message_append_array(reply, 'y', c->stdin_data, c->stdin_data_size); } +static int property_get_restrict_filesystems( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + ExecContext *c = userdata; + _cleanup_free_ char **l = NULL; + int r; + + assert(bus); + assert(reply); + assert(c); + + r = sd_bus_message_open_container(reply, 'r', "bas"); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "b", c->restrict_filesystems_allow_list); + if (r < 0) + return r; + +#if HAVE_LIBBPF + l = set_get_strv(c->restrict_filesystems); + if (!l) + return -ENOMEM; +#endif + + strv_sort(l); + + r = sd_bus_message_append_strv(reply, l); + if (r < 0) + return r; + + return sd_bus_message_close_container(reply); +} + static int property_get_bind_paths( sd_bus *bus, const char *path, @@ -1199,6 +1240,7 @@ const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_PROPERTY("RestrictRealtime", "b", bus_property_get_bool, offsetof(ExecContext, restrict_realtime), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RestrictSUIDSGID", "b", bus_property_get_bool, offsetof(ExecContext, restrict_suid_sgid), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RestrictNamespaces", "t", bus_property_get_ulong, offsetof(ExecContext, restrict_namespaces), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("RestrictFileSystems", "(bas)", property_get_restrict_filesystems, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("BindPaths", "a(ssbt)", property_get_bind_paths, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("BindReadOnlyPaths", "a(ssbt)", property_get_bind_paths, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("TemporaryFileSystem", "a(ss)", property_get_temporary_filesystems, 0, SD_BUS_VTABLE_PROPERTY_CONST), @@ -1875,6 +1917,64 @@ int bus_exec_context_set_transient_property( if (streq(name, "RestrictNamespaces")) return bus_set_transient_namespace_flag(u, name, &c->restrict_namespaces, message, flags, error); + if (streq(name, "RestrictFileSystems")) { + int allow_list; + _cleanup_strv_free_ char **l = NULL; + + r = sd_bus_message_enter_container(message, 'r', "bas"); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "b", &allow_list); + if (r < 0) + return r; + + r = sd_bus_message_read_strv(message, &l); + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + _cleanup_free_ char *joined = NULL; + FilesystemParseFlags invert_flag = allow_list ? 0 : FILESYSTEM_PARSE_INVERT; + char **s; + + if (strv_isempty(l)) { + c->restrict_filesystems_allow_list = false; + c->restrict_filesystems = set_free(c->restrict_filesystems); + + unit_write_setting(u, flags, name, "RestrictFileSystems="); + return 1; + } + + if (!c->restrict_filesystems) + c->restrict_filesystems_allow_list = allow_list; + + STRV_FOREACH(s, l) { + r = lsm_bpf_parse_filesystem( + *s, + &c->restrict_filesystems, + FILESYSTEM_PARSE_LOG| + (invert_flag ? FILESYSTEM_PARSE_INVERT : 0)| + (c->restrict_filesystems_allow_list ? FILESYSTEM_PARSE_ALLOW_LIST : 0), + u->id, NULL, 0); + if (r < 0) + return r; + } + + joined = strv_join(l, " "); + if (!joined) + return -ENOMEM; + + unit_write_settingf(u, flags, name, "%s=%s%s", name, allow_list ? "" : "~", joined); + } + + return 1; + } + if (streq(name, "MountFlags")) return bus_set_transient_mount_flags(u, name, &c->mount_flags, message, flags, error); diff --git a/src/core/execute.c b/src/core/execute.c index d68e31eb7d..6397bab315 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -41,6 +41,7 @@ #endif #include "async.h" #include "barrier.h" +#include "bpf-lsm.h" #include "cap-list.h" #include "capability-util.h" #include "cgroup-setup.h" @@ -1685,6 +1686,29 @@ static int apply_restrict_namespaces(const Unit *u, const ExecContext *c) { return seccomp_restrict_namespaces(c->restrict_namespaces); } +#if HAVE_LIBBPF +static bool skip_lsm_bpf_unsupported(const Unit* u, const char* msg) { + if (lsm_bpf_supported()) + return false; + + log_unit_debug(u, "LSM BPF not supported, skipping %s", msg); + return true; +} + +static int apply_restrict_filesystems(Unit *u, const ExecContext *c) { + assert(u); + assert(c); + + if (!exec_context_restrict_filesystems_set(c)) + return 0; + + if (skip_lsm_bpf_unsupported(u, "RestrictFileSystems=")) + return 0; + + return lsm_bpf_unit_restrict_filesystems(u, c->restrict_filesystems, c->restrict_filesystems_allow_list); +} +#endif + static int apply_lock_personality(const Unit* u, const ExecContext *c) { unsigned long personality; int r; @@ -3813,7 +3837,7 @@ static int exec_child( /* In case anything used libc syslog(), close this here, too */ closelog(); - int keep_fds[n_fds + 2]; + int keep_fds[n_fds + 3]; memcpy_safe(keep_fds, fds, n_fds * sizeof(int)); n_keep_fds = n_fds; @@ -3823,6 +3847,24 @@ static int exec_child( return log_unit_error_errno(unit, r, "Failed to shift fd and set FD_CLOEXEC: %m"); } +#if HAVE_LIBBPF + if (MANAGER_IS_SYSTEM(unit->manager) && lsm_bpf_supported()) { + int bpf_map_fd = -1; + + bpf_map_fd = lsm_bpf_map_restrict_fs_fd(unit); + if (bpf_map_fd < 0) { + *exit_status = EXIT_FDS; + return log_unit_error_errno(unit, r, "Failed to get restrict filesystems BPF map fd: %m"); + } + + r = add_shifted_fd(keep_fds, ELEMENTSOF(keep_fds), &n_keep_fds, bpf_map_fd, &bpf_map_fd); + if (r < 0) { + *exit_status = EXIT_FDS; + return log_unit_error_errno(unit, r, "Failed to shift fd and set FD_CLOEXEC: %m"); + } + } +#endif + r = close_remaining_fds(params, runtime, dcreds, user_lookup_fd, socket_fd, keep_fds, n_keep_fds); if (r < 0) { *exit_status = EXIT_FDS; @@ -4682,6 +4724,15 @@ static int exec_child( return log_unit_error_errno(unit, r, "Failed to apply system call filters: %m"); } #endif + +#if HAVE_LIBBPF + r = apply_restrict_filesystems(unit, context); + if (r < 0) { + *exit_status = EXIT_BPF; + return log_unit_error_errno(unit, r, "Failed to restrict filesystems: %m"); + } +#endif + } if (!strv_isempty(context->unset_environment)) { @@ -4967,6 +5018,8 @@ void exec_context_done(ExecContext *c) { c->apparmor_profile = mfree(c->apparmor_profile); c->smack_process_label = mfree(c->smack_process_label); + c->restrict_filesystems = set_free(c->restrict_filesystems); + c->syscall_filter = hashmap_free(c->syscall_filter); c->syscall_archs = set_free(c->syscall_archs); c->address_families = set_free(c->address_families); @@ -5734,6 +5787,12 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) { prefix, strna(s)); } +#if HAVE_LIBBPF + if (exec_context_restrict_filesystems_set(c)) + SET_FOREACH(e, c->restrict_filesystems) + fprintf(f, "%sRestrictFileSystems: %s\n", prefix, *e); +#endif + if (c->network_namespace_path) fprintf(f, "%sNetworkNamespacePath: %s\n", diff --git a/src/core/execute.h b/src/core/execute.h index 64a38b2d26..560dcbcc5e 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -314,6 +314,9 @@ struct ExecContext { unsigned long restrict_namespaces; /* The CLONE_NEWxyz flags permitted to the unit's processes */ + Set *restrict_filesystems; + bool restrict_filesystems_allow_list:1; + Hashmap *syscall_filter; Set *syscall_archs; int syscall_errno; @@ -342,6 +345,13 @@ static inline bool exec_context_restrict_namespaces_set(const ExecContext *c) { return (c->restrict_namespaces & NAMESPACE_FLAGS_ALL) != NAMESPACE_FLAGS_ALL; } +static inline bool exec_context_restrict_filesystems_set(const ExecContext *c) { + assert(c); + + return c->restrict_filesystems_allow_list || + !set_isempty(c->restrict_filesystems); +} + static inline bool exec_context_with_rootfs(const ExecContext *c) { assert(c); diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 799a179430..480ec66bf9 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -81,6 +81,7 @@ {{type}}.RestrictAddressFamilies, config_parse_warn_compat, DISABLED_CONFIGURATION, 0 {{type}}.LockPersonality, config_parse_warn_compat, DISABLED_CONFIGURATION, 0 {% endif %} +{{type}}.RestrictFileSystems, config_parse_restrict_filesystems, 0, offsetof({{type}}, exec_context) {{type}}.LimitCPU, config_parse_rlimit, RLIMIT_CPU, offsetof({{type}}, exec_context.rlimit) {{type}}.LimitFSIZE, config_parse_rlimit, RLIMIT_FSIZE, offsetof({{type}}, exec_context.rlimit) {{type}}.LimitDATA, config_parse_rlimit, RLIMIT_DATA, offsetof({{type}}, exec_context.rlimit) diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index f971084c28..10fe579aeb 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -19,6 +19,7 @@ #include "all-units.h" #include "alloc-util.h" #include "bpf-firewall.h" +#include "bpf-lsm.h" #include "bpf-program.h" #include "bpf-socket-bind.h" #include "bus-error.h" @@ -3597,6 +3598,76 @@ int config_parse_restrict_namespaces( } #endif +int config_parse_restrict_filesystems( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + ExecContext *c = data; + bool invert = false; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + /* Empty assignment resets the list */ + c->restrict_filesystems = set_free(c->restrict_filesystems); + c->restrict_filesystems_allow_list = false; + return 0; + } + + if (rvalue[0] == '~') { + invert = true; + rvalue++; + } + + if (!c->restrict_filesystems) { + if (invert) + /* Allow everything but the ones listed */ + c->restrict_filesystems_allow_list = false; + else + /* Allow nothing but the ones listed */ + c->restrict_filesystems_allow_list = true; + } + + for (const char *p = rvalue;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, NULL, EXTRACT_UNQUOTE); + if (r == 0) + break; + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Trailing garbage in %s, ignoring: %s", lvalue, rvalue); + break; + } + + r = lsm_bpf_parse_filesystem( + word, + &c->restrict_filesystems, + FILESYSTEM_PARSE_LOG| + (invert ? FILESYSTEM_PARSE_INVERT : 0)| + (c->restrict_filesystems_allow_list ? FILESYSTEM_PARSE_ALLOW_LIST : 0), + unit, filename, line); + + if (r < 0) + return r; + } + + return 0; +} + int config_parse_unit_slice( const char *unit, const char *filename, @@ -6030,6 +6101,7 @@ void unit_dump_config_items(FILE *f) { { config_parse_address_families, "FAMILIES" }, { config_parse_restrict_namespaces, "NAMESPACES" }, #endif + { config_parse_restrict_filesystems, "FILESYSTEMS" }, { config_parse_cpu_shares, "SHARES" }, { config_parse_cg_weight, "WEIGHT" }, { config_parse_memory_limit, "LIMIT" }, diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index e84f9ee391..9173f30add 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -113,6 +113,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_sec_fix_0); CONFIG_PARSER_PROTOTYPE(config_parse_user_group_compat); CONFIG_PARSER_PROTOTYPE(config_parse_user_group_strv_compat); CONFIG_PARSER_PROTOTYPE(config_parse_restrict_namespaces); +CONFIG_PARSER_PROTOTYPE(config_parse_restrict_filesystems); CONFIG_PARSER_PROTOTYPE(config_parse_bind_paths); CONFIG_PARSER_PROTOTYPE(config_parse_exec_keyring_mode); CONFIG_PARSER_PROTOTYPE(config_parse_protect_proc); diff --git a/src/core/main.c b/src/core/main.c index 059ba6dd49..62f39c7378 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -22,6 +22,9 @@ #include "alloc-util.h" #include "apparmor-setup.h" #include "architecture.h" +#if HAVE_LIBBPF +#include "bpf-lsm.h" +#endif #include "build.h" #include "bus-error.h" #include "bus-util.h" diff --git a/src/core/manager.c b/src/core/manager.c index 0b2e29ae14..6bcb6bd153 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -930,6 +930,14 @@ int manager_new(UnitFileScope scope, ManagerTestRunFlags test_run_flags, Manager r = manager_setup_sigchld_event_source(m); if (r < 0) return r; + +#if HAVE_LIBBPF + if (MANAGER_IS_SYSTEM(m) && lsm_bpf_supported()) { + r = lsm_bpf_setup(m); + if (r < 0) + return r; + } +#endif } if (test_run_flags == 0) { @@ -1535,6 +1543,10 @@ Manager* manager_free(Manager *m) { m->prefix[dt] = mfree(m->prefix[dt]); free(m->received_credentials); +#if BPF_FRAMEWORK + lsm_bpf_destroy(m->restrict_fs); +#endif + return mfree(m); } diff --git a/src/core/manager.h b/src/core/manager.h index 97c12ce48f..29ce812121 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -450,6 +450,9 @@ struct Manager { * we're a user manager, this object manages the client connection from the user manager to * systemd-oomd to report changes in ManagedOOM settings (systemd client - oomd server). */ Varlink *managed_oom_varlink; + + /* Reference to RestrictFileSystems= BPF program */ + struct restrict_fs_bpf *restrict_fs; }; static inline usec_t manager_default_timeout_abort_usec(Manager *m) { diff --git a/src/core/meson.build b/src/core/meson.build index 4b53d7e43b..de7c2ae798 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -13,6 +13,8 @@ libcore_sources = ''' bpf-firewall.h bpf-foreign.c bpf-foreign.h + bpf-lsm.c + bpf-lsm.h bpf-socket-bind.c bpf-socket-bind.h cgroup.c @@ -134,6 +136,8 @@ libcore_sources = ''' subdir('bpf/socket_bind') if conf.get('BPF_FRAMEWORK') == 1 libcore_sources += [socket_bind_skel_h] + subdir('bpf/restrict_fs') + libcore_sources += [restrict_fs_skel_h] endif subdir('bpf/restrict_ifaces') diff --git a/src/core/unit.h b/src/core/unit.h index c8e99acf70..0dd6a9591d 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -294,6 +294,7 @@ typedef struct Unit { /* Counterparts in the cgroup filesystem */ char *cgroup_path; + uint64_t cgroup_id; CGroupMask cgroup_realized_mask; /* In which hierarchies does this unit's cgroup exist? (only relevant on cgroup v1) */ CGroupMask cgroup_enabled_mask; /* Which controllers are enabled (or more correctly: enabled for the children) for this unit's cgroup? (only relevant on cgroup v2) */ CGroupMask cgroup_invalidated_mask; /* A mask specifying controllers which shall be considered invalidated, and require re-realization */ diff --git a/src/shared/bpf-dlopen.c b/src/shared/bpf-dlopen.c index 45ab986ab2..6f82002ff8 100644 --- a/src/shared/bpf-dlopen.c +++ b/src/shared/bpf-dlopen.c @@ -8,13 +8,17 @@ static void *bpf_dl = NULL; struct bpf_link* (*sym_bpf_program__attach_cgroup)(struct bpf_program *, int); +struct bpf_link* (*sym_bpf_program__attach_lsm)(struct bpf_program *); long (*sym_libbpf_get_error)(const void *); int (*sym_bpf_link__fd)(const struct bpf_link *); int (*sym_bpf_link__destroy)(struct bpf_link *); int (*sym_bpf_map__fd)(const struct bpf_map *); const char* (*sym_bpf_map__name)(const struct bpf_map *); +int (*sym_bpf_create_map)(enum bpf_map_type, int key_size, int value_size, int max_entries, __u32 map_flags); int (*sym_bpf_map__resize)(struct bpf_map *, __u32); int (*sym_bpf_map_update_elem)(int, const void *, const void *, __u64); +int (*sym_bpf_map_delete_elem)(int, const void *); +int (*sym_bpf_map__set_inner_map_fd)(struct bpf_map *, int); int (*sym_bpf_object__open_skeleton)(struct bpf_object_skeleton *, const struct bpf_object_open_opts *); int (*sym_bpf_object__load_skeleton)(struct bpf_object_skeleton *); int (*sym_bpf_object__attach_skeleton)(struct bpf_object_skeleton *); @@ -30,8 +34,11 @@ int dlopen_bpf(void) { DLSYM_ARG(bpf_link__fd), DLSYM_ARG(bpf_map__fd), DLSYM_ARG(bpf_map__name), + DLSYM_ARG(bpf_create_map), DLSYM_ARG(bpf_map__resize), DLSYM_ARG(bpf_map_update_elem), + DLSYM_ARG(bpf_map_delete_elem), + DLSYM_ARG(bpf_map__set_inner_map_fd), DLSYM_ARG(bpf_object__open_skeleton), DLSYM_ARG(bpf_object__load_skeleton), DLSYM_ARG(bpf_object__attach_skeleton), @@ -39,6 +46,7 @@ int dlopen_bpf(void) { DLSYM_ARG(bpf_object__destroy_skeleton), DLSYM_ARG(bpf_probe_prog_type), DLSYM_ARG(bpf_program__attach_cgroup), + DLSYM_ARG(bpf_program__attach_lsm), DLSYM_ARG(bpf_program__name), DLSYM_ARG(libbpf_get_error)); } diff --git a/src/shared/bpf-dlopen.h b/src/shared/bpf-dlopen.h index fbb5531aff..713c41c3f4 100644 --- a/src/shared/bpf-dlopen.h +++ b/src/shared/bpf-dlopen.h @@ -7,13 +7,17 @@ #include extern struct bpf_link* (*sym_bpf_program__attach_cgroup)(struct bpf_program *, int); +extern struct bpf_link* (*sym_bpf_program__attach_lsm)(struct bpf_program *); extern long (*sym_libbpf_get_error)(const void *); extern int (*sym_bpf_link__fd)(const struct bpf_link *); extern int (*sym_bpf_link__destroy)(struct bpf_link *); extern int (*sym_bpf_map__fd)(const struct bpf_map *); extern const char* (*sym_bpf_map__name)(const struct bpf_map *); +extern int (*sym_bpf_create_map)(enum bpf_map_type, int key_size, int value_size, int max_entries, __u32 map_flags); extern int (*sym_bpf_map__resize)(struct bpf_map *, __u32); extern int (*sym_bpf_map_update_elem)(int, const void *, const void *, __u64); +extern int (*sym_bpf_map_delete_elem)(int, const void *); +extern int (*sym_bpf_map__set_inner_map_fd)(struct bpf_map *, int); /* The *_skeleton APIs are autogenerated by bpftool, the targets can be found * in ./build/src/core/bpf/socket_bind/socket-bind.skel.h */ extern int (*sym_bpf_object__open_skeleton)(struct bpf_object_skeleton *, const struct bpf_object_open_opts *); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 01b78952cd..8b81e8058b 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -1386,6 +1386,7 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con } if (STR_IN_SET(field, "RestrictAddressFamilies", + "RestrictFileSystems", "SystemCallFilter", "SystemCallLog", "RestrictNetworkInterfaces")) { diff --git a/src/shared/exit-status.c b/src/shared/exit-status.c index db978fb896..9063f231e1 100644 --- a/src/shared/exit-status.c +++ b/src/shared/exit-status.c @@ -18,8 +18,8 @@ const ExitStatusMapping exit_status_mappings[256] = { * 8…63 │ (Currently unmapped) * 64…78 │ BSD defined exit codes * 79…199 │ (Currently unmapped) - * 200…243 │ systemd's private error codes (might be extended to 254 in future development) - * 244…254 │ (Currently unmapped, but see above) + * 200…244 │ systemd's private error codes (might be extended to 254 in future development) + * 245…254 │ (Currently unmapped, but see above) * * 255 │ EXIT_EXCEPTION (We use this to propagate exit-by-signal events. It's frequently used by others apps (like bash) * │ to indicate exit reason that cannot really be expressed in a single exit status value — such as a propagated @@ -71,6 +71,7 @@ const ExitStatusMapping exit_status_mappings[256] = { [EXIT_CONFIGURATION_DIRECTORY] = { "CONFIGURATION_DIRECTORY", EXIT_STATUS_SYSTEMD }, [EXIT_NUMA_POLICY] = { "NUMA_POLICY", EXIT_STATUS_SYSTEMD }, [EXIT_CREDENTIALS] = { "CREDENTIALS", EXIT_STATUS_SYSTEMD }, + [EXIT_BPF] = { "BPF", EXIT_STATUS_SYSTEMD }, [EXIT_EXCEPTION] = { "EXCEPTION", EXIT_STATUS_SYSTEMD }, diff --git a/src/shared/exit-status.h b/src/shared/exit-status.h index 05707bf722..3f9a2ad54f 100644 --- a/src/shared/exit-status.h +++ b/src/shared/exit-status.h @@ -71,6 +71,7 @@ enum { EXIT_CONFIGURATION_DIRECTORY, EXIT_NUMA_POLICY, EXIT_CREDENTIALS, + EXIT_BPF, EXIT_EXCEPTION = 255, /* Whenever we want to propagate an abnormal/signal exit, in line with bash */ }; diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c index d008c4172a..9e10560f80 100644 --- a/src/systemctl/systemctl-show.c +++ b/src/systemctl/systemctl-show.c @@ -1023,7 +1023,7 @@ static int print_property(const char *name, const char *expected_value, sd_bus_m return 1; - } else if (STR_IN_SET(name, "SystemCallFilter", "SystemCallLog", "RestrictAddressFamilies", "RestrictNetworkInterfaces")) { + } else if (STR_IN_SET(name, "SystemCallFilter", "SystemCallLog", "RestrictAddressFamilies", "RestrictNetworkInterfaces", "RestrictFileSystems")) { _cleanup_strv_free_ char **l = NULL; int allow_list; diff --git a/src/test/meson.build b/src/test/meson.build index ee7c1a1c83..cef2532407 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -357,6 +357,17 @@ tests += [ [], core_includes], + [['src/test/test-bpf-lsm.c'], + [libcore, + libshared], + [libmount, + threads, + librt, + libseccomp, + libselinux, + libblkid], + core_includes], + [['src/test/test-watch-pid.c'], [libcore, libshared], diff --git a/src/test/test-bpf-lsm.c b/src/test/test-bpf-lsm.c new file mode 100644 index 0000000000..258c2e575e --- /dev/null +++ b/src/test/test-bpf-lsm.c @@ -0,0 +1,109 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bpf-lsm.h" +#include "load-fragment.h" +#include "manager.h" +#include "process-util.h" +#include "rlimit-util.h" +#include "rm-rf.h" +#include "service.h" +#include "strv.h" +#include "tests.h" +#include "unit.h" +#include "virt.h" + +static int test_restrict_filesystems(Manager *m, const char *unit_name, const char *file_path, char **allowed_filesystems) { + _cleanup_free_ char *exec_start = NULL; + _cleanup_(unit_freep) Unit *u = NULL; + ExecContext *ec = NULL; + char **allow_filesystem; + int cld_code, r; + + assert_se(u = unit_new(m, sizeof(Service))); + assert_se(unit_add_name(u, unit_name) == 0); + assert_se(ec = unit_get_exec_context(u)); + + STRV_FOREACH(allow_filesystem, allowed_filesystems) { + r = config_parse_restrict_filesystems( + u->id, "filename", 1, "Service", 1, "RestrictFileSystems", 0, + *allow_filesystem, ec, u); + if (r < 0) + return log_unit_error_errno(u, r, "Failed to parse RestrictFileSystems: %m"); + } + + assert_se(exec_start = strjoin("cat ", file_path)); + r = config_parse_exec(u->id, "filename", 1, "Service", 1, "ExecStart", + SERVICE_EXEC_START, exec_start, SERVICE(u)->exec_command, u); + if (r < 0) + return log_error_errno(r, "Failed to parse ExecStart"); + + SERVICE(u)->type = SERVICE_ONESHOT; + u->load_state = UNIT_LOADED; + + r = unit_start(u); + if (r < 0) + return log_error_errno(r, "Unit start failed %m"); + + while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED)) { + r = sd_event_run(m->event, UINT64_MAX); + if (r < 0) + return log_error_errno(errno, "Event run failed %m"); + } + + cld_code = SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.code; + if (cld_code != CLD_EXITED) { + return log_error_errno(-SYNTHETIC_ERRNO(EBUSY), "ExecStart didn't exited, code='%s'", sigchld_code_to_string(cld_code)); + } + + if (SERVICE(u)->state != SERVICE_DEAD) { + return log_error_errno(-SYNTHETIC_ERRNO(EBUSY), "Service is not dead"); + } + + return 0; +} + +int main(int argc, char *argv[]) { + _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL; + _cleanup_(manager_freep) Manager *m = NULL; + _cleanup_free_ char *unit_dir = NULL; + struct rlimit rl; + int r; + + test_setup_logging(LOG_DEBUG); + + if (getuid() != 0) + return log_tests_skipped("not running as root"); + + assert_se(getrlimit(RLIMIT_MEMLOCK, &rl) >= 0); + rl.rlim_cur = rl.rlim_max = MAX(rl.rlim_max, CAN_MEMLOCK_SIZE); + (void) setrlimit_closest(RLIMIT_MEMLOCK, &rl); + + if (!can_memlock()) + return log_tests_skipped("Can't use mlock(), skipping."); + + r = lsm_bpf_supported(); + if (r <= 0) + return log_tests_skipped("LSM BPF hooks are not supported"); + + r = enter_cgroup_subroot(NULL); + if (r == -ENOMEDIUM) + return log_tests_skipped("cgroupfs not available"); + + assert_se(get_testdata_dir("units", &unit_dir) >= 0); + assert_se(set_unit_path(unit_dir) >= 0); + assert_se(runtime_dir = setup_fake_runtime_dir()); + + assert_se(manager_new(UNIT_FILE_SYSTEM, MANAGER_TEST_RUN_BASIC, &m) >= 0); + assert_se(manager_startup(m, NULL, NULL, NULL) >= 0); + + /* We need to enable access to the filesystem where the binary is so we + * add @common-block */ + assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/tracing/printk_formats", STRV_MAKE("@common-block")) < 0); + assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/tracing/printk_formats", STRV_MAKE("tracefs", "@common-block")) >= 0); + assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/tracing/printk_formats", STRV_MAKE("tracefs", "@common-block", "~tracefs")) < 0); + assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/debug/sleep_time", STRV_MAKE("@common-block")) < 0); + assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/debug/sleep_time", STRV_MAKE("debugfs", "@common-block")) >= 0); + assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/debug/sleep_time", STRV_MAKE("~debugfs")) < 0); + + return 0; +} diff --git a/test/fuzz/fuzz-unit-file/directives.mount b/test/fuzz/fuzz-unit-file/directives.mount index bdb8f95917..a7ee579a9a 100644 --- a/test/fuzz/fuzz-unit-file/directives.mount +++ b/test/fuzz/fuzz-unit-file/directives.mount @@ -144,6 +144,7 @@ ReadWritePaths= RemoveIPC= RestartKillSignal= RestrictAddressFamilies= +RestrictFileSystems= RestrictNamespaces= RestrictNetworkInterfaces= RestrictRealtime= diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service index d9781f0492..4615a2229a 100644 --- a/test/fuzz/fuzz-unit-file/directives.service +++ b/test/fuzz/fuzz-unit-file/directives.service @@ -275,6 +275,7 @@ RestartKillSignal= RestartPreventExitStatus= RestartSec= RestrictAddressFamilies= +RestrictFileSystems= RestrictNamespaces= RestrictNetworkInterfaces= RestrictRealtime= diff --git a/test/fuzz/fuzz-unit-file/directives.socket b/test/fuzz/fuzz-unit-file/directives.socket index 04aeb476f1..df64142982 100644 --- a/test/fuzz/fuzz-unit-file/directives.socket +++ b/test/fuzz/fuzz-unit-file/directives.socket @@ -180,6 +180,7 @@ RemoveIPC= RemoveOnStop= RestartKillSignal= RestrictAddressFamilies= +RestrictFileSystems= RestrictNamespaces= RestrictNetworkInterfaces= RestrictRealtime= diff --git a/test/fuzz/fuzz-unit-file/directives.swap b/test/fuzz/fuzz-unit-file/directives.swap index ff54ebfc18..1dfdffb379 100644 --- a/test/fuzz/fuzz-unit-file/directives.swap +++ b/test/fuzz/fuzz-unit-file/directives.swap @@ -141,6 +141,7 @@ ReadWritePaths= RemoveIPC= RestartKillSignal= RestrictAddressFamilies= +RestrictFileSystems= RestrictNamespaces= RestrictNetworkInterfaces= RestrictRealtime=