tmpfiles: Add commands for file capabilites

This commit is contained in:
Valentin David
2026-05-09 11:36:55 +02:00
committed by Daan De Meyer
parent ba93240758
commit 89148192f8
3 changed files with 416 additions and 3 deletions
+39 -3
View 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>
+314
View 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) {
+63
View File
@@ -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