diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 38fd7098aa..0835481f37 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 7176e3c046..e0eba1cf64 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 b18aa1d4ff..d6b2dcb114 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 7277696196..3fef44e668 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 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 4570f06b73..c2b6fc1db6 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 36881de479..f8d469ebf6 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 486eda11b6..08dcee3ce7 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 74adeadf38..9e9a20f66f 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 05673e7cc7..c6a81b4b4e 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