The branch main has been updated by delphij:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=12444a4da514e91fdf984b31e1691d042d5f88d2

commit 12444a4da514e91fdf984b31e1691d042d5f88d2
Author:     Xin LI <[email protected]>
AuthorDate: 2025-12-29 09:32:24 +0000
Commit:     Xin LI <[email protected]>
CommitDate: 2026-01-21 02:47:16 +0000

    cron: Implement full PAM session lifecycle for user jobs
    
    Extend PAM integration beyond account checks to include credential
    establishment and session management, allowing PAM modules to configure
    the execution environment for user cron jobs.
    
    Previously, cron only called pam_acct_mgmt() to verify account validity
    but immediately terminated the PAM handle before job execution. This
    prevented PAM modules from establishing sessions, setting credentials
    (e.g., Kerberos tickets), or exporting environment variables needed by
    jobs.
    
    The PAM handle now persists in the intermediate process throughout the
    job execution, enabling proper session open/close pairing. Credentials
    are established and sessions opened while still running as root, before
    dropping privileges in the grandchild. PAM environment variables are
    exported in the job process with user crontab variables taking precedence.
    
    A session rule (pam_permit.so) is added to /etc/pam.d/cron to enable
    session support without changing default behavior. Administrators can
    replace this with other modules as needed.
    
    System crontab entries continue to bypass all PAM operations.
    
    PR:             bin/244844
    Reviewed by:    des
    MFC after:      2 weeks
    Differential Revision: https://reviews.freebsd.org/D54415
---
 lib/libpam/pam.d/cron           |   3 +
 usr.sbin/cron/cron/do_command.c | 144 ++++++++++++++++++++++++++++++++++++++--
 2 files changed, 142 insertions(+), 5 deletions(-)

diff --git a/lib/libpam/pam.d/cron b/lib/libpam/pam.d/cron
index 733631306641..490663508a72 100644
--- a/lib/libpam/pam.d/cron
+++ b/lib/libpam/pam.d/cron
@@ -6,3 +6,6 @@
 # account
 account                required        pam_nologin.so
 account                required        pam_unix.so
+
+# session
+session                required        pam_permit.so
diff --git a/usr.sbin/cron/cron/do_command.c b/usr.sbin/cron/cron/do_command.c
index 58eed70c1be6..f757b7175126 100644
--- a/usr.sbin/cron/cron/do_command.c
+++ b/usr.sbin/cron/cron/do_command.c
@@ -76,6 +76,41 @@ do_command(entry *e, user *u)
        Debug(DPROC, ("[%d] main process returning to work\n", getpid()))
 }
 
+#ifdef PAM
+static void
+pam_cleanup(pam_handle_t **pamhp, int *session_opened, int *cred_established,
+    char ***pam_envp, const char *usernm, pid_t pid, int log_errors,
+    int end_status)
+{
+       int pam_err;
+
+       if (*pamhp == NULL)
+               return;
+       if (*session_opened) {
+               pam_err = pam_close_session(*pamhp, PAM_SILENT);
+               if (log_errors && pam_err != PAM_SUCCESS) {
+                       log_it(usernm, pid, "SESSION-CLOSE",
+                           pam_strerror(*pamhp, pam_err));
+               }
+               *session_opened = 0;
+       }
+       if (*cred_established) {
+               pam_err = pam_setcred(*pamhp, PAM_DELETE_CRED);
+               if (log_errors && pam_err != PAM_SUCCESS) {
+                       log_it(usernm, pid, "CRED-DELETE",
+                           pam_strerror(*pamhp, pam_err));
+               }
+               *cred_established = 0;
+       }
+       if (*pam_envp != NULL) {
+               openpam_free_envlist(*pam_envp);
+               *pam_envp = NULL;
+       }
+       pam_end(*pamhp, end_status);
+       *pamhp = NULL;
+}
+#endif
+
 
 static void
 child_process(entry *e, user *u)
@@ -88,6 +123,14 @@ child_process(entry *e, user *u)
        int bytes = 1;
        int status = 0;
        const char *homedir = NULL;
+#ifdef PAM
+       pam_handle_t *pamh = NULL;
+       int pam_err = PAM_SUCCESS;
+       int pam_session_opened = 0;
+       int pam_cred_established = 0;
+       /* Keep PAM env list in the middle process for the grandchild to use. */
+       char **pam_envp = NULL;
+#endif
 # if defined(LOGIN_CAP)
        struct passwd *pwd;
        login_cap_t *lc;
