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
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/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"
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 - <