c2dfb7
From f45cb6d6a2c247c7594d9965cbb745303adb61d6 Mon Sep 17 00:00:00 2001
c2dfb7
From: Chris Down <chris@chrisdown.name>
c2dfb7
Date: Thu, 28 Mar 2019 12:50:50 +0000
c2dfb7
Subject: [PATCH] cgroup: Implement default propagation of MemoryLow with
c2dfb7
 DefaultMemoryLow
c2dfb7
c2dfb7
In cgroup v2 we have protection tunables -- currently MemoryLow and
c2dfb7
MemoryMin (there will be more in future for other resources, too). The
c2dfb7
design of these protection tunables requires not only intermediate
c2dfb7
cgroups to propagate protections, but also the units at the leaf of that
c2dfb7
resource's operation to accept it (by setting MemoryLow or MemoryMin).
c2dfb7
c2dfb7
This makes sense from an low-level API design perspective, but it's a
c2dfb7
good idea to also have a higher-level abstraction that can, by default,
c2dfb7
propagate these resources to children recursively. In this patch, this
c2dfb7
happens by having descendants set memory.low to N if their ancestor has
c2dfb7
DefaultMemoryLow=N -- assuming they don't set a separate MemoryLow
c2dfb7
value.
c2dfb7
c2dfb7
Any affected unit can opt out of this propagation by manually setting
c2dfb7
`MemoryLow` to some value in its unit configuration. A unit can also
c2dfb7
stop further propagation by setting `DefaultMemoryLow=` with no
c2dfb7
argument. This removes further propagation in the subtree, but has no
c2dfb7
effect on the unit itself (for that, use `MemoryLow=0`).
c2dfb7
c2dfb7
Our use case in production is simplifying the configuration of machines
c2dfb7
which heavily rely on memory protection tunables, but currently require
c2dfb7
tweaking a huge number of unit files to make that a reality. This
c2dfb7
directive makes that significantly less fragile, and decreases the risk
c2dfb7
of misconfiguration.
c2dfb7
c2dfb7
After this patch is merged, I will implement DefaultMemoryMin= using the
c2dfb7
same principles.
c2dfb7
c2dfb7
(cherry picked from commit c52db42b78f6fbeb7792cc4eca27e2767a48b6ca)
c2dfb7
c2dfb7
Related: #1763435
c2dfb7
---
c2dfb7
 doc/TRANSIENT-SETTINGS.md             |   1 +
c2dfb7
 man/systemd.resource-control.xml      |   4 +
c2dfb7
 src/core/cgroup.c                     |  58 +++++++++--
c2dfb7
 src/core/cgroup.h                     |   6 ++
c2dfb7
 src/core/dbus-cgroup.c                |   7 ++
c2dfb7
 src/core/load-fragment-gperf.gperf.m4 |   1 +
c2dfb7
 src/core/load-fragment.c              |  13 ++-
c2dfb7
 src/shared/bus-unit-util.c            |   2 +-
c2dfb7
 src/shared/bus-util.c                 |   2 +-
c2dfb7
 src/systemctl/systemctl.c             |   3 +
c2dfb7
 src/test/meson.build                  |   6 ++
c2dfb7
 src/test/test-cgroup-unit-default.c   | 145 ++++++++++++++++++++++++++
c2dfb7
 test/dml-discard-empty.service        |   7 ++
c2dfb7
 test/dml-discard-set-ml.service       |   8 ++
c2dfb7
 test/dml-discard.slice                |   5 +
c2dfb7
 test/dml-override-empty.service       |   7 ++
c2dfb7
 test/dml-override.slice               |   5 +
c2dfb7
 test/dml-passthrough-empty.service    |   7 ++
c2dfb7
 test/dml-passthrough-set-dml.service  |   8 ++
c2dfb7
 test/dml-passthrough-set-ml.service   |   8 ++
c2dfb7
 test/dml-passthrough.slice            |   5 +
c2dfb7
 test/dml.slice                        |   5 +
c2dfb7
 test/meson.build                      |  10 ++
c2dfb7
 23 files changed, 310 insertions(+), 13 deletions(-)
c2dfb7
 create mode 100644 src/test/test-cgroup-unit-default.c
c2dfb7
 create mode 100644 test/dml-discard-empty.service
c2dfb7
 create mode 100644 test/dml-discard-set-ml.service
c2dfb7
 create mode 100644 test/dml-discard.slice
c2dfb7
 create mode 100644 test/dml-override-empty.service
c2dfb7
 create mode 100644 test/dml-override.slice
c2dfb7
 create mode 100644 test/dml-passthrough-empty.service
c2dfb7
 create mode 100644 test/dml-passthrough-set-dml.service
c2dfb7
 create mode 100644 test/dml-passthrough-set-ml.service
c2dfb7
 create mode 100644 test/dml-passthrough.slice
c2dfb7
 create mode 100644 test/dml.slice
c2dfb7
c2dfb7
diff --git a/doc/TRANSIENT-SETTINGS.md b/doc/TRANSIENT-SETTINGS.md
c2dfb7
index 93865c0333..5a8fa0727e 100644
c2dfb7
--- a/doc/TRANSIENT-SETTINGS.md
c2dfb7
+++ b/doc/TRANSIENT-SETTINGS.md
c2dfb7
@@ -223,6 +223,7 @@ All cgroup/resource control settings are available for transient units
c2dfb7
 ✓ AllowedMemoryNodes=
