OVERHEAD

Take a seat, if you dare!

Shoe-in Bad Idea Linux

In the previous entry of this now-a-series series, you’ll of seen me use seatd.service instead of systemd-logind.service to acquire a seat for my custom Shoe service. I had avoided using logind before because it refused to actually allocate a seat to my services, no matter what I did.

I was never sure if logind didn’t support VT-less setups, if my PAM files were wrong, or if the Arch Linux package for cage wasn’t compiled with logind support, but now I know why…

In case you don’t know: A “seat” is a group of input & output devices which can be allocated to a process. This concept enables multi-seat setups, where one physical device has multiple people logged in at same time, with each login getting its own set of devices.

Why it didn’t work

The answer is frustratingly simple: Because I was lazy.

When the VT subsystem is present, it will always try to read user input, and that’s the problem. If some other process tries to read user input, then both the process and the VT subsystem will receive the input.

Therefore, if logind finds /dev/tty0 (a couple other files are checked too), it will only give seats to processes with a TTY device in its control; the only way to solve the split-input issue is to send the input to a TTY device and then have it redirect the input to the relevant process.

Proper solution

Just, ya know, actually compile out the kernel VT support?

After that, set the XDG_SEAT environment variable to seat0 and use a PAM config containing at least pam_systemd.so to acquire a session from systemd-logind.service. Here’s an example of what such a PAM config might look like:

A minimal PAM config example that creates a valid systemd-logind.service session. Based on the PAM config from the Cage wiki.

auth		required	pam_unix.so	nullok
account		required	pam_unix.so
session		required	pam_unix.so

# This is the line that handles the session creation.
session		required	pam_systemd.so

# This ensures the environment is set, which is needed for Wayland compositors
# that read the `XKB_DEFAULT_*` environment variables.
session		required	pam_env.so

seat0 is the hardcoded seat containing all input devices not allocated to another seat, which for most people (who probably aren’t manually creating seats) is all the available input devices.

Very hacky solution(s)

To avoid needing to touch the /dev/tty[0-63] devices (among others), or if you’re someone who likes their terrible ideas to have some certainty to them damnit, you can add the following systemd-logind.service drop-in file to force logind to allocate a seat anyways:

Contents of the /etc/systemd/system/systemd-logind.service.d/skip-vt.conf service drop-in file.

[Service]
InaccessiblePaths=/sys/class/tty/tty0

This is a terrible idea:

  1. systemd-logind.service will complain that /sys/class/tty/tty0 doesn’t exist while /dev/tty0 does, which ordinarily wouldn’t be possible.
  2. This leaves the split-input problem entirely unsolved and, if you stop the relevant processes and inspect the underlying TTY device (probably /dev/tty1), you’re usually able to see everything you’ve typed up to that point.

Similarly, using seatd (with SEATD_VTBOUND=0) and logind simultaneously has the same input problem as described above, only now there’s two seat-managing services involved which is arguably even worse.

Probably hacky solution

Another way of getting logind to cooperate, while being at least slightly less hacky than the above proposals, is to remove the VT subsystem devices early on in the boot process, which you can accomplish with this hardened service:

Contents of the rm-vt-dev.service systemd service.

[Unit]
Description=Remove VT subsystem devices
# `systemd-vconsole-setup.service` checks for `/dev/tty0`.
Before=systemd-vconsole-setup.service shutdown.target
DefaultDependencies=no
# Don't run this service again if `/dev/tty0` has already been removed.
ConditionPathExists=/dev/tty0

[Service]
Type=oneshot
# Removes VT devices, letting `systemd-logind` give seats to VT-less services.
## The `/dev/tty` TTY controller file is required, since `/dev/pts/N` devices
## also need the controller.
ExecStart=/usr/bin/bash -c 'rm /dev/{tty?,vcs,console}*'

# Hardening
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectSystem=strict
ProtectClock=yes
ProtectHome=yes
ProtectProc=noaccess
ProcSubset=pid

PrivateNetwork=yes
IPAddressDeny=any
PrivateTmp=yes
PrivateIPC=yes
UMask=0077

RestrictAddressFamilies=none
RestrictNamespaces=yes
RestrictRealtime=yes

MemoryDenyWriteExecute=yes
RestrictSUIDSGID=yes
NoNewPrivileges=yes
LockPersonality=yes

CapabilityBoundingSet=
SystemCallArchitectures=native
SystemCallFilter=@system-service

# `strict` causes STDIN errors.
DevicePolicy=closed
DeviceAllow=char-vcs w
DeviceAllow=char-tty w
DeviceAllow=char-ttyS w
DeviceAllow=char-ttyUSB w

Add the Requires=rm-vt-dev.service & After=rm-vt-dev.service lines to the necessary service to pull in rm-vt-dev.service.

Additionally, you can set the console=ttynull kernel cmdline option, which stops /dev/console (the TTY device used by the /init process) from redirecting to /dev/tty0 (which in theory redirects to /dev/tty1, since /dev/tty0 represents the “current” TTY device). You can check what devices redirect where by looking at /sys/class/tty/{console,tty0}/active and reading the device name.

This is probably a bad idea, mainly because I’m uncertain of two things:

  1. Does this prevent other processes from accessing the VT subsystem from this point onwards?
  2. Does the VT subsystem still receive user input if not even systemd-vconsole-setup.service has touched the TTY devices?

Without an answer to these questions, I cannot recommend using this method for anything except demonstrations; the proper solution is the only secure method I know of.

The dream solution

In an ideal world, the VT subsystem could be disabled with a runtime toggle (e.g. a kernel cmdline like vt_disabled=1) and avoid the need for recompiling, but until then, these are the only available methods I’m aware of.