HDM is a production display manager for Linux, built with:
- Rust daemon (
hdm) — manages PAM auth, sessions, VT switching - Tauri + React greeter (
hdm-greeter) — the login UI - Unix socket IPC — secure daemon↔greeter communication
HDM is a rival to SDDM, GDM, and LightDM, designed for the HackerOS.
- 🔐 Real PAM authentication via
/etc/shadow+ crypt(3) - 🖼️ Aurora glassmorphism UI — animated background, user avatars
- 🖥️ Session management — Wayland & X11 sessions from
.desktopfiles - 👥 Multi-user — lists system users (UID ≥ 1000), user avatars from
~/.face - ⚡ Autologin support with configurable delay, plus marker-file live-mode autologin for live/installer boots (no config, no created user needed — see Autologin below)
- 🎯 Configurable default session — pick which
.desktopsession HDM pre-selects and launches into (see Default session) - 🪟 Cage-composited greeter — the greeter runs inside the
cageWayland kiosk compositor by default (see Compositor) - 🔌 Power menu — shutdown, reboot, suspend, hibernate with countdown
- 🔒 Brute-force protection — 5 attempt limit per session
- 📋 systemd integration — replaces
display-manager.service - 🎨 Wallpaper support — reads
/etc/hdm/wallpaper.png
┌───────────────────────────────────────────────────────┐
│ TTY1 / VT1 │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ hdm (daemon, root) │ │
│ │ PAM / crypt(3) authentication │ │
│ │ Session launching (drop privs to user) │ │
│ │ VT management │ │
│ │ Spawns + supervises the greeter (see below) │ │
│ │ IPC: /run/hdm/hdm.sock │ │
│ └───────────────┬─────────────────────────────────────┘ │
│ │ spawns, as a child process │
│ ┌───────────────▼─────────────────────────────────────┐ │
│ │ cage -s -- hdm-greeter │ │
│ │ (single-app Wayland kiosk compositor, see │ │
│ │ "Compositor" below — hosts the WebKit view │ │
│ │ the greeter itself has no compositor of its own) │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ hdm-greeter (Tauri, runs as _hdm user) │ │ │
│ │ │ Solid.js UI (TypeScript + Tailwind) │ │ │
│ │ │ Clock, user list, password input │ │ │
│ │ │ Session picker, power menu │ │ │
│ │ └─────────────────┬─────────────────────────────────┘ │ │
│ └────────────────────┼─────────────────────────────────┘ │
│ │ Unix socket (JSON) │
│ ▼ │
│ back to hdm's IPC server above │
└───────────────────────────────────────────────────────┘
Does hdm launch hdm-greeter itself? Yes — hdm is the only thing
that ever starts hdm-greeter; there's no separate systemd unit for it.
launch_greeter() in daemon/src/main.rs spawns it (by default wrapped in
cage, see below) on every boot and again every time the greeter process
exits (e.g. after "Cancel" or a crash), in a loop for as long as hdm
itself is running. hdm-greeter then connects back to hdm's Unix socket
(/run/hdm/hdm.sock) as an ordinary IPC client — it never talks to PAM,
/etc/shadow, or spawns sessions itself.
# Clone / extract HDM
cd HDM
# Build and install (requires Rust + Node.js)
sudo bash install.sh
# With autologin:
sudo bash install.sh --autologin myusername# Install build dependencies (Debian/Ubuntu)
apt install cargo nodejs npm libpam-dev
# Enable
sudo systemctl enable --now hdmRust toolchain requirement: HDM's config parser depends on
hk-parser(crates.io,hk-parser = "0.3.2"), which pulls inindexmap 2.14.x→hashbrown 0.17.x, a dependency that declares Rust's 2024 edition. That means buildingdaemon/andgreeter/now requires Rust 1.85 or newer (rustup update stableif you're on an older toolchain — therustc/cargoshipped by some LTS distro repos, e.g. Ubuntu 24.04'sapt install cargo, is only 1.75 and is too old for this). This is a requirement of the.hk-parsing dependency itself, not something HDM's own code opts into.
Edit /etc/hdm/hdm.hk — HDM's config is in HackerOS's own
.hk format,
not TOML:
[general]
-> greeter_path => "/usr/bin/hdm-greeter"
-> vt => 1
-> theme => graphite
-> background => "/usr/share/wallpapers/HackerOS-Wallpapers/Wallpaper23.png"
-> show_user_list => true
-> allow_root => false
-> minimum_uid => 1000
-> compositor => cage
[autologin]
-> live_detect => true
-> live_marker => ".config/Blue-Environment/.live"
! Force autologin into a specific user/session (optional) — uncomment and
! fill in:
! -> user => username
! -> session => blue-environment
[default]
-> session => blue-environment
[power]
-> shutdown => "shutdown -h now"
-> reboot => reboot
-> suspend => "systemctl suspend"
-> hibernate => "systemctl hibernate"
/etc/hdm/hdm.hk always wins. Anything it doesn't set falls back to a
second, user-editable layer at ~/.config/hdm/hdm.hk (same .hk format,
auto-created with the same defaults the first time the greeter runs — see
ensure_user_default_config() in greeter/src/main.rs), and only then to
the greeter's own built-in defaults.
There are two independent autologin mechanisms:
-
Config-based autologin —
[autologin] -> user(+ optionalsessionanddelay) in hdm.hk. Skips the greeter entirely and launches straight into that user's session afterdelayseconds. Ifsessionis left unset, it uses the default session instead of a hardcoded value. -
Live-mode (marker-file) autologin — for live-USB / installer boots (e.g. Blue Installer). Independent of
user/session/delayabove and needs no[autologin]content at all to work: if any user's home directory contains the marker file named bylive_marker(default.config/Blue-Environment/.live), HDM logs that user in automatically — no password prompt, no[autologin] -> userto configure, and the account doesn't need to fall insideminimum_uid/maximum_uideither (daemon/src/users.rs::find_live_userdeliberately doesn't gate on that range, since a live image's built-in account commonly sits outside whatever range an admin configures for an installed system). Root andnologin/false-shell accounts are still excluded unlessallow_rootis set. Blue Installer deletes the marker file once installation completes, so the next boot goes back to a normal password prompt.This is on by default; disable it with
[autologin] -> live_detect => false, or point it at a different marker with[autologin] -> live_marker => "..."(relative to each candidate user's home — a leading~/or/is stripped and ignored).Note that this still requires an actual Linux user account to exist (Unix has no way to run a session without a UID/GID to drop privileges to) — what it does not require is that account to have been set up through HDM/the greeter, given an
[autologin]entry, or fall inside the configured UID range. A live image's baked-in account is enough.
Config-based autologin takes priority if both are configured; live-mode is
only checked when [autologin] -> user is unset.
[default] -> session names the session id HDM treats as the default —
matched against the id of a .desktop file found by scanning [general] -> sessions_dir, or the built-in "blue-environment" fallback session
(which always exists even with no matching .desktop file — see
list_sessions() in daemon/src/session.rs). It's used to:
- pre-select a session in the greeter's session picker (sent to the
greeter as
default_sessionin the daemon'sGetInfo/welcome IPC response), - launch into for live-mode autologin, and
- launch into for a config-based
[autologin]entry that omitssession.
If unset, or if it names something sessions_dir doesn't actually contain,
HDM logs a warning and falls back to "blue-environment".
[general] -> compositor (default cage) is the Wayland compositor HDM
wraps greeter_path in before launching it — hdm-greeter is a windowed
Tauri/WebKit application, not a compositor, so something has to give it a
Wayland display to render into pre-login. By default HDM runs it as:
cage -s -- /usr/bin/hdm-greeter
cage is a minimal single-application
Wayland kiosk compositor built exactly for this (fullscreen one app, exit
when it exits) and needs to be installed and on $PATH (e.g. apt install cage / dnf install cage). Set compositor to "none" to launch
greeter_path directly instead (e.g. for an X11 greeter build, or one that
brings up its own compositor) — and if the configured compositor binary
can't be found at all, HDM logs that and falls back to launching
greeter_path directly for that cycle rather than refusing to show a login
screen.
Set compositor to "labwc" to use
labwc instead — a full wlroots-based
stacking window manager (not a single-app kiosk compositor like cage), run
here single-app-style via its own session flag:
labwc -S /usr/bin/hdm-greeter
This is a genuinely different code path, not just a different binary name:
apply_compositor_args() in daemon/src/main.rs builds cage's -s -- <cmd> and labwc's -S <cmd> separately, because the two aren't
interchangeable — labwc doesn't understand -- as an argument terminator
at all (-S -- <path> would hand it the literal string "--" as its
startup command and the greeter would never run), and only labwc's
-S/--session — not its -s/--startup — exits the compositor when the
greeter does, which HDM's respawn loop in launch_greeter() needs.
compositor values other than "none", "cage", or "labwc" are assumed
to be cage-compatible (most other single-app Wayland kiosk compositors
are); if that's wrong for some other binary, apply_compositor_args() is
the place to teach HDM its contract too.
Since cage and labwc are separate compositors with their own renderer code
(wlroots' GLES2/EGL/GBM path, in both cases, unless overridden — see
compositor_renderer below), switching from one to the other is a
legitimate thing to try if a crash turns out to be specific to one of
them — see the Assertion 'surface->initialized' failed entry under
Troubleshooting.
HDM always creates and owns /run/user/<uid> and sets XDG_RUNTIME_DIR to
it before spawning the compositor/greeter — cage refuses to start at all
without one. By default that's /run/user/0 (root); [general] -> greeter_user (unset by default) lets you run the greeter as an
unprivileged system user instead, but only do that once that user has the
device access cage needs — see Troubleshooting and
config/sysusers.d/hdm.conf.
HDM reads user avatars from (in priority order):
~/.face~/.face.icon~/.config/hdm/avatar.png/var/lib/AccountsService/icons/<username>
# View HDM logs
journalctl -u hdm -f
# Or from file
tail -f /var/log/hdm/hdm.logRunning hdm manually without root: HDM must run as root (UID 0).
Expected — HDM reads /etc/shadow and issues VT ioctls, both of which
require root, so it refuses to start otherwise (see main.rs, checked via
libc::getuid()). This has nothing to do with sudo specifically: the
shipped hdm.service sets User=root/Group=root explicitly, so
systemctl start hdm (or booting into it as display-manager.service)
already runs it as root automatically — no interactive sudo involved at
all. sudo hdm in a terminal is only useful for manually testing/debugging
outside systemd, and by default sudo does not forward your shell's
XDG_RUNTIME_DIR, WAYLAND_DISPLAY, etc. into root's environment — which
matters for the next issue below.
Greeter log spammed with [../cage.c:298] XDG_RUNTIME_DIR is not set in the environment, repeating forever until you Ctrl+C. This was a real
bug, fixed: launch_greeter() used to spawn cage without ever setting
XDG_RUNTIME_DIR, so cage exited immediately every time — and because
launch_greeter() relaunches the greeter whenever it exits, that turned
into a tight crash/respawn loop rather than a one-time error. It now
creates and owns /run/user/<uid> (uid 0 unless you've set [general] -> greeter_user) and sets XDG_RUNTIME_DIR to it before spawning, exactly
like session::launch_session() already did for real user sessions. A
1-second backoff was also added between relaunch attempts so any future
fast-crash-loop stays readable in the log instead of flooding it. If you
still see this after updating, check that /run isn't mounted read-only
and that nothing else is deleting /run/user/0 out from under HDM.
sudo: nie udało się rozwiązać nazwy hosta ... /
sudo: unable to resolve host ... before HDM even starts. Unrelated to
HDM — it's sudo itself trying to resolve your machine's configured
hostname and failing, almost always because that hostname isn't listed in
/etc/hosts. Fix it at the OS level, e.g. add a line like
127.0.1.1 <your-hostname> to /etc/hosts (hostnamectl hostname shows
what it's currently set to). It's a warning, not a fatal error — sudo
still runs the command afterwards.
Greeter runs but the screen is black / cage fails to open a device.
This means cage started (so XDG_RUNTIME_DIR is fine) but couldn't open
/dev/dri//dev/input — typically because [general] -> greeter_user is
set to a user that lacks device access. Either leave greeter_user unset
(root can always open these devices directly), or make sure that user is
in the video/render/input groups and has an active seat session
(seatd, or systemd-logind on a system with elogind/logind support) —
see config/sysusers.d/hdm.conf for a starting point.
**Cage crashes with Assertion \surface->initialized' failedright after the DRM/EGL init lines in the log (GPU/CRTCs/planes all found fine, so this isn't the black-screen device-access issue above), and the greeter's own log separately shows aGdk-Message: ... Error reading
events from display: Broken pipe.** Read those as two separate crashes, in order, not one: the Gdk-Message line is printed by GDK *inside the greeter process* (hdm-greeteris a GTK/WebKitGTK app) when it loses its Wayland connection because the *server* — cage — already died. So the real crash is cage'sAssertion 'surface->initialized' failed, and it happens in cage/wlroots' own GLES2-over-EGL/GBM renderer while setting up its *own* output render surface — before the greeter's WebKit view has rendered anything at all. (An earlier version of this note attributed this to WebKitGTK's DMA-BUF renderer instead; hdm-greeterdoes hit a similar-looking but genuinely different crash from that, which HDM works around unconditionally by settingWEBKIT_DISABLE_DMABUF_RENDERER=1` in
the greeter's environment — but that setting can't touch a crash that
happens this early, inside cage itself, before WebKit is in the picture.)
This is a real cage/wlroots bug on the affected GPU driver combination
(seen on some Intel i915 setups) — not something fixable from HDM's Rust
code — but HDM can work around it: set
[general]
-> compositor_renderer => pixman
in /etc/hdm/hdm.hk (see the commented-out example already in the
shipped config) and restart HDM. This forces cage/wlroots onto its
software Pixman renderer via WLR_RENDERER=pixman, which never touches
EGL/GBM and so never hits this assertion — at some GPU compositing
performance cost that doesn't matter for a login screen. This isn't
HDM's default because it's a targeted workaround for specific broken
driver combinations, not something every install needs.
If you set compositor_renderer => pixman and still hit the same
Assertion 'surface->initialized' failed, but now preceded by libEGL warning: failed to get driver name for fd -1 / MESA-LOADER: failed to retrieve device information instead of any cage-side [render/egl.c:...]
lines — that's not the same crash recurring, it's the next one: cage
itself is fine now (the pixman renderer log line and successful DRM
modesetting confirm that), but hdm-greeter is crashing instead, because
it tries to open its own hardware-accelerated EGL/GL context, and a
pixman-backed cage advertises no GBM/DRM device for clients to use, so
that EGL init gets handed an invalid fd and the greeter dies mid-setup —
which cage then reports as the same assertion while tearing down that
now-orphaned surface.
There are actually two independent places this EGL probe can come from
in hdm-greeter, and current HDM disables both unconditionally in the
greeter's environment (see spawn_greeter_command() in
daemon/src/main.rs):
- WebKit's own accelerated-compositing path —
WEBKIT_DISABLE_COMPOSITING_MODE=1 - GDK's, one layer further down —
hdm-greeteris a GTK3 application (webkit2gtk-4.1 requires GTK3, not GTK4), and GDK's Wayland backend initializes its own EGL display independently of whatever WebKit does with it, soWEBKIT_DISABLE_COMPOSITING_MODEalone doesn't reach it —GDK_GL=disable(a separate, long-standing GTK3 env var) is needed too, and is what actually stops the fd -1 probe from happening at all
If you're still seeing this exact crash, make sure you're running a build
from after both of those were added — check that GDK_GL (not just
WEBKIT_DISABLE_COMPOSITING_MODE) appears in daemon/src/main.rs — and
that the daemon binary was actually rebuilt and reinstalled, not just the
config reloaded (none of this is configurable from hdm.hk; it's set in
Rust code).
If pixman plus all three of WEBKIT_DISABLE_DMABUF_RENDERER,
WEBKIT_DISABLE_COMPOSITING_MODE, and GDK_GL=disable still doesn't
help, or you'd rather not run a software renderer at all, try
switching compositors entirely instead — set [general] -> compositor => labwc (with labwc installed; see Compositor). cage and
labwc are two separate compositors built on the same wlroots libraries
but with their own renderer/output code, so a crash specific to cage's
implementation doesn't necessarily follow it to labwc. If neither helps,
it's likely a different, underlying wlroots bug shared by both — try
updating cage/wlroots (or labwc/wlroots) and check
https://github.com/cage-kiosk/cage/issues.
Content Security Policy. greeter/tauri.conf.json sets a CSP rather
than leaving it null. style-src includes 'unsafe-inline' — this is a
deliberate, known tradeoff, not an oversight: the greeter UI sets style="..."
attributes at runtime throughout (a carry-over from the original Svelte
template's inline style bindings, kept for readability of long conditional
style strings as template literals rather than large object literals).
Runtime-set inline styles aren't covered by Tauri's automatic build-time
script/style hashing, so 'unsafe-inline' is required for the UI to render
at all under a strict CSP. Everything else (script-src, font-src,
default-src) is locked to 'self' with no external origins. If this
codebase migrates from string styles to style={{...}} objects or CSS
classes/custom properties in the future, 'unsafe-inline' can be dropped
from style-src entirely.
Fonts are self-hosted, not loaded from Google's CDN — see
ui/src/fonts.css. A login screen has to render before networking is
necessarily up (fresh install, wifi still associating, airgapped
machines), so HDM ships its fonts (@fontsource/oxanium,
@fontsource/dm-sans, @fontsource/jetbrains-mono) as part of the built
UI bundle instead of fetching them at runtime.
Authentication rate limiting is enforced server-side, in
daemon/src/pam_auth.rs::RateLimiter, keyed by username and shared across
every IPC connection — not just in the greeter UI's own countdown display.
A client that skips the UI and calls the daemon's IPC commands directly
still hits the same lockout.
| Feature | HDM | SDDM | GDM | LightDM |
|---|---|---|---|---|
| Wayland native | ✅ | ✅ | ✅ | |
| X11 support | ✅ | ✅ | ✅ | ✅ |
| PAM auth | ✅ | ✅ | ✅ | ✅ |
| Autologin | ✅ | ✅ | ✅ | ✅ |
| Custom themes | ✅ | ✅ | ❌ | ✅ |
| User avatars | ✅ | ✅ | ✅ | ✅ |
| Blue Environment | ✅ | ❌ | ❌ | ❌ |
| Aurora UI | ✅ | ❌ | ❌ | ❌ |
| Glassmorphism | ✅ | ❌ | ❌ | ❌ |
| Rust backend | ✅ | ✅ | ❌ | ❌ |
| Solid.js frontend | ✅ | ❌ | ❌ | ❌ |
GPL-3.0 — © 2026 HackerOS Team