FreeBSD/Mach PAM Port — Plan

Status: v1 — ready to execute

This plan scopes the replacement of FreeBSD’s PAM stack (FreeBSD-pam + FreeBSD-pam-lib packages plus /etc/pam.d/* policy) on the freebsd-launchd-mach image with pure-Apple vendored source. Trigger: pam_xdg.so session-failure mode during hostnamed iter 1. Deeper goal: stop carrying FreeBSD’s PAM modules + policy.

OpenDirectory is intentionally out of scope here — it’s deferred to a separate OpenDirectory port scoping doc for later consideration. The PAM port doesn’t depend on any of that work.

1. Goals (stated, in priority order)

  1. Drop FreeBSD-pam* packages from pkglist-base.txt. No FreeBSD PAM modules, no FreeBSD /etc/pam.d/* shipped on the image.
  2. Drop pam_xdg and the XDG runtime-dir convention entirely.
  3. Basic auth still works: root console login via login(1) + LoginWindow.app, sshd, su, sudo, passwd.

2. What we vendor

Pure Apple vendoring — zero new module code authored.

ComponentApple repoProvides
OpenPAM framework + bundled modules apple-oss-distributions/OpenPAM (457 KB) libpam.so framework (replaces FreeBSD-pam-lib), plus three modules from openpam/modules/:
  • pam_unix.so — 196 LOC upstream Dag-Erling Smørgrav code, getpwnam + crypt; defers store selection to NSS
  • pam_deny.so — always-deny terminator
  • pam_permit.so — always-permit terminator
Apple standalone modules apple-oss-distributions/pam_modules (316 KB) The 5 portable ones (the others need OD / SecurityServer / SmartCard subsystems we don’t have):
  • pam_self.so — allow root to su to anyone
  • pam_rootok.so — allow root without password
  • pam_uwtmp.so — utmp/wtmp accounting
  • pam_nologin.so — respect /etc/nologin
  • pam_env.so — set environment variables

Net: 1 framework + 8 modules, all from Apple upstream. Plus an overlay /etc/pam.d/{login,su,sshd,sudo,passwd,other} composing the 8 modules into working policies.

3. Auth backend: defer to NSS

Apple’s upstream pam_unix.c (196 LOC) does getpwnam(user)crypt(password, hash) → compare. It defers backend selection to NSS via the standard libc API. Whatever the system’s nsswitch.conf resolves through is what authenticates. The PAM stack has no opinion about where users live.

This means the same module handles:

Gershwin integration is out of scope for this plan. The gershwin desktop ships its own NSS bridge and auth daemon which downstream of this PAM port will transparently provide the user store. Verifying that integration happens at integration time, not as part of the PAM port itself.

4. What breaks in FreeBSD-runtime / FreeBSD-rescue?

Dropping FreeBSD-pam* removes /usr/lib/pam/pam_*.so and FreeBSD’s stock /etc/pam.d/*. Question: does this break the PAM-consuming binaries shipped in FreeBSD-runtime or FreeBSD-rescue?

4.1 Why the binaries themselves are safe

SourceBinarylibpam linkageModule loading
FreeBSD-runtime /usr/bin/login dynamic -lpam runtime dlopen from /usr/lib/pam/
/usr/bin/su dynamicruntime dlopen
/usr/bin/passwd (+ chsh, chfn, chpass hardlinks) dynamicruntime dlopen
/usr/sbin/sshd dynamic (built --with-pam) runtime dlopen
/usr/sbin/cron dynamicruntime dlopen
/usr/bin/at dynamicruntime dlopen
FreeBSD-rescue /rescue/login static libpam.a embedded runtime dlopen — static libpam still loads .so modules at run-time
/rescue/su staticruntime dlopen
/rescue/passwd staticruntime dlopen

Key fact: OpenPAM is the same upstream framework on FreeBSD and on Apple. The ABI is stable. The pam_sm_* entry points haven’t changed in years. So a binary statically-linked with FreeBSD’s libpam happily loads modules from Apple’s pam_modules, and a binary dynamically-linked against Apple OpenPAM happily loads modules with FreeBSD-shape entry points too. No binary rebuild or shim required.

4.2 Where breakage CAN occur: pam.d references to absent modules

Each /etc/pam.d/<service> file names modules by basename. When a service runs, libpam parses the file and dlopens each named module. If a referenced module file doesn’t exist, PAM returns PAM_OPEN_ERR / PAM_MODULE_UNKNOWN and the whole service stack fails.

FreeBSD-only moduleUsed in stock pam.d?What we loseMitigation
pam_login_access.so /etc/pam.d/login (auth + account) Reading /etc/login.access for per-user/per-tty allow/deny rules Drop the lines from our overlay’s pam.d/login. sshd has its own AllowUsers/DenyUsers.
pam_lastlog.so /etc/pam.d/login (session) Updating /var/log/lastlog on login; affects last(1) and finger(1) Drop the line. Apple’s pam_uwtmp still writes utmp/wtmp; lastlog file stays empty. Could be patched into pam_uwtmp later.
pam_securetty.so Sometimes /etc/pam.d/login Reading /etc/ttys “secure” flag to restrict root tty logins Drop the line. sshd uses PermitRootLogin; console root login restrictions can use login.conf classes.
pam_deny.so / pam_permit.so /etc/pam.d/other (catch-all default) Explicit always-deny / always-permit terminators Vendored by Apple OpenPAM (openpam/modules/{pam_deny,pam_permit}/). No additional work.
pam_unix.so Almost every pam.d service UNIX password verification Vendored by Apple OpenPAM (196 LOC upstream DES, getpwnam+crypt). Goes through NSS.
pam_xdg.so /etc/pam.d/login (session) Creating /var/run/xdg/$USER Intentionally dropped. Goal #2.
pam_ssh.so /etc/pam.d/sshd in some configs PAM-managed ssh-agent forwarding Drop. ssh-agent works directly without PAM glue.

4.3 Net effect

Provided iter 3 ships a complete overlay /etc/pam.d/{login,su,sshd,sudo,passwd,cron,other} set that references only the 8 modules we vendor, the following continue to work unmodified:

Functional regressions accepted (all small):

5. Iteration plan

IterScopeCI gateEst. LOC
1 Vendor Apple OpenPAM as src/openpam/. Builds libpam.so + pam_unix.so + pam_deny.so + pam_permit.so as a single source drop. Install libpam.so into /usr/lib/, the 3 modules into /usr/lib/pam/. Replace FreeBSD-pam-lib (lib only; modules still come from FreeBSD-pam at this point so we can verify ABI compatibility). existing post-login markers stay green; new PAM-FRAMEWORK-OK verifies framework dlopen of one of our new modules succeeds vendor ~10k, shim ~200
2 Vendor 5 standalone Apple modules from pam_modules (pam_self, pam_rootok, pam_uwtmp, pam_nologin, pam_env) into src/pam_modules/<name>/; install as /usr/lib/pam/<name>.so. No policy change yet. new PAM-MODULES-OK: each .so loadable via dlopen + has required entry points vendor ~3k, shim ~100
3 Compose overlay /etc/pam.d/{login,su,sshd,sudo,passwd,other} using only Apple modules (8 total — 3 from OpenPAM + 5 from pam_modules). Drop FreeBSD-pam from pkglist-base.txt. CI’s existing post-login marker chain (and a fresh PAM-LOGIN-OK marker via su root -c true) is the regression gate. existing markers green via the new stack + PAM-LOGIN-OK ~50 (configs only)
4 Restore RunAtLoad=true on hostnamed + syslogd plists (no pam_xdg means no race on /var/run/xdg). Verify boot banner shows synthesized hostname instead of Amnesiac. boot banner regex on first login ~10 (plist edits)

6. Open questions

  1. OpenPAM build system: Apple’s OpenPAM uses autoconf + xcodeproj. Cross-build cleanly via our bsd.lib.mk wrapping (libdispatch pattern), or run autoconf in the chroot? Likely the former.
  2. pam_uwtmp utmp ABI: Apple’s utmp/wtmp format may differ from FreeBSD’s. Investigate in iter 2 — if incompatible, either patch pam_uwtmp to write FreeBSD utmpx, or accept that last(1) / who(1) output stays empty (low priority).
  3. nsswitch.conf ordering: confirm whichever build overlays the right passwd: line so PAM consults the expected user store. Iter 3 verifies.
  4. sshd UsePAM: confirm OpenSSH ships with UsePAM yes. If not, overlay sshd_config.
  5. passwd(1) integration: FreeBSD’s passwd(1) may bypass PAM and edit /etc/master.passwd directly via pw_* APIs. If so, dropping FreeBSD-pam’s passwd entry has no effect on actual password changes. Test in iter 3.

7. Out of scope


v1 generated 2026-05-25. PAM port is independent of all directory-services decisions; those live in the companion OD port scoping doc.