2022-08-16 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.

You can find greetd on sourcehut

Most information in this article came from reading the greetd code and making notes on good old paper.

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.

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:

  1. It invokes the commandline and configuration parser to get its complete configuration.
  2. It invokes mlockall() to prevent sensitive data like buffers containing passwords from being swapped to disk.
  3. 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 relevant worker() function in greetd/src/session/worker.rs

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:

  1. Wait for an InitiateLogin or Cancel message.
  2. Setup a conversation with PAM.
  3. Let a PAM adaptor implemented in a separate file do the authentication conversation.
  4. Do pam_acct_mgmt() and pam_setcred() and send a Success message.
  5. Wait for a Args (sets the command for ) or Cancel message and answer Success.
  6. Wait for a Start or Cancel message.
  7. Fetch user information from PAM.
  8. Set a session id using setsid() to make the process the root of a new session.
  9. If we have a specific virtual-terminal configured.
    1. Tell PAM that we want to run in that tty.
    2. Set the XDG_VTNR environment variable via PAM.
    3. Open the terminal file, set the tty to text mode, clear it and switch to it if necessary.
    4. Use the greetd terminal abstraction to do the plumbing for the pipes and switch the controlling tty.
  10. Set the pwd for the new session, either to the users home or the filesystem root as fallback.
  11. Set a whole bunch of environment variables for the session (including the GREETD_SOCK for the greeter, and the ones received with the Args message) using PAM.
  12. Prepare the command to execute in the new session (shell + profile files + session command)
  13. Fetch the environment variables for the new session from PAM.
  14. 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:

What it does …

… on startup

The entry point is the main() function in the greetd/src/server.rs file

After the initialisation has decided that it will be a greetd server the main function also does a few things:

  1. Determine the service name it will tell PAM based on which configurations for PAM are available. (warns when using the login fallback)
  2. Get some info about the user configured for the default_session
  3. Create the Unix Socket (Listener) for the greeter, set permissions and set the GREETD_SOCK environment variable.
  4. 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 to false in config).
  5. Create a Context to do the housekeeping for us.
  6. 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).
  7. Create the runfile to know the initial run apart form the others in terms of system restarts, not greetd restarts.
  8. Loop to handle posix signals (SIGALRM, SIGCHLD, SIGTERM and 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).

The client_handler() in greetd/src/server.rs handles the communication

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 the Ready 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 calls wrap_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

The Context mechanism is implemented in greetd/src/context.rs

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:

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

The session interface is implemented in greetd/src/session/interface.rs

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).

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 …

… please contact me!