summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/TRANSIENT-SETTINGS.md1
-rw-r--r--man/org.freedesktop.systemd1.xml24
-rw-r--r--man/systemd.exec.xml28
-rw-r--r--src/basic/nulstr-util.c15
-rw-r--r--src/basic/nulstr-util.h3
-rw-r--r--src/core/cgroup.c46
-rw-r--r--src/core/cgroup.h1
-rw-r--r--src/core/dbus-execute.c104
-rw-r--r--src/core/execute.c16
-rw-r--r--src/core/execute.h3
-rw-r--r--src/core/load-fragment-gperf.gperf.in1
-rw-r--r--src/core/load-fragment.c53
-rw-r--r--src/core/load-fragment.h1
-rw-r--r--src/journal/journald-client.c110
-rw-r--r--src/journal/journald-client.h7
-rw-r--r--src/journal/journald-context.c6
-rw-r--r--src/journal/journald-context.h4
-rw-r--r--src/journal/journald-native.c8
-rw-r--r--src/journal/journald-stream.c5
-rw-r--r--src/journal/journald-syslog.c4
-rw-r--r--src/journal/meson.build2
-rw-r--r--src/shared/bus-unit-util.c10
-rw-r--r--src/shared/pcre2-util.c9
-rw-r--r--src/shared/pcre2-util.h3
-rw-r--r--src/systemctl/systemctl-show.c18
-rw-r--r--src/test/test-load-fragment.c51
-rw-r--r--src/test/test-nulstr-util.c45
-rw-r--r--test/fuzz/fuzz-unit-file/directives-all.service1
-rw-r--r--test/testsuite-04.units/logs-filtering.service5
-rwxr-xr-xtest/units/testsuite-04.sh80
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