From 89148192f8fb6fef31616af458a942ad0fdd5acc Mon Sep 17 00:00:00 2001 From: Valentin David Date: Sat, 28 Mar 2026 19:48:36 +0100 Subject: [PATCH 1/3] tmpfiles: Add commands for file capabilites --- man/tmpfiles.d.xml | 42 +++- src/tmpfiles/tmpfiles.c | 314 ++++++++++++++++++++++++++++++ test/units/TEST-22-TMPFILES.22.sh | 63 ++++++ 3 files changed, 416 insertions(+), 3 deletions(-) create mode 100755 test/units/TEST-22-TMPFILES.22.sh diff --git a/man/tmpfiles.d.xml b/man/tmpfiles.d.xml index 8a908c412519f..e5f694611a60d 100644 --- a/man/tmpfiles.d.xml +++ b/man/tmpfiles.d.xml @@ -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 @@ -484,6 +488,37 @@ L /tmp/foobar - - - - /dev/null + + + k + k+ + Set file capabilities, see capabilities7. + Lines of this type accept shell-style globs in place of normal path names. Does not follow + symlinks. + + The syntax follows cap_text_formats7. It + also supports rootuid=INT for the user namespace root + user ID. + + If suffixed with +, current capabilities on the file that are not touched by the expression + will be kept. For example, if all cap_setuid capabilities need to be removed but + others should be kept, one can use k+ with cap_setuid= or + cap_setuid-eip. + + + + + + K + K+ + Same as k and + k+, but recursive. Does not follow + symlinks. + + + @@ -565,8 +600,8 @@ w- /proc/sys/vm/swappiness - - - - 10 -, the default is used: 0755 for directories, 0644 for all other file objects. For z, Z lines, if omitted or when set to -, the file access mode will not be modified. This parameter is ignored for x, - r, R, L, t, and - a lines. + r, R, L, t, + a, and k lines. Optionally, if prefixed with ~, 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 - suffixed by a newline. For C, specifies the source file or directory. For t and T, determines extended attributes to be set. For a and A, determines ACL attributes to be set. For h and H, - determines the file attributes to set. Ignored for all other lines. + determines the file attributes to set. For k and K, determines + file capabilities to be set. Ignored for all other lines. This field can contain specifiers, see below. diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c index 44843f3ca77ec..b6007f6207ed6 100644 --- a/src/tmpfiles/tmpfiles.c +++ b/src/tmpfiles/tmpfiles.c @@ -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 index 0000000000000..37c9f709b75ce --- /dev/null +++ b/test/units/TEST-22-TMPFILES.22.sh @@ -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 - < Date: Sat, 9 May 2026 05:59:10 +0000 Subject: [PATCH 2/3] po: Translated using Weblate (Romanian) Currently translated at 68.7% (183 of 266 strings) Co-authored-by: Petru Rebeja Translate-URL: https://translate.fedoraproject.org/projects/systemd/main/ro/ Translation: systemd/main --- po/ro.po | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/po/ro.po b/po/ro.po index 876dc27773b09..97dcf594331a3 100644 --- a/po/ro.po +++ b/po/ro.po @@ -4,21 +4,22 @@ # va511e , 2015. # Daniel Șerbănescu , 2015, 2017. # Vlad , 2020, 2021. +# Petru Rebeja , 2026. msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-03-06 03:46+0900\n" -"PO-Revision-Date: 2021-01-12 17:36+0000\n" -"Last-Translator: Vlad \n" +"PO-Revision-Date: 2026-05-09 05:59+0000\n" +"Last-Translator: Petru Rebeja \n" "Language-Team: Romanian \n" +"systemd/main/ro/>\n" "Language: ro\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " "20)) ? 1 : 2;\n" -"X-Generator: Weblate 4.4\n" +"X-Generator: Weblate 5.17.1\n" #: src/core/org.freedesktop.systemd1.policy.in:22 msgid "Send passphrase back to system" @@ -125,16 +126,12 @@ msgstr "" "utilizator." #: src/home/org.freedesktop.home1.policy:53 -#, fuzzy msgid "Update your home area" -msgstr "Actualizează un spațiu personal" +msgstr "Actualizați-vă spațiu personal" #: src/home/org.freedesktop.home1.policy:54 -#, fuzzy msgid "Authentication is required to update your home area." -msgstr "" -"Autentificarea este necesară pentru a actualiza spațiul personal al unui " -"utilizator." +msgstr "Pentru a-ți actualiza spațiul personal, este necesară autentificarea." #: src/home/org.freedesktop.home1.policy:63 msgid "Resize a home area" @@ -158,16 +155,14 @@ msgstr "" "al unui utilizator." #: src/home/org.freedesktop.home1.policy:83 -#, fuzzy msgid "Activate a home area" -msgstr "Crează un spațiu personal" +msgstr "Activează un spațiu personal" #: src/home/org.freedesktop.home1.policy:84 -#, fuzzy msgid "Authentication is required to activate a user's home area." msgstr "" -"Autentificarea este necesară pentru a crea spațiul personal al unui " -"utilizator." +"Pentru a activa spațiul personal al unui utilizator este necesară " +"autentificarea." #: src/home/org.freedesktop.home1.policy:93 msgid "Manage Home Directory Signing Keys" From b7be9ccc8f4299269f72bde49e426a7a9d484da9 Mon Sep 17 00:00:00 2001 From: Matheus Afonso Martins Moreira Date: Sat, 9 May 2026 08:53:01 -0300 Subject: [PATCH 3/3] hwdb/keyboard: fix KP_Enter on Clevo PA70ES The ITE keyboard controller firmware (version 0xAB83) is shared between the Clevo PA70ES and the X+ piccolo series. The piccolo's hwdb rule matches by input device ID (evdev:input:b0011v0001p0001eAB83*) and remaps scan code 0x9c (KP_Enter) to Enter, since the piccolo has no numpad and its main Enter key sends the wrong scan code. The Clevo PA70ES has a real numpad. The piccolo rule matches it because both laptops use the same ITE controller firmware, which breaks KP_Enter on the PA70ES. Add a DMI-specific override that restores KEY_KPENTER for 0x9c on the PA70ES. The piccolo rule should ideally be narrowed to use DMI matching instead of input device ID to avoid catching other laptops with the same ITE controller firmware. --- hwdb.d/60-keyboard.hwdb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/hwdb.d/60-keyboard.hwdb b/hwdb.d/60-keyboard.hwdb index ebc08560fe9e8..23023740901ac 100644 --- a/hwdb.d/60-keyboard.hwdb +++ b/hwdb.d/60-keyboard.hwdb @@ -344,6 +344,15 @@ evdev:atkbd:dmi:bvn*:bvr*:bd*:svn*BenQ*:pn*Joybook*R22*:* # Clevo ########################################################### +# Clevo PA70ES (Avell C73) +# The ITE keyboard controller firmware (version 0xAB83) is shared with +# the X+ piccolo. The piccolo rule (below) matches by input device ID +# and remaps KP_Enter to Enter since the piccolo has no numpad and its +# main Enter sends the wrong scan code. The PA70ES has a real numpad, +# so the remap breaks KP_Enter. This restores the correct mapping. +evdev:atkbd:dmi:bvn*:bvr*:bd*:svnNotebook:pnPA70ES:* + KEYBOARD_KEY_9c=kpenter + evdev:atkbd:dmi:bvn*:bvr*:bd*:svnNotebook:pnW65_67SZ:* KEYBOARD_KEY_a0=!mute KEYBOARD_KEY_a2=!playpause