core/unit: fail earlier before spawning executor when we failed to realize cgroup

Before 23ac08115a, even if we failed to
create the cgroup for a unit, a cgroup runtime object for the cgroup is
created with the cgroup path. Hence, the creation of cgroup is failed,
execution of the unit will fail in posix_spawn_wrapper() and logged
something like the following:
```
systemd[1]: testservice.service: Failed to create cgroup /testslice.slice/testservice.service: Cannot allocate memory
systemd[1]: testservice.service: Failed to spawn executor: No such file or directory
systemd[1]: testservice.service: Failed to spawn 'start' task: No such file or directory
systemd[1]: testservice.service: Failed with result 'resources'.
systemd[1]: Failed to start testservice.service.
```

However, after the commit, when we failed to create the cgroup, a cgroup
runtime object is not created, hence NULL will be assigned to
ExecParameters.cgroup_path in unit_set_exec_params().
Hence, the unit process will be invoked in the init.scope.
```
systemd[1]: testservice.service: Failed to create cgroup /testslice.slice/testservice.service: Cannot allocate memory
systemd[1]: Starting testservice.service...
cat[1094]: 0::/init.scope
systemd[1]: testservice.service: Deactivated successfully.
systemd[1]: Finished testservice.service.
```
where the test service calls 'cat /proc/self/cgroup'.

To fix the issue, let's fail earlier when we failed to create cgroup.

Follow-up for 23ac08115a (v258).
This commit is contained in:
Yu Watanabe
2025-09-26 09:28:12 +09:00
parent b3243f4bee
commit 8b4ee3d68d
2 changed files with 53 additions and 5 deletions
+7 -5
View File
@@ -5559,11 +5559,11 @@ int unit_fork_helper_process(Unit *u, const char *name, bool into_cgroup, PidRef
* with the child's PID. */
if (into_cgroup) {
(void) unit_realize_cgroup(u);
r = unit_realize_cgroup(u);
if (r < 0)
return r;
crt = unit_setup_cgroup_runtime(u);
if (!crt)
return -ENOMEM;
crt = unit_get_cgroup_runtime(u);
}
r = safe_fork(name, FORK_REOPEN_LOG|FORK_DEATHSIG_SIGTERM, &pid);
@@ -6013,7 +6013,9 @@ int unit_prepare_exec(Unit *u) {
/* Prepares everything so that we can fork of a process for this unit */
(void) unit_realize_cgroup(u);
r = unit_realize_cgroup(u);
if (r < 0)
return r;
CGroupRuntime *crt = unit_get_cgroup_runtime(u);
if (crt && crt->reset_accounting) {
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -ex
set -o pipefail
# Test that the service is not invoked if the cgroup cannot be created.
# It seems openSUSE kernel (at least kernel-default-6.16.8-1.1.x86_64.rpm) has a
# bag in kernel oom killer or clone3 syscall, and spawning executor on a cgroup
# with too small MemoryMax= triggers infinite loop of OOM kill, and posix_spawn()
# will never return, and the service manager will stuck.
####
# [ 119.776797] systemd invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=0
# [ 119.776859] CPU: 1 UID: 0 PID: 1472 Comm: systemd Not tainted 6.16.8-1-default #1 PREEMPT(voluntary) openSUSE Tumbleweed 6c85865973e4ae641870ed68afe8933a6986c974
# [ 119.776865] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.17.0-5.fc42 04/01/2014
# [ 119.776867] Call Trace:
# (snip)
# [ 119.778126] Out of memory and no killable processes...
####
# On other distributions, the oom killer is triggered, but clone3 immediately
# fails with ENOMEM, and such problematic loop does not happen.
. /etc/os-release
if [[ "$ID" =~ opensuse ]]; then
echo "Skipping cgroup test with too small MemoryMax= setting on openSUSE."
exit 0
fi
cat >/run/systemd/system/testslice.slice <<EOF
[Slice]
MemoryMax=1
EOF
cat >/run/systemd/system/testservice.service <<EOF
[Service]
Type=oneshot
ExecStart=cat /proc/self/cgroup
Slice=testslice.slice
EOF
systemctl daemon-reload
(! systemctl start testservice.service)
rm /run/systemd/system/testslice.slice
rm /run/systemd/system/testservice.service
exit 0