From nobody Wed Feb 4 03:56:42 2026 X-Original-To: dev-commits-src-branches@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4f5RP70s4Dz6FZyw for ; Wed, 04 Feb 2026 03:56:43 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R13" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4f5RP63KXRz3Zrb for ; Wed, 04 Feb 2026 03:56:42 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1770177402; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=qxXn1cw8mzvwgCyWmCQLO2UKDMMO5LvGUR/3wTCO0d4=; b=su2yYatfE4UB42c+jn98OZODqZVAQrgs3Ntlw/yA/nQsoXoBagfWi9VZHxAf3Uxesw11xr LYlixNRBkS/ulHPXnzVejlErn1vEDnSNhGtCwoCy1H2vTpRjtsK62npWsFO0W114lN8ZbP /5Jl2iq7dFt/19niMZUJJy+6e2abxKA55aQxIACD9rgrneVXKnamSE+yDtwaiTw9X1EFEx p3Qws143xz+miL44OWcaEaL5iZOLK7YyrmDPPOROctJMdihdhGpjTupUVeRhq16YbWh+1d BEr8gq/1XAYy1nJ07Bbb0Mz1AwiKTOk2ZWttPRtmmoIPUdNjTWxMWVdl9maLEA== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1770177402; a=rsa-sha256; cv=none; b=jrNMyj4KF4XRdqBYTD55rvvreoBNTQQwGnX9/RaIHtRkK/sEdKSDlYba396fwp9zpzWJpr J6OQelqDGtXcaUh06qBFI+DA/QGdNZBECyFGuW2fNa4RaYsd+/tSPv2UqV8ONBYCxpjAvC i7J1yOaOZkiPdZkdLOaULPvgVbK7AH9/vocFdNjuQH951PnZX5IZq2xBuzjZJ7yYfIchSP bEkMnRB/8K6NEPJcZUY5hfiOsMe/c6llWP19XtS3XvNqwROmf4pSz7eNyfWxZnqHhEmTsk RwtzZF8/mVdlXHQNYMmkJ+RCK5F88wJXd38ioYdRP5aqYyMrVFzHcjxOgn0T9A== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1770177402; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=qxXn1cw8mzvwgCyWmCQLO2UKDMMO5LvGUR/3wTCO0d4=; b=pc6Zem/hipV9N49UGKKowD7dPx+jfIwKvCvUd7aBYxYp+D8E6gig0hylMe7/+QFhHCwLda f/BnamcKOkepVrmlzddGN1ORvKNoLmx347NifE1PWyKmjvGT6wQn4TwoSeARicpnWsPcRG qZ+2d3Pb6lXLQEwJAl7BnlP6/jd4PpY5SsCqk37kcLDcMfDhk/1Fo1T9t7w8q3WRyMoihi 7pjgizNnEYWQsV9NvCfMl8nrhhZOoOwJrMs+gy8vsTYV0Punc+kwasM30rkiZuOMWT9YZh eURIXJlwyX/c8uuPGXvFUD9KoQa4Z9MAr2X3nGu62KRy9i1vfDELzzpXEMpXGA== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4f5RP62k9MzjlC for ; Wed, 04 Feb 2026 03:56:42 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 3e67c by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Wed, 04 Feb 2026 03:56:42 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org From: Xin LI Subject: git: b5e328b19456 - stable/14 - cron: Implement full PAM session lifecycle for user jobs List-Id: Commits to the stable branches of the FreeBSD src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-branches List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-branches@freebsd.org Sender: owner-dev-commits-src-branches@FreeBSD.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: delphij X-Git-Repository: src X-Git-Refname: refs/heads/stable/14 X-Git-Reftype: branch X-Git-Commit: b5e328b194561a50a6991e0a506a2532eec32ecc Auto-Submitted: auto-generated Date: Wed, 04 Feb 2026 03:56:42 +0000 Message-Id: <6982c37a.3e67c.a3002a@gitrepo.freebsd.org> The branch stable/14 has been updated by delphij: URL: https://cgit.FreeBSD.org/src/commit/?id=b5e328b194561a50a6991e0a506a2532eec32ecc commit b5e328b194561a50a6991e0a506a2532eec32ecc Author: Xin LI AuthorDate: 2025-12-29 09:32:24 +0000 Commit: Xin LI CommitDate: 2026-02-04 03:56:22 +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 Differential Revision: https://reviews.freebsd.org/D54415 (cherry picked from commit 12444a4da514e91fdf984b31e1691d042d5f88d2) --- 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 43b3269d3087..fde76b8c60d1 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; @@ -113,8 +156,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 @@ -137,14 +178,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 @@ -159,6 +236,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); } @@ -205,12 +289,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) { @@ -327,8 +422,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); @@ -349,6 +444,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. @@ -398,6 +516,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. */ @@ -636,6 +762,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