66-userd¶
66-userd is a session and user tracker. It records which users are logged in and from where, owns each user's runtime directory, and drives that user's 66 services on the first login and the last logout—without D-Bus.
Interface¶
66-userd [ -h ] [ -v verbosity ] [ -d number ]
66-userd is the only long-lived process of the trio it belongs to. It runs as root, listens on a Unix socket, and answers the session events reported by pam_userd. It is complementary to seatd: seatd owns seats—the hardware context, devices and VTs—where 66-userd owns sessions—who and where—and the services that follow them.
There is no runtime option to relocate the socket or the state directory. Both are fixed at compile time—see Configuration.
66-userd is inert on its own: nothing is ever tracked until pam_userd.so is referenced from the system PAM login stack. That step is mandatory and is described in pam_userd.
Exit codes¶
- 0 : success.
- 100 : wrong usage (invalid option, bad argument).
- 111 : a system call failed.
Options¶
-
-h, --help : print the help and exit.
-
-v, --verbosity verbosity : set the verbosity level of the daemon, from 0 to 5.
- 0 : only print error messages.
- 1 : also print informative messages. This is the default.
- 2 : also print warning messages.
- 3 : also print tracing messages.
- 4 : also print the function name and line of the messages.
- 5 : also display the sequence of the current process function by function.
-
-d, --notify number : notify readiness on the file descriptor number. The daemon writes a single newline to that descriptor once it is listening on its socket. This matters because the PAM module connects to the socket at every login: a service declared up before the socket exists would produce a window of untracked logins at boot.
Architecture¶
Three artifacts share the state kept under /run/66/userd/:
login / sshd / display manager
│
│ PAM session stack
▼
┌───────────────┐ REGISTER / RELEASE ┌──────────────────┐
│ pam_userd.so │ ──────────────────────► │ 66-userd │ (root)
│ (login proc) │ ◄────────────────────── │ event loop │
└───────────────┘ session id, context └────────┬─────────┘
│ fork
▼
┌────────────────┐
│ guardian (uid) │ one per user
└───────┬────────┘
│
66-scandir + 66 tree start
pam_userd.soruns inside the login process. It captures the session context PAM knows about and hands it to the daemon. It never blocks a login—see pam_userd.66-userdis the only long-lived process. It owns the truth: the session and user tables, the runtime directory of each user, and the decision of when services start and stop.66-userctlreads the persisted state and, for power actions, talks to the daemon. It holds no state of its own—see 66-userctl.
Session lifecycle¶
-
A user logs in. PAM runs the session stack; the
open_sessionhook ofpam_userd.sosends a REGISTER carrying uid, leader pid, tty, display, seat, vtnr, class, type and remote information. -
The daemon normalizes that context—it, not the greeter or sshd, owns the canonical
SEAT,VTNR,TYPEandCLASS—assigns a session id, and increments the session count of that user. -
If this is the first session for that uid, the daemon mounts the runtime directory of the user and forks a guardian: a per-user process that parents the user's
66-scandir, waits for it to signal readiness, then runs66 tree startfor the enabled trees of that user. -
The daemon replies with the session id. The module re-exports the normalized context into the session environment as
XDG_*variables. -
Further logins by the same user are tracked, but start nothing—the services are already up.
-
On logout, the
close_sessionhook sends a RELEASE. The count drops. -
When the last session goes away, the daemon signals the guardian, which runs
66 tree stopthen66 scandir quit—in that order, since the trees need the scandir alive to be stopped—and exits. The runtime directory is unmounted.
Services therefore start on the 0 → 1 transition of the session count of a user and stop on the 1 → 0 transition, never in between.
The daemon itself never changes its uid and never calls into 66 in-process. Every operation that must run as the user runs in a throwaway child that drops privilege first.
Garbage collection and reconciliation¶
A session leader that dies without a clean close_session—a killed terminal, a crashed session—is not leaked. The daemon holds a pidfd on the leader and treats its death as a logout, releasing the session and, if it was the last one, tearing the services of that user down.
On start, the daemon reloads its persisted state and reconciles it with reality: sessions whose leader is gone, or whose pid was reused by a different process—detected through the recorded start time—are dropped; guardians still alive are re-adopted rather than restarted; users whose scandir is already up are re-probed rather than started again. A restart of the daemon therefore does not disturb running user sessions.
State on disk¶
/run/66/userd/
├── s # the listening Unix socket
├── sessions/ # one serialized record per tracked session, named by session id
└── users/ # one serialized record per tracked user, named by uid
The records are plain KEY=value text, one field per line. 66-userctl reads them directly rather than querying the daemon.
The socket is the single rendez-vous point with the PAM module. Only a peer running as uid 0 may REGISTER or RELEASE—the daemon checks this with SO_PEERCRED, so an unprivileged process cannot invent or destroy sessions. Power actions are the exception, since they are meant to be requested by ordinary users; their own policy is described in 66-userctl.
Runtime directories¶
The daemon owns /run/user/UID. On the first session of a user it creates the directory and mounts a private tmpfs over it with mode=0700, the uid and gid of the user, and MS_NOSUID | MS_NODEV. On the last session it unmounts and removes it, falling back to a lazy detach if the mount is still busy.
This is why XDG_RUNTIME_DIR is exported by the PAM module only after checking that the directory exists and is owned by the right user: the daemon is the only writer, and the module refuses to guess.
Per-user environment¶
Before exec'ing the scandir of the user, the guardian sets up a stable per-user environment which the scandir and every service under it inherit: identity (HOME, USER, LOGNAME, SHELL), XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS built from the runtime directory, and a PATH.
These are safe defaults, not a forced injection. 66 natively merges the user's own ~/.66/environment on top at scandir start, and that merge wins—a user can override any of them. The PATH the daemon inherited from PID 1 is passed through untouched; the compile-time fallback is only a floor for the abnormal case where the daemon inherited no PATH at all.
Configuration¶
Build-time only. There is no configuration file and no environment variable tier—the socket path of a session daemon must not be redirectable by an inherited variable.
The knobs are meson options, resolved into the generated <66-tools/config.h>:
| Option | Default | Meaning |
|---|---|---|
userd-runtime-base |
/run/user |
base directory of the runtime directory of each user |
userd-runtime-size |
10% |
size= of the per-user tmpfs, as a share of RAM |
userd-dbus-addr-prefix |
unix:path= |
prefix of the DBUS_SESSION_BUS_ADDRESS built for the user |
userd-default-path |
/usr/bin:/sbin:/bin |
fallback PATH for the services of a user, used only if the daemon inherited none |
userd-pam-dir |
/usr/lib/security |
where pam_userd.so is installed |
The state directory and the socket path are not options. They are constants composed in C, anchored on the SS_LIVE of 66, so that they can never drift from the live tree of 66:
#define USERD_STATEDIR SS_LIVE "userd"
#define USERD_SOCKET_PATH USERD_STATEDIR "/s"
Running it under 66¶
An example frontend file is provided in contributions/service/66-userd:
[Main]
Type = classic
Description = "66-userd session and user tracker daemon"
Depends = ( seatd )
[Start]
Notify = 3
Execute = ( /usr/bin/66-userd -d 3 )
[Event]
EventType = service
From = ( seatd )
On = ( up )
Do = restart
Notify = 3 and -d 3 are the two halves of the same contract: 66 treats the service as up only once the daemon is listening on its socket.
By default, the frontend file can be installed at /usr/share/66/service.
Logging¶
The daemon logs through oblibs, to stderr, at the verbosity set by -v. Under 66 that stream is captured by the logger of the service. Session registrations, user state transitions, guardian lifecycle events and every failure path are reported there.
Failures inside a guardian or one of its children are reported with the uid they concern, so a scandir that fails to come up for one user is identifiable in a log shared by all of them.
Design boundaries¶
Two things are deliberately not the job of 66-userd:
-
Seats, VTs and device access belong to
seatd. 66-userd records the seat and VT number of a session as reported, but never arbitrates device access and never switches VTs. -
The live directory of 66 belongs to 66. Every 66 operation the daemon triggers leaves the livedir unset, so that 66 resolves its own compiled default. There is no knob to relocate it from here.
See also¶
- pam_userd : the module that feeds the daemon; required setup.
- 66-userctl : inspecting what the daemon tracks.