mirror of
https://github.com/troglobit/finit.git
synced 2026-09-30 21:13:01 +07:00
Fix #492: add per-service directories, systemd RuntimeDirectory style
A service that drops privileges cannot create its own PID file in /run, root owns it. Finit can create the file with pidfile-create, but the daemon still cannot touch it to confirm a SIGHUP. Five new settings, block format only: runtime-dir, state-dir, cache-dir, logs-dir, and config-dir. The value is a directory name, resolved under /run, /var/lib, /var/cache, /var/log, and /etc, respectively. The directory is created before the service starts, mode 0755 owned by user/group, and the full path is exported to the process as RUNTIME_DIRECTORY, STATE_DIRECTORY, CACHE_DIRECTORY, LOGS_DIRECTORY, and CONFIGURATION_DIRECTORY. Mode and ownership are asserted at creation only, a daemon may tighten them afterwards. The runtime directory is removed when the unit stops, after any exec-stop-post script, like systemd with RuntimeDirectoryPreserve=no. A completed run/task counts as stopped unless remain-after-exit keeps it up. The other four persist across restarts. These are the first settings with no legacy token: they are validated by service_set_dir() and stored on the svc that service_register() now returns. systemd accepts a list of directories per setting; this is a single name for now, widening later is compatible since libconfuse accepts a bare value for a list option. The test sysroot gains libnss_files.so.2, which ldd cannot see, glibc dlopen()s it. Without it getpwnam() fails inside the chroot, so user/group settings never resolved and directory ownership could not be tested. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -96,6 +96,45 @@ following settings are available:
|
||||
* `oncrash = "script"` -- similarly, but instead of rebooting, call
|
||||
the `exec-stop-post` script with exit code `crashed`, see below
|
||||
|
||||
Service directories
|
||||
-------------------
|
||||
|
||||
Five settings ask Finit to create a directory for the service before it
|
||||
starts, owned by its `user` and `group`, mode 0755. The value is a
|
||||
directory name, resolved under a fixed base -- absolute paths and `..`
|
||||
are refused:
|
||||
|
||||
| Setting | Base | Environment variable |
|
||||
|---|---|---|
|
||||
| `runtime-dir` | `/run` | `RUNTIME_DIRECTORY` |
|
||||
| `state-dir` | `/var/lib` | `STATE_DIRECTORY` |
|
||||
| `cache-dir` | `/var/cache` | `CACHE_DIRECTORY` |
|
||||
| `logs-dir` | `/var/log` | `LOGS_DIRECTORY` |
|
||||
| `config-dir` | `/etc` | `CONFIGURATION_DIRECTORY` |
|
||||
|
||||
Each resolved path is exported to the process environment under the
|
||||
listed name, the same names systemd uses for `RuntimeDirectory=` and
|
||||
friends.
|
||||
|
||||
The runtime directory is removed again when the service stops, after
|
||||
any `exec-stop-post` script has run; `/run` is a tmpfs so it would not
|
||||
survive a reboot anyway. The other four persist. A completed `run` or
|
||||
`task` counts as stopped, unless `remain-after-exit` keeps it alive
|
||||
until stopped for real.
|
||||
|
||||
This is what lets a service drop privileges and still create, and
|
||||
later touch, its own PID file:
|
||||
|
||||
service ntpd {
|
||||
user = "ntp"
|
||||
group = "ntp"
|
||||
runtime-dir = "ntpd"
|
||||
pidfile = "/run/ntpd/ntpd.pid"
|
||||
command = "/usr/sbin/ntpd -n -p /run/ntpd/ntpd.pid"
|
||||
}
|
||||
|
||||
These settings exist only in the block format.
|
||||
|
||||
Stopping and reloading
|
||||
----------------------
|
||||
|
||||
|
||||
+35
-1
@@ -269,6 +269,11 @@ static cfg_opt_t svc_opts[] = {
|
||||
|
||||
CFG_STR_LIST("capabilities", NULL, CFGF_NODEFAULT),
|
||||
CFG_STR_LIST("caps", NULL, CFGF_NODEFAULT), /* alias */
|
||||
CFG_STR ("runtime-dir", NULL, CFGF_NODEFAULT),
|
||||
CFG_STR ("state-dir", NULL, CFGF_NODEFAULT),
|
||||
CFG_STR ("cache-dir", NULL, CFGF_NODEFAULT),
|
||||
CFG_STR ("logs-dir", NULL, CFGF_NODEFAULT),
|
||||
CFG_STR ("config-dir", NULL, CFGF_NODEFAULT),
|
||||
CFG_STR_LIST("conflicts", NULL, CFGF_NODEFAULT),
|
||||
CFG_STR ("if", NULL, CFGF_NODEFAULT),
|
||||
CFG_STR ("tty", NULL, CFGF_NODEFAULT),
|
||||
@@ -1289,6 +1294,30 @@ static int if_translate(const char *str, char *buf, size_t len, char *file, cons
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* These have no legacy token, they are validated by service_set_dir()
|
||||
* and stored directly on the registered svc. Empty means unset,
|
||||
* service_register() has already cleared the fields.
|
||||
*/
|
||||
static void dirs_translate(cfg_t *sec, svc_t *svc, char *file)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < NUM_SVCDIRS; i++) {
|
||||
const char *str;
|
||||
|
||||
str = sec_getstr(sec, svcdirs[i].key, NULL);
|
||||
if (!str || !str[0])
|
||||
continue;
|
||||
|
||||
if (service_set_dir(svc, &svcdirs[i], str))
|
||||
logit(LOG_ERR, "%s: %s: %s '%s' %s, ignoring",
|
||||
file, cfg_title(sec), svcdirs[i].key, str,
|
||||
errno == ENAMETOOLONG ? "is too long"
|
||||
: "must be relative, no '..'");
|
||||
}
|
||||
}
|
||||
|
||||
static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *file)
|
||||
{
|
||||
struct rlimit local_rlimit[RLIMIT_NLIMITS];
|
||||
@@ -1299,6 +1328,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi
|
||||
long num;
|
||||
char buf[512];
|
||||
char nm[80];
|
||||
svc_t *svc;
|
||||
char *id;
|
||||
|
||||
cmd = sec_getstr(sec, "command", NULL);
|
||||
@@ -1505,7 +1535,11 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi
|
||||
addtok(line, sizeof(line), "-- %s", str);
|
||||
|
||||
dbg("translated: %s", line);
|
||||
service_register(type, line, rlimit, file);
|
||||
svc = service_register(type, line, rlimit, file);
|
||||
if (!svc)
|
||||
return;
|
||||
|
||||
dirs_translate(sec, svc, file);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
+113
@@ -570,6 +570,101 @@ static void set_uid(uid_t uid, svc_t *svc)
|
||||
err(1, "%s: failed setuid(%d)", svc_ident(svc, NULL, 0), uid);
|
||||
}
|
||||
|
||||
/*
|
||||
* systemd-style per-service directories: created before each start,
|
||||
* owned by the service user, and exported to the environment. The
|
||||
* runtime directory is removed again when the service stops, after
|
||||
* any post: script has run. See issue #492.
|
||||
*/
|
||||
const struct svcdir svcdirs[NUM_SVCDIRS] = {
|
||||
{ "runtime-dir", "/run", "RUNTIME_DIRECTORY", offsetof(svc_t, runtime_dir) },
|
||||
{ "state-dir", "/var/lib", "STATE_DIRECTORY", offsetof(svc_t, state_dir) },
|
||||
{ "cache-dir", "/var/cache", "CACHE_DIRECTORY", offsetof(svc_t, cache_dir) },
|
||||
{ "logs-dir", "/var/log", "LOGS_DIRECTORY", offsetof(svc_t, logs_dir) },
|
||||
{ "config-dir", "/etc", "CONFIGURATION_DIRECTORY", offsetof(svc_t, config_dir) },
|
||||
};
|
||||
|
||||
static char *svcdir_path(svc_t *svc, const struct svcdir *sd, char *path, size_t len)
|
||||
{
|
||||
char *name = (char *)svc + sd->off;
|
||||
|
||||
if (!name[0])
|
||||
return NULL;
|
||||
|
||||
paste(path, len, sd->base, name);
|
||||
return path;
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate and set a per-service directory. The value is a name
|
||||
* resolved under sd->base, so an absolute path or an escape is
|
||||
* refused with -1 and errno set.
|
||||
*/
|
||||
int service_set_dir(svc_t *svc, const struct svcdir *sd, const char *name)
|
||||
{
|
||||
char *dir = (char *)svc + sd->off;
|
||||
|
||||
if (name[0] == '/' || strstr(name, "..")) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strlcpy(dir, name, MAX_ARG_LEN) >= MAX_ARG_LEN) {
|
||||
dir[0] = 0;
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void service_mkdirs(svc_t *svc)
|
||||
{
|
||||
char path[256];
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NELEMS(svcdirs); i++) {
|
||||
char *ptr;
|
||||
|
||||
ptr = svcdir_path(svc, &svcdirs[i], path, sizeof(path));
|
||||
if (!ptr)
|
||||
continue;
|
||||
|
||||
/*
|
||||
* Script forks land here too, so an existing directory
|
||||
* is left alone -- mode and ownership are asserted at
|
||||
* creation only, a daemon may have tightened them.
|
||||
*/
|
||||
if (fisdir(ptr))
|
||||
continue;
|
||||
|
||||
/* only the named directory is chowned, like systemd */
|
||||
mkpath(ptr, 0755);
|
||||
if (mksubsys(ptr, 0755, svc->username, svc->group))
|
||||
logit(LOG_WARNING, "%s: failed creating %s", svc_ident(svc, NULL, 0), ptr);
|
||||
}
|
||||
}
|
||||
|
||||
static void service_dir_env(svc_t *svc)
|
||||
{
|
||||
char path[256];
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NELEMS(svcdirs); i++) {
|
||||
if (svcdir_path(svc, &svcdirs[i], path, sizeof(path)))
|
||||
setenv(svcdirs[i].env, path, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void service_rmdirs(svc_t *svc)
|
||||
{
|
||||
char path[256];
|
||||
|
||||
/* only the runtime directory, /run is tmpfs, the rest persist */
|
||||
if (svcdir_path(svc, &svcdirs[0], path, sizeof(path)))
|
||||
rmrf(path);
|
||||
}
|
||||
|
||||
static pid_t service_fork(svc_t *svc)
|
||||
{
|
||||
const char *cgnm;
|
||||
@@ -591,6 +686,8 @@ static pid_t service_fork(svc_t *svc)
|
||||
|
||||
if (pid == 0) {
|
||||
char *home = NULL;
|
||||
|
||||
service_mkdirs(svc);
|
||||
#ifdef ENABLE_STATIC
|
||||
int uid = 0; /* XXX: Fix better warning that dropprivs is disabled. */
|
||||
int gid = 0;
|
||||
@@ -664,6 +761,8 @@ static pid_t service_fork(svc_t *svc)
|
||||
err(1, "%s: failed setgid(%d)", svc_ident(svc, NULL, 0), gid);
|
||||
}
|
||||
|
||||
service_dir_env(svc);
|
||||
|
||||
if (uid >= 0) {
|
||||
set_uid(uid, svc);
|
||||
|
||||
@@ -2272,6 +2371,10 @@ svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
|
||||
else
|
||||
memset(svc->capabilities, 0, sizeof(svc->capabilities));
|
||||
|
||||
/* block format only, set by conf.c after registration */
|
||||
for (int i = 0; i < NUM_SVCDIRS; i++)
|
||||
memset((char *)svc + svcdirs[i].off, 0, MAX_ARG_LEN);
|
||||
|
||||
if (!svc_is_tty(svc) && ctty) {
|
||||
char *dev = ctty;
|
||||
|
||||
@@ -2894,6 +2997,16 @@ static void svc_set_state(svc_t *svc, svc_state_t new_state)
|
||||
return;
|
||||
*state = new_state;
|
||||
|
||||
/*
|
||||
* The unit has stopped: HALTED comes after any post:/cleanup:
|
||||
* script, DONE is a completed run/task, where remain-after-exit
|
||||
* keeps it alive until stopped for real. Same removal rules as
|
||||
* systemd RuntimeDirectory with RuntimeDirectoryPreserve=no.
|
||||
*/
|
||||
if (new_state == SVC_HALTED_STATE ||
|
||||
(new_state == SVC_DONE_STATE && !svc_is_remain(svc)))
|
||||
service_rmdirs(svc);
|
||||
|
||||
if (svc_is_runtask(svc)) {
|
||||
char success[MAX_COND_LEN], failure[MAX_COND_LEN];
|
||||
|
||||
|
||||
@@ -27,6 +27,22 @@
|
||||
|
||||
#include "svc.h"
|
||||
|
||||
/*
|
||||
* systemd-style per-service directories, block format only: the .conf
|
||||
* key, the base the name resolves under, and the environment variable
|
||||
* the path is exported as. Shared by service.c and conf.c so adding
|
||||
* one means touching the table and svc.h alone.
|
||||
*/
|
||||
struct svcdir {
|
||||
const char *key;
|
||||
const char *base;
|
||||
const char *env;
|
||||
size_t off; /* offsetof() in svc_t */
|
||||
};
|
||||
#define NUM_SVCDIRS 5
|
||||
extern const struct svcdir svcdirs[NUM_SVCDIRS];
|
||||
|
||||
int service_set_dir (svc_t *svc, const struct svcdir *sd, const char *name);
|
||||
svc_t *service_register (int type, char *line, struct rlimit rlimit[], char *file);
|
||||
void service_unregister (svc_t *svc);
|
||||
|
||||
|
||||
@@ -199,6 +199,14 @@ typedef struct svc {
|
||||
int num_supgroups;
|
||||
char capabilities[MAX_CMD_LEN];
|
||||
|
||||
/* Directories set up for the service, block format only, the
|
||||
* name is resolved under a fixed base, e.g. /run/NAME */
|
||||
char runtime_dir[MAX_ARG_LEN];
|
||||
char state_dir[MAX_ARG_LEN];
|
||||
char cache_dir[MAX_ARG_LEN];
|
||||
char logs_dir[MAX_ARG_LEN];
|
||||
char config_dir[MAX_ARG_LEN];
|
||||
|
||||
/* Command, arguments and service description */
|
||||
char cmd[MAX_CMD_LEN];
|
||||
char args[MAX_NUM_SVC_ARGS][MAX_CMD_LEN];
|
||||
|
||||
@@ -32,6 +32,7 @@ EXTRA_DIST += add-remove-dynamic-service-sub-config.sh
|
||||
EXTRA_DIST += bootstrap-crash.sh
|
||||
EXTRA_DIST += cond-start-task.sh
|
||||
EXTRA_DIST += conf-format.sh
|
||||
EXTRA_DIST += conf-dirs.sh
|
||||
EXTRA_DIST += conf-if.sh
|
||||
EXTRA_DIST += conf-template.sh
|
||||
EXTRA_DIST += script-timeout.sh
|
||||
@@ -80,6 +81,7 @@ TESTS += add-remove-dynamic-service-sub-config.sh
|
||||
TESTS += bootstrap-crash.sh
|
||||
TESTS += cond-start-task.sh
|
||||
TESTS += conf-format.sh
|
||||
TESTS += conf-dirs.sh
|
||||
TESTS += conf-if.sh
|
||||
TESTS += conf-template.sh
|
||||
TESTS += script-timeout.sh
|
||||
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
# Verify the per-service directory settings: runtime-dir, state-dir,
|
||||
# cache-dir, logs-dir, and config-dir. The directory is created before
|
||||
# the service starts, owned by the service user, and exported to the
|
||||
# environment. The runtime directory is removed again when the service
|
||||
# stops, the others persist.
|
||||
set -eu
|
||||
|
||||
TEST_DIR=$(dirname "$0")
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
BOOTSTRAP="service owned {
|
||||
runlevel = \"S12345\"
|
||||
user = \"daemon\"
|
||||
group = \"daemon\"
|
||||
runtime-dir = \"owned\"
|
||||
state-dir = \"owned\"
|
||||
pidfile = \"/run/owned/serv.pid\"
|
||||
command = \"/sbin/serv -np -P /run/owned/serv.pid -i owned -e STATE_DIRECTORY:/var/lib/owned\"
|
||||
}
|
||||
task probe {
|
||||
runlevel = \"S12345\"
|
||||
runtime-dir = \"probe\"
|
||||
cache-dir = \"probe\"
|
||||
command = \"/sbin/serv -h -e RUNTIME_DIRECTORY:/run/probe -e CACHE_DIRECTORY:/var/cache/probe\"
|
||||
}
|
||||
service escape {
|
||||
runlevel = \"S12345\"
|
||||
runtime-dir = \"../escape\"
|
||||
command = \"serv -np -i escape\"
|
||||
}"
|
||||
|
||||
# ls output is empty for an empty directory, so test -d instead
|
||||
assert_dir()
|
||||
{
|
||||
assert "Directory $1 exists" "$(texec test -d "$1" && echo yes)" = "yes"
|
||||
}
|
||||
|
||||
assert_nodir()
|
||||
{
|
||||
assert "Directory $1 removed" "$(texec test -d "$1" || echo gone)" = "gone"
|
||||
}
|
||||
|
||||
assert_owner()
|
||||
{
|
||||
assert "$1 owned by $2" "$(texec stat -c %U:%G "$1")" = "$2"
|
||||
}
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$TEST_DIR/lib/setup.sh"
|
||||
|
||||
say 'Directory created before start, owned by the service user'
|
||||
retry 'assert_status owned running'
|
||||
assert_dir /run/owned
|
||||
assert_owner /run/owned daemon:daemon
|
||||
assert_dir /var/lib/owned
|
||||
assert_owner /var/lib/owned daemon:daemon
|
||||
|
||||
say 'The paths are exported to the process environment; both commands'
|
||||
say 'verify their own with serv -e, and refuse to run on a mismatch'
|
||||
|
||||
say 'A completed task no longer holds its runtime directory'
|
||||
retry 'assert_status probe done'
|
||||
assert_nodir /run/probe
|
||||
assert_dir /var/cache/probe
|
||||
|
||||
say 'Stopping the service removes the runtime directory, state persists'
|
||||
run "initctl stop owned"
|
||||
retry 'assert_status owned stopped'
|
||||
assert_nodir /run/owned
|
||||
assert_dir /var/lib/owned
|
||||
|
||||
say 'Starting again recreates it'
|
||||
run "initctl start owned"
|
||||
retry 'assert_status owned running'
|
||||
assert_dir /run/owned
|
||||
|
||||
say 'A path escaping the base directory is refused, service still runs'
|
||||
retry 'assert_status escape running'
|
||||
assert_nodir /escape
|
||||
+6
-1
@@ -34,7 +34,12 @@ BBBIN = busybox-$(ARCH)
|
||||
BBHOME ?= https://github.com/troglobit/busybox-builder/releases/download
|
||||
BBURL ?= $(BBHOME)/$(BBVER)/$(BBBIN)
|
||||
|
||||
_libs_src = $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+')
|
||||
# glibc dlopen()s NSS modules at runtime, so ldd does not list them, but
|
||||
# without libnss_files getpwnam() cannot resolve users inside the chroot
|
||||
_libs_nss = $(firstword $(wildcard /lib/$(ARCH)-linux-gnu/libnss_files.so.2 \
|
||||
/usr/lib/$(ARCH)-linux-gnu/libnss_files.so.2 \
|
||||
/lib64/libnss_files.so.2 /lib/libnss_files.so.2))
|
||||
_libs_src = $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') $(_libs_nss)
|
||||
libs = $(foreach path,$(_libs_src),$(abspath $(DEST))$(path))
|
||||
|
||||
all: $(libs) $(DEST)/bin/$(BBBIN)
|
||||
|
||||
Reference in New Issue
Block a user