Fix #420: run services inside a PAM session

Apply a PAM session to run/task/sysv/services Finit starts, pam_limits
above all, so a service running as a given user picks up that user's
limits the way a login does.

Add a new `pam` setting for the new block format (only), like the
per-service directories, naming a file in /etc/pam.d:

    service weston {
        user    = "weston"
        pam     = "weston-autologin"
        command = "/usr/bin/weston --continue-without-input"
    }

pam_close_session() has to be called by a process still holding the
handle, and the handle does not survive exec().  Hence the keeper: it
holds the handle, drops to the service's credentials, and waits for a
parent-death signal before closing the session.  Same shape as
systemd's (sd-pam), for the same reason, and one per fork, so the
script hooks open and close their own.

The keeper closes the descriptors it inherited from Finit and only
those.  Closing everything would also take out what pam_open_session()
opened for itself, a keyring fd or a lock file, and leave the modules
to close a session with those pulled out from under them.  Closing
nothing, as (sd-pam) does, would leave it holding the write end of the
notify pipe for the service's whole lifetime and starve notify = "s6"
services of their ready signal.  So the fds open before pam_start()
are snapshotted and exactly those are closed, while the ones PAM opens
after are marked close-on-exec so the daemon does not inherit them
either.

A refused value, a denied account stack, an uninstalled pam.d file,
and a build without PAM support all keep the service from starting
rather than running it with the stacks skipped: one that quietly loses
pam_limits and its private /tmp, with nothing said.  Capabilities a
module like pam_cap.so granted are merged into the IAB Finit applies
instead of being replaced by it, which only helps a service that also
sets capabilities, the other arm being a plain setuid() with nothing
left to restore once permitted is empty.

The test sysroot gains pam_permit.so, pam_deny.so and pam_limits.so,
which ldd cannot see, libpam dlopen()s them, and the test skips when
the host has none to stage.  The negative cases pin the exit status
rather than only asserting crashed, which serv reports for any early
exit, so a bad command or an unwritable pidfile cannot pass for a
rejected session.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-09-23 16:30:14 +02:00
1 parent bad7c5c99a
commit 61b0e0f3e6
21 files changed
+964 -6

No files matched your search