@@ -115,8 +158,6 @@ child_process(entry *e, user *u)
         * as any user.
         */
        if (strcmp(u->name, SYS_NAME)) {        /* not equal */
-               pam_handle_t *pamh = NULL;
-               int pam_err;
                struct pam_conv pamc = {
                        .conv = openpam_nullconv,
                        .appdata_ptr = NULL
@@ -139,14 +180,50 @@ child_process(entry *e, user *u)
                        exit(ERROR_EXIT);
                }
 
+               pam_err = pam_set_item(pamh, PAM_TTY, "cron");
+               if (pam_err != PAM_SUCCESS) {
+                       log_it("CRON", getpid(), "error", "can't set PAM_TTY");
+                       pam_cleanup(&pamh, &pam_session_opened,
+                           &pam_cred_established, &pam_envp, usernm,
+                           getpid(), 0, pam_err);
+                       exit(ERROR_EXIT);
+               }
+
                pam_err = pam_acct_mgmt(pamh, PAM_SILENT);
                /* Expired password shouldn't prevent the job from running. */
                if (pam_err != PAM_SUCCESS && pam_err != PAM_NEW_AUTHTOK_REQD) {
                        log_it(usernm, getpid(), "USER", "account unavailable");
+                       pam_cleanup(&pamh, &pam_session_opened,
+                           &pam_cred_established, &pam_envp, usernm,
+                           getpid(), 0, pam_err);
+                       exit(ERROR_EXIT);
+               }
+
+               pam_err = pam_setcred(pamh, PAM_ESTABLISH_CRED);
+               if (pam_err != PAM_SUCCESS) {
+                       log_it(usernm, getpid(), "CRED",
+                           pam_strerror(pamh, pam_err));
+                       pam_cleanup(&pamh, &pam_session_opened,
+                           &pam_cred_established, &pam_envp, usernm,
+                           getpid(), 0, pam_err);
+                       exit(ERROR_EXIT);
+               }
+               pam_cred_established = 1;
+
+               /* Establish the session while still root in the middle 
process. */
+               pam_err = pam_open_session(pamh, PAM_SILENT);
+               if (pam_err != PAM_SUCCESS) {
+                       log_it(usernm, getpid(), "SESSION",
+                           pam_strerror(pamh, pam_err));
+                       pam_cleanup(&pamh, &pam_session_opened,
+                           &pam_cred_established, &pam_envp, usernm,
+                           getpid(), 0, pam_err);
                        exit(ERROR_EXIT);
                }
+               pam_session_opened = 1;
 
-               pam_end(pamh, pam_err);
+               /* Collect PAM env now; apply only in grandchild before exec. */
+               pam_envp = pam_getenvlist(pamh);
        }
 #endif
 
@@ -161,6 +238,13 @@ child_process(entry *e, user *u)
         */
        if (pipe(stdin_pipe) != 0 || pipe(stdout_pipe) != 0) {
                log_it("CRON", getpid(), "error", "can't pipe");
+#ifdef PAM
+               if (pamh != NULL && strcmp(u->name, SYS_NAME)) {
+                       pam_cleanup(&pamh, &pam_session_opened,
+                           &pam_cred_established, &pam_envp, usernm,
+                           getpid(), 1, pam_err);
+               }
+#endif
                exit(ERROR_EXIT);
        }
 
@@ -207,12 +291,23 @@ child_process(entry *e, user *u)
        switch (jobpid = fork()) {
        case -1:
                log_it("CRON", getpid(), "error", "can't fork");
+#ifdef PAM
+               if (pamh != NULL && strcmp(u->name, SYS_NAME)) {
+                       pam_cleanup(&pamh, &pam_session_opened,
+                           &pam_cred_established, &pam_envp, usernm,
+                           getpid(), 1, pam_err);
+               }
+#endif
                exit(ERROR_EXIT);
                /*NOTREACHED*/
        case 0:
                Debug(DPROC, ("[%d] grandchild process fork()'ed\n",
                              getpid()))
 
+#ifdef PAM
+               /* Grandchild runs the user job; PAM handle remains in parent. 
*/
+               pamh = NULL;
+#endif
                if (e->uid == ROOT_UID)
                        Jitter = RootJitter;
                if (Jitter != 0) {
@@ -329,8 +424,8 @@ child_process(entry *e, user *u)
                 * the homedir given by the pw entry otherwise.
                 *
                 * If !LOGIN_CAP, then HOME is always set in e->envp.
-                *
-                * XXX: probably should also consult PAM.
+                * PAM environment is applied later for the job; we do not
+                * use it for cwd to avoid changing historical behavior.
                 */
                {
                        char    *new_home = env_get("HOME", e->envp);
@@ -351,6 +446,29 @@ child_process(entry *e, user *u)
                        char    *shell = env_get("SHELL", e->envp);
                        char    **p;
 
+#ifdef PAM
+                       if (pam_envp != NULL) {
+                               char **pp;
+
+                               /* Apply PAM-provided env only to the job 
process. */
+                               for (pp = pam_envp; *pp != NULL; pp++) {
+                                       /*
+                                        * Hand off each PAM string directly to 
the
+                                        * environment; this process must not 
free
+                                        * pam_envp after putenv() since the 
strings
+                                        * must persist until exec. The parent 
will
+                                        * free its copy after fork.
+                                        */
+                                       if (putenv(*pp) != 0) {
+                                               warn("putenv");
+                                               _exit(ERROR_EXIT);
+                                       }
+                               }
+                               /* Free the pointer array; strings stay for 
exec. */
+                               free(pam_envp);
+                               pam_envp = NULL;
+                       }
+#endif
                        /* Apply the environment from the entry, overriding
                         * existing values (this will always set LOGNAME and
                         * SHELL). putenv should not fail unless malloc does.
@@ -400,6 +518,14 @@ child_process(entry *e, user *u)
                break;
        }
 
+#ifdef PAM
+       if (jobpid > 0 && pam_envp != NULL) {
+               /* Parent doesn't need PAM env list after the fork. */
+               openpam_free_envlist(pam_envp);
+               pam_envp = NULL;
+       }
+#endif
+
        /* middle process, child of original cron, parent of process running
         * the user's command.
         */
@@ -640,6 +766,14 @@ child_process(entry *e, user *u)
 
        if (*input_data && stdinjob > 0)
                wait_on_child(stdinjob, "grandchild stdinjob");
+
+#ifdef PAM
+       if (pamh != NULL && strcmp(u->name, SYS_NAME)) {
+               /* Close the PAM session after the job finishes. */
+               pam_cleanup(&pamh, &pam_session_opened, &pam_cred_established,
+                   &pam_envp, usernm, getpid(), 1, PAM_SUCCESS);
+       }
+#endif
 }
 
 static WAIT_T

Reply via email to