diff options
30 files changed, 663 insertions, 1 deletions
diff --git a/docs/TRANSIENT-SETTINGS.md b/docs/TRANSIENT-SETTINGS.md index 2c893cad6e..07e248f8d5 100644 --- a/docs/TRANSIENT-SETTINGS.md +++ b/docs/TRANSIENT-SETTINGS.md @@ -148,6 +148,7 @@ All execution-related settings are available for transient units. ✓ SyslogLevelPrefix= ✓ LogLevelMax= ✓ LogExtraFields= +✓ LogFilterPatterns= ✓ LogRateLimitIntervalSec= ✓ LogRateLimitBurst= ✓ SecureBits= diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 4f8936e866..32ead7f272 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -2928,6 +2928,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly aay LogExtraFields = [[...], ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly a(bs) LogFilterPatterns = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s LogNamespace = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly i SecureBits = ...; @@ -3479,6 +3481,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { <!--property LogExtraFields is not documented!--> + <!--property LogFilterPatterns is not documented!--> + <!--property LogNamespace is not documented!--> <!--property AmbientCapabilities is not documented!--> @@ -4083,6 +4087,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { <variablelist class="dbus-property" generated="True" extra-ref="LogExtraFields"/> + <variablelist class="dbus-property" generated="True" extra-ref="LogFilterPatterns"/> + <variablelist class="dbus-property" generated="True" extra-ref="LogNamespace"/> <variablelist class="dbus-property" generated="True" extra-ref="SecureBits"/> @@ -4822,6 +4828,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly aay LogExtraFields = [[...], ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly a(bs) LogFilterPatterns = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s LogNamespace = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly i SecureBits = ...; @@ -5397,6 +5405,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { <!--property LogExtraFields is not documented!--> + <!--property LogFilterPatterns is not documented!--> + <!--property LogNamespace is not documented!--> <!--property AmbientCapabilities is not documented!--> @@ -5995,6 +6005,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { <variablelist class="dbus-property" generated="True" extra-ref="LogExtraFields"/> + <variablelist class="dbus-property" generated="True" extra-ref="LogFilterPatterns"/> + <variablelist class="dbus-property" generated="True" extra-ref="LogNamespace"/> <variablelist class="dbus-property" generated="True" extra-ref="SecureBits"/> @@ -6623,6 +6635,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly aay LogExtraFields = [[...], ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly a(bs) LogFilterPatterns = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s LogNamespace = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly i SecureBits = ...; @@ -7126,6 +7140,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { <!--property LogExtraFields is not documented!--> + <!--property LogFilterPatterns is not documented!--> + <!--property LogNamespace is not documented!--> <!--property AmbientCapabilities is not documented!--> @@ -7642,6 +7658,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { <variablelist class="dbus-property" generated="True" extra-ref="LogExtraFields"/> + <variablelist class="dbus-property" generated="True" extra-ref="LogFilterPatterns"/> + <variablelist class="dbus-property" generated="True" extra-ref="LogNamespace"/> <variablelist class="dbus-property" generated="True" extra-ref="SecureBits"/> @@ -8397,6 +8415,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly aay LogExtraFields = [[...], ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly a(bs) LogFilterPatterns = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s LogNamespace = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly i SecureBits = ...; @@ -8886,6 +8906,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { <!--property LogExtraFields is not documented!--> + <!--property LogFilterPatterns is not documented!--> + <!--property LogNamespace is not documented!--> <!--property AmbientCapabilities is not documented!--> @@ -9388,6 +9410,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { <variablelist class="dbus-property" generated="True" extra-ref="LogExtraFields"/> + <variablelist class="dbus-property" generated="True" extra-ref="LogFilterPatterns"/> + <variablelist class="dbus-property" generated="True" extra-ref="LogNamespace"/> <variablelist class="dbus-property" generated="True" extra-ref="SecureBits"/> diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index d003ab1838..5e6658ff06 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -2920,6 +2920,34 @@ StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZX </varlistentry> <varlistentry> + <term><varname>LogFilterPatterns=</varname></term> + + <listitem><para>Define an extended regular expression to filter log messages based on the + <varname>MESSAGE=</varname> field of the structured message. If the first character of the pattern is + <literal>~</literal>, log entries matching the pattern should be discarded. This option takes a single + pattern as an argument but can be used multiple times to create a list of allowed and denied patterns. + If the empty string is assigned, the filter is reset, and all prior assignments will have no effect.</para> + + <para>Because the <literal>~</literal> character is used to define denied patterns, it must be replaced + with <literal>\x7e</literal> to allow a message starting with <literal>~</literal>. For example, + <literal>~foobar</literal> would add a pattern matching <literal>foobar</literal> to the deny list, while + <literal>\x7efoobar</literal> would add a pattern matching <literal>~foobar</literal> to the allow list.</para> + + <para>Log messages are tested against denied patterns (if any), then against allowed patterns + (if any). If a log message matches any of the denied patterns, it will be discarded, whatever the + allowed patterns. Then, remaining log messages are tested against allowed patterns. Messages matching + against none of the allowed pattern are discarded. If no allowed patterns are defined, then all + messages are processed directly after going through denied filters.</para> + + <para>Filtering is based on the unit for which <varname>LogFilterPatterns=</varname> is defined, meaning log + messages coming from + <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry> about the + unit are not taken into account. Filtered log messages won't be forwarded to traditional syslog daemons, + the kernel log buffer (kmsg), the systemd console, or sent as wall messages to all logged-in + users.</para></listitem> + </varlistentry> + + <varlistentry> <term><varname>LogNamespace=</varname></term> <listitem><para>Run the unit's processes in the specified journal namespace. Expects a short diff --git a/src/basic/nulstr-util.c b/src/basic/nulstr-util.c index 44b88ca753..98d68e0b01 100644 --- a/src/basic/nulstr-util.c +++ b/src/basic/nulstr-util.c @@ -115,6 +115,21 @@ int strv_make_nulstr(char * const *l, char **ret, size_t *ret_size) { return 0; } +int set_make_nulstr(Set *s, char **ret, size_t *ret_size) { + /* Use _cleanup_free_ instead of _cleanup_strv_free_ because we need to clean the strv only, not + * the strings owned by the set. */ + _cleanup_free_ char **strv = NULL; + + assert(ret); + assert(ret_size); + + strv = set_get_strv(s); + if (!strv) + return -ENOMEM; + + return strv_make_nulstr(strv, ret, ret_size); +} + const char* nulstr_get(const char *nulstr, const char *needle) { if (!nulstr) return NULL; diff --git a/src/basic/nulstr-util.h b/src/basic/nulstr-util.h index 19f4edd384..fd0ed44528 100644 --- a/src/basic/nulstr-util.h +++ b/src/basic/nulstr-util.h @@ -6,6 +6,8 @@ #include <stdbool.h> #include <string.h> +#include "set.h" + #define NULSTR_FOREACH(i, l) \ for (typeof(*(l)) *(i) = (l); (i) && *(i); (i) = strchr((i), 0)+1) @@ -21,6 +23,7 @@ static inline bool nulstr_contains(const char *nulstr, const char *needle) { char** strv_parse_nulstr(const char *s, size_t l); char** strv_split_nulstr(const char *s); int strv_make_nulstr(char * const *l, char **p, size_t *n); +int set_make_nulstr(Set *s, char **ret, size_t *ret_size); static inline int strv_from_nulstr(char ***ret, const char *nulstr) { char **t; diff --git a/src/core/cgroup.c b/src/core/cgroup.c index ecc3cb32ef..2d671566ac 100644 --- a/src/core/cgroup.c +++ b/src/core/cgroup.c @@ -781,6 +781,51 @@ void cgroup_oomd_xattr_apply(Unit *u, const char *cgroup_path) { unit_remove_xattr_graceful(u, cgroup_path, "user.oomd_omit"); } +int cgroup_log_xattr_apply(Unit *u, const char *cgroup_path) { + ExecContext *c; + size_t len, allowed_patterns_len, denied_patterns_len; + _cleanup_free_ char *patterns = NULL, *allowed_patterns = NULL, *denied_patterns = NULL; + int r; + + assert(u); + + c = unit_get_exec_context(u); + if (!c) + /* Some unit types have a cgroup context but no exec context, so we do not log + * any error here to avoid confusion. */ + return 0; + + if (set_isempty(c->log_filter_allowed_patterns) && set_isempty(c->log_filter_denied_patterns)) { + unit_remove_xattr_graceful(u, cgroup_path, "user.journald_log_filter_patterns"); + return 0; + } + + r = set_make_nulstr(c->log_filter_allowed_patterns, &allowed_patterns, &allowed_patterns_len); + if (r < 0) + return log_debug_errno(r, "Failed to make nulstr from set: %m"); + + r = set_make_nulstr(c->log_filter_denied_patterns, &denied_patterns, &denied_patterns_len); + if (r < 0) + return log_debug_errno(r, "Failed to make nulstr from set: %m"); + + /* Use nul character separated strings without trailing nul */ + allowed_patterns_len = LESS_BY(allowed_patterns_len, 1u); + denied_patterns_len = LESS_BY(denied_patterns_len, 1u); + + len = allowed_patterns_len + 1 + denied_patterns_len; + patterns = new(char, len); + if (!patterns) + return log_oom_debug(); + + memcpy_safe(patterns, allowed_patterns, allowed_patterns_len); + patterns[allowed_patterns_len] = '\xff'; + memcpy_safe(&patterns[allowed_patterns_len + 1], denied_patterns, denied_patterns_len); + + unit_set_xattr_graceful(u, cgroup_path, "user.journald_log_filter_patterns", patterns, len); + + return 0; +} + static void cgroup_xattr_apply(Unit *u) { bool b; @@ -788,6 +833,7 @@ static void cgroup_xattr_apply(Unit *u) { /* The 'user.*' xattrs can be set from a user manager. */ cgroup_oomd_xattr_apply(u, u->cgroup_path); + cgroup_log_xattr_apply(u, u->cgroup_path); if (!MANAGER_IS_SYSTEM(u->manager)) return; diff --git a/src/core/cgroup.h b/src/core/cgroup.h index 09352bafc6..a6a3d186ac 100644 --- a/src/core/cgroup.h +++ b/src/core/cgroup.h @@ -240,6 +240,7 @@ int cgroup_add_device_allow(CGroupContext *c, const char *dev, const char *mode) int cgroup_add_bpf_foreign_program(CGroupContext *c, uint32_t attach_type, const char *path); void cgroup_oomd_xattr_apply(Unit *u, const char *cgroup_path); +int cgroup_log_xattr_apply(Unit *u, const char *cgroup_path); CGroupMask unit_get_own_mask(Unit *u); CGroupMask unit_get_delegate_mask(Unit *u); diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 41eeb1ee94..f514b8fd12 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -31,6 +31,7 @@ #include "namespace.h" #include "parse-util.h" #include "path-util.h" +#include "pcre2-util.h" #include "process-util.h" #include "rlimit-util.h" #if HAVE_SECCOMP @@ -799,6 +800,53 @@ static int property_get_log_extra_fields( return sd_bus_message_close_container(reply); } +static int sd_bus_message_append_log_filter_patterns(sd_bus_message *reply, Set *patterns, bool is_allowlist) { + const char *pattern; + int r; + + assert(reply); + + SET_FOREACH(pattern, patterns) { + r = sd_bus_message_append(reply, "(bs)", is_allowlist, pattern); + if (r < 0) + return r; + } + + return 0; +} + +static int property_get_log_filter_patterns( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + ExecContext *c = userdata; + int r; + + assert(c); + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "(bs)"); + if (r < 0) + return r; + + r = sd_bus_message_append_log_filter_patterns(reply, c->log_filter_allowed_patterns, + /* is_allowlist = */ true); + if (r < 0) + return r; + + r = sd_bus_message_append_log_filter_patterns(reply, c->log_filter_denied_patterns, + /* is_allowlist = */ false); + if (r < 0) + return r; + + return sd_bus_message_close_container(reply); +} + static int property_get_set_credential( sd_bus *bus, const char *path, @@ -1195,6 +1243,7 @@ const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_PROPERTY("LogRateLimitIntervalUSec", "t", bus_property_get_usec, offsetof(ExecContext, log_ratelimit_interval_usec), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("LogRateLimitBurst", "u", bus_property_get_unsigned, offsetof(ExecContext, log_ratelimit_burst), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("LogExtraFields", "aay", property_get_log_extra_fields, 0, SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("LogFilterPatterns", "a(bs)", property_get_log_filter_patterns, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("LogNamespace", "s", NULL, offsetof(ExecContext, log_namespace), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("SecureBits", "i", bus_property_get_int, offsetof(ExecContext, secure_bits), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("CapabilityBoundingSet", "t", NULL, offsetof(ExecContext, capability_bounding_set), SD_BUS_VTABLE_PROPERTY_CONST), @@ -1792,6 +1841,61 @@ int bus_exec_context_set_transient_property( if (streq(name, "LogRateLimitBurst")) return bus_set_transient_unsigned(u, name, &c->log_ratelimit_burst, message, flags, error); + if (streq(name, "LogFilterPatterns")) { + /* Use _cleanup_free_, not _cleanup_strv_free_, as we don't want the content of the strv + * to be freed. */ + _cleanup_free_ char **allow_list = NULL, **deny_list = NULL; + const char *pattern; + int is_allowlist; + + r = sd_bus_message_enter_container(message, 'a', "(bs)"); + if (r < 0) + return r; + + while ((r = sd_bus_message_read(message, "(bs)", &is_allowlist, &pattern)) > 0) { + _cleanup_(pattern_freep) pcre2_code *compiled_pattern = NULL; + + if (isempty(pattern)) + continue; + + r = pattern_compile_and_log(pattern, 0, &compiled_pattern); + if (r < 0) + return r; + + r = strv_push(is_allowlist ? &allow_list : &deny_list, (char *)pattern); + if (r < 0) + return r; + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + if (strv_isempty(allow_list) && strv_isempty(deny_list)) { + c->log_filter_allowed_patterns = set_free(c->log_filter_allowed_patterns); + c->log_filter_denied_patterns = set_free(c->log_filter_denied_patterns); + unit_write_settingf(u, flags, name, "%s=", name); + } else { + r = set_put_strdupv(&c->log_filter_allowed_patterns, allow_list); + if (r < 0) + return r; + r = set_put_strdupv(&c->log_filter_denied_patterns, deny_list); + if (r < 0) + return r; + + STRV_FOREACH(unit_pattern, allow_list) + unit_write_settingf(u, flags, name, "%s=%s", name, *unit_pattern); + STRV_FOREACH(unit_pattern, deny_list) + unit_write_settingf(u, flags, name, "%s=~%s", name, *unit_pattern); + } + } + + return 1; + } + if (streq(name, "Personality")) return bus_set_transient_personality(u, name, &c->personality, message, flags, error); diff --git a/src/core/execute.c b/src/core/execute.c index 7963582ea2..42c95556ac 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -5250,9 +5250,10 @@ int exec_spawn(Unit *unit, if (r < 0) return log_unit_error_errno(unit, r, "Failed to create control group '%s': %m", subcgroup_path); - /* Normally we would not propagate the oomd xattrs to children but since we created this + /* Normally we would not propagate the xattrs to children but since we created this * sub-cgroup internally we should do it. */ cgroup_oomd_xattr_apply(unit, subcgroup_path); + cgroup_log_xattr_apply(unit, subcgroup_path); } } @@ -5406,6 +5407,8 @@ void exec_context_done(ExecContext *c) { c->log_level_max = -1; exec_context_free_log_extra_fields(c); + c->log_filter_allowed_patterns = set_free(c->log_filter_allowed_patterns); + c->log_filter_denied_patterns = set_free(c->log_filter_denied_patterns); c->log_ratelimit_interval_usec = 0; c->log_ratelimit_burst = 0; @@ -6000,6 +6003,17 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) { if (c->log_ratelimit_burst > 0) fprintf(f, "%sLogRateLimitBurst: %u\n", prefix, c->log_ratelimit_burst); + if (!set_isempty(c->log_filter_allowed_patterns) || !set_isempty(c->log_filter_denied_patterns)) { + fprintf(f, "%sLogFilterPatterns:", prefix); + + char *pattern; + SET_FOREACH(pattern, c->log_filter_allowed_patterns) + fprintf(f, " %s", pattern); + SET_FOREACH(pattern, c->log_filter_denied_patterns) + fprintf(f, " ~%s", pattern); + fputc('\n', f); + } + for (size_t j = 0; j < c->n_log_extra_fields; j++) { fprintf(f, "%sLogExtraFields: ", prefix); fwrite(c->log_extra_fields[j].iov_base, diff --git a/src/core/execute.h b/src/core/execute.h index a2cf22806b..24cd4640d7 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -24,6 +24,7 @@ typedef struct Manager Manager; #include "nsflags.h" #include "numa-util.h" #include "path-util.h" +#include "set.h" #include "time-util.h" #define EXEC_STDIN_DATA_MAX (64U*1024U*1024U) @@ -286,6 +287,8 @@ struct ExecContext { struct iovec* log_extra_fields; size_t n_log_extra_fields; + Set *log_filter_allowed_patterns; + Set *log_filter_denied_patterns; usec_t log_ratelimit_interval_usec; unsigned log_ratelimit_burst; diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index b315dd0afa..2850da5cc1 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -52,6 +52,7 @@ {{type}}.LogRateLimitIntervalSec, config_parse_sec, 0, offsetof({{type}}, exec_context.log_ratelimit_interval_usec) {{type}}.LogRateLimitBurst, config_parse_unsigned, 0, offsetof({{type}}, exec_context.log_ratelimit_burst) {{type}}.LogExtraFields, config_parse_log_extra_fields, 0, offsetof({{type}}, exec_context) +{{type}}.LogFilterPatterns, config_parse_log_filter_patterns, 0, offsetof({{type}}, exec_context) {{type}}.Capabilities, config_parse_warn_compat, DISABLED_LEGACY, offsetof({{type}}, exec_context) {{type}}.SecureBits, config_parse_exec_secure_bits, 0, offsetof({{type}}, exec_context.secure_bits) {{type}}.CapabilityBoundingSet, config_parse_capability_set, 0, offsetof({{type}}, exec_context.capability_bounding_set) diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index ac8c64fe3b..3a95f0d504 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -52,6 +52,7 @@ #include "parse-helpers.h" #include "parse-util.h" #include "path-util.h" +#include "pcre2-util.h" #include "percent-util.h" #include "process-util.h" #if HAVE_SECCOMP @@ -6225,6 +6226,7 @@ void unit_dump_config_items(FILE *f) { { config_parse_job_mode, "MODE" }, { config_parse_job_mode_isolate, "BOOLEAN" }, { config_parse_personality, "PERSONALITY" }, + { config_parse_log_filter_patterns, "REGEX" }, }; const char *prev = NULL; @@ -6489,3 +6491,54 @@ int config_parse_tty_size( return config_parse_unsigned(unit, filename, line, section, section_line, lvalue, ltype, rvalue, data, userdata); } + +int config_parse_log_filter_patterns( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + ExecContext *c = ASSERT_PTR(data); + _cleanup_(pattern_freep) pcre2_code *compiled_pattern = NULL; + const char *pattern = ASSERT_PTR(rvalue); + bool is_allowlist = true; + int r; + + assert(filename); + assert(lvalue); + + if (isempty(pattern)) { + /* Empty assignment resets the lists. */ + c->log_filter_allowed_patterns = set_free(c->log_filter_allowed_patterns); + c->log_filter_denied_patterns = set_free(c->log_filter_denied_patterns); + return 0; + } + + if (pattern[0] == '~') { + is_allowlist = false; + pattern++; + if (isempty(pattern)) + /* LogFilterPatterns=~ is not considered a valid pattern. */ + return log_syntax(unit, LOG_WARNING, filename, line, 0, + "Regex pattern invalid, ignoring: %s=%s", lvalue, rvalue); + } + + if (pattern_compile_and_log(pattern, 0, &compiled_pattern) < 0) + return 0; + + r = set_put_strdup(is_allowlist ? &c->log_filter_allowed_patterns : &c->log_filter_denied_patterns, + pattern); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store log filtering pattern, ignoring: %s=%s", lvalue, rvalue); + return 0; + } + + return 0; +} diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index c57a6b2277..74b3633695 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -150,6 +150,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_cgroup_socket_bind); CONFIG_PARSER_PROTOTYPE(config_parse_restrict_network_interfaces); CONFIG_PARSER_PROTOTYPE(config_parse_watchdog_sec); CONFIG_PARSER_PROTOTYPE(config_parse_tty_size); +CONFIG_PARSER_PROTOTYPE(config_parse_log_filter_patterns); /* gperf prototypes */ const struct ConfigPerfItem* load_fragment_gperf_lookup(const char *key, GPERF_LEN_TYPE length); diff --git a/src/journal/journald-client.c b/src/journal/journald-client.c new file mode 100644 index 0000000000..22090aa93c --- /dev/null +++ b/src/journal/journald-client.c @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cgroup-util.h" +#include "errno-util.h" +#include "journald-client.h" +#include "nulstr-util.h" +#include "pcre2-util.h" + +/* This consumes both `allow_list` and `deny_list` arguments. Hence, those arguments are not owned by the + * caller anymore and should not be freed. */ +static void client_set_filtering_patterns(ClientContext *c, Set *allow_list, Set *deny_list) { + assert(c); + + set_free_and_replace(c->log_filter_allowed_patterns, allow_list); + set_free_and_replace(c->log_filter_denied_patterns, deny_list); +} + +static int client_parse_log_filter_nulstr(const char *nulstr, size_t len, Set **ret) { + _cleanup_set_free_ Set *s = NULL; + _cleanup_strv_free_ char **patterns_strv = NULL; + int r; + + assert(nulstr); + assert(ret); + + patterns_strv = strv_parse_nulstr(nulstr, len); + if (!patterns_strv) + return log_oom_debug(); + + STRV_FOREACH(pattern, patterns_strv) { + _cleanup_(pattern_freep) pcre2_code *compiled_pattern = NULL; + + r = pattern_compile_and_log(*pattern, 0, &compiled_pattern); + if (r < 0) + return r; + + r = set_ensure_consume(&s, &pcre2_code_hash_ops_free, TAKE_PTR(compiled_pattern)); + if (r < 0) + return log_debug_errno(r, "Failed to insert regex into set: %m"); + } + + *ret = TAKE_PTR(s); + + return 0; +} + +int client_context_read_log_filter_patterns(ClientContext *c, const char *cgroup) { + char *deny_list_xattr, *xattr_end; + _cleanup_free_ char *xattr = NULL; + _cleanup_set_free_ Set *allow_list = NULL, *deny_list = NULL; + int r; + + assert(c); + + r = cg_get_xattr_malloc(SYSTEMD_CGROUP_CONTROLLER, cgroup, "user.journald_log_filter_patterns", &xattr); + if (r < 0) { + if (!ERRNO_IS_XATTR_ABSENT(r)) + return log_debug_errno(r, "Failed to get user.journald_log_filter_patterns xattr for %s: %m", cgroup); + + client_set_filtering_patterns(c, NULL, NULL); + return 0; + } + + xattr_end = xattr + r; + + /* We expect '0xff' to be present in the attribute, even if the lists are empty. We expect the + * following: + * - Allow list, but no deny list: 0xXX, ...., 0xff + * - No allow list, but deny list: 0xff, 0xXX, .... + * - Allow list, and deny list: 0xXX, ...., 0xff, 0xXX, .... + * This is due to the fact allowed and denied patterns list are two nulstr joined together with '0xff'. + * None of the allowed or denied nulstr have a nul-termination character. + * + * We do not expect both the allow list and deny list to be empty, as this condition is tested + * before writing to xattr. */ + deny_list_xattr = memchr(xattr, (char)0xff, r); + if (!deny_list_xattr) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Missing delimiter in cgroup user.journald_log_filter_patterns attribute: %m"); + + r = client_parse_log_filter_nulstr(xattr, deny_list_xattr - xattr, &allow_list); + if (r < 0) + return r; + + /* Use 'deny_list_xattr + 1' to skip '0xff'. */ + ++deny_list_xattr; + r = client_parse_log_filter_nulstr(deny_list_xattr, xattr_end - deny_list_xattr, &deny_list); + if (r < 0) + return r; + + client_set_filtering_patterns(c, TAKE_PTR(allow_list), TAKE_PTR(deny_list)); + + return 0; +} + +int client_context_check_keep_log(ClientContext *c, const char *message, size_t len) { + pcre2_code *regex; + + if (!c || !message) + return true; + + SET_FOREACH(regex, c->log_filter_denied_patterns) + if (pattern_matches_and_log(regex, message, len, NULL) > 0) + return false; + + SET_FOREACH(regex, c->log_filter_allowed_patterns) + if (pattern_matches_and_log(regex, message, len, NULL) > 0) + return true; + + return set_isempty(c->log_filter_allowed_patterns); +} diff --git a/src/journal/journald-client.h b/src/journal/journald-client.h new file mode 100644 index 0000000000..629cd41c7d --- /dev/null +++ b/src/journal/journald-client.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "journald-context.h" + +int client_context_read_log_filter_patterns(ClientContext *c, const char *cgroup); +int client_context_check_keep_log(ClientContext *c, const char *message, size_t len); diff --git a/src/journal/journald-context.c b/src/journal/journald-context.c index 222855ae60..55e657c7e1 100644 --- a/src/journal/journald-context.c +++ b/src/journal/journald-context.c @@ -14,6 +14,7 @@ #include "io-util.h" #include "journal-internal.h" #include "journal-util.h" +#include "journald-client.h" #include "journald-context.h" #include "parse-util.h" #include "path-util.h" @@ -180,6 +181,9 @@ static void client_context_reset(Server *s, ClientContext *c) { c->log_ratelimit_interval = s->ratelimit_interval; c->log_ratelimit_burst = s->ratelimit_burst; + + c->log_filter_allowed_patterns = set_free(c->log_filter_allowed_patterns); + c->log_filter_denied_patterns = set_free(c->log_filter_denied_patterns); } static ClientContext* client_context_free(Server *s, ClientContext *c) { @@ -290,6 +294,8 @@ static int client_context_read_cgroup(Server *s, ClientContext *c, const char *u return r; } + (void) client_context_read_log_filter_patterns(c, t); + /* Let's shortcut this if the cgroup path didn't change */ if (streq_ptr(c->cgroup, t)) return 0; diff --git a/src/journal/journald-context.h b/src/journal/journald-context.h index 9bf74b2347..4a998ba42e 100644 --- a/src/journal/journald-context.h +++ b/src/journal/journald-context.h @@ -7,6 +7,7 @@ #include "sd-id128.h" +#include "set.h" #include "time-util.h" typedef struct ClientContext ClientContext; @@ -55,6 +56,9 @@ struct ClientContext { usec_t log_ratelimit_interval; unsigned log_ratelimit_burst; + + Set *log_filter_allowed_patterns; + Set *log_filter_denied_patterns; }; int client_context_get( diff --git a/src/journal/journald-native.c b/src/journal/journald-native.c index 847f69c1ff..ca23508454 100644 --- a/src/journal/journald-native.c +++ b/src/journal/journald-native.c @@ -13,6 +13,7 @@ #include "journal-importer.h" #include "journal-internal.h" #include "journal-util.h" +#include "journald-client.h" #include "journald-console.h" #include "journald-kmsg.h" #include "journald-native.h" @@ -261,6 +262,13 @@ static int server_process_entry( goto finish; if (message) { + /* Ensure message is not NULL, otherwise strlen(message) would crash. This check needs to + * be here until server_process_entry() is able to process messages containing \0 characters, + * as we would have access to the actual size of message. */ + r = client_context_check_keep_log(context, message, strlen(message)); + if (r <= 0) + goto finish; + if (s->forward_to_syslog) server_forward_syslog(s, syslog_fixup_facility(priority), identifier, message, ucred, tv); diff --git a/src/journal/journald-stream.c b/src/journal/journald-stream.c index 49f28972ea..4446b2daec 100644 --- a/src/journal/journald-stream.c +++ b/src/journal/journald-stream.c @@ -20,6 +20,7 @@ #include "fs-util.h" #include "io-util.h" #include "journal-internal.h" +#include "journald-client.h" #include "journald-console.h" #include "journald-context.h" #include "journald-kmsg.h" @@ -284,6 +285,10 @@ static int stdout_stream_log( if (isempty(p)) return 0; + r = client_context_check_keep_log(s->context, p, strlen(p)); + if (r <= 0) + return r; + if (s->forward_to_syslog || s->server->forward_to_syslog) server_forward_syslog(s->server, syslog_fixup_facility(priority), s->identifier, p, &s->ucred, NULL); diff --git a/src/journal/journald-syslog.c b/src/journal/journald-syslog.c index d8708b0775..1ecbd226da 100644 --- a/src/journal/journald-syslog.c +++ b/src/journal/journald-syslog.c @@ -11,6 +11,7 @@ #include "format-util.h" #include "io-util.h" #include "journal-internal.h" +#include "journald-client.h" #include "journald-console.h" #include "journald-kmsg.h" #include "journald-server.h" @@ -374,6 +375,9 @@ void server_process_syslog_message( if (!client_context_test_priority(context, priority)) return; + if (client_context_check_keep_log(context, msg, strlen(msg)) <= 0) + return; + syslog_ts = msg; syslog_ts_len = syslog_skip_timestamp(&msg); if (syslog_ts_len == 0) diff --git a/src/journal/meson.build b/src/journal/meson.build index 1e41ea149b..9abab298cc 100644 --- a/src/journal/meson.build +++ b/src/journal/meson.build @@ -3,6 +3,8 @@ sources = files( 'journald-audit.c', 'journald-audit.h', + 'journald-client.c', + 'journald-client.h', 'journald-console.c', 'journald-console.h', 'journald-context.c', diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 7d136df2d9..7154c9b4c0 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -1218,6 +1218,16 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con return 1; } + if (streq(field, "LogFilterPatterns")) { + r = sd_bus_message_append(m, "(sv)", "LogFilterPatterns", "a(bs)", 1, + eq[0] != '~', + eq[0] != '~' ? eq : eq + 1); + if (r < 0) + return bus_log_create_error(r); + + return 1; + } + if (STR_IN_SET(field, "StandardInput", "StandardOutput", "StandardError")) { diff --git a/src/shared/pcre2-util.c b/src/shared/pcre2-util.c index 998dab0491..a2c8687cf9 100644 --- a/src/shared/pcre2-util.c +++ b/src/shared/pcre2-util.c @@ -14,6 +14,15 @@ pcre2_code* (*sym_pcre2_compile)(PCRE2_SPTR, PCRE2_SIZE, uint32_t, int *, PCRE2_ int (*sym_pcre2_get_error_message)(int, PCRE2_UCHAR *, PCRE2_SIZE); int (*sym_pcre2_match)(const pcre2_code *, PCRE2_SPTR, PCRE2_SIZE, PCRE2_SIZE, uint32_t, pcre2_match_data *, pcre2_match_context *); PCRE2_SIZE* (*sym_pcre2_get_ovector_pointer)(pcre2_match_data *); + +DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR( + pcre2_code_hash_ops_free, + pcre2_code, + (void (*)(const pcre2_code *, struct siphash*))trivial_hash_func, + (int (*)(const pcre2_code *, const pcre2_code*))trivial_compare_func, + sym_pcre2_code_free); +#else +const struct hash_ops pcre2_code_hash_ops_free = {}; #endif int dlopen_pcre2(void) { diff --git a/src/shared/pcre2-util.h b/src/shared/pcre2-util.h index 11f1d77f4f..f1e744d577 100644 --- a/src/shared/pcre2-util.h +++ b/src/shared/pcre2-util.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "hash-funcs.h" #include "macro.h" #if HAVE_PCRE2 @@ -24,6 +25,8 @@ typedef struct {} pcre2_code; #endif +extern const struct hash_ops pcre2_code_hash_ops_free; + typedef enum { PATTERN_COMPILE_CASE_AUTO, PATTERN_COMPILE_CASE_SENSITIVE, diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c index 77dd075eb3..f78cf307ca 100644 --- a/src/systemctl/systemctl-show.c +++ b/src/systemctl/systemctl-show.c @@ -1651,6 +1651,24 @@ static int print_property(const char *name, const char *expected_value, sd_bus_m bus_print_property_value(name, expected_value, flags, affinity); return 1; + } else if (streq(name, "LogFilterPatterns")) { + int is_allowlist; + const char *pattern; + + r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, "(bs)"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = sd_bus_message_read(m, "(bs)", &is_allowlist, &pattern)) > 0) + bus_print_property_valuef(name, expected_value, flags, "%s%s", is_allowlist ? "" : "~", pattern); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(m); + if (r < 0) + return bus_log_parse_error(r); + + return 1; } else if (streq(name, "MountImages")) { _cleanup_free_ char *paths = NULL; diff --git a/src/test/test-load-fragment.c b/src/test/test-load-fragment.c index 997c2b2524..8d3c58a800 100644 --- a/src/test/test-load-fragment.c +++ b/src/test/test-load-fragment.c @@ -22,6 +22,7 @@ #include "load-fragment.h" #include "macro.h" #include "memory-util.h" +#include "pcre2-util.h" #include "rm-rf.h" #include "specifier.h" #include "string-util.h" @@ -997,6 +998,56 @@ TEST(unit_is_recursive_template_dependency) { assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.mount", "foobar@%n.mount") == 0); } +#define TEST_PATTERN(_regex, _allowed_patterns_count, _denied_patterns_count) \ + { \ + .regex = _regex, \ + .allowed_patterns_count = _allowed_patterns_count, \ + .denied_patterns_count = _denied_patterns_count \ + } + +TEST(config_parse_log_filter_patterns) { + ExecContext c = {}; + int r; + + static const struct { + const char *regex; + size_t allowed_patterns_count; + size_t denied_patterns_count; + } regex_tests[] = { + TEST_PATTERN("", 0, 0), + TEST_PATTERN(".*", 1, 0), + TEST_PATTERN("~.*", 1, 1), + TEST_PATTERN("", 0, 0), + TEST_PATTERN("~.*", 0, 1), + TEST_PATTERN("[.*", 0, 1), /* Invalid pattern. */ + TEST_PATTERN(".*gg.*", 1, 1), + TEST_PATTERN("~.*", 1, 1), /* Already in the patterns list. */ + TEST_PATTERN("[.*", 1, 1), /* Invalid pattern. */ + TEST_PATTERN("\\x7ehello", 2, 1), + TEST_PATTERN("", 0, 0), + TEST_PATTERN("~foobar", 0, 1), + }; + + if (ERRNO_IS_NOT_SUPPORTED(dlopen_pcre2())) + return (void) log_tests_skipped("PCRE2 support is not available"); + + for (size_t i = 0; i < ELEMENTSOF(regex_tests); i++) { + r = config_parse_log_filter_patterns(NULL, "fake", 1, "section", 1, "LogFilterPatterns", 1, + regex_tests[i].regex, &c, NULL); + assert_se(r >= 0); + + assert_se(set_size(c.log_filter_allowed_patterns) == regex_tests[i].allowed_patterns_count); + assert_se(set_size(c.log_filter_denied_patterns) == regex_tests[i].denied_patterns_count); + + /* Ensure `~` is properly removed */ + const char *p; + SET_FOREACH(p, c.log_filter_allowed_patterns) + assert_se(p && p[0] != '~'); + SET_FOREACH(p, c.log_filter_denied_patterns) + assert_se(p && p[0] != '~'); + } +} + static int intro(void) { if (enter_cgroup_subroot(NULL) == -ENOMEDIUM) return log_tests_skipped("cgroupfs not available"); diff --git a/src/test/test-nulstr-util.c b/src/test/test-nulstr-util.c index a068e5f870..1b7e4c1db1 100644 --- a/src/test/test-nulstr-util.c +++ b/src/test/test-nulstr-util.c @@ -2,6 +2,7 @@ #include "alloc-util.h" #include "nulstr-util.h" +#include "set.h" #include "strv.h" #include "tests.h" @@ -129,6 +130,50 @@ TEST(strv_make_nulstr) { test_strv_make_nulstr_one(STRV_MAKE("foo", "bar", "quuux")); } +TEST(set_make_nulstr) { + _cleanup_set_free_free_ Set *set = NULL; + size_t len = 0; + int r; + + { + /* Unallocated and empty set. */ + char expect[] = { 0x00, 0x00 }; + _cleanup_free_ char *nulstr = NULL; + + r = set_make_nulstr(set, &nulstr, &len); + assert_se(r == 0); + assert_se(len == 0); + assert_se(memcmp(expect, nulstr, len + 2) == 0); + } + + { + /* Allocated by empty set. */ + char expect[] = { 0x00, 0x00 }; + _cleanup_free_ char *nulstr = NULL; + + set = set_new(NULL); + assert_se(set); + + r = set_make_nulstr(set, &nulstr, &len); + assert_se(r == 0); + assert_se(len == 0); + assert_se(memcmp(expect, nulstr, len + 2) == 0); + } + + { + /* Non-empty set. */ + char expect[] = { 'a', 'a', 'a', 0x00, 0x00 }; + _cleanup_free_ char *nulstr = NULL; + + assert_se(set_put_strdup(&set, "aaa") >= 0); + + r = set_make_nulstr(set, &nulstr, &len); + assert_se(r == 0); + assert_se(len == 4); + assert_se(memcmp(expect, nulstr, len + 1) == 0); + } +} + static void test_strv_make_nulstr_binary_one(char **l, const char *b, size_t n) { _cleanup_strv_free_ char **z = NULL; _cleanup_free_ char *a = NULL; diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service index b4cfca2814..f8237d74eb 100644 --- a/test/fuzz/fuzz-unit-file/directives-all.service +++ b/test/fuzz/fuzz-unit-file/directives-all.service @@ -846,6 +846,7 @@ LogExtraFields= LogLevelMax= LogRateLimitIntervalSec= LogRateLimitBurst= +LogFilterPatterns= LogsDirectory= LogsDirectoryMode= MACVLAN= diff --git a/test/testsuite-04.units/logs-filtering.service b/test/testsuite-04.units/logs-filtering.service new file mode 100644 index 0000000000..fc89021ca9 --- /dev/null +++ b/test/testsuite-04.units/logs-filtering.service @@ -0,0 +1,5 @@ +[Unit] +Description=Log filtering unit + +[Service] +ExecStart=sh -c 'while true; do echo "Logging from the service, and ~more~"; sleep .25; done' diff --git a/test/units/testsuite-04.sh b/test/units/testsuite-04.sh index fdc3273fea..2874fc778f 100755 --- a/test/units/testsuite-04.sh +++ b/test/units/testsuite-04.sh @@ -179,4 +179,84 @@ sleep 3 # https://github.com/systemd/systemd/issues/15528 journalctl --follow --file=/var/log/journal/*/* | head -n1 || [[ $? -eq 1 ]] +function add_logs_filtering_override() { + UNIT=${1:?} + OVERRIDE_NAME=${2:?} + LOG_FILTER=${3:-""} + + mkdir -p /etc/systemd/system/"$UNIT".d/ + echo "[Service]" >/etc/systemd/system/logs-filtering.service.d/"${OVERRIDE_NAME}".conf + echo "LogFilterPatterns=$LOG_FILTER" >>/etc/systemd/system/logs-filtering.service.d/"${OVERRIDE_NAME}".conf + systemctl daemon-reload +} + +function run_service_and_fetch_logs() { + UNIT=$1 + + START=$(date '+%Y-%m-%d %T.%6N') + systemctl restart "$UNIT" + sleep .5 + journalctl --sync + END=$(date '+%Y-%m-%d %T.%6N') + + journalctl -q -u "$UNIT" -S "$START" -U "$END" | grep -Pv "systemd\[[0-9]+\]" + systemctl stop "$UNIT" +} + +function is_xattr_supported() { + START=$(date '+%Y-%m-%d %T.%6N') + systemd-run --unit text_xattr --property LogFilterPatterns=log sh -c "sleep .5" + sleep .5 + journalctl --sync + END=$(date '+%Y-%m-%d %T.%6N') + systemctl stop text_xattr + + if journalctl -q -u "text_xattr" -S "$START" -U "$END" --grep "Failed to set 'user.journald_log_filter_patterns' xattr.*not supported$"; then + return 1 + fi + + return 0 +} + +if is_xattr_supported; then + # Accept all log messages + add_logs_filtering_override "logs-filtering.service" "00-reset" "" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + add_logs_filtering_override "logs-filtering.service" "01-allow-all" ".*" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Discard all log messages + add_logs_filtering_override "logs-filtering.service" "02-discard-all" "~.*" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Accept all test messages + add_logs_filtering_override "logs-filtering.service" "03-reset" "" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Discard all test messages + add_logs_filtering_override "logs-filtering.service" "04-discard-gg" "~.*gg.*" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Deny filter takes precedence + add_logs_filtering_override "logs-filtering.service" "05-allow-all-but-too-late" ".*" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Use tilde in a deny pattern + add_logs_filtering_override "logs-filtering.service" "06-reset" "" + add_logs_filtering_override "logs-filtering.service" "07-prevent-tilde" "~~more~" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Only allow a pattern that won't be matched + add_logs_filtering_override "logs-filtering.service" "08-reset" "" + add_logs_filtering_override "logs-filtering.service" "09-allow-only-non-existing" "non-existing string" + [[ -z $(run_service_and_fetch_logs "logs-filtering.service") ]] + + # Allow a pattern starting with a tilde + add_logs_filtering_override "logs-filtering.service" "10-allow-with-escape-char" "\x7emore~" + [[ -n $(run_service_and_fetch_logs "logs-filtering.service") ]] + + rm -rf /etc/systemd/system/logs-filtering.service.d +fi + touch /testok |