From 53d6987f9e46927bbc9ad683c091c070ebe06658 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Fri, 28 Apr 2023 13:10:23 +0100 Subject: [PATCH 1/2] ratelimit: add ratelimit_left helper --- src/basic/ratelimit.c | 9 +++++++++ src/basic/ratelimit.h | 1 + 2 files changed, 10 insertions(+) diff --git a/src/basic/ratelimit.c b/src/basic/ratelimit.c index 5675ec2f46b..41ca0709e8e 100644 --- a/src/basic/ratelimit.c +++ b/src/basic/ratelimit.c @@ -49,3 +49,12 @@ usec_t ratelimit_end(const RateLimit *rl) { return usec_add(rl->begin, rl->interval); } + +usec_t ratelimit_left(const RateLimit *rl) { + assert(rl); + + if (rl->begin == 0) + return 0; + + return usec_sub_unsigned(ratelimit_end(rl), now(CLOCK_MONOTONIC)); +} diff --git a/src/basic/ratelimit.h b/src/basic/ratelimit.h index 048084ece49..bb7160a895b 100644 --- a/src/basic/ratelimit.h +++ b/src/basic/ratelimit.h @@ -25,3 +25,4 @@ bool ratelimit_below(RateLimit *r); unsigned ratelimit_num_dropped(RateLimit *r); usec_t ratelimit_end(const RateLimit *rl); +usec_t ratelimit_left(const RateLimit *rl); From d936595672cf3ee7c1c547f8fd30512f82be8784 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Thu, 27 Apr 2023 23:23:30 +0100 Subject: [PATCH 2/2] 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. --- 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 | 2 ++ src/core/org.freedesktop.systemd1.policy.in | 10 ++++++ test/units/testsuite-65.sh | 15 +++++++++ 10 files changed, 97 insertions(+), 5 deletions(-) diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 38fd7098aa2..0835481f37d 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -1403,7 +1403,8 @@ node /org/freedesktop/systemd1 { DumpByFileDescriptor()/DumpUnitsMatchingPatternsByFileDescriptor() 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. + remotely, as file descriptors are strictly local to a system. All the Dump*() + methods are rate limited for unprivileged users. Reload() may be invoked to reload all unit files. @@ -1778,7 +1779,9 @@ node /org/freedesktop/systemd1 { UnsetAndSetEnvironment()) require org.freedesktop.systemd1.set-environment. Reload() and Reexecute() require - org.freedesktop.systemd1.reload-daemon. + org.freedesktop.systemd1.reload-daemon. Operations which dump internal + state require org.freedesktop.systemd1.bypass-dump-ratelimit to avoid + rate limits. diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index 7176e3c0468..e0eba1cf644 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -278,7 +278,7 @@ multi-user.target @47.820s 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. + notice and should not be parsed by applications. This command is rate limited for unprivileged users. Show the internal state of user manager diff --git a/src/core/dbus-manager.c b/src/core/dbus-manager.c index b18aa1d4ff2..d6b2dcb1145 100644 --- a/src/core/dbus-manager.c +++ b/src/core/dbus-manager.c @@ -1413,17 +1413,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 7277696196e..3fef44e6687 100644 --- a/src/core/dbus.c +++ b/src/core/dbus.c @@ -1203,6 +1203,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 369d9f56a28..50e7bb400e9 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 4570f06b731..c2b6fc1db6a 100644 --- a/src/core/manager-serialize.c +++ b/src/core/manager-serialize.c @@ -165,6 +165,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); @@ -550,6 +558,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 36881de4790..f8d469ebf64 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -913,6 +913,11 @@ int manager_new(RuntimeScope runtime_scope, ManagerTestRunFlags test_run_flags, .default_memory_pressure_watch = CGROUP_PRESSURE_WATCH_AUTO, .default_memory_pressure_threshold_usec = USEC_INFINITY, + + .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 486eda11b60..08dcee3ce76 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -471,6 +471,8 @@ struct Manager { /* Allow users to configure a rate limit for Reload() operations */ RateLimit reload_ratelimit; + /* Dump*() are slow, so always rate limit them to 10 per 10 minutes */ + RateLimit dump_ratelimit; sd_event_source *memory_pressure_event_source; }; diff --git a/src/core/org.freedesktop.systemd1.policy.in b/src/core/org.freedesktop.systemd1.policy.in index 74adeadf38c..9e9a20f66f6 100644 --- a/src/core/org.freedesktop.systemd1.policy.in +++ b/src/core/org.freedesktop.systemd1.policy.in @@ -70,4 +70,14 @@ + + Dump the systemd state without rate limits + Authentication is required to dump the systemd state without rate limits. + + auth_admin + auth_admin + auth_admin_keep + + + diff --git a/test/units/testsuite-65.sh b/test/units/testsuite-65.sh index 05673e7cc73..c6a81b4b4eb 100755 --- a/test/units/testsuite-65.sh +++ b/test/units/testsuite-65.sh @@ -53,6 +53,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