+3
View File
@@ -76,6 +76,9 @@ endif
if DBUS
finit_SOURCES += dbus.c
endif
if PAM
finit_SOURCES += pam.c pam.h
endif
pkginclude_HEADERS = cgroup.h cond.h conf.h finit.h helpers.h log.h \
plugin.h svc.h service.h
+37
View File
@@ -276,6 +276,7 @@ static cfg_opt_t svc_opts[] = {
CFG_STR_LIST("capabilities", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("caps", NULL, CFGF_NODEFAULT), /* alias */
CFG_STR ("pam", NULL, CFGF_NODEFAULT),
CFG_STR ("runtime-dir", NULL, CFGF_NODEFAULT),
CFG_STR ("state-dir", NULL, CFGF_NODEFAULT),
CFG_STR ("cache-dir", NULL, CFGF_NODEFAULT),
@@ -1485,6 +1486,41 @@ static void dirs_translate(cfg_t *sec, svc_t *svc, char *file)
}
}
/*
* Also block format only. The value names a file in /etc/pam.d, so a
* path is refused rather than followed.
*
* A bad value is stored anyway, or as much of it as fits, and
* service_start() refuses to start the service. Dropping it here
* instead would start the service with no session at all, which is the
* one outcome the service asking for a session cannot live with.
*/
static void pam_translate(cfg_t *sec, svc_t *svc, char *file)
{
const char *str;
str = sec_getstr(sec, "pam", NULL);
if (!str || !str[0])
return;
if (strlcpy(svc->pam, str, sizeof(svc->pam)) >= sizeof(svc->pam)) {
logit(LOG_ERR, "%s: %s: pam '%s' is too long, not starting",
file, cfg_title(sec), str);
return;
}
if (strchr(str, '/') || strstr(str, "..")) {
logit(LOG_ERR, "%s: %s: pam '%s' names a file in /etc/pam.d,"
" not a path, not starting", file, cfg_title(sec), str);
return;
}
if (svc->forking)
logit(LOG_WARNING, "%s: %s: pam with type = forking closes the"
" session when the initial process exits",
file, cfg_title(sec));
}
static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *file)
{
struct rlimit local_rlimit[RLIMIT_NLIMITS];
@@ -1699,6 +1735,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi
dirs_translate(sec, svc, file);
provides_translate(sec, svc, file);
pam_translate(sec, svc, file);
}
/*
+398
View File
@@ -0,0 +1,398 @@
/* Finit PAM session support
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "config.h" /* Generated by configure script */
#include <dirent.h> /* opendir(), readdir(), dirfd() */
#include <errno.h>
#include <fcntl.h> /* fcntl(), FD_CLOEXEC */
#include <grp.h> /* setgroups() */
#include <security/pam_appl.h>
#include <signal.h>
#include <stdlib.h> /* malloc(), free(), strtol() */
#include <string.h>
#include <sys/prctl.h>
#include <unistd.h>
#include "helpers.h"
#include "log.h"
#include "pam.h"
#include "svc.h"
/*
* A service has no terminal to prompt on and this path never
* authenticates, so a module that asks a question gets an error and its
* stack entry fails. For a non-interactive service that is the answer
* we want.
*/
static int pam_dialog(int num_msg, const struct pam_message **msg,
struct pam_response **resp, void *appdata)
{
return PAM_CONV_ERR;
}
static const struct pam_conv conv = {
.conv = pam_dialog,
.appdata_ptr = NULL,
};
/*
* The keeper never exec()s, so O_CLOEXEC does nothing for it, and it
* must not sit on Finit's event loop, api socket, inotify and
* notify-pipe descriptors for as long as the service lives. But PAM
* and its modules may have opened descriptors of their own while
* pam_open_session() ran, e.g. a pam_systemd/pam_elogind bus
* connection, a keyring fd, or a lock file, and those have to survive
* until pam_close_session() runs in the keeper. A blanket close of
* everything >= 3 cannot tell the two kinds apart, so take a snapshot
* of Finit's own fds before pam_start() opens anything of its own, and
* have the keeper close only those.
*
* Returns the number of fds found and sets *fdv to a malloc'd array of
* them, or -1 if /proc/self/fd could not be read, in which case the
* caller falls back to closing everything and PAM's own descriptors
* are lost along with Finit's.
*/
static int fdsnap(int **fdv)
{
struct dirent *d;
int *v;
int n, i;
DIR *dir;
int dfd;
dir = opendir("/proc/self/fd");
if (!dir)
return -1;
dfd = dirfd(dir);
n = 0;
while ((d = readdir(dir))) {
char *end;
int fd;
fd = (int)strtol(d->d_name, &end, 10);
if (*end || fd < 3 || fd == dfd)
continue;
n++;
}
if (!n) {
closedir(dir);
*fdv = NULL;
return 0;
}
v = malloc(n * sizeof(*v));
if (!v) {
closedir(dir);
return -1;
}
rewinddir(dir);
i = 0;
while (i < n && (d = readdir(dir))) {
char *end;
int fd;
fd = (int)strtol(d->d_name, &end, 10);
if (*end || fd < 3 || fd == dfd)
continue;
v[i++] = fd;
}
closedir(dir);
*fdv = v;
return i;
}
/*
* Close exactly the fds fdsnap() found, leaving PAM's own alone. With
* no snapshot (nfd < 0), fall back to closing everything >= 3, PAM's
* descriptors lost along with Finit's.
*/
static void closefds(int *fdv, int nfd)
{
long max;
int i;
if (nfd >= 0) {
for (i = 0; i < nfd; i++)
close(fdv[i]);
return;
}
max = sysconf(_SC_OPEN_MAX);
#ifdef HAVE_CLOSE_RANGE
if (!close_range(3, ~0U, 0))
return;
#endif
if (max < 0)
max = 1024;
for (i = 3; i < max; i++)
close(i);
}
/*
* The descriptors PAM opened have to survive in the keeper, but not in
* the process that goes on to exec() the service: a bus connection, a
* keyring fd or a lock file the stack opened would otherwise be
* inherited by the daemon, and by the logit helper forked next to it,
* for as long as the service lives. Finit's own fds are O_CLOEXEC,
* PAM's are not reliably, so diff a fresh snapshot against the one
* taken before pam_start() and mark everything new close-on-exec. The
* keeper is unaffected, it was forked already and has its own
* descriptor table.
*
* With no snapshot to diff against (nfd < 0) there is nothing to do
* here, telling Finit's fds from PAM's is exactly what the snapshot is
* for.
*/
static void cloexec_pamfds(const char *id, int *fdv, int nfd)
{
int *now = NULL;
int i, j, n;
if (nfd < 0)
return;
n = fdsnap(&now);
if (n < 0)
return;
for (i = 0; i < n; i++) {
int flags, old = 0;
for (j = 0; j < nfd; j++) {
if (now[i] == fdv[j]) {
old = 1;
break;
}
}
if (old)
continue;
flags = fcntl(now[i], F_GETFD);
if (flags == -1 || fcntl(now[i], F_SETFD, flags | FD_CLOEXEC))
logit(LOG_WARNING, "%s: pam: failed FD_CLOEXEC on fd %d,"
" the service inherits it", id, now[i]);
}
free(now);
}
/*
* pam_getenvlist() hands back a NUL-terminated array of malloc'd
* "NAME=value" strings. Free all of it so a failure past that point
* never leaves pamsess_open() returning a dangling *envp.
*/
static void free_envlist(char **envp)
{
char **e;
if (!envp)
return;
for (e = envp; *e; e++)
free(*e);
free(envp);
}
/*
* Holds the PAM handle for the lifetime of the service and closes the
* session when it dies. The handle was created before the fork, so
* pam_end() gets PAM_DATA_SILENT: this process is a copy, the modules
* must not run their full cleanup twice.
*/
static void keeper(pam_handle_t *h, uid_t uid, gid_t gid, pid_t ppid,
sigset_t *mask, const char *id, int *fdv, int nfd)
{
int rc, sig;
/* Finit's syslog socket is one of the fds about to be closed;
* close it here first so syslog(3) knows to reconnect instead
* of writing to a fd pulled out from under it. */
closelog();
closefds(fdv, nfd);
setprocnm("(finit-pam)");
/* No groups are needed to close a session */
if (setgroups(0, NULL))
warn("%s: pam: failed setgroups()", id);
if (setgid(gid))
warn("%s: pam: failed setgid(%d)", id, gid);
if (setuid(uid))
warn("%s: pam: failed setuid(%d)", id, uid);
/* After the credential change, which clears a pending pdeath signal */
if (prctl(PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0))
warn("%s: pam: failed PR_SET_PDEATHSIG", id);
/* The service may have exited already, then no signal is coming.
* Anything but success means SIGTERM has not arrived yet, so keep
* waiting: closing the session under a service that is still
* running would leave nobody to close it later. glibc reports
* the error number as the return value and musl returns -1 with
* errno set, hence the test on rc alone. The mask is static, so
* a failure that never clears is not reachable here. */
if (getppid() == ppid) {
do {
rc = sigwait(mask, &sig);
} while (rc);
}
rc = pam_setcred(h, PAM_DELETE_CRED | PAM_SILENT);
if (rc != PAM_SUCCESS)
logit(LOG_WARNING, "%s: pam: failed pam_setcred(DELETE_CRED): %s",
id, pam_strerror(h, rc));
rc = pam_close_session(h, PAM_SILENT);
if (rc != PAM_SUCCESS)
logit(LOG_WARNING, "%s: pam: failed pam_close_session(): %s",
id, pam_strerror(h, rc));
pam_end(h, rc | PAM_DATA_SILENT);
_exit(0);
}
/*
* Open a PAM session for svc, as root, before privileges are dropped.
* On success a keeper process has been forked and *envp holds the
* session environment for the caller to apply. The process that goes
* on to exec() must not call pam_end(), its handle disappears with the
* exec; only the keeper ever closes the session. *envp is only ever
* non-NULL on success.
*/
int pamsess_open(svc_t *svc, uid_t uid, gid_t gid, char ***envp)
{
char *id = svc_ident(svc, NULL, 0);
pam_handle_t *h = NULL;
const char *user;
sigset_t mask, omask;
pid_t pid, ppid;
int *fdv = NULL;
int rc, nfd;
const char *why;
*envp = NULL;
/* Every fork funnels through here, service_start() only guards
* the daemon itself, so this is where a value that must not
* reach pam_start() is stopped. */
why = pam_invalid(svc->pam);
if (why) {
logit(LOG_ERR, "%s: pam '%s' %s", id, svc->pam, why);
return -1;
}
/* Snapshot Finit's own fds before pam_start() opens anything */
nfd = fdsnap(&fdv);
user = svc->username[0] ? svc->username : "root";
rc = pam_start(svc->pam, user, &conv, &h);
if (rc != PAM_SUCCESS) {
logit(LOG_ERR, "%s: failed pam_start(%s, %s): %s", id,
svc->pam, user, pam_strerror(h, rc));
free(fdv);
return -1;
}
if (svc_has_ctty(svc)) {
rc = pam_set_item(h, PAM_TTY, svc->log.ctty);
if (rc != PAM_SUCCESS)
logit(LOG_WARNING, "%s: failed pam_set_item(PAM_TTY, %s): %s",
id, svc->log.ctty, pam_strerror(h, rc));
}
rc = pam_acct_mgmt(h, PAM_SILENT);
if (rc != PAM_SUCCESS) {
logit(LOG_ERR, "%s: account denied by %s: %s", id, svc->pam,
pam_strerror(h, rc));
goto fail;
}
rc = pam_setcred(h, PAM_ESTABLISH_CRED | PAM_SILENT);
if (rc != PAM_SUCCESS) {
logit(LOG_ERR, "%s: failed pam_setcred(%s): %s", id, svc->pam,
pam_strerror(h, rc));
goto fail;
}
rc = pam_open_session(h, PAM_SILENT);
if (rc != PAM_SUCCESS) {
logit(LOG_ERR, "%s: failed pam_open_session(%s): %s", id,
svc->pam, pam_strerror(h, rc));
goto cred;
}
*envp = pam_getenvlist(h);
/* Block SIGTERM before forking, the keeper must not miss it */
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, &omask);
/* Forked here, before the service child calls setsid(), so the
* keeper stays in Finit's process group. Teardown does
* kill(-svc->pid, SIGKILL) on the service's group, and a keeper
* that had ended up there would be killed with the service,
* before it ever gets to close the session. */
ppid = getpid();
pid = fork();
if (pid < 0) {
err(1, "%s: failed forking off PAM session keeper", id);
sigprocmask(SIG_SETMASK, &omask, NULL);
pam_close_session(h, PAM_SILENT);
free_envlist(*envp);
*envp = NULL;
goto cred;
}
if (!pid)
keeper(h, uid, gid, ppid, &mask, id, fdv, nfd); /* does not return */
sigprocmask(SIG_SETMASK, &omask, NULL);
cloexec_pamfds(id, fdv, nfd);
free(fdv);
dbg("%s: PAM session %s open, keeper is PID %d", id, svc->pam, pid);
return 0;
cred:
pam_setcred(h, PAM_DELETE_CRED | PAM_SILENT);
fail:
free(fdv);
pam_end(h, rc);
return -1;
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+60
View File
@@ -0,0 +1,60 @@
/* Finit PAM session support
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef FINIT_PAM_H_
#define FINIT_PAM_H_
#include <string.h> /* strchr(), strlen(), strstr() */
#include <sys/types.h>
#include "svc.h"
/*
* A pam value names a file in /etc/pam.d, it is not a path, and conf.c
* stores an over-long value truncated, so a value filling the buffer
* must not be passed on as some other, shorter name that happens to
* exist. Both users share this one definition: service_start(), which
* refuses the service outright, log line and a visible state, and
* pamsess_open(), which every fork funnels through, including the pre:,
* post:, ready:, cleanup:, stop and reload scripts that run before
* service_start() ever does.
*
* Returns why the value cannot be used, or NULL when it can.
*/
static inline const char *pam_invalid(const char *pam)
{
if (!pam[0])
return NULL;
if (strlen(pam) >= MAX_ARG_LEN - 1)
return "is too long";
if (strchr(pam, '/') || strstr(pam, ".."))
return "names a file in /etc/pam.d, not a path";
return NULL;
}
int pamsess_open(svc_t *svc, uid_t uid, gid_t gid, char ***envp);
#endif /* FINIT_PAM_H_ */
+99 -2
View File
@@ -54,6 +54,7 @@
#include "devmon.h"
#include "finit.h"
#include "helpers.h"
#include "pam.h"
#include "pid.h"
#include "private.h"
#include "sig.h"
@@ -544,10 +545,22 @@ static void set_uid(uid_t uid, svc_t *svc)
*/
if (svc->capabilities[0]) {
cap_iab_t cap_iab;
#ifdef HAVE_LIBPAM
cap_iab_t pam_iab = NULL;
cap_value_t c;
/*
* A module like pam_cap.so may have granted ambient
* capabilities. Capture them while we are still root,
* cap_setuid() below clears them.
*/
if (svc->pam[0])
pam_iab = cap_iab_get_proc();
#endif
if (cap_setuid(uid)) {
err(1, "%s: failed cap_setuid(%d)", svc_ident(svc, NULL, 0), uid);
return;
goto out;
}
/* After dropping privileges, set the specific capabilities we need */
@@ -555,15 +568,30 @@ static void set_uid(uid_t uid, svc_t *svc)
if (!cap_iab) {
err(1, "%s: failed parsing capabilities '%s'",
svc_ident(svc, NULL, 0), svc->capabilities);
return;
goto out;
}
#ifdef HAVE_LIBPAM
/* Merge, do not overwrite, what PAM granted */
if (pam_iab) {
for (c = 0; c <= CAP_LAST_CAP; c++) {
if (cap_iab_get_vector(pam_iab, CAP_IAB_AMB, c))
cap_iab_set_vector(cap_iab, CAP_IAB_AMB, c, CAP_SET);
}
}
#endif
if (cap_iab_set_proc(cap_iab) != 0) {
cap_free(cap_iab);
err(1, "%s: failed setting capabilities",
svc_ident(svc, NULL, 0));
}
cap_free(cap_iab);
out:
#ifdef HAVE_LIBPAM
cap_free(pam_iab);
#endif
return;
} else
#endif
if (setuid(uid))
@@ -703,6 +731,9 @@ static pid_t service_fork(svc_t *svc)
if (pid == 0) {
char *home = NULL;
#ifdef HAVE_LIBPAM
char **pam_env = NULL;
#endif
#ifdef ENABLE_STATIC
int uid = 0; /* XXX: Fix better warning that dropprivs is disabled. */
int gid = 0;
@@ -735,6 +766,15 @@ static pid_t service_fork(svc_t *svc)
svc_ident(svc, NULL, 0), rlim2str(i));
}
#ifdef HAVE_LIBPAM
/*
* After our own rlimits so pam_limits wins, and before
* any credential change: the session stack needs root.
*/
if (svc->pam[0] && pamsess_open(svc, uid, gid, &pam_env))
_exit(EX_OSERR);
#endif
#ifndef ENABLE_STATIC
/* Set supplementary groups from /etc/group and config */
{
@@ -797,6 +837,28 @@ static pid_t service_fork(svc_t *svc)
}
}
#ifdef HAVE_LIBPAM
/* pam_env after our defaults, before env:file */
if (pam_env) {
const char *pam_home;
for (int i = 0; pam_env[i]; i++)
putenv(pam_env[i]);
/* pam_env has the last word on HOME, so the
* working directory has to follow it, the chdir()
* above used the passwd home. */
pam_home = getenv("HOME");
if (pam_home && (!home || strcmp(pam_home, home))) {
if (chdir(pam_home)) {
if (chdir("/"))
err(1, "%s: failed chdir(%s) and chdir(/)",
svc_ident(svc, NULL, 0), pam_home);
}
}
}
#endif
/* Source any environment from env:/path/to/file */
source_env(svc);
}
@@ -811,6 +873,26 @@ static pid_t service_fork(svc_t *svc)
return pid;
}
/*
* Checked here, next to the other preconditions, and not only in
* conf.c, because a .conf reload rewrites the service and would
* otherwise let a refused value start it after all. Running without
* the session the service asked for costs it pam_limits, its private
* /tmp, and its logind session, with nothing said.
*/
static int pam_refused(svc_t *svc)
{
const char *why = pam_invalid(svc->pam);
if (!why)
return 0;
logit(LOG_ERR, "%s: pam '%s' %s, not starting",
svc_ident(svc, NULL, 0), svc->pam, why);
return 1;
}
/**
* service_start - Start service
* @svc: Service to start
@@ -854,6 +936,20 @@ static int service_start(svc_t *svc)
return 1;
}
if (pam_refused(svc)) {
svc_missing(svc);
return 1;
}
#ifndef HAVE_LIBPAM
if (svc->pam[0]) {
logit(LOG_ERR, "%s: pam %s requires Finit built with --enable-pam",
svc_ident(svc, NULL, 0), svc->pam);
svc_missing(svc);
return 1;
}
#endif
if (svc_is_tty(svc) && !svc->notty) {
char *dev = tty_canonicalize(svc->dev);
@@ -2505,6 +2601,7 @@ svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
memset(svc->capabilities, 0, sizeof(svc->capabilities));
/* block format only, set by conf.c after registration */
memset(svc->pam, 0, sizeof(svc->pam));
for (int i = 0; i < NUM_SVCDIRS; i++) {
memset((char *)svc + svcdirs[i].off, 0, MAX_ARG_LEN);
svc->dir_mode[i] = 0755;
+1
View File
@@ -203,6 +203,7 @@ typedef struct svc {
char supgroups[MAX_NUM_SUPGROUPS][MAX_USER_LEN];
int num_supgroups;
char capabilities[MAX_CMD_LEN];
char pam[MAX_ARG_LEN]; /* /etc/pam.d/NAME, block format only */
/* Directories set up for the service, block format only, the
* name is resolved under a fixed base, e.g. /run/NAME */