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