c2dfb7
 ✓ MemoryAccounting=
c2dfb7
 ✓ MemoryMin=
c2dfb7
+✓ DefaultMemoryLow=
c2dfb7
 ✓ MemoryLow=
c2dfb7
 ✓ MemoryHigh=
c2dfb7
 ✓ MemoryMax=
c2dfb7
diff --git a/man/systemd.resource-control.xml b/man/systemd.resource-control.xml
c2dfb7
index 63a0c87565..27f16001dd 100644
c2dfb7
--- a/man/systemd.resource-control.xml
c2dfb7
+++ b/man/systemd.resource-control.xml
c2dfb7
@@ -305,6 +305,10 @@
c2dfb7
 
c2dfb7
           <para>This setting is supported only if the unified control group hierarchy is used and disables
c2dfb7
           <varname>MemoryLimit=</varname>.</para>
c2dfb7
+
c2dfb7
+          <para>Units may can have their children use a default <literal>memory.low</literal> value by specifying
c2dfb7
+          <varname>DefaultMemoryLow=</varname>, which has the same usage as <varname>MemoryLow=</varname>. This setting
c2dfb7
+          does not affect <literal>memory.low</literal> in the unit itself.</para>
c2dfb7
         </listitem>
c2dfb7
       </varlistentry>
c2dfb7
 
