/* Finit PAM session support * * Copyright (c) 2026 Joachim Wiberg * * 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 /* opendir(), readdir(), dirfd() */ #include #include /* fcntl(), FD_CLOEXEC */ #include /* setgroups() */ #include #include #include /* malloc(), free(), strtol() */ #include #include #include #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: */