diff --git a/NEWS b/NEWS
index a5f882c5ec..4ef7fb2af3 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,23 @@
systemd System and Service Manager
+CHANGES WITH 254 in spe:
+
+ Security relevant changes:
+
+ * pam_systemd will now by default pass the CAP_WAKE_ALARM ambient
+ process capability to invoked session processes of regular users on
+ local seats (as well as to systemd --user), unless configured
+ otherwise via data from JSON user records, or via the PAM module's
+ parameter list. This is useful in order allow desktop tools such as
+ GNOME's Alarm Clock application to set a timer for
+ CLOCK_REALTIME_ALARM that wakes up the system when it elapses. A
+ per-user service unit file may thus use AmbientCapability= to pass
+ the capability to invoked processes. Note that this capability is
+ relatively narrow in focus (in particular compared to other process
+ capabilities such as CAP_SYS_ADMIN) and we already — by default —
+ permit more impactful operations such as system suspend to local
+ users.
+
CHANGES WITH 253:
Announcements of Future Feature Removals and Incompatible Changes:
diff --git a/man/homectl.xml b/man/homectl.xml
index 634b95a081..36c1741eb5 100644
--- a/man/homectl.xml
+++ b/man/homectl.xml
@@ -271,6 +271,19 @@
the user will be removed from the group.
+
+ CAPABILITIES
+ CAPABILITIES
+
+ These options take a space separated list of process capabilities
+ (e.g. CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, …) that shall be
+ set in the capability bounding and ambient sets for all the user's sessions. See capabilities7
+ for details on the capabilities concept. These options may be used more than once, in which case the
+ specified lists are combined. If the parameter begins with a ~ character the
+ effect is inverted: the specified capability is dropped from the specific set.
+
+
PATH
diff --git a/man/pam_systemd.xml b/man/pam_systemd.xml
index 60b8577822..f2bd3de0b0 100644
--- a/man/pam_systemd.xml
+++ b/man/pam_systemd.xml
@@ -122,6 +122,22 @@
further details.
+
+ default-capability-bounding-set=
+ default-capability-ambient-set=
+
+ Takes a comma-separated list of process capabilities
+ (e.g. CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, …) to set for the
+ invoked session's processes, if the user record does not encode appropriate sets of capabilities
+ directly. See capabilities7
+ for details on the capabilities concept. If not specified, the default bounding set is left as is
+ (i.e. usually contains the full set of capabilities). The default ambient set is set to
+ CAP_WAKE_ALARM for regular users if the PAM session is associated with a local
+ seat or if it is invoked for the systemd-user service. Otherwise defaults to the
+ empty set.
+
+
debug=
diff --git a/mkosi.conf.d/arch/10-arch.conf b/mkosi.conf.d/arch/10-arch.conf
index ee51396e04..a1bff77096 100644
--- a/mkosi.conf.d/arch/10-arch.conf
+++ b/mkosi.conf.d/arch/10-arch.conf
@@ -32,6 +32,7 @@ Packages=
popt
python-pefile
quota-tools
+ shadow
tpm2-tss
vim
diff --git a/mkosi.conf.d/centos/10-centos.conf b/mkosi.conf.d/centos/10-centos.conf
index 94aeaed9c9..91099a7f0a 100644
--- a/mkosi.conf.d/centos/10-centos.conf
+++ b/mkosi.conf.d/centos/10-centos.conf
@@ -28,6 +28,7 @@ Packages=
libasan
libbpf
libcap-ng
+ libcap-ng-utils
libfido2
libmicrohttpd
libmnl
@@ -38,6 +39,7 @@ Packages=
numactl-libs
p11-kit
pam
+ passwd
polkit
popt
procps-ng
diff --git a/mkosi.conf.d/debian/10-debian.conf b/mkosi.conf.d/debian/10-debian.conf
index 905378a9b4..0712a70bea 100644
--- a/mkosi.conf.d/debian/10-debian.conf
+++ b/mkosi.conf.d/debian/10-debian.conf
@@ -20,6 +20,7 @@ Packages=
libasound2
libbpf1
libc6-i386
+ libcap-ng-utils
libcap-ng0
libfido2-1
libglib2.0-0
@@ -35,6 +36,7 @@ Packages=
libqrencode4
libtss2-dev # Use the -dev package to avoid churn in updating version numbers
netcat-openbsd
+ passwd
policykit-1
procps
python3-pefile
diff --git a/mkosi.conf.d/fedora/10-fedora.conf b/mkosi.conf.d/fedora/10-fedora.conf
index 5d4959ba16..5c85eebc03 100644
--- a/mkosi.conf.d/fedora/10-fedora.conf
+++ b/mkosi.conf.d/fedora/10-fedora.conf
@@ -24,6 +24,7 @@ Packages=
libasan
libbpf
libcap-ng
+ libcap-ng-utils
libfido2
libgcrypt
libmicrohttpd
@@ -34,6 +35,7 @@ Packages=
netcat
numactl-libs
pam
+ passwd
polkit
popt
procps-ng
diff --git a/mkosi.conf.d/opensuse/10-opensuse.conf b/mkosi.conf.d/opensuse/10-opensuse.conf
index 6034b8a87e..3b0a643efd 100644
--- a/mkosi.conf.d/opensuse/10-opensuse.conf
+++ b/mkosi.conf.d/opensuse/10-opensuse.conf
@@ -18,6 +18,7 @@ Packages=
glibc-locale-base
libasound2
libbpf1
+ libcap-ng-utils
libcap-ng0
libcrypt1
libcryptsetup12
@@ -38,6 +39,7 @@ Packages=
libxkbcommon0
pam
python3-pefile
+ shadow
tpm2-0-tss
vim
diff --git a/mkosi.conf.d/ubuntu/10-ubuntu.conf b/mkosi.conf.d/ubuntu/10-ubuntu.conf
index 647a71826c..a34fe8d94f 100644
--- a/mkosi.conf.d/ubuntu/10-ubuntu.conf
+++ b/mkosi.conf.d/ubuntu/10-ubuntu.conf
@@ -21,6 +21,7 @@ Packages=
libasound2
libbpf0
libc6-i386
+ libcap-ng-utils
libcap-ng0
libfdisk1
libfido2-1
@@ -37,6 +38,7 @@ Packages=
linux-tools-common
linux-tools-generic
netcat-openbsd
+ passwd
policykit-1
procps
python3-pefile
diff --git a/src/home/homectl.c b/src/home/homectl.c
index cd24b5e9a5..531b685e22 100644
--- a/src/home/homectl.c
+++ b/src/home/homectl.c
@@ -9,6 +9,8 @@
#include "bus-common-errors.h"
#include "bus-error.h"
#include "bus-locator.h"
+#include "cap-list.h"
+#include "capability-util.h"
#include "cgroup-util.h"
#include "dns-domain.h"
#include "env-util.h"
@@ -38,10 +40,10 @@
#include "spawn-polkit-agent.h"
#include "terminal-util.h"
#include "uid-alloc-range.h"
+#include "user-record.h"
#include "user-record-pwquality.h"
#include "user-record-show.h"
#include "user-record-util.h"
-#include "user-record.h"
#include "user-util.h"
#include "verbs.h"
@@ -76,6 +78,8 @@ static enum {
EXPORT_FORMAT_STRIPPED, /* strip "state" + "binding", but leave signature in place */
EXPORT_FORMAT_MINIMAL, /* also strip signature */
} arg_export_format = EXPORT_FORMAT_FULL;
+static uint64_t arg_capability_bounding_set = UINT64_MAX;
+static uint64_t arg_capability_ambient_set = UINT64_MAX;
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
@@ -2210,6 +2214,10 @@ static int help(int argc, char *argv[], void *userdata) {
" -d --home-dir=PATH Home directory\n"
" -u --uid=UID Numeric UID for user\n"
" -G --member-of=GROUP Add user to group\n"
+ " --capability-bounding-set=CAPS\n"
+ " Bounding POSIX capability set\n"
+ " --capability-ambient-set=CAPS\n"
+ " Ambient POSIX capability set\n"
" --skel=PATH Skeleton directory to use\n"
" --shell=PATH Shell for account\n"
" --setenv=VARIABLE[=VALUE] Set an environment variable at log-in\n"
@@ -2402,6 +2410,8 @@ static int parse_argv(int argc, char *argv[]) {
ARG_AUTO_RESIZE_MODE,
ARG_REBALANCE_WEIGHT,
ARG_FIDO2_CRED_ALG,
+ ARG_CAPABILITY_BOUNDING_SET,
+ ARG_CAPABILITY_AMBIENT_SET,
};
static const struct option options[] = {
@@ -2492,6 +2502,8 @@ static int parse_argv(int argc, char *argv[]) {
{ "luks-extra-mount-options", required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS },
{ "auto-resize-mode", required_argument, NULL, ARG_AUTO_RESIZE_MODE },
{ "rebalance-weight", required_argument, NULL, ARG_REBALANCE_WEIGHT },
+ { "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET },
+ { "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET },
{}
};
@@ -3735,6 +3747,61 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
+ case ARG_CAPABILITY_AMBIENT_SET:
+ case ARG_CAPABILITY_BOUNDING_SET: {
+ _cleanup_strv_free_ char **l = NULL;
+ bool subtract = false;
+ uint64_t parsed, *which, updated;
+ const char *p, *field;
+
+ if (c == ARG_CAPABILITY_AMBIENT_SET) {
+ which = &arg_capability_ambient_set;
+ field = "capabilityAmbientSet";
+ } else {
+ assert(c == ARG_CAPABILITY_BOUNDING_SET);
+ which = &arg_capability_bounding_set;
+ field = "capabilityBoundingSet";
+ }
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ *which = UINT64_MAX;
+ break;
+ }
+
+ p = optarg;
+ if (*p == '~') {
+ subtract = true;
+ p++;
+ }
+
+ r = capability_set_from_string(p, &parsed);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid capabilities in capability string '%s'.", p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse capability string '%s': %m", p);
+
+ if (*which == UINT64_MAX)
+ updated = subtract ? all_capabilities() & ~parsed : parsed;
+ else if (subtract)
+ updated = *which & ~parsed;
+ else
+ updated = *which | parsed;
+
+ if (capability_set_to_strv(updated, &l) < 0)
+ return log_oom();
+
+ r = json_variant_set_field_strv(&arg_identity_extra, field, l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ *which = updated;
+ break;
+ }
+
case '?':
return -EINVAL;
diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c
index 2cfe52288e..65d09886be 100644
--- a/src/login/pam_systemd.c
+++ b/src/login/pam_systemd.c
@@ -21,6 +21,8 @@
#include "bus-error.h"
#include "bus-internal.h"
#include "bus-locator.h"
+#include "cap-list.h"
+#include "capability-util.h"
#include "cgroup-setup.h"
#include "devnum-util.h"
#include "errno-util.h"
@@ -47,20 +49,79 @@
#define LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
+static int parse_caps(
+ pam_handle_t *handle,
+ const char *value,
+ uint64_t *caps) {
+
+ bool subtract;
+ int r;
+
+ assert(handle);
+ assert(value);
+
+ if (value[0] == '~') {
+ subtract = true;
+ value++;
+ } else
+ subtract = false;
+
+ for (;;) {
+ _cleanup_free_ char *s = NULL;
+ uint64_t b, m;
+ int c;
+
+ /* We can't use spaces as separators here, as PAM's simplistic argument parser doesn't allow
+ * spaces inside of arguments. We use commas instead (which is similar to cap_from_text(),
+ * which also uses commas). */
+ r = extract_first_word(&value, &s, ",", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ c = capability_from_name(s);
+ if (c < 0) {
+ pam_syslog(handle, LOG_WARNING, "Unknown capability, ignoring: %s", s);
+ continue;
+ }
+
+ m = UINT64_C(1) << c;
+
+ if (!caps)
+ continue;
+
+ if (*caps == UINT64_MAX)
+ b = subtract ? all_capabilities() : 0;
+ else
+ b = *caps;
+
+ if (subtract)
+ *caps = b & ~m;
+ else
+ *caps = b | m;
+ }
+
+ return 0;
+}
+
static int parse_argv(
pam_handle_t *handle,
int argc, const char **argv,
const char **class,
const char **type,
const char **desktop,
- bool *debug) {
+ bool *debug,
+ uint64_t *default_capability_bounding_set,
+ uint64_t *default_capability_ambient_set) {
- unsigned i;
+ int r;
+ assert(handle);
assert(argc >= 0);
assert(argc == 0 || argv);
- for (i = 0; i < (unsigned) argc; i++) {
+ for (int i = 0; i < argc; i++) {
const char *p;
if ((p = startswith(argv[i], "class="))) {
@@ -80,16 +141,24 @@ static int parse_argv(
*debug = true;
} else if ((p = startswith(argv[i], "debug="))) {
- int k;
-
- k = parse_boolean(p);
- if (k < 0)
+ r = parse_boolean(p);
+ if (r < 0)
pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", p);
else if (debug)
- *debug = k;
+ *debug = r;
+
+ } else if ((p = startswith(argv[i], "default-capability-bounding-set="))) {
+ r = parse_caps(handle, p, default_capability_bounding_set);
+ if (r < 0)
+ pam_syslog(handle, LOG_WARNING, "Failed to parse default-capability-bounding-set= argument, ignoring: %s", p);
+
+ } else if ((p = startswith(argv[i], "default-capability-ambient-set="))) {
+ r = parse_caps(handle, p, default_capability_ambient_set);
+ if (r < 0)
+ pam_syslog(handle, LOG_WARNING, "Failed to parse default-capability-ambient-set= argument, ignoring: %s", p);
} else
- pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring", argv[i]);
+ pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring.", argv[i]);
}
return 0;
@@ -531,7 +600,12 @@ static int pam_putenv_and_log(pam_handle_t *handle, const char *e, bool debug) {
return PAM_SUCCESS;
}
-static int apply_user_record_settings(pam_handle_t *handle, UserRecord *ur, bool debug) {
+static int apply_user_record_settings(
+ pam_handle_t *handle,
+ UserRecord *ur,
+ bool debug,
+ uint64_t default_capability_bounding_set,
+ uint64_t default_capability_ambient_set) {
int r;
assert(handle);
@@ -645,6 +719,31 @@ static int apply_user_record_settings(pam_handle_t *handle, UserRecord *ur, bool
"Resource limit %s set, based on user record.", rlimit_to_string(rl));
}
+ uint64_t a, b;
+ a = user_record_capability_ambient_set(ur);
+ if (a == UINT64_MAX)
+ a = default_capability_ambient_set;
+
+ b = user_record_capability_bounding_set(ur);
+ if (b == UINT64_MAX)
+ b = default_capability_bounding_set;
+
+ if (a != UINT64_MAX && a != 0) {
+ a &= b;
+
+ r = capability_ambient_set_apply(a, /* also_inherit= */ true);
+ if (r < 0)
+ pam_syslog_errno(handle, LOG_ERR, r,
+ "Failed to set ambient capabilities, ignoring: %m");
+ }
+
+ if (b != UINT64_MAX && !cap_test_all(b)) {
+ r = capability_bounding_set_drop(b, /* right_now= */ false);
+ if (r < 0)
+ pam_syslog_errno(handle, LOG_ERR, r,
+ "Failed to set bounding capabilities, ignoring: %m");
+ }
+
return PAM_SUCCESS;
}
@@ -669,6 +768,21 @@ static int configure_runtime_directory(
return export_legacy_dbus_address(handle, rt);
}
+static uint64_t pick_default_capability_ambient_set(
+ UserRecord *ur,
+ const char *service,
+ const char *seat) {
+
+ /* If not configured otherwise, let's enable CAP_WAKE_ALARM for regular users when logging in on a
+ * seat (i.e. when they are present physically on the device), or when invoked for the systemd --user
+ * instances. This allows desktops to install CAP_WAKE_ALARM to implement alarm clock apps without
+ * much fuss. */
+
+ return ur &&
+ user_record_disposition(ur) == USER_REGULAR &&
+ (streq_ptr(service, "systemd-user") || !isempty(seat)) ? (UINT64_C(1) << CAP_WAKE_ALARM) : UINT64_MAX;
+}
+
_public_ PAM_EXTERN int pam_sm_open_session(
pam_handle_t *handle,
int flags,
@@ -685,6 +799,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
*type = NULL, *class = NULL,
*class_pam = NULL, *type_pam = NULL, *cvtnr = NULL, *desktop = NULL, *desktop_pam = NULL,
*memory_max = NULL, *tasks_max = NULL, *cpu_weight = NULL, *io_weight = NULL, *runtime_max_sec = NULL;
+ uint64_t default_capability_bounding_set = UINT64_MAX, default_capability_ambient_set = UINT64_MAX;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int session_fd = -EBADF, existing, r;
@@ -699,7 +814,9 @@ _public_ PAM_EXTERN int pam_sm_open_session(
&class_pam,
&type_pam,
&desktop_pam,
- &debug) < 0)
+ &debug,
+ &default_capability_bounding_set,
+ &default_capability_ambient_set) < 0)
return PAM_SESSION_ERR;
if (debug)
@@ -988,7 +1105,10 @@ _public_ PAM_EXTERN int pam_sm_open_session(
}
success:
- r = apply_user_record_settings(handle, ur, debug);
+ if (default_capability_ambient_set == UINT64_MAX)
+ default_capability_ambient_set = pick_default_capability_ambient_set(ur, service, seat);
+
+ r = apply_user_record_settings(handle, ur, debug, default_capability_bounding_set, default_capability_ambient_set);
if (r != PAM_SUCCESS)
return r;
@@ -1016,7 +1136,9 @@ _public_ PAM_EXTERN int pam_sm_close_session(
NULL,
NULL,
NULL,
- &debug) < 0)
+ &debug,
+ NULL,
+ NULL) < 0)
return PAM_SESSION_ERR;
if (debug)
diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c
index da181c6078..28fa7a8632 100644
--- a/src/shared/user-record-show.c
+++ b/src/shared/user-record-show.c
@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include "cap-list.h"
#include "format-util.h"
#include "fs-util.h"
#include "process-util.h"
@@ -287,6 +288,22 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
if (hr->access_mode != MODE_INVALID)
printf(" Access Mode: 0%03o\n", user_record_access_mode(hr));
+ uint64_t caps = user_record_capability_bounding_set(hr);
+ if (caps != UINT64_MAX) {
+ _cleanup_free_ char *scaps = NULL;
+
+ (void) capability_set_to_string_negative(caps, &scaps);
+ printf(" Bound. Caps: %s\n", strna(scaps));
+ }
+
+ caps = user_record_capability_ambient_set(hr);
+ if (caps != UINT64_MAX) {
+ _cleanup_free_ char *scaps = NULL;
+
+ (void) capability_set_to_string(caps, &scaps);
+ printf("Ambient Caps: %s\n", strna(scaps));
+ }
+
if (storage == USER_LUKS) {
printf("LUKS Discard: online=%s offline=%s\n", yes_no(user_record_luks_discard(hr)), yes_no(user_record_luks_offline_discard(hr)));
diff --git a/src/shared/user-record.c b/src/shared/user-record.c
index 06bc699572..b51d8add1a 100644
--- a/src/shared/user-record.c
+++ b/src/shared/user-record.c
@@ -2,6 +2,7 @@
#include
+#include "cap-list.h"
#include "cgroup-util.h"
#include "dns-domain.h"
#include "env-util.h"
@@ -165,6 +166,8 @@ static UserRecord* user_record_free(UserRecord *h) {
free(h->home_directory_auto);
strv_free(h->member_of);
+ strv_free(h->capability_bounding_set);
+ strv_free(h->capability_ambient_set);
free(h->file_system_type);
free(h->luks_cipher);
@@ -1203,6 +1206,8 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp
{ "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 },
{ "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 },
{ "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), JSON_RELAX},
+ { "capabilityBoundingSet", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, capability_bounding_set), JSON_SAFE },
+ { "capabilityAmbientSet", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, capability_ambient_set), JSON_SAFE },
{ "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
{ "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 },
{ "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 },
@@ -1557,6 +1562,8 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla
{ "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 },
{ "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 },
{ "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), JSON_RELAX},
+ { "capabilityBoundingSet", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, capability_bounding_set), JSON_SAFE },
+ { "capabilityAmbientSet", JSON_VARIANT_ARRAY, json_dispatch_strv, offsetof(UserRecord, capability_ambient_set), JSON_SAFE },
{ "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
{ "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 },
{ "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 },
@@ -2018,6 +2025,43 @@ uint64_t user_record_rebalance_weight(UserRecord *h) {
return h->rebalance_weight;
}
+static uint64_t parse_caps_strv(char **l) {
+ uint64_t c = 0;
+ int r;
+
+ STRV_FOREACH(i, l) {
+ r = capability_from_name(*i);
+ if (r < 0)
+ log_debug_errno(r, "Don't know capability '%s', ignoring: %m", *i);
+ else
+ c |= UINT64_C(1) << r;
+ }
+
+ return c;
+}
+
+uint64_t user_record_capability_bounding_set(UserRecord *h) {
+ assert(h);
+
+ /* Returns UINT64_MAX if no bounding set is configured (!) */
+
+ if (!h->capability_bounding_set)
+ return UINT64_MAX;
+
+ return parse_caps_strv(h->capability_bounding_set);
+}
+
+uint64_t user_record_capability_ambient_set(UserRecord *h) {
+ assert(h);
+
+ /* Returns UINT64_MAX if no ambient set is configured (!) */
+
+ if (!h->capability_ambient_set)
+ return UINT64_MAX;
+
+ return parse_caps_strv(h->capability_ambient_set) & user_record_capability_bounding_set(h);
+}
+
uint64_t user_record_ratelimit_next_try(UserRecord *h) {
assert(h);
diff --git a/src/shared/user-record.h b/src/shared/user-record.h
index 73fb86cc94..298dc244fe 100644
--- a/src/shared/user-record.h
+++ b/src/shared/user-record.h
@@ -366,6 +366,9 @@ typedef struct UserRecord {
RecoveryKey *recovery_key;
size_t n_recovery_key;
+ char **capability_bounding_set;
+ char **capability_ambient_set;
+
JsonVariant *json;
} UserRecord;
@@ -410,6 +413,8 @@ bool user_record_can_authenticate(UserRecord *h);
bool user_record_drop_caches(UserRecord *h);
AutoResizeMode user_record_auto_resize_mode(UserRecord *h);
uint64_t user_record_rebalance_weight(UserRecord *h);
+uint64_t user_record_capability_bounding_set(UserRecord *h);
+uint64_t user_record_capability_ambient_set(UserRecord *h);
int user_record_build_image_path(UserStorage storage, const char *user_name_and_realm, char **ret);
diff --git a/test/units/testsuite-35.sh b/test/units/testsuite-35.sh
index 85925f2471..02f22cf2a2 100755
--- a/test/units/testsuite-35.sh
+++ b/test/units/testsuite-35.sh
@@ -573,6 +573,58 @@ EOF
assert_eq "$(loginctl --no-legend | grep -c "logind-test-user")" 0
}
+test_ambient_caps() {
+ local PAMSERVICE TRANSIENTUNIT SCRIPT
+
+ # Verify that pam_systemd works and assigns ambient caps as it should
+
+ if ! grep -q 'CapAmb:' /proc/self/status ; then
+ echo "ambient caps not available, skipping test." >&2
+ return
+ fi
+
+ typeset -i BND MASK
+
+ # Get PID 1's bounding set
+ BND="0x$(grep 'CapBnd:' /proc/1/status | cut -d: -f2 | tr -d '[:space:]')"
+
+ # CAP_CHOWN | CAP_KILL
+ MASK=$(((1 << 0) | (1 << 5)))
+
+ if [ $(("$BND" & "$MASK")) -ne "$MASK" ] ; then
+ echo "CAP_CHOWN or CAP_KILL not available in bounding set, skipping test." >&2
+ return
+ fi
+
+ PAMSERVICE="pamserv$RANDOM"
+ TRANSIENTUNIT="capwakealarm$RANDOM.service"
+ SCRIPT="/tmp/capwakealarm$RANDOM.sh"
+
+ cat > /etc/pam.d/"$PAMSERVICE" < "$SCRIPT" <<'EOF'
+#!/bin/bash
+set -ex
+typeset -i AMB MASK
+AMB="0x$(grep 'CapAmb:' /proc/self/status | cut -d: -f2 | tr -d '[:space:]')"
+MASK=$(((1 << 0) | (1 << 5)))
+test "$AMB" -eq "$MASK"
+EOF
+
+ chmod +x "$SCRIPT"
+
+ systemd-run -u "$TRANSIENTUNIT" -p PAMName="$PAMSERVICE" -p Type=oneshot -p User=logind-test-user -p StandardError=tty "$SCRIPT"
+
+ rm -f "$SCRIPT" "$PAMSERVICE"
+}
+
: >/failed
setup_test_user
@@ -587,6 +639,7 @@ test_lock_idle_action
test_session_properties
test_list_users
test_stop_idle_session
+test_ambient_caps
touch /testok
rm /failed