]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tmpfiles: Add commands for file capabilites
authorValentin David <me@valentindavid.com>
Sat, 28 Mar 2026 18:48:36 +0000 (19:48 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sat, 9 May 2026 09:36:55 +0000 (11:36 +0200)
man/tmpfiles.d.xml
src/tmpfiles/tmpfiles.c
test/units/TEST-22-TMPFILES.22.sh [new file with mode: 0755]

index 8a908c412519fdab4ac729dbda73b173dad3a1b4..e5f694611a60d25010e0f34987d690e4d4f83e95 100644 (file)
@@ -76,6 +76,10 @@ a     /path-or-glob/to/set/acls                -    -    -     -           POSIX
 a+    /path-or-glob/to/append/acls             -    -    -     -           POSIX ACLs
 A     /path-or-glob/to/set/acls/recursively    -    -    -     -           POSIX ACLs
 A+    /path-or-glob/to/append/acls/recursively -    -    -     -           POSIX ACLs
+k     /path-or-glob/to/set/caps                -    -    -     -           file capabilities
+k+    /path-or-glob/to/adjust/caps             -    -    -     -           file capabilities
+K     /path-or-glob/to/set/caps/recursively    -    -    -     -           file capabilities
+K+    /path-or-glob/to/adjust/caps/recursively -    -    -     -           file capabilities
 
 </programlisting>
   </refsynopsisdiv>
@@ -484,6 +488,37 @@ L     /tmp/foobar -    -    -     -   /dev/null</programlisting>
 
           <xi:include href="version-info.xml" xpointer="v219"/></listitem>
         </varlistentry>
+
+        <varlistentry>
+          <term><varname>k</varname></term>
+          <term><varname>k+</varname></term>
+          <listitem><para>Set file capabilities, see <citerefentry
+          project='man-pages'><refentrytitle>capabilities</refentrytitle><manvolnum>7</manvolnum></citerefentry>.
+          Lines of this type accept shell-style globs in place of normal path names. Does not follow
+          symlinks.</para>
+
+          <para>The syntax follows <citerefentry
+          project='man-pages'><refentrytitle>cap_text_formats</refentrytitle><manvolnum>7</manvolnum></citerefentry>. It
+          also supports <literal>rootuid=<replaceable>INT</replaceable></literal> for the user namespace root
+          user ID.</para>
+
+          <para>If suffixed with <varname>+</varname>, current capabilities on the file that are not touched by the expression
+          will be kept. For example, if all <literal>cap_setuid</literal> capabilities need to be removed but
+          others should be kept, one can use <varname>k+</varname> with <literal>cap_setuid=</literal> or
+          <literal>cap_setuid-eip</literal>.</para>
+
+          <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term><varname>K</varname></term>
+          <term><varname>K+</varname></term>
+          <listitem><para>Same as <varname>k</varname> and
+          <varname>k+</varname>, but recursive. Does not follow
+          symlinks.</para>
+
+          <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+        </varlistentry>
       </variablelist>
     </refsect2>
 
@@ -565,8 +600,8 @@ w- /proc/sys/vm/swappiness - - - - 10</programlisting></para>
       <literal>-</literal>, the default is used: 0755 for directories, 0644 for all other file objects. For
       <varname>z</varname>, <varname>Z</varname> lines, if omitted or when set to <literal>-</literal>, the
       file access mode will not be modified. This parameter is ignored for <varname>x</varname>,
-      <varname>r</varname>, <varname>R</varname>, <varname>L</varname>, <varname>t</varname>, and
-      <varname>a</varname> lines.</para>
+      <varname>r</varname>, <varname>R</varname>, <varname>L</varname>, <varname>t</varname>,
+      <varname>a</varname>, and <varname>k</varname> lines.</para>
 
       <para>Optionally, if prefixed with <literal>~</literal>, the access mode is masked based on the already
       set access bits for existing file or directories: if the existing file has all executable bits unset,
@@ -707,7 +742,8 @@ d /tmp/foo/bar - - - bmA:1h -</programlisting></para>
       suffixed by a newline. For <varname>C</varname>, specifies the source file or directory. For <varname>t</varname>
       and <varname>T</varname>, determines extended attributes to be set. For <varname>a</varname> and
       <varname>A</varname>, determines ACL attributes to be set. For <varname>h</varname> and <varname>H</varname>,
-      determines the file attributes to set. Ignored for all other lines.</para>
+      determines the file attributes to set. For <varname>k</varname> and <varname>K</varname>, determines
+      file capabilities to be set. Ignored for all other lines.</para>
 
       <para>This field can contain specifiers, see below.</para>
     </refsect2>
index 44843f3ca77eca203c86e48f1dde4b68b89ddb27..b6007f6207ed66a1219b4612f77068c59a5107ab 100644 (file)
@@ -14,6 +14,7 @@
 #include "bitfield.h"
 #include "btrfs-util.h"
 #include "build.h"
+#include "capability-list.h"
 #include "capability-util.h"
 #include "chase.h"
 #include "chattr-util.h"
@@ -104,6 +105,8 @@ typedef enum ItemType {
         RECURSIVE_SET_XATTR            = 'T',
         SET_ACL                        = 'a',
         RECURSIVE_SET_ACL              = 'A',
+        SET_FCAPS                      = 'k',
+        RECURSIVE_SET_FCAPS            = 'K',
         SET_ATTRIBUTE                  = 'h',
         RECURSIVE_SET_ATTRIBUTE        = 'H',
         IGNORE_PATH                    = 'x',
@@ -126,6 +129,18 @@ typedef enum AgeBy {
         AGE_BY_DEFAULT_DIR  = AGE_BY_ATIME | AGE_BY_BTIME | AGE_BY_MTIME,
 } AgeBy;
 
+typedef struct FCapsPatch {
+        uint64_t mask;
+        uint64_t set;
+} FCapsPatch;
+
+typedef struct FCapsUpdate {
+        uid_t rootuid;
+        FCapsPatch inheritable;
+        FCapsPatch permitted;
+        FCapsPatch effective;
+} FCapsUpdate;
+
 typedef struct Item {
         ItemType type;
 
@@ -139,6 +154,7 @@ typedef struct Item {
         acl_t acl_access_exec;
         acl_t acl_default;
 #endif
+        FCapsUpdate fcaps;
         uid_t uid;
         gid_t gid;
         mode_t mode;
@@ -171,6 +187,8 @@ typedef struct Item {
 
         bool ignore_if_target_missing:1;
 
+        bool fcaps_set:1;
+
         OperationMask done;
 } Item;
 
@@ -408,6 +426,8 @@ static bool needs_glob(ItemType t) {
                       RECURSIVE_SET_XATTR,
                       SET_ACL,
                       RECURSIVE_SET_ACL,
+                      SET_FCAPS,
+                      RECURSIVE_SET_FCAPS,
                       SET_ATTRIBUTE,
                       RECURSIVE_SET_ATTRIBUTE,
                       IGNORE_PATH,
@@ -1502,6 +1522,271 @@ static int path_set_acls(
         return r;
 }
 
+static int capability_vfs_from_string(const char *s, FCapsUpdate *ret) {
+        FCapsUpdate set = {
+                .rootuid = UID_INVALID,
+        };
+
+        assert(s);
+        assert(ret);
+
+        for (const char *p = s;;) {
+                _cleanup_free_ char *word = NULL, *keys = NULL;
+                char *value, sep;
+                int r;
+
+                r = extract_first_word(&p, &word, NULL, EXTRACT_UNQUOTE|EXTRACT_RELAX);
+                if (r < 0)
+                        return log_debug_errno(r, "Failed to split words from '%s': %m", p);
+                if (r == 0)
+                        break;
+
+                value = strpbrk(word, "=+-");
+                if (!value)
+                        return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse key/value '%s': %m", word);
+                keys = strndup(word, value - word);
+                if (!keys)
+                        return log_oom();
+                sep = *value;
+                value++;
+
+                if (sep == '=' && streq(keys, "rootuid")) {
+                        r = parse_uid(value, &set.rootuid);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to parse rootuid value '%s': %m", value);
+                } else {
+                        uint64_t caps = 0;
+
+                        if (!in_charset(value, "eip"))
+                                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse value '%s': %m", value);
+
+                        if (STR_IN_SET(keys, "all", ""))
+                                caps = all_capabilities();
+                        else
+                                for (const char *remaining_keys = keys; remaining_keys && *remaining_keys;) {
+                                        _cleanup_free_ char *key = NULL;
+
+                                        r = extract_first_word(&remaining_keys, &key, ",", /* flags= */0);
+                                        if (r < 0)
+                                                return log_debug_errno(r, "Failed to parse capability list '%s': %m", keys);
+                                        if (r == 0)
+                                                break;
+                                        if (streq(key, "all"))
+                                                caps = all_capabilities();
+                                        else {
+                                                r = capability_from_name(key);
+                                                if (r < 0)
+                                                        return log_debug_errno(r, "Failed to parse capability '%s': %m", key);
+                                                caps |= UINT64_C(1) << r;
+                                        }
+                                }
+
+                        if (sep == '=') {
+                                set.permitted.mask |= caps;
+                                set.inheritable.mask |= caps;
+                                set.effective.mask |= caps;
+                        }
+                        if (IN_SET(sep, '=', '+')) {
+                                if (strchr(value, 'p'))
+                                        set.permitted.set |= caps;
+                                if (strchr(value, 'i'))
+                                        set.inheritable.set |= caps;
+                                if (strchr(value, 'e'))
+                                        set.effective.set |= caps;
+                        } else {
+                                if (strchr(value, 'p')) {
+                                        set.permitted.mask |= caps;
+                                        set.permitted.set &= ~caps;
+                                }
+                                if (strchr(value, 'i')) {
+                                        set.inheritable.mask |= caps;
+                                        set.inheritable.set &= ~caps;
+                                }
+                                if (strchr(value, 'e')) {
+                                        set.effective.mask |= caps;
+                                        set.effective.set &= ~caps;
+                                }
+                        }
+                }
+        }
+
+        *ret = set;
+
+        return 0;
+}
+
+static size_t cap_data_size(uint32_t revision) {
+        switch (revision) {
+        case VFS_CAP_REVISION_1:
+                return XATTR_CAPS_SZ_1;
+        case VFS_CAP_REVISION_2:
+                return XATTR_CAPS_SZ_2;
+        case VFS_CAP_REVISION_3:
+                return XATTR_CAPS_SZ_3;
+        default:
+                return SIZE_MAX;
+        }
+}
+
+static bool inode_type_can_fcaps(mode_t mode) {
+        return S_ISREG(mode);
+}
+
+static int apply_fcaps(int fd, const char *path, bool append, const FCapsUpdate *set) {
+        struct vfs_ns_cap_data val = {
+                .magic_etc = htole32(VFS_CAP_REVISION),
+        };
+        le32_t effective[VFS_CAP_U32] = {};
+        struct stat st;
+        int r;
+
+        assert(fd >= 0);
+        assert(path);
+        assert(set);
+
+        if (fstat(fd, &st) < 0)
+                return log_error_errno(errno, "Failed to fstat(%s): %m", path);
+
+        if (hardlink_vulnerable(&st))
+                return log_error_errno(SYNTHETIC_ERRNO(EPERM),
+                                       "Refusing to set file capabilities on hardlinked file %s while the fs.protected_hardlinks sysctl is turned off.",
+                                       path);
+
+        if (!inode_type_can_fcaps(st.st_mode)) {
+                log_debug("Skipping file capabilities for '%s' (inode type does not support file capabilities).", path);
+                return 0;
+        }
+
+        if (append) {
+                _cleanup_free_ char *xattr_data = NULL;
+                size_t xattr_data_len;
+
+                r = fgetxattr_malloc(fd, "security.capability", &xattr_data, &xattr_data_len);
+                if (r == -ENODATA)
+                        log_debug("No capabilities found for '%s'", path);
+                else if (r < 0)
+                        return log_error_errno(r, "Failed to read capabilities of '%s': %m", path);
+                else {
+                        _cleanup_free_ struct vfs_ns_cap_data *original = NULL;
+
+                        if (xattr_data_len < endoffsetof_field(struct vfs_ns_cap_data, magic_etc))
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extended attributes for capabilities are too small");
+
+                        original = realloc0(xattr_data, sizeof(struct vfs_ns_cap_data));
+                        if (!original)
+                                return log_oom();
+                        xattr_data = NULL;
+
+                        size_t expected_size = cap_data_size(le32toh(original->magic_etc) & VFS_CAP_REVISION_MASK);
+                        if (expected_size == SIZE_MAX)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown type of file capabilities");
+                        if (xattr_data_len != expected_size)
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Size of file capabilities does not match its type");
+
+                        if (FLAGS_SET(le32toh(original->magic_etc), VFS_CAP_FLAGS_EFFECTIVE))
+                                for (size_t n = 0; n < VFS_CAP_U32; n++)
+                                        effective[n] = original->data[n].permitted | original->data[n].inheritable;
+
+                        for (size_t n = 0; n < VFS_CAP_U32; n++) {
+                                val.data[n].permitted = original->data[n].permitted;
+                                val.data[n].inheritable = original->data[n].inheritable;
+                        }
+
+                        val.rootid = original->rootid;
+                }
+        }
+
+        for (size_t n = 0; n < VFS_CAP_U32; n++) {
+                size_t bit_shift = 32*n;
+                val.data[n].inheritable &= htole32(~(set->inheritable.mask >> bit_shift) & UINT32_C(0xffffffff));
+                val.data[n].inheritable |= htole32((set->inheritable.set >> bit_shift) & UINT32_C(0xffffffff));
+                val.data[n].permitted &= htole32(~(set->permitted.mask >> bit_shift) & UINT32_C(0xffffffff));
+                val.data[n].permitted |= htole32((set->permitted.set >> bit_shift) & UINT32_C(0xffffffff));
+                effective[n] &= htole32(~(set->effective.mask >> bit_shift) & UINT32_C(0xffffffff));
+                effective[n] |= htole32((set->effective.set >> bit_shift) & UINT32_C(0xffffffff));
+                if (effective[n] != 0)
+                        val.magic_etc |= htole32(VFS_CAP_FLAGS_EFFECTIVE);
+        }
+
+        if (FLAGS_SET(le32toh(val.magic_etc), VFS_CAP_FLAGS_EFFECTIVE))
+                for (size_t n = 0; n < VFS_CAP_U32; n++)
+                        if ((val.data[n].permitted | val.data[n].inheritable) != effective[n])
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Inconsistent effective bits");
+
+        if (set->rootuid != UID_INVALID)
+                val.rootid = htole32(set->rootuid);
+
+        log_action("Would try to set", "Trying to set",
+                   "%s capabilities on %s", path);
+
+        if (!arg_dry_run) {
+                r = xsetxattr_full(fd, /* path= */ NULL, AT_EMPTY_PATH, "security.capability", (void*)&val, sizeof(val), /* xattr_flags= */ 0);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to setcap '%s': %m", path);
+        }
+
+        return 0;
+}
+
+static int parse_caps_from_arg(Item *item) {
+        FCapsUpdate fcaps;
+        int r;
+
+        assert(item);
+
+        r = capability_vfs_from_string(item->argument, &fcaps);
+        if (r < 0) {
+                log_full_errno(arg_graceful ? LOG_DEBUG : LOG_WARNING,
+                               r, "Failed to parse capabilities \"%s\", ignoring: %m", item->argument);
+                return 0;
+        }
+
+        item->fcaps_set = true;
+        item->fcaps = fcaps;
+
+        return 0;
+}
+
+static int fd_set_caps(
+                Context *c,
+                Item *item,
+                int fd,
+                const char *path,
+                const struct stat *st,
+                CreationMode creation) {
+        assert(c);
+        assert(item);
+        assert(fd >= 0);
+        assert(path);
+
+        if (!item->fcaps_set)
+                return 0;
+        return apply_fcaps(fd, path, item->append_or_force, &item->fcaps);
+}
+
+static int path_set_caps(
+                Context *c,
+                Item *item,
+                const char *path,
+                CreationMode creation) {
+        _cleanup_close_ int fd = -EBADF;
+
+        assert(c);
+        assert(item);
+        assert(path);
+
+        if (!item->fcaps_set)
+                return 0;
+
+        fd = path_open_safe(path);
+        if (fd == -ENOENT)
+                return 0;
+        if (fd < 0)
+                return fd;
+
+        return apply_fcaps(fd, path, item->append_or_force, &item->fcaps);
+}
+
 static int parse_attribute_from_arg(Item *item) {
         static const struct {
                 char character;
@@ -2955,6 +3240,18 @@ static int create_item(Context *c, Item *i) {
                         return r;
                 break;
 
+        case SET_FCAPS:
+                r = glob_item(c, i, path_set_caps);
+                if (r < 0)
+                        return r;
+                break;
+
+        case RECURSIVE_SET_FCAPS:
+                r = glob_item_recursively(c, i, fd_set_caps);
+                if (r < 0)
+                        return r;
+                break;
+
         case SET_ATTRIBUTE:
                 r = glob_item(c, i, path_set_attribute);
                 if (r < 0)
@@ -3816,6 +4113,23 @@ static int parse_line(
                         return r;
                 break;
 
+        case SET_FCAPS:
+        case RECURSIVE_SET_FCAPS:
+                if (unbase64) {
+                        *invalid_config = true;
+                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
+                                          "base64 decoding not supported for capabilities.");
+                }
+                if (!i.argument) {
+                        *invalid_config = true;
+                        return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
+                                          "Set capabilities requires argument.");
+                }
+                r = parse_caps_from_arg(&i);
+                if (r < 0)
+                        return r;
+                break;
+
         case SET_ATTRIBUTE:
         case RECURSIVE_SET_ATTRIBUTE:
                 if (unbase64) {
diff --git a/test/units/TEST-22-TMPFILES.22.sh b/test/units/TEST-22-TMPFILES.22.sh
new file mode 100755 (executable)
index 0000000..37c9f70
--- /dev/null
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+rm -f /tmp/setcap
+touch /tmp/setcap
+
+systemd-tmpfiles --dry-run --create - <<EOF
+k /tmp/setcap - - - - cap_setuid,cap_setgid=ep cap_setgid+i
+EOF
+cap="$(getcap /tmp/setcap)"
+assert_not_in 'cap_setuid' "$cap"
+assert_not_in 'cap_setgid' "$cap"
+
+systemd-tmpfiles --create - <<EOF
+k /tmp/setcap - - - - cap_setuid,cap_setgid=ep cap_setgid+i
+EOF
+assert_in 'cap_setgid=eip cap_setuid[+]ep' "$(getcap /tmp/setcap)"
+
+systemd-tmpfiles --create - <<EOF
+k+ /tmp/setcap - - - - cap_setuid=
+EOF
+cap="$(getcap /tmp/setcap)"
+assert_not_in 'cap_setuid' "$cap"
+assert_in 'cap_setgid=eip' "$cap"
+
+systemd-tmpfiles --create - <<EOF
+k /tmp/setcap - - - - cap_setuid=
+EOF
+cap="$(getcap /tmp/setcap)"
+assert_not_in 'cap_setuid' "$cap"
+assert_not_in 'cap_setgid' "$cap"
+
+systemd-tmpfiles --create - <<EOF
+k /tmp/setcap - - - - cap_setuid=eip rootuid=1000
+EOF
+
+assert_in '[[]rootid=1000[]]' "$(getcap -n /tmp/setcap)"
+
+rm -f /tmp/setcap
+
+rm -rf /tmp/setcap-dir
+mkdir /tmp/setcap-dir
+touch /tmp/setcap-dir/file
+
+systemd-tmpfiles --create - <<EOF
+K /tmp/setcap-dir - - - - cap_setuid,cap_setgid=ep cap_setgid+i
+EOF
+assert_in 'cap_setgid=eip cap_setuid[+]ep' "$(getcap /tmp/setcap-dir/file)"
+
+systemd-tmpfiles --create - <<EOF
+K+ /tmp/setcap-dir - - - - cap_setuid=
+EOF
+cap="$(getcap /tmp/setcap-dir/file)"
+assert_not_in 'cap_setuid' "$cap"
+assert_in 'cap_setgid=eip' "$cap"
+
+rm -rf /tmp/setcap-dir