Having a look at greetd
A writeup of the internals of the greetd login manager
What is greetd?
greetd is a minimal and flexible login manager daemon that makes no assumptions about what you want to launch.
In other words Kenny Levinsen created an awesome piece of software that allows anyone to easily launch anything on login and that without being bound to a fronted.
Most information in this article came from reading the greetd code and making notes on good old paper.
Note: Kenny Levinsen also wrote an interesting blogpost on login managers.
Little Disclaimer …
Please be aware that the information may be outdated by the time you read this, Also it is entirely possible that some mistakes made it into this article. If you want to understand how greetd works please read the code yourself, this article alone is not enough! (but hopefully helpful)
In case you find a mistake or have feedback please contact me.
How greetd is structured
Greetd has a pretty modular approach to things and is made of a bunch of components one can look at separately.
- The managing greetd server daemon (main focus)
greetd --session-workerprocess (good to know)
- The greeter
I won't be looking at the greeter as the interface is pretty well described in the manual.
The other two are both compiled into the same binary, the main function decides which component it should become after parsing the command line parameters.
The common part
There is a small common part that always happens when the greetd binary starts up:
- It invokes the commandline and configuration parser to get its complete configuration.
- It invokes
mlockall()to prevent sensitive data like buffers containing passwords from being swapped to disk.
- Then it decides what to become.
The greetd session worker
The simpler part of greetd is the actual session worker, so I'll start with that …
The session worker is started by starting greetd with the
--session-worker or simply
-w flag and passing the number of a file descriptor that is supposed to be a Unix socket for exchanging commands and feedback.
What it does
The session worker itself is one strand of pretty straightforward code that feels a bit like a shellscript that asks for input and aborts with an error message on failure.
It does a few things:
- Wait for an
- Setup a conversation with PAM.
- Let a PAM adaptor implemented in a separate file do the authentication conversation.
pam_setcred()and send a
- Wait for a
Args(sets the command for ) or
Cancelmessage and answer
- Wait for a
- Fetch user information from PAM.
- Set a session id using
setsid()to make the process the root of a new session.
- If we have a specific virtual-terminal configured.
- Tell PAM that we want to run in that tty.
- Set the
XDG_VTNRenvironment variable via PAM.
- Open the terminal file, set the tty to text mode, clear it and switch to it if necessary.
- Use the greetd terminal abstraction to do the plumbing for the pipes and switch the controlling tty.
- Set the pwd for the new session, either to the users home or the filesystem root as fallback.
- Set a whole bunch of environment variables for the session (including the
GREETD_SOCKfor the greeter, and the ones received with the
Argsmessage) using PAM.
- Prepare the command to execute in the new session (shell + profile files + session command)
- Fetch the environment variables for the new session from PAM.
After the fork the parent sends the pid to the the greetd server process and closes the socket, sets its parent death signal to
SIGTERM and waits for the child thread to terminate before ending the PAM session.
The child process drops its privileges, also sets its parent death signal and then uses
execve() to launch the prepared command with the environment provided by PAM.
Note on Environment variables: The variables received using the
Args call get set before the ones coming from the greeter to avoid overriding some important variables. This behaviour was introduced after making it possible for the greeter to set the environment variables (again). Personal opinion: If you have a good reason for changing those you shouldn't be doing that through the greeter anyway.
Messages between Session Worker and Server
For later reference here are the messages that are exchanged between session worker and server via given socket using a JSON based protocol.
Parent to Session Messages
Message sent to start the login process.
- string service
- The PAM service id so that PAM knows which configuration to apply
- string class
- ends up in the
XDG_SESSION_CLASS, either 'user' or 'greeter'
- string user
- the username to log in as
- bool authenticate
- set to
falseif we have an autologin session
- TerminalMode tty
- The description of the terminal to attach to
- bool source_profile
- Whether or not to read the profile files before executing the session command
A response to a question from PAM.
- string? response
- The response, may be null
Sets the command to be executed for the session when
Start is sent.
- string cmd
- An array of arguments representing the command
- string env
- An array of environment variables to pass to the given command (it was added (back) on 2022-08-13)
Gives the final go after the session is full set up.
Can be used at almost any step to cancel the session setup.
Session to Parent Messages
What it says on the tin. Signals that a command has successfully finished.
Signals that an error has been caught somewhere.
- Error error
- A rusty error response, probably gets serialised to an error message
A Question or other Output from PAM.
- AuthMsgType style
- an enum telling what kind of message this is and what the expected response is, one of Visible, Secret, Info or Error
- string msg
- a string intended for the human trying to log in
Sends back the process id of the session child after the session was launched.
- uint pid
- the process id
The greetd server
The server process is the one started by the init-system, it is responsible for launching the session workers and telling them what to do, it also houses the internal state machine that knows which session to launch next, it also does some "managing" for the processes in the session.
The server is made up of multiple components:
- A base to orchestrate the startup and running sequence, also handles signals
ContextInnerto keep track of multiple sessions and scheduling
Sessionobjects that launch the session workers and handle the communication
- A function that does the communicating with the greeter
What it does …
… on startup
After the initialisation has decided that it will be a greetd server the main function also does a few things:
- Determine the service name it will tell PAM based on which configurations for PAM are available. (warns when using the
- Get some info about the user configured for the
- Create the Unix Socket (
Listener) for the greeter, set permissions and set the
- Get information about the tty it is supposed to be on. Returns an error told to wait for the terminal to become active it waits (
- Create a
Contextto do the housekeeping for us.
- Start initial session or greeter for the default session depending on which one is configured. (Other state transitions after initial bootstrapping are handled by the
- Create the runfile to know the initial run apart form the others in terms of system restarts, not greetd restarts.
- Loop to handle posix signals (
SIGINT) as well as Incoming connections on the greeter socket.
… on an incoming connection from the greeter
The connections are accepted using the main-loop (see previous section) the handler gets the bidirectional datagram stream and a copy of the
Context structure (the inner context staying the same, this is how the information about the current global state is exchanged between possibly multiple connections).
Inside this function is a loop that reads the request and then decides which function to call on the
Context, wraps up the response and sends it back.
- Calls the
create_session()function with the given username, if successful, responds with the first question
- Calls the
post_response()function to answer the PAM question, fetches and returns the next question or the
Readyresponse if that didn't fail
- Calls the
start()function and returns the result
cancel()function and returns the result
There are two helper functions:
- This function fetches the next question from the context using the
get_question()method, if it returns a question it packs that into an object that can be serialised and sent back, if not it calls
wrap_result()on the result and returns that, meaning automatic error handling or an automatic success message in case of an ok.
- This function takes a Result and spits out a spendable success or error message containing appropriate information about the error.
As already stated greetd manages its state using
Context objects (no idea what the correct rust terminology is, but its a data-structure with a set of functions and quacks like an object). To be more precise it uses two context objects, the outer
Context for configuration and connection state and the
ContextInner for storing global state of a 3-stage session pipeline that is shared between all interested parts of greetd.
Context object holds the following values:
- RwLock<ContextInner> inner
ContextInnerobject for global state wrapped in a lock that allows shared reading or exclusive writing
- string greeter_bin
- The command for the greeter
- string greeter_user
- The user the greeter runs as
- string greeter_service
- The PAM service name for starting greeter sessions
- string pam_service
- The PAM service name for starting normal sessions
- TerminalMode term_mode
- The vt in which this context spawns sessions
- bool source_profile
- string runfile
- The path of the runfile for persisting sate across possible greetd restarts
ContextInner has the following fields:
- SessionChildSet? current
- Information about the currently running session
- SessionSet? scheduled
- The session that will be started once the currently running session terminates
- SessionSet? configuring
- Stage before
scheduled, a session that isn't ready to be launched just yet because the greeter is still answering the questions PAM has
SessionSet here is a
Session (representing a session that is being set up) with a timestamp attached. The
SessionChildSet stores a
SessionChild (representing a running session) and has an
is_greeter flag in addition to the timestamp.
Note: There is a distinction between the
SessionChild object because a session that is already running doesn't care about all of the communications foo and is only interested in finding out if the session is still running and ending it if it isn't supposed to be running anymore, the only shared value is the pid of the session worker.
The methods on the context seem to be made to make it easy to write down what should happen from the outside, also they are pretty well commented so you know how they are supposes to be hooked up.
Directly start an unauthenticated session, bypassing the normal scheduling.
This one translates to spawning a session worker and sending it all commands that should be needed for an autologin session and gives empty answers for any questions PAM asks. This is used for the
initial_session and the sessions for the greeters. It returns the created
Session as an object. Username, session-class, PAM-service and the command are given as arguments.
Directly start a greeter session, bypassing the normal scheduling.
A small convenience wrapper that preconfigures the
start_unauthenticated_session() with stored settings for a greeter session, uses the
greeter_service as the PAM service. It also returns the created
Directly start a greeter session, bypassing the normal scheduling.
This one starts a greeter session and sets it as the
current session in the inner context. It returns an error if that
current session is already occupied.
Directly start an initial session, bypassing the normal scheduling.
This will use
start_unauthenticated_session() to autologin a given user with the given command. (used to implement the
Create a new session for configuration.
Creates a session preconfigured with the Context settings and the given username and swaps it into
configuring. Also mitigates a race-condition by cancelling a swapped out session.
Returns an error if there is no
current session or there already is a session
Cancel the session being configured.
Retrieve a question from the session under configuration.
get_state() method on the
Session and returns either an ok when there are no more questions (
Ready) or the question that is currently open.
Answer a question to the session under configuration.
post_response() on the currently
Schedule the session under configuration with the provided arguments.
Returns an error if the
configuring session has not signalled that it is ready yet or isn't present.
Sets the sessions command to the given value, and swaps it into the
scheduled stage, a session that already was in
scheduled will be cancelled.
Sets a timer to fire the
SIGALRM signal in five seconds which will be received using the
Gets called by the main-loop when a
SIGALRM (timer) arrives.
Is a noop if no session is scheduled.
If the current session is still running it will be sent a
SIGKILL, depending on how much time has elapsed since the session was scheduled. The
SIGALRM will be set up to fire again in one second.
If the current session is cleared the
seduled session will be sent a
Start command and be turned into the
Gets called by the main-loop when a
SIGCHLD (a child process has terminated, usually) arrives.
This function calls the
waitpid() function with the
NoHang flag set to tell it that we don't want to wait if there is nothing new it can tell us. It can react to a bunch of events:
In case of a
StillAliveor an error indicating that we currently don't have any children the function exits.
States indicating some kind of normal event but not matching as well as Interruptions get thrown away.
Something exited or was killed with a signal and the
currentsession owns that pid:
If a session is
scheduledwe try to start it and make it the
currentsession, if not and the session was our greeter return an error, in case of a normal session use
start_greeter()directly and make it the
Notify the Context that we want to terminate. This should be called on SIGTERM.
Shuts down the
current session while holding the write lock to avoid race conditions and sessions being rescheduled.
All these functions form the state machine that cycles between greeter and user sessions and makes sure the pipeline from
current doesn't get clogged up by anything misbehaving and that greetd cleans up after itself.
The Session Interface
To make the external session processes and the context talk to each other Greetd has the
Session class. It wraps an entire session process and handles the communication. It also remembers the last incoming message as
last_msg and the session processes
However, after setting up the session (as noted earlier) the Communication channel becomes useless as we now only care about the sessions current running status detecting when it exits and ending it if it misbehaves. This is the reason a second object, the
SessionChild exists which holds the session managers pid as
task and the pid of the process in the session as
Session object and spawns a session worker process with a datagram socket attached for communication.
InitiateLogin message to the session worker.
Tries to receive a message from the session worker if there is no
last_msg and stores it in
Then it returns a
SessionState struct contain either a question from PAM or a
Ready if PAM has no further questions. This is used in the
Cancel message and resets
Send a response to an authentication question, or None to cancel the authentication attempt.
PostAuthMessageResponse message and resets
Send the arguments that will be used to start the session.
Since 2022-08-13 this also sends a list of environment variables to match the new
Also reads a message after that and returns whether the command was successful.
Start request and waits either for an
Error or a
FinalChildPid answer while dismissing questions from PAM with empty answers.
After that it shouts down the socket and returns a
SessionChild object as the
Session object mainly made for session setup is no longer useful once the session is running.
Tests if the
task equals the given process id. (Used for checking if the session terminated on a
SIGCHLD signal in the
Used to send the session process (
SIGTERM to tell it that we want it shut down.
SIGKILL to both, the session manager and the session process. Used by the
Context.alert() method when a session takes too long to shut down.
Updates to this Page
Added information about the commits adding back the environment variables and another round of spellchecking. May also have broken some links (I hope not too many).
Added Link to Kenny Levinsens blogpost.
The reason I created this writeup is to first of all have an understanding of what my login manager of choice does and also to have a reference when tinkering with it. One of my plans with this includes making greetd capable of running multiple sessions simultaneously to allow user switching and to be able to reuse the greeter for the initial login for locking my screen.
In case you have found a mistake, have questions or just feedback in general …