c2dfb7
diff --git a/src/core/cgroup.c b/src/core/cgroup.c
c2dfb7
index a17b38f914..f804bf4727 100644
c2dfb7
--- a/src/core/cgroup.c
c2dfb7
+++ b/src/core/cgroup.c
c2dfb7
@@ -220,6 +220,7 @@ void cgroup_context_dump(CGroupContext *c, FILE* f, const char *prefix) {
c2dfb7
                 "%sStartupIOWeight=%" PRIu64 "\n"
c2dfb7
                 "%sBlockIOWeight=%" PRIu64 "\n"
c2dfb7
                 "%sStartupBlockIOWeight=%" PRIu64 "\n"
c2dfb7
+                "%sDefaultMemoryLow=%" PRIu64 "\n"
c2dfb7
                 "%sMemoryMin=%" PRIu64 "\n"
c2dfb7
                 "%sMemoryLow=%" PRIu64 "\n"
c2dfb7
                 "%sMemoryHigh=%" PRIu64 "\n"
c2dfb7
@@ -247,6 +248,7 @@ void cgroup_context_dump(CGroupContext *c, FILE* f, const char *prefix) {
c2dfb7
                 prefix, c->startup_io_weight,
c2dfb7
                 prefix, c->blockio_weight,
c2dfb7
                 prefix, c->startup_blockio_weight,
c2dfb7
+                prefix, c->default_memory_low,
c2dfb7
                 prefix, c->memory_min,
c2dfb7
                 prefix, c->memory_low,
c2dfb7
                 prefix, c->memory_high,
c2dfb7
@@ -370,6 +372,32 @@ int cgroup_add_device_allow(CGroupContext *c, const char *dev, const char *mode)
c2dfb7
         return 0;
c2dfb7
 }
c2dfb7
 
c2dfb7
+uint64_t unit_get_ancestor_memory_low(Unit *u) {
c2dfb7
+        CGroupContext *c;
c2dfb7
+
c2dfb7
+        /* 1. Is MemoryLow set in this unit? If so, use that.
c2dfb7
+         * 2. Is DefaultMemoryLow set in any ancestor? If so, use that.
c2dfb7
+         * 3. Otherwise, return CGROUP_LIMIT_MIN. */
c2dfb7
+
c2dfb7
+        assert(u);
c2dfb7
+
c2dfb7
+        c = unit_get_cgroup_context(u);
c2dfb7
+
c2dfb7
+        if (c->memory_low_set)
c2dfb7
+                return c->memory_low;
c2dfb7
+
c2dfb7
+        while (UNIT_ISSET(u->slice)) {
c2dfb7
+                u = UNIT_DEREF(u->slice);
c2dfb7
+                c = unit_get_cgroup_context(u);
c2dfb7
+
c2dfb7
+                if (c->default_memory_low_set)
c2dfb7
+                        return c->default_memory_low;
c2dfb7
+        }
c2dfb7
+
c2dfb7
+        /* We've reached the root, but nobody had DefaultMemoryLow set, so set it to the kernel default. */
c2dfb7
+        return CGROUP_LIMIT_MIN;
c2dfb7
+}
c2dfb7
+
c2dfb7
 static int lookup_block_device(const char *p, dev_t *ret) {
c2dfb7
         struct stat st;
c2dfb7
         int r;
c2dfb7
@@ -807,8 +835,17 @@ static void cgroup_apply_blkio_device_limit(Unit *u, const char *dev_path, uint6
c2dfb7
                               "Failed to set blkio.throttle.write_bps_device: %m");
c2dfb7
 }
c2dfb7
 
c2dfb7
-static bool cgroup_context_has_unified_memory_config(CGroupContext *c) {
c2dfb7
-        return c->memory_min > 0 || c->memory_low > 0 || c->memory_high != CGROUP_LIMIT_MAX || c->memory_max != CGROUP_LIMIT_MAX || c->memory_swap_max != CGROUP_LIMIT_MAX;
c2dfb7
+static bool unit_has_unified_memory_config(Unit *u) {
c2dfb7
+        CGroupContext *c;
c2dfb7
+
c2dfb7
+        assert(u);
c2dfb7
+
c2dfb7
+        c = unit_get_cgroup_context(u);
c2dfb7
+        assert(c);
c2dfb7
+
c2dfb7
+        return c->memory_min > 0 || unit_get_ancestor_memory_low(u) > 0 ||
c2dfb7
+               c->memory_high != CGROUP_LIMIT_MAX || c->memory_max != CGROUP_LIMIT_MAX ||
c2dfb7
+               c->memory_swap_max != CGROUP_LIMIT_MAX;
c2dfb7
 }
c2dfb7
 
c2dfb7
 static void cgroup_apply_unified_memory_limit(Unit *u, const char *file, uint64_t v) {
c2dfb7
@@ -1056,7 +1093,7 @@ static void cgroup_context_apply(
c2dfb7
                 if (cg_all_unified() > 0) {
c2dfb7
                         uint64_t max, swap_max = CGROUP_LIMIT_MAX;
c2dfb7
 
c2dfb7
-                        if (cgroup_context_has_unified_memory_config(c)) {
c2dfb7
+                        if (unit_has_unified_memory_config(u)) {
c2dfb7
                                 max = c->memory_max;
c2dfb7
                                 swap_max = c->memory_swap_max;
c2dfb7
                         } else {
c2dfb7
@@ -1067,7 +1104,7 @@ static void cgroup_context_apply(
c2dfb7
                         }
c2dfb7
 
c2dfb7
                         cgroup_apply_unified_memory_limit(u, "memory.min", c->memory_min);
c2dfb7
-                        cgroup_apply_unified_memory_limit(u, "memory.low", c->memory_low);
c2dfb7
+                        cgroup_apply_unified_memory_limit(u, "memory.low", unit_get_ancestor_memory_low(u));
c2dfb7
                         cgroup_apply_unified_memory_limit(u, "memory.high", c->memory_high);
c2dfb7
                         cgroup_apply_unified_memory_limit(u, "memory.max", max);
c2dfb7
                         cgroup_apply_unified_memory_limit(u, "memory.swap.max", swap_max);
c2dfb7
@@ -1075,7 +1112,7 @@ static void cgroup_context_apply(
c2dfb7
                         char buf[DECIMAL_STR_MAX(uint64_t) + 1];
c2dfb7
                         uint64_t val;
c2dfb7
 
c2dfb7
-                        if (cgroup_context_has_unified_memory_config(c)) {
c2dfb7
+                        if (unit_has_unified_memory_config(u)) {
c2dfb7
                                 val = c->memory_max;
c2dfb7
                                 log_cgroup_compat(u, "Applying MemoryMax %" PRIi64 " as MemoryLimit", val);
c2dfb7
                         } else
c2dfb7
@@ -1204,8 +1241,13 @@ static void cgroup_context_apply(
c2dfb7
                 cgroup_apply_firewall(u);
c2dfb7
 }
c2dfb7
 
c2dfb7
-CGroupMask cgroup_context_get_mask(CGroupContext *c) {
c2dfb7
+static CGroupMask unit_get_cgroup_mask(Unit *u) {
c2dfb7
         CGroupMask mask = 0;
c2dfb7
+        CGroupContext *c;
c2dfb7
+
c2dfb7
+        assert(u);
c2dfb7
+
c2dfb7
+        c = unit_get_cgroup_context(u);
c2dfb7
 
c2dfb7
         /* Figure out which controllers we need */
c2dfb7
 
c2dfb7
@@ -1223,7 +1265,7 @@ CGroupMask cgroup_context_get_mask(CGroupContext *c) {
c2dfb7
 
c2dfb7
         if (c->memory_accounting ||
c2dfb7
             c->memory_limit != CGROUP_LIMIT_MAX ||
c2dfb7
-            cgroup_context_has_unified_memory_config(c))
c2dfb7
+            unit_has_unified_memory_config(u))
c2dfb7
                 mask |= CGROUP_MASK_MEMORY;
c2dfb7
 
c2dfb7
         if (c->device_allow ||
c2dfb7
@@ -1246,7 +1288,7 @@ CGroupMask unit_get_own_mask(Unit *u) {
c2dfb7
         if (!c)
c2dfb7
                 return 0;
c2dfb7
 
c2dfb7
-        return cgroup_context_get_mask(c) | unit_get_delegate_mask(u);
c2dfb7
+        return unit_get_cgroup_mask(u) | unit_get_delegate_mask(u);
c2dfb7
 }
c2dfb7
 
c2dfb7
 CGroupMask unit_get_delegate_mask(Unit *u) {
c2dfb7
diff --git a/src/core/cgroup.h b/src/core/cgroup.h
c2dfb7
index 8102b442b8..a263d6a169 100644
c2dfb7
--- a/src/core/cgroup.h
c2dfb7
+++ b/src/core/cgroup.h
c2dfb7
@@ -95,12 +95,16 @@ struct CGroupContext {
c2dfb7
         LIST_HEAD(CGroupIODeviceLimit, io_device_limits);
c2dfb7
         LIST_HEAD(CGroupIODeviceLatency, io_device_latencies);
c2dfb7
 
c2dfb7
+        uint64_t default_memory_low;
c2dfb7
         uint64_t memory_min;
c2dfb7
         uint64_t memory_low;
c2dfb7
         uint64_t memory_high;
c2dfb7
         uint64_t memory_max;
c2dfb7
         uint64_t memory_swap_max;
c2dfb7
 
c2dfb7
+        bool default_memory_low_set;
c2dfb7
+        bool memory_low_set;
c2dfb7
+
c2dfb7
         LIST_HEAD(IPAddressAccessItem, ip_address_allow);
c2dfb7
         LIST_HEAD(IPAddressAccessItem, ip_address_deny);
c2dfb7
 
c2dfb7
@@ -191,6 +195,8 @@ Unit *manager_get_unit_by_cgroup(Manager *m, const char *cgroup);
c2dfb7
 Unit *manager_get_unit_by_pid_cgroup(Manager *m, pid_t pid);
c2dfb7
 Unit* manager_get_unit_by_pid(Manager *m, pid_t pid);
c2dfb7
 
c2dfb7
+uint64_t unit_get_ancestor_memory_low(Unit *u);
c2dfb7
+
c2dfb7
 int unit_search_main_pid(Unit *u, pid_t *ret);
c2dfb7
 int unit_watch_all_pids(Unit *u);
c2dfb7
 
c2dfb7
diff --git a/src/core/dbus-cgroup.c b/src/core/dbus-cgroup.c
c2dfb7
index 6ce5984a02..2115d43b0c 100644
c2dfb7
--- a/src/core/dbus-cgroup.c
c2dfb7
+++ b/src/core/dbus-cgroup.c
c2dfb7
@@ -353,6 +353,7 @@ const sd_bus_vtable bus_cgroup_vtable[] = {
c2dfb7
         SD_BUS_PROPERTY("BlockIOReadBandwidth", "a(st)", property_get_blockio_device_bandwidths, 0, 0),
c2dfb7
         SD_BUS_PROPERTY("BlockIOWriteBandwidth", "a(st)", property_get_blockio_device_bandwidths, 0, 0),
c2dfb7
         SD_BUS_PROPERTY("MemoryAccounting", "b", bus_property_get_bool, offsetof(CGroupContext, memory_accounting), 0),
c2dfb7
+        SD_BUS_PROPERTY("DefaultMemoryLow", "t", NULL, offsetof(CGroupContext, default_memory_low), 0),
c2dfb7
         SD_BUS_PROPERTY("MemoryMin", "t", NULL, offsetof(CGroupContext, memory_min), 0),
c2dfb7
         SD_BUS_PROPERTY("MemoryLow", "t", NULL, offsetof(CGroupContext, memory_low), 0),
c2dfb7
         SD_BUS_PROPERTY("MemoryHigh", "t", NULL, offsetof(CGroupContext, memory_high), 0),
c2dfb7
@@ -668,6 +669,9 @@ int bus_cgroup_set_property(
c2dfb7
         if (streq(name, "MemoryLow"))
c2dfb7
                 return bus_cgroup_set_memory(u, name, &c->memory_low, message, flags, error);
c2dfb7
 
c2dfb7
+        if (streq(name, "DefaultMemoryLow"))
c2dfb7
+                return bus_cgroup_set_memory(u, name, &c->default_memory_low, message, flags, error);
c2dfb7
+
c2dfb7
         if (streq(name, "MemoryHigh"))
c2dfb7
                 return bus_cgroup_set_memory(u, name, &c->memory_high, message, flags, error);
c2dfb7
 
c2dfb7
@@ -686,6 +690,9 @@ int bus_cgroup_set_property(
c2dfb7
         if (streq(name, "MemoryLowScale"))
c2dfb7
                 return bus_cgroup_set_memory_scale(u, name, &c->memory_low, message, flags, error);
c2dfb7
 
c2dfb7
+        if (streq(name, "DefaultMemoryLowScale"))
c2dfb7
+                return bus_cgroup_set_memory_scale(u, name, &c->default_memory_low, message, flags, error);
c2dfb7
+
c2dfb7
         if (streq(name, "MemoryHighScale"))
c2dfb7
                 return bus_cgroup_set_memory_scale(u, name, &c->memory_high, message, flags, error);
c2dfb7
 
c2dfb7
diff --git a/src/core/load-fragment-gperf.gperf.m4 b/src/core/load-fragment-gperf.gperf.m4
c2dfb7
index 1c6e539f30..43cc78fdea 100644
c2dfb7
--- a/src/core/load-fragment-gperf.gperf.m4
c2dfb7
+++ b/src/core/load-fragment-gperf.gperf.m4
c2dfb7
@@ -172,6 +172,7 @@ $1.CPUQuota,                     config_parse_cpu_quota,             0,
c2dfb7
 $1.CPUQuotaPeriodSec,            config_parse_sec_def_infinity,      0,                             offsetof($1, cgroup_context.cpu_quota_period_usec)
c2dfb7
 $1.MemoryAccounting,             config_parse_bool,                  0,                             offsetof($1, cgroup_context.memory_accounting)
c2dfb7
 $1.MemoryMin,                    config_parse_memory_limit,          0,                             offsetof($1, cgroup_context)
c2dfb7
+$1.DefaultMemoryLow,             config_parse_memory_limit,          0,                             offsetof($1, cgroup_context)
c2dfb7
 $1.MemoryLow,                    config_parse_memory_limit,          0,                             offsetof($1, cgroup_context)
c2dfb7
 $1.MemoryHigh,                   config_parse_memory_limit,          0,                             offsetof($1, cgroup_context)
c2dfb7
 $1.MemoryMax,                    config_parse_memory_limit,          0,                             offsetof($1, cgroup_context)
c2dfb7
diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c
c2dfb7
index 89a3457acc..20faed02ad 100644
c2dfb7
--- a/src/core/load-fragment.c
c2dfb7
+++ b/src/core/load-fragment.c
c2dfb7
@@ -3096,11 +3096,18 @@ int config_parse_memory_limit(
c2dfb7
                 }
c2dfb7
         }
c2dfb7
 
c2dfb7
-        if (streq(lvalue, "MemoryMin"))
c2dfb7
+        if (streq(lvalue, "DefaultMemoryLow")) {
c2dfb7
+                c->default_memory_low_set = true;
c2dfb7
+                if (isempty(rvalue))
c2dfb7
+                        c->default_memory_low = CGROUP_LIMIT_MIN;
c2dfb7
+                else
c2dfb7
+                        c->default_memory_low = bytes;
c2dfb7
+        } else if (streq(lvalue, "MemoryMin"))
c2dfb7
                 c->memory_min = bytes;
c2dfb7
-        else if (streq(lvalue, "MemoryLow"))
c2dfb7
+        else if (streq(lvalue, "MemoryLow")) {
c2dfb7
                 c->memory_low = bytes;
c2dfb7
-        else if (streq(lvalue, "MemoryHigh"))
c2dfb7
+                c->memory_low_set = true;
c2dfb7
+        } else if (streq(lvalue, "MemoryHigh"))
c2dfb7
                 c->memory_high = bytes;
c2dfb7
         else if (streq(lvalue, "MemoryMax"))
c2dfb7
                 c->memory_max = bytes;
c2dfb7
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
c2dfb7
index 203c068d02..f88730a85d 100644
c2dfb7
--- a/src/shared/bus-unit-util.c
c2dfb7
+++ b/src/shared/bus-unit-util.c
c2dfb7
@@ -429,7 +429,7 @@ static int bus_append_cgroup_property(sd_bus_message *m, const char *field, cons
c2dfb7
                 return 1;
c2dfb7
         }
c2dfb7
 
c2dfb7
-        if (STR_IN_SET(field, "MemoryMin", "MemoryLow", "MemoryHigh", "MemoryMax", "MemorySwapMax", "MemoryLimit", "TasksMax")) {
c2dfb7
+        if (STR_IN_SET(field, "MemoryMin", "DefaultMemoryLow", "MemoryLow", "MemoryHigh", "MemoryMax", "MemorySwapMax", "MemoryLimit", "TasksMax")) {
c2dfb7
 
c2dfb7
                 if (isempty(eq) || streq(eq, "infinity")) {
c2dfb7
                         r = sd_bus_message_append(m, "(sv)", field, "t", CGROUP_LIMIT_MAX);
c2dfb7
diff --git a/src/shared/bus-util.c b/src/shared/bus-util.c
c2dfb7
index 5ed68429be..0ba2712deb 100644
c2dfb7
--- a/src/shared/bus-util.c
c2dfb7
+++ b/src/shared/bus-util.c
c2dfb7
@@ -774,7 +774,7 @@ int bus_print_property(const char *name, sd_bus_message *m, bool value, bool all
c2dfb7
 
c2dfb7
                         print_property(name, "%s", "[not set]");
c2dfb7
 
c2dfb7
-                else if ((STR_IN_SET(name, "MemoryLow", "MemoryHigh", "MemoryMax", "MemorySwapMax", "MemoryLimit") && u == CGROUP_LIMIT_MAX) ||
c2dfb7
+                else if ((STR_IN_SET(name, "DefaultMemoryLow", "MemoryLow", "MemoryHigh", "MemoryMax", "MemorySwapMax", "MemoryLimit") && u == CGROUP_LIMIT_MAX) ||
c2dfb7
                          (STR_IN_SET(name, "TasksMax", "DefaultTasksMax") && u == (uint64_t) -1) ||
c2dfb7
                          (startswith(name, "Limit") && u == (uint64_t) -1) ||
c2dfb7
                          (startswith(name, "DefaultLimit") && u == (uint64_t) -1))
c2dfb7
diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c
c2dfb7
index 35ad20f510..763ca0c6b7 100644
c2dfb7
--- a/src/systemctl/systemctl.c
c2dfb7
+++ b/src/systemctl/systemctl.c
c2dfb7
@@ -3918,6 +3918,8 @@ typedef struct UnitStatusInfo {
c2dfb7
         uint64_t ip_ingress_bytes;
c2dfb7
         uint64_t ip_egress_bytes;
c2dfb7
 
c2dfb7
+        uint64_t default_memory_low;
c2dfb7
+
c2dfb7
         LIST_HEAD(ExecStatusInfo, exec);
c2dfb7
 } UnitStatusInfo;
c2dfb7
 
c2dfb7
@@ -5028,6 +5030,7 @@ static int show_one(
c2dfb7
                 { "Where",                          "s",              NULL,           offsetof(UnitStatusInfo, where)                             },
c2dfb7
                 { "What",                           "s",              NULL,           offsetof(UnitStatusInfo, what)                              },
c2dfb7
                 { "MemoryCurrent",                  "t",              NULL,           offsetof(UnitStatusInfo, memory_current)                    },
c2dfb7
+                { "DefaultMemoryLow",               "t",              NULL,           offsetof(UnitStatusInfo, default_memory_low)                },
c2dfb7
                 { "MemoryMin",                      "t",              NULL,           offsetof(UnitStatusInfo, memory_min)                        },
c2dfb7
                 { "MemoryLow",                      "t",              NULL,           offsetof(UnitStatusInfo, memory_low)                        },
c2dfb7
                 { "MemoryHigh",                     "t",              NULL,           offsetof(UnitStatusInfo, memory_high)                       },
c2dfb7
diff --git a/src/test/meson.build b/src/test/meson.build
c2dfb7
index 22264d034c..7b310d4ec7 100644
c2dfb7
--- a/src/test/meson.build
c2dfb7
+++ b/src/test/meson.build
c2dfb7
@@ -518,6 +518,12 @@ tests += [
c2dfb7
           libshared],
c2dfb7
          []],
c2dfb7
 
c2dfb7
+        [['src/test/test-cgroup-unit-default.c',
c2dfb7
+          'src/test/test-helper.c'],
c2dfb7
+         [libcore,
c2dfb7
+          libshared],
c2dfb7
+         []],
c2dfb7
+
c2dfb7
         [['src/test/test-cgroup-mask.c',
c2dfb7
           'src/test/test-helper.c'],
c2dfb7
          [libcore,
c2dfb7
diff --git a/src/test/test-cgroup-unit-default.c b/src/test/test-cgroup-unit-default.c
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..54f7d50c45
c2dfb7
--- /dev/null
c2dfb7
+++ b/src/test/test-cgroup-unit-default.c
c2dfb7
@@ -0,0 +1,145 @@
c2dfb7
+/* SPDX-License-Identifier: LGPL-2.1+ */
c2dfb7
+
c2dfb7
+#include <stdio.h>
c2dfb7
+
c2dfb7
+#include "cgroup.h"
c2dfb7
+#include "manager.h"
c2dfb7
+#include "rm-rf.h"
c2dfb7
+#include "test-helper.h"
c2dfb7
+#include "tests.h"
c2dfb7
+#include "unit.h"
c2dfb7
+
c2dfb7
+static int test_default_memory_low(void) {
c2dfb7
+        _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
c2dfb7
+        _cleanup_(manager_freep) Manager *m = NULL;
c2dfb7
+        Unit *root, *dml,
c2dfb7
+             *dml_passthrough, *dml_passthrough_empty, *dml_passthrough_set_dml, *dml_passthrough_set_ml,
c2dfb7
+             *dml_override, *dml_override_empty,
c2dfb7
+             *dml_discard, *dml_discard_empty, *dml_discard_set_ml;
c2dfb7
+        uint64_t dml_tree_default;
c2dfb7
+        int r;
c2dfb7
+
c2dfb7
+        r = enter_cgroup_subroot();
c2dfb7
+        if (r == -ENOMEDIUM)
c2dfb7
+                return EXIT_TEST_SKIP;
c2dfb7
+
c2dfb7
+        assert_se(set_unit_path(get_testdata_dir()) >= 0);
c2dfb7
+        assert_se(runtime_dir = setup_fake_runtime_dir());
c2dfb7
+        r = manager_new(UNIT_FILE_USER, MANAGER_TEST_RUN_BASIC, &m);
c2dfb7
+        if (IN_SET(r, -EPERM, -EACCES)) {
c2dfb7
+                log_error_errno(r, "manager_new: %m");
c2dfb7
+                return EXIT_TEST_SKIP;
c2dfb7
+        }
c2dfb7
+
c2dfb7
+        assert_se(r >= 0);
c2dfb7
+        assert_se(manager_startup(m, NULL, NULL) >= 0);
c2dfb7
+
c2dfb7
+        /* dml.slice has DefaultMemoryLow=50. Beyond that, individual subhierarchies look like this:
c2dfb7
+         *
c2dfb7
+         * 1. dml-passthrough.slice sets MemoryLow=100. This should not affect its children, as only
c2dfb7
+         *    DefaultMemoryLow is propagated, not MemoryLow. As such, all leaf services should end up with
c2dfb7
+         *    memory.low as 50, inherited from dml.slice, *except* for dml-passthrough-set-ml.service, which
c2dfb7
+         *    should have the value of 25, as it has MemoryLow explicitly set.
c2dfb7
+         *
c2dfb7
+         *                                                  ┌───────────┐
c2dfb7
+         *                                                  │ dml.slice │
c2dfb7
+         *                                                  └─────┬─────┘
c2dfb7
+         *                                                  MemoryLow=100
c2dfb7
+         *                                            ┌───────────┴───────────┐
c2dfb7
+         *                                            │ dml-passthrough.slice │
c2dfb7
+         *                                            └───────────┬───────────┘
c2dfb7
+         *                    ┌───────────────────────────────────┼───────────────────────────────────┐
c2dfb7
+         *             no new settings                   DefaultMemoryLow=15                     MemoryLow=25
c2dfb7
+         *    ┌───────────────┴───────────────┐  ┌────────────────┴────────────────┐  ┌───────────────┴────────────────┐
c2dfb7
+         *    │ dml-passthrough-empty.service │  │ dml-passthrough-set-dml.service │  │ dml-passthrough-set-ml.service │
c2dfb7
+         *    └───────────────────────────────┘  └─────────────────────────────────┘  └────────────────────────────────┘
c2dfb7
+         *
c2dfb7
+         * 2. dml-override.slice sets DefaultMemoryLow=10. As such, dml-override-empty.service should also
c2dfb7
+         *    end up with a memory.low of 10. dml-override.slice should still have a memory.low of 50.
c2dfb7
+         *
c2dfb7
+         *            ┌───────────┐
c2dfb7
+         *            │ dml.slice │
c2dfb7
+         *            └─────┬─────┘
c2dfb7
+         *         DefaultMemoryLow=10
c2dfb7
+         *        ┌─────────┴──────────┐
c2dfb7
+         *        │ dml-override.slice │
c2dfb7
+         *        └─────────┬──────────┘
c2dfb7
+         *           no new settings
c2dfb7
+         *    ┌─────────────┴──────────────┐
c2dfb7
+         *    │ dml-override-empty.service │
c2dfb7
+         *    └────────────────────────────┘
c2dfb7
+         *
c2dfb7
+         * 3. dml-discard.slice sets DefaultMemoryLow= with no rvalue. As such,
c2dfb7
+         *    dml-discard-empty.service should end up with a value of 0.
c2dfb7
+         *    dml-discard-explicit-ml.service sets MemoryLow=70, and as such should have that override the
c2dfb7
+         *    reset DefaultMemoryLow value. dml-discard.slice should still have an eventual memory.low of 50.
c2dfb7
+         *
c2dfb7
+         *                           ┌───────────┐
c2dfb7
+         *                           │ dml.slice │
c2dfb7
+         *                           └─────┬─────┘
c2dfb7
+         *                         DefaultMemoryLow=
c2dfb7
+         *                       ┌─────────┴─────────┐
c2dfb7
+         *                       │ dml-discard.slice │
c2dfb7
+         *                       └─────────┬─────────┘
c2dfb7
+         *                  ┌──────────────┴───────────────┐
c2dfb7
+         *           no new settings                  MemoryLow=15
c2dfb7
+         *    ┌─────────────┴─────────────┐  ┌─────────────┴──────────────┐
c2dfb7
+         *    │ dml-discard-empty.service │  │ dml-discard-set-ml.service │
c2dfb7
+         *    └───────────────────────────┘  └────────────────────────────┘
c2dfb7
+         */
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml.slice", NULL, &dml) >= 0);
c2dfb7
+
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough.slice", NULL, &dml_passthrough) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_passthrough->slice) == dml);
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-empty.service", NULL, &dml_passthrough_empty) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_passthrough_empty->slice) == dml_passthrough);
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-set-dml.service", NULL, &dml_passthrough_set_dml) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_passthrough_set_dml->slice) == dml_passthrough);
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-set-ml.service", NULL, &dml_passthrough_set_ml) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_passthrough_set_ml->slice) == dml_passthrough);
c2dfb7
+
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-override.slice", NULL, &dml_override) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_override->slice) == dml);
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-override-empty.service", NULL, &dml_override_empty) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_override_empty->slice) == dml_override);
c2dfb7
+
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-discard.slice", NULL, &dml_discard) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_discard->slice) == dml);
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-discard-empty.service", NULL, &dml_discard_empty) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_discard_empty->slice) == dml_discard);
c2dfb7
+        assert_se(manager_load_startable_unit_or_warn(m, "dml-discard-set-ml.service", NULL, &dml_discard_set_ml) >= 0);
c2dfb7
+        assert_se(UNIT_DEREF(dml_discard_set_ml->slice) == dml_discard);
c2dfb7
+
c2dfb7
+        root = UNIT_DEREF(dml->slice);
c2dfb7
+        assert_se(!UNIT_ISSET(root->slice));
c2dfb7
+
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(root) == CGROUP_LIMIT_MIN);
c2dfb7
+
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml) == CGROUP_LIMIT_MIN);
c2dfb7
+        dml_tree_default = unit_get_cgroup_context(dml)->default_memory_low;
c2dfb7
+        assert_se(dml_tree_default == 50);
c2dfb7
+
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_passthrough) == 100);
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_passthrough_empty) == dml_tree_default);
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_passthrough_set_dml) == 50);
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_passthrough_set_ml) == 25);
c2dfb7
+
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_override) == dml_tree_default);
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_override_empty) == 10);
c2dfb7
+
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_discard) == dml_tree_default);
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_discard_empty) == CGROUP_LIMIT_MIN);
c2dfb7
+        assert_se(unit_get_ancestor_memory_low(dml_discard_set_ml) == 15);
c2dfb7
+
c2dfb7
+        return 0;
c2dfb7
+}
c2dfb7
+
c2dfb7
+int main(int argc, char* argv[]) {
c2dfb7
+        int rc = EXIT_SUCCESS;
c2dfb7
+
c2dfb7
+        test_setup_logging(LOG_DEBUG);
c2dfb7
+
c2dfb7
+        TEST_REQ_RUNNING_SYSTEMD(rc = test_default_memory_low());
c2dfb7
+
c2dfb7
+        return rc;
c2dfb7
+}
c2dfb7
diff --git a/test/dml-discard-empty.service b/test/dml-discard-empty.service
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..75228f6470
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-discard-empty.service
c2dfb7
@@ -0,0 +1,7 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML discard empty service
c2dfb7
+
c2dfb7
+[Service]
c2dfb7
+Slice=dml-discard.slice
c2dfb7
+Type=oneshot
c2dfb7
+ExecStart=/bin/true
c2dfb7
diff --git a/test/dml-discard-set-ml.service b/test/dml-discard-set-ml.service
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..591c99270c
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-discard-set-ml.service
c2dfb7
@@ -0,0 +1,8 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML discard set ml service
c2dfb7
+
c2dfb7
+[Service]
c2dfb7
+Slice=dml-discard.slice
c2dfb7
+Type=oneshot
c2dfb7
+ExecStart=/bin/true
c2dfb7
+MemoryLow=15
c2dfb7
diff --git a/test/dml-discard.slice b/test/dml-discard.slice
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..e26d86846c
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-discard.slice
c2dfb7
@@ -0,0 +1,5 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML discard slice
c2dfb7
+
c2dfb7
+[Slice]
c2dfb7
+DefaultMemoryLow=
c2dfb7
diff --git a/test/dml-override-empty.service b/test/dml-override-empty.service
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..142c98720c
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-override-empty.service
c2dfb7
@@ -0,0 +1,7 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML override empty service
c2dfb7
+
c2dfb7
+[Service]
c2dfb7
+Slice=dml-override.slice
c2dfb7
+Type=oneshot
c2dfb7
+ExecStart=/bin/true
c2dfb7
diff --git a/test/dml-override.slice b/test/dml-override.slice
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..feb6773e39
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-override.slice
c2dfb7
@@ -0,0 +1,5 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML override slice
c2dfb7
+
c2dfb7
+[Slice]
c2dfb7
+DefaultMemoryLow=10
c2dfb7
diff --git a/test/dml-passthrough-empty.service b/test/dml-passthrough-empty.service
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..34832de491
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-passthrough-empty.service
c2dfb7
@@ -0,0 +1,7 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML passthrough empty service
c2dfb7
+
c2dfb7
+[Service]
c2dfb7
+Slice=dml-passthrough.slice
c2dfb7
+Type=oneshot
c2dfb7
+ExecStart=/bin/true
c2dfb7
diff --git a/test/dml-passthrough-set-dml.service b/test/dml-passthrough-set-dml.service
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..5bdf4ed4b7
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-passthrough-set-dml.service
c2dfb7
@@ -0,0 +1,8 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML passthrough set DML service
c2dfb7
+
c2dfb7
+[Service]
c2dfb7
+Slice=dml-passthrough.slice
c2dfb7
+Type=oneshot
c2dfb7
+ExecStart=/bin/true
c2dfb7
+DefaultMemoryLow=15
c2dfb7
diff --git a/test/dml-passthrough-set-ml.service b/test/dml-passthrough-set-ml.service
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..2abd591389
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-passthrough-set-ml.service
c2dfb7
@@ -0,0 +1,8 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML passthrough set ML service
c2dfb7
+
c2dfb7
+[Service]
c2dfb7
+Slice=dml-passthrough.slice
c2dfb7
+Type=oneshot
c2dfb7
+ExecStart=/bin/true
c2dfb7
+MemoryLow=25
c2dfb7
diff --git a/test/dml-passthrough.slice b/test/dml-passthrough.slice
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..1b1a848edb
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml-passthrough.slice
c2dfb7
@@ -0,0 +1,5 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML passthrough slice
c2dfb7
+
c2dfb7
+[Slice]
c2dfb7
+MemoryLow=100
c2dfb7
diff --git a/test/dml.slice b/test/dml.slice
c2dfb7
new file mode 100644
c2dfb7
index 0000000000..84e333ef04
c2dfb7
--- /dev/null
c2dfb7
+++ b/test/dml.slice
c2dfb7
@@ -0,0 +1,5 @@
c2dfb7
+[Unit]
c2dfb7
+Description=DML slice
c2dfb7
+
c2dfb7
+[Slice]
c2dfb7
+DefaultMemoryLow=50
c2dfb7
diff --git a/test/meson.build b/test/meson.build
c2dfb7
index 070731c4a9..52e4fa2e3c 100644
c2dfb7
--- a/test/meson.build
c2dfb7
+++ b/test/meson.build
c2dfb7
@@ -7,6 +7,16 @@ test_data_files = '''
c2dfb7
         c.service
c2dfb7
         d.service
c2dfb7
         daughter.service
c2dfb7
+        dml.slice
c2dfb7
+        dml-passthrough.slice
c2dfb7
+        dml-passthrough-empty.service
c2dfb7
+        dml-passthrough-set-dml.service
c2dfb7
+        dml-passthrough-set-ml.service
c2dfb7
+        dml-override.slice
c2dfb7
+        dml-override-empty.service
c2dfb7
+        dml-discard.slice
c2dfb7
+        dml-discard-empty.service
c2dfb7
+        dml-discard-set-ml.service
c2dfb7
         e.service
c2dfb7
         end.service
c2dfb7
         f.service