core/service: Prevent accidental service termination during reload (#39692)

We currently blindly send the configured reload signal (e.g. SIGHUP or
SIGUSR1) to the main service PID for Type=notify-reload units, even if
the service hasn't installed a userspace handler. This can lead to
invoking unintended default behavior (typically process termination) in
daemons which later deprecate and remove their reload handler.

This is a real problem we have seen in production on multiple occasions.
In one particularly egregious instance, a production distributed storage
service had a large percentage of its nodes all terminate at once when
sent a reload signal. In this case a signal handler had been removed,
but another place still sending the signal was missed.

To mitigate this, introduce two new checks for Type=notify-reload
services:

1. On READY=1: When the service first sends READY=1 during initial
startup, we check for the handler. If it's missing, the service startup
is failed with SERVICE_FAILURE_PROTOCOL. This enforces the contract that
services *must* install their handler before signalling readiness.

2. On reload: Immediately before sending the reload signal, we check
again. If the handler is now missing, we log a warning but still send
the signal. This preserves the requested operator action while making
the missing handler visible.

The startup check catches definite static misconfiguration before the
service is accepted as ready. The reload check is advisory because the
service has already started successfully, but warns if its handler later
disappears. Together these provide a best-effort safety net while
preserving the agreed reload semantics.
This commit is contained in:
Yu Watanabe
2026-08-07 01:23:03 +09:00
committed by GitHub
11 changed files with 289 additions and 0 deletions
+48
View File
@@ -21,6 +21,7 @@
#include "alloc-util.h"
#include "architecture.h"
#include "argv-util.h"
#include "bitfield.h"
#include "capability-util.h"
#include "cgroup-util.h"
#include "dirent-util.h"
@@ -804,6 +805,53 @@ int get_process_umask(pid_t pid, mode_t *ret) {
return parse_mode(m, ret);
}
static int pid_get_sigmask(pid_t pid, const char *field_name, uint64_t *ret) {
_cleanup_free_ char *field = NULL;
int r;
assert(pid >= 0);
assert(field_name);
assert(ret);
r = procfs_file_get_field(pid, "status", field_name, &field);
if (r == -ENOENT)
return -ESRCH;
if (r < 0)
return r;
return safe_atou64_full(field, 16, ret);
}
static int pidref_has_sigmask(const PidRef *pidref, int sig, const char *field_name) {
uint64_t mask;
int r;
if (!pidref_is_set(pidref))
return -ESRCH;
if (pidref_is_remote(pidref))
return -EREMOTE;
if (!SIGNAL_VALID(sig))
return -EINVAL;
r = pid_get_sigmask(pidref->pid, field_name, &mask);
if (r < 0)
return r;
r = pidref_verify(pidref);
if (r < 0)
return r;
return BIT_SET(mask, sig - 1);
}
int pidref_has_sigcgt(const PidRef *pidref, int sig) {
return pidref_has_sigmask(pidref, sig, "SigCgt");
}
int pidref_has_sigblk(const PidRef *pidref, int sig) {
return pidref_has_sigmask(pidref, sig, "SigBlk");
}
/*
* Return values:
* < 0 : pidref_wait_for_terminate() failed to get the state of the
+2
View File
@@ -53,6 +53,8 @@ int pidref_get_ppid_as_pidref(const PidRef *pidref, PidRef *ret);
int pid_get_start_time(pid_t pid, usec_t *ret);
int pidref_get_start_time(const PidRef *pid, usec_t *ret);
int get_process_umask(pid_t pid, mode_t *ret);
int pidref_has_sigcgt(const PidRef *pidref, int sig);
int pidref_has_sigblk(const PidRef *pidref, int sig);
static inline bool SIGINFO_CODE_IS_DEAD(int code) {
return IN_SET(code, CLD_EXITED, CLD_KILLED, CLD_DUMPED);