systemd/0803-manager-restrict-Dump-to-privileged-callers-or-ratel.patch
Jan Macku 75aa201631 systemd-252-37
Resolves: RHEL-13159,RHEL-20322,RHEL-27512,RHEL-30372,RHEL-31070,RHEL-31219,RHEL-33890,RHEL-35703,RHEL-38864,RHEL-40878,RHEL-6589
2024-06-13 16:16:12 +02:00

265 lines
13 KiB
Diff

From 10e811e931efd55cab87b85eb75cbe73139cec43 Mon Sep 17 00:00:00 2001
From: Luca Boccassi <bluca@debian.org>
Date: Thu, 27 Apr 2023 23:23:30 +0100
Subject: [PATCH] manager: restrict Dump*() to privileged callers or ratelimit
Dump*() methods can take quite some time due to the amount of data to
serialize, so they can potentially stall the manager. Make them
privileged, as they are debugging tools anyway. Use a new 'dump'
capability for polkit, and the 'reload' capability for SELinux, as
that's also non-destructive but slow.
If the caller is not privileged, allow it but rate limited to 10 calls
every 10 minutes.
(cherry picked from commit d936595672cf3ee7c1c547f8fd30512f82be8784)
Resolves: RHEL-35703
---
man/org.freedesktop.systemd1.xml | 7 +++--
man/systemd-analyze.xml | 2 +-
src/core/dbus-manager.c | 34 +++++++++++++++++++--
src/core/dbus.c | 3 ++
src/core/dbus.h | 1 +
src/core/manager-serialize.c | 23 ++++++++++++++
src/core/manager.c | 5 +++
src/core/manager.h | 3 ++
src/core/org.freedesktop.systemd1.policy.in | 10 ++++++
test/units/testsuite-65.sh | 15 +++++++++
10 files changed, 98 insertions(+), 5 deletions(-)
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index c18428a092..7ee649f6a7 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -1363,7 +1363,8 @@ node /org/freedesktop/systemd1 {
<function>DumpByFileDescriptor()</function>/<function>DumpUnitsMatchingPatternsByFileDescriptor()</function>
are usually the preferred interface, since it ensures the data can be passed reliably from the service
manager to the client. Note though that they cannot work when communicating with the service manager
- remotely, as file descriptors are strictly local to a system.</para>
+ remotely, as file descriptors are strictly local to a system. All the <function>Dump*()</function>
+ methods are rate limited for unprivileged users.</para>
<para><function>Reload()</function> may be invoked to reload all unit files.</para>
@@ -1726,7 +1727,9 @@ node /org/freedesktop/systemd1 {
<function>UnsetAndSetEnvironment()</function>) require
<interfacename>org.freedesktop.systemd1.set-environment</interfacename>. <function>Reload()</function>
and <function>Reexecute()</function> require
- <interfacename>org.freedesktop.systemd1.reload-daemon</interfacename>.
+ <interfacename>org.freedesktop.systemd1.reload-daemon</interfacename>. Operations which dump internal
+ state require <interfacename>org.freedesktop.systemd1.bypass-dump-ratelimit</interfacename> to avoid
+ rate limits.
</para>
</refsect2>
</refsect1>
diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml
index 5ba0d40fa0..ff5c84108d 100644
--- a/man/systemd-analyze.xml
+++ b/man/systemd-analyze.xml
@@ -249,7 +249,7 @@ multi-user.target @47.820s
<para>Without any parameter, this command outputs a (usually very long) human-readable serialization of
the complete service manager state. Optional glob pattern may be specified, causing the output to be
limited to units whose names match one of the patterns. The output format is subject to change without
- notice and should not be parsed by applications.</para>
+ notice and should not be parsed by applications. This command is rate limited for unprivileged users.</para>
<example>
<title>Show the internal state of user manager</title>
diff --git a/src/core/dbus-manager.c b/src/core/dbus-manager.c
index 00380cc9c1..44b1027588 100644
--- a/src/core/dbus-manager.c
+++ b/src/core/dbus-manager.c
@@ -1357,17 +1357,47 @@ static int dump_impl(
assert(message);
- /* Anyone can call this method */
-
+ /* 'status' access is the bare minimum always needed for this, as the policy might straight out
+ * forbid a client from querying any information from systemd, regardless of any rate limiting. */
r = mac_selinux_access_check(message, "status", error);
if (r < 0)
return r;
+ /* Rate limit reached? Check if the caller is privileged/allowed by policy to bypass this. We
+ * check the rate limit first to avoid the expensive roundtrip to polkit when not needed. */
+ if (!ratelimit_below(&m->dump_ratelimit)) {
+ /* We need a way for SELinux to constrain the operation when the rate limit is active, even
+ * if polkit would allow it, but we cannot easily add new named permissions, so we need to
+ * use an existing one. Reload/reexec are also slow but non-destructive/modifying
+ * operations, and can cause PID1 to stall. So it seems similar enough in terms of security
+ * considerations and impact, and thus use the same access check for dumps which, given the
+ * large amount of data to fetch, can stall PID1 for quite some time. */
+ r = mac_selinux_access_check(message, "reload", error);
+ if (r < 0)
+ goto ratelimited;
+
+ r = bus_verify_bypass_dump_ratelimit_async(m, message, error);
+ if (r < 0)
+ goto ratelimited;
+ if (r == 0)
+ /* No authorization for now, but the async polkit stuff will call us again when it
+ * has it */
+ return 1;
+ }
+
r = manager_get_dump_string(m, patterns, &dump);
if (r < 0)
return r;
return reply(message, dump);
+
+ratelimited:
+ log_warning("Dump request rejected due to rate limit on unprivileged callers, blocked for %s.",
+ FORMAT_TIMESPAN(ratelimit_left(&m->dump_ratelimit), USEC_PER_SEC));
+ return sd_bus_error_setf(error,
+ SD_BUS_ERROR_LIMITS_EXCEEDED,
+ "Dump request rejected due to rate limit on unprivileged callers, blocked for %s.",
+ FORMAT_TIMESPAN(ratelimit_left(&m->dump_ratelimit), USEC_PER_SEC));
}
static int reply_dump(sd_bus_message *message, char *dump) {
diff --git a/src/core/dbus.c b/src/core/dbus.c
index 141c3ffe12..3cbe9c5cfd 100644
--- a/src/core/dbus.c
+++ b/src/core/dbus.c
@@ -1177,6 +1177,9 @@ int bus_verify_reload_daemon_async(Manager *m, sd_bus_message *call, sd_bus_erro
int bus_verify_set_environment_async(Manager *m, sd_bus_message *call, sd_bus_error *error) {
return bus_verify_polkit_async(call, CAP_SYS_ADMIN, "org.freedesktop.systemd1.set-environment", NULL, false, UID_INVALID, &m->polkit_registry, error);
}
+int bus_verify_bypass_dump_ratelimit_async(Manager *m, sd_bus_message *call, sd_bus_error *error) {
+ return bus_verify_polkit_async(call, CAP_SYS_ADMIN, "org.freedesktop.systemd1.bypass-dump-ratelimit", NULL, false, UID_INVALID, &m->polkit_registry, error);
+}
uint64_t manager_bus_n_queued_write(Manager *m) {
uint64_t c = 0;
diff --git a/src/core/dbus.h b/src/core/dbus.h
index 369d9f56a2..50e7bb400e 100644
--- a/src/core/dbus.h
+++ b/src/core/dbus.h
@@ -27,6 +27,7 @@ int bus_verify_manage_units_async(Manager *m, sd_bus_message *call, sd_bus_error
int bus_verify_manage_unit_files_async(Manager *m, sd_bus_message *call, sd_bus_error *error);
int bus_verify_reload_daemon_async(Manager *m, sd_bus_message *call, sd_bus_error *error);
int bus_verify_set_environment_async(Manager *m, sd_bus_message *call, sd_bus_error *error);
+int bus_verify_bypass_dump_ratelimit_async(Manager *m, sd_bus_message *call, sd_bus_error *error);
int bus_forward_agent_released(Manager *m, const char *path);
diff --git a/src/core/manager-serialize.c b/src/core/manager-serialize.c
index 27cb0925ae..f3b2d7ee16 100644
--- a/src/core/manager-serialize.c
+++ b/src/core/manager-serialize.c
@@ -164,6 +164,14 @@ int manager_serialize(
(void) serialize_item_format(f, "user-lookup", "%i %i", copy0, copy1);
}
+ (void) serialize_item_format(f,
+ "dump-ratelimit",
+ USEC_FMT " " USEC_FMT " %u %u",
+ m->dump_ratelimit.begin,
+ m->dump_ratelimit.interval,
+ m->dump_ratelimit.num,
+ m->dump_ratelimit.burst);
+
bus_track_serialize(m->subscribed, f, "subscribed");
r = dynamic_user_serialize(m, f, fds);
@@ -549,6 +557,21 @@ int manager_deserialize(Manager *m, FILE *f, FDSet *fds) {
* remains set until all serialized contents are handled. */
if (deserialize_varlink_sockets)
(void) varlink_server_deserialize_one(m->varlink_server, val, fds);
+ } else if ((val = startswith(l, "dump-ratelimit="))) {
+ usec_t begin, interval;
+ unsigned num, burst;
+
+ if (sscanf(val, USEC_FMT " " USEC_FMT " %u %u", &begin, &interval, &num, &burst) != 4)
+ log_notice("Failed to parse dump ratelimit, ignoring: %s", val);
+ else {
+ /* If we changed the values across versions, flush the counter */
+ if (interval != m->dump_ratelimit.interval || burst != m->dump_ratelimit.burst)
+ m->dump_ratelimit.num = 0;
+ else
+ m->dump_ratelimit.num = num;
+ m->dump_ratelimit.begin = begin;
+ }
+
} else {
ManagerTimestamp q;
diff --git a/src/core/manager.c b/src/core/manager.c
index eeee395b90..b44c7785cf 100644
--- a/src/core/manager.c
+++ b/src/core/manager.c
@@ -866,6 +866,11 @@ int manager_new(LookupScope scope, ManagerTestRunFlags test_run_flags, Manager *
.test_run_flags = test_run_flags,
.default_oom_policy = OOM_STOP,
+
+ .dump_ratelimit = {
+ .interval = 10 * USEC_PER_MINUTE,
+ .burst = 10,
+ },
};
#if ENABLE_EFI
diff --git a/src/core/manager.h b/src/core/manager.h
index 87e63c3b68..86e7e40989 100644
--- a/src/core/manager.h
+++ b/src/core/manager.h
@@ -461,6 +461,9 @@ struct Manager {
struct restrict_fs_bpf *restrict_fs;
char *default_smack_process_label;
+
+ /* Dump*() are slow, so always rate limit them to 10 per 10 minutes */
+ RateLimit dump_ratelimit;
};
static inline usec_t manager_default_timeout_abort_usec(Manager *m) {
diff --git a/src/core/org.freedesktop.systemd1.policy.in b/src/core/org.freedesktop.systemd1.policy.in
index 74adeadf38..9e9a20f66f 100644
--- a/src/core/org.freedesktop.systemd1.policy.in
+++ b/src/core/org.freedesktop.systemd1.policy.in
@@ -70,4 +70,14 @@
</defaults>
</action>
+ <action id="org.freedesktop.systemd1.bypass-dump-ratelimit">
+ <description gettext-domain="systemd">Dump the systemd state without rate limits</description>
+ <message gettext-domain="systemd">Authentication is required to dump the systemd state without rate limits.</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ </action>
+
</policyconfig>
diff --git a/test/units/testsuite-65.sh b/test/units/testsuite-65.sh
index 7c34948f82..f416194922 100755
--- a/test/units/testsuite-65.sh
+++ b/test/units/testsuite-65.sh
@@ -51,6 +51,21 @@ systemd-analyze dot --require systemd-journald.service systemd-logind.service >/
systemd-analyze dot "systemd-*.service" >/dev/null
(! systemd-analyze dot systemd-journald.service systemd-logind.service "*" bbb ccc)
# dump
+# this should be rate limited to 10 calls in 10 minutes for unprivileged callers
+for _ in {1..10}; do
+ runas testuser systemd-analyze dump systemd-journald.service >/dev/null
+done
+(! runas testuser systemd-analyze dump >/dev/null)
+# still limited after a reload
+systemctl daemon-reload
+(! runas testuser systemd-analyze dump >/dev/null)
+# and a re-exec
+systemctl daemon-reexec
+(! runas testuser systemd-analyze dump >/dev/null)
+# privileged call, so should not be rate limited
+for _ in {1..10}; do
+ systemd-analyze dump systemd-journald.service >/dev/null
+done
systemd-analyze dump >/dev/null
systemd-analyze dump "*" >/dev/null
systemd-analyze dump "*.socket" >/dev/null