Having a look at greetd
A writeup of the internals of the greetd login manager
Table of Contents
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.
Note: 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)
- The
greetd --session-worker
process (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
InitiateLogin
orCancel
message. - Setup a conversation with PAM.
- Let a PAM adaptor implemented in a separate file do the authentication conversation.
- Do
pam_acct_mgmt()
andpam_setcred()
and send aSuccess
message. - Wait for a
Args
(sets the command for ) orCancel
message and answerSuccess
. - Wait for a
Start
orCancel
message. - 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_VTNR
environment 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_SOCK
for the greeter, and the ones received with theArgs
message) 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.
- Fork
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
InitiateLogin
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
false
if 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
PamResponse
A response to a question from PAM.
- string? response
- The response, may be null
Args
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)
Start
Gives the final go after the session is full set up.
Cancel
Can be used at almost any step to cancel the session setup.
Session to Parent Messages
Success
What it says on the tin. Signals that a command has successfully finished.
Error
Signals that an error has been caught somewhere.
- Error error
- A rusty error response, probably gets serialised to an error message
PamMessage
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
FinalChildPid
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
- A
Context
and andContextInner
to keep track of multiple sessions and scheduling Session
objects 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
login
fallback) - Get some info about the user configured for the
default_session
- Create the Unix Socket (
Listener
) for the greeter, set permissions and set theGREETD_SOCK
environment variable. - 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 (
terminal.switch
set tofalse
in config). - Create a
Context
to 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
Context
). - 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 (
SIGALRM
,SIGCHLD
,SIGTERM
andSIGINT
) 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.
- CreateSession
- Calls the
create_session()
function with the given username, if successful, responds with the first question - PostAuthMessageResponse
- Calls the
post_response()
function to answer the PAM question, fetches and returns the next question or theReady
response if that didn't fail - StartSession
- Calls the
start()
function and returns the result - CancelSession
- Calls
cancel()
function and returns the result
There are two helper functions:
- client_get_question(context)
- 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 callswrap_result()
on the result and returns that, meaning automatic error handling or an automatic success message in case of an ok. - wrap_result(result)
- This function takes a Result and spits out a spendable success or error message containing appropriate information about the error.
Context
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.
Datastructures
The Context
object holds the following values:
- RwLock<ContextInner> inner
- The
ContextInner
object 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
- The
source_profile
setting - string runfile
- The path of the runfile for persisting sate across possible greetd restarts
The 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
The 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 Session
and 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.
Methods
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.
start_unauthenticated_session()
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.
start_greeter()
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 Session
object.
greet()
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.
start_user_session()
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 initial_session
)
create_session()
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 configuring
or scheduled
.
cancel()
Cancel the session being configured.
get_question()
Retrieve a question from the session under configuration.
Uses the 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.
post_response()
Answer a question to the session under configuration.
Calls the post_response()
on the currently configuring
Session
.
start()
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 alarm()
method.
alarm()
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 SIGTERM
or 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 current
session.
check_children()
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
StillAlive
or 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
current
session owns that pid:If a session is
scheduled
we try to start it and make it thecurrent
session, if not and the session was our greeter return an error, in case of a normal session usestart_greeter()
directly and make it thecurrent
session.
terminate()
Notify the Context that we want to terminate. This should be called on SIGTERM.
Shuts down the configuring
, scheduled
and current
session while holding the write lock to avoid race conditions and sessions being rescheduled.
State Machine
All these functions form the state machine that cycles between greeter and user sessions and makes sure the pipeline from configuring
to 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 pid
.
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 sub_task
.
Methods of Session
new_external()
Creates the Session
object and spawns a session worker process with a datagram socket attached for communication.
initiate()
Send an InitiateLogin
message to the session worker.
get_state()
Tries to receive a message from the session worker if there is no last_msg
and stores it in last_msg
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 Context.get_question()
method.
cancel()
Sends a Cancel
message and resets last_msg
.
post_response()
Send a response to an authentication question, or None to cancel the authentication attempt.
Sends a PostAuthMessageResponse
message and resets last_msg
.
send_args()
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 Args
message.
Also reads a message after that and returns whether the command was successful.
start()
Sends a 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.
Methods of SessionChild
owns_pid()
Tests if the task
equals the given process id. (Used for checking if the session terminated on a SIGCHLD
signal in the Context
)
term()
Used to send the session process (sub_task
) a SIGTERM
to tell it that we want it shut down.
kill()
Sends a 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
2022-09-11
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).
2023-03-03
Added Link to Kenny Levinsens blogpost.
But Why?
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 …