A hardened, minimalist screen locker for Wayland compositors that
implement ext-session-lock-v1.
lockme aims at one thing: keep a typed password out of every place
the kernel and userspace would otherwise let it leak.
The password buffer is page-aligned, mlock'd, marked
MADV_DONTDUMP, and wiped with explicit_bzero on every clear. The
locker process drops dumpability, denies new privileges, and
suppresses core dumps. PAM runs in a forked child that talks back
over a length-prefixed pipe, so a misbehaving auth module never
shares an address space with your secret.
The UI defaults to GPU-rendered Matrix rain while idle. Typing switches to a
solid configurable palette, failed auth shows a solid failure color, and Alt-B
toggles between Matrix and a blank screen. Colors and the rest of the runtime
knobs live in a small KDL 2.0 config file (see Configuration).
It is small on disk too: a stripped release binary is 558 KiB,
well under the size of hyprlock and several times smaller than
waylock while shipping more hardening than either. See
compare.md for the full security and size comparison
against swaylock, waylock, and hyprlock.
Press Alt-B to toggle between a blank screen and the matrix rainfall.
I was missing waylock on the Niri window manager and decided to write a new one from scratch — and because I dig Nim.
lockme needs Nim/Nimble, a C toolchain, pkg-config, the Nimble package
nimkdl >= 2.1.0, and development headers for Wayland, Wayland EGL, EGL,
OpenGL ES 3, xkbcommon, and PAM. The OpenGL ES link dependency is discovered
through the standard glesv2 pkg-config module, even though the renderer
includes GLES3 headers.
Void Linux:
sudo xbps-install -Sy nim base-devel pkg-config wayland-devel libglvnd-devel libxkbcommon-devel pam-devel
nimble install -y nimkdlVoid, Arch Linux, Debian, and Ubuntu package nimble with nim.
Arch Linux:
sudo pacman -S --needed nim base-devel wayland libglvnd libxkbcommon pam pkgconf
nimble install -y nimkdlDebian/Ubuntu:
sudo apt update
sudo apt install nim build-essential libwayland-dev libegl-dev libgles-dev libxkbcommon-dev libpam0g-dev pkg-config
nimble install -y nimkdlBuild dependencies:
- Nim
2.2.0or newer - Nimble package
nimkdl >= 2.1.0 - a C compiler
pkg-config- development packages for
wayland-client,xkbcommon,pam,egl,glesv2, andwayland-egl - OpenGL ES 3 headers, normally provided by the same development package
that provides
glesv2
Protocol refresh dependency:
wayland-scanner
nimble buildRelease builds use checked-in Wayland protocol stubs, so wayland-scanner
and wayland-protocols are not required unless you are refreshing those
generated files.
Formatting uses nph:
nimble setupTools
nimble fmt
nimble fmtChecknimble setupTools installs the pinned formatter as a user-level Nimble tool.
nimble fmt formats maintained Nim sources. nimble fmtCheck is the
non-mutating check form for review or CI. The generated Matrix font source is
left to nimble regenFont.
nimble deployThis builds an optimized release, installs lockme to ~/.local/bin/lockme,
and installs the PAM service file to /etc/pam.d/lockme. The binary install
runs as your user; the PAM install uses sudo because /etc/pam.d is
root-owned.
lockme --check-protocolsAt runtime, the compositor must advertise:
ext_session_lock_manager_v1wp_viewporter- either
wl_shmorwp_single_pixel_buffer_manager_v1
wl_shm is used for the default color surfaces when available.
wp_single_pixel_buffer_manager_v1 is optional and is used for solid-color
buffers when available.
lockme uses libwayland-client directly through a small C shim and
generated protocol stubs. It does not depend on a third-party Wayland wrapper
library; this keeps the C/Nim boundary explicit and leaves protocol handling
on the standard Wayland C stack.
The generated protocol files are checked in under src/lockme/protocols.
Their XML sources are vendored in src/lockme/protocols/xml:
ext-session-lock-v1fromwayland-protocols/stagingsingle-pixel-buffer-v1fromwayland-protocols/stagingviewporterfromwayland-protocols/stablexdg-shellfromwayland-protocols/stable
To refresh the generated C/header files after updating the XML:
nimble regenProtocolsRefreshing protocols requires wayland-scanner. The task regenerates C/H
from the vendored XML only; update the XML from wayland-protocols first
when intentionally moving to a newer protocol revision. Commit the XML and
generated C/H changes together.
lockmePlain lockme shows Matrix rain while idle and ignores the Enter key on an
empty password buffer.
If you use a laptop, set --idle-timeout. A continuously rendering GPU
screensaver prevents deep sleep and drains the battery noticeably on an
unattended locked machine. Setting an idle timeout blanks the screen after the
specified number of seconds, letting the system reach low-power states:
lockme --idle-timeout 60 # blank after 60 s of inactivityThe next keypress wakes the screen back to matrix. You can also set this in
~/.config/lockme/config.kdl so you never forget it:
idle-timeout 60--blank starts on a blank screen instead of matrix rain.
--allow-empty-password allows an empty Enter press to reach PAM.
--no-gpu forces the CPU renderer and removes EGL from the process (also
useful on systems with unreliable GPU drivers).
For development only, lockme --dev-mode makes Esc unlock and exit cleanly
without talking to PAM. This is intentionally insecure and should not be used
for a real screen lock, but it provides a compositor-safe escape hatch while
testing lockme itself.
For screenshots while developing the Matrix renderer, use:
lockme --dev-mode --dev-windowThis opens the Matrix rain in a normal Wayland window and does not lock the session or start PAM.
Matrix rain is rendered through a Sokol/EGL/GLES path when available. The glyph
atlas is generated from lockme's built-in high-resolution alpha glyph source,
which is rasterized from the CNTR Koine Greek TrueType font; if GPU setup fails,
lockme warns and falls back to the existing software renderer. The GPU rain
pipeline adapts MIT-licensed shader logic from Rezmason's Matrix rain renderer.
While locked, Alt-B toggles between Matrix rain and a blank screen.
Typing rotates the surface through a configurable input palette, and failed
authentication shows a solid failure color. The --init-color, --input-color
(repeatable), and --fail-color flags override the defaults.
| State | Color | RGB |
|---|---|---|
At rest (init) |
Black | 0x000000 |
| Typing (Father) | Indigo | 0x4B0082 |
| Typing (Son) | Royal Blue | 0x003366 |
| Typing (Spirit) | Life Green | 0x006400 |
| Auth failure | Crimson | 0x8B0000 |
Repeating --input-color defines a custom palette. The first occurrence
replaces the built-in palette; later occurrences append:
lockme --input-color 0x111111 --input-color 0x222222 --input-color 0x333333lockme reads an optional KDL 2.0 configuration file. The discovery order is:
--config <path>(must exist if specified),$XDG_CONFIG_HOME/lockme/config.kdl(default~/.config/lockme/config.kdl),- each
$XDG_CONFIG_DIRS/lockme/config.kdlin order (default/etc/xdg).
--no-config disables the search entirely. CLI flags always win over values
set in the config file. Parse and validation errors abort startup with a
diagnostic on stderr.
A documented template lives at examples/config.kdl and is dropped into
~/.config/lockme/config.kdl by nimble installBin/nimble deploy only if
that file does not already exist. Keys absent from the config always take the
built-in default, so an existing config never breaks when new options are
added; diff your file against examples/config.kdl after pulling updates to
see what is new. Both 0xRRGGBB and #RRGGBB color forms
are accepted in the config file (the CLI requires the 0x form).
Legacy Matrix font keys from older templates are accepted as no-ops; Matrix
glyphs now always come from the built-in Koine Greek glyph source.
By default, Matrix glyph size follows the upstream Matrix renderer's 80-column
grid. Set matrix-cell-scale "auto" in the config for responsive sizing, or
set it to a number from 1.0 through 8.0 only if you want a fixed glyph scale.
The built-in Matrix glyph alpha data is generated from the vendored CNTR
KoineGreek.ttf font in third_party/cntr-font, copyright 2012-2023 Alan
Bunning / Center for New Testament Restoration and distributed under CC BY-SA
4.0. Run nimble regenFont after changing the vendored font or glyph list; the
generator uses Python with Pillow and fontTools.
The release build uses size-oriented flags (--opt:size --mm:orc -d:useMalloc -flto -Wl,--gc-sections -Wl,-s) so that the KDL parser
and its transitive dependencies (bigints, unicodedb) do not bloat
the binary. The current stripped output is 558 KiB (570,808 bytes).
Run nimble sizecheck to print the size of your build.
lockme is Linux-only. It relies on the following Linux-specific facilities
to harden the password buffer and the auth child:
mlock(2)andmadvise(MADV_DONTDUMP)on a page-aligned password buffer, preventing it from being paged to swap or included in core dumps.- Best-effort
mlockall(2)on the locker process and auth child to keep transient password material on the stack and in libc/PAM internals out of swap whenRLIMIT_MEMLOCKpermits it. Matrix mode and the auth child useMCL_CURRENT;--blankusesMCL_CURRENT | MCL_FUTUREin the locker process. explicit_bzero(3)(glibc/musl) for password clearing that the compiler is not permitted to elide.prctl(PR_SET_DUMPABLE, 0)on both the parent and the auth child to block ptrace and/procsnooping by other processes of the same UID.prctl(PR_SET_NO_NEW_PRIVS, 1)on the parent after the PAM auth child is forked, so future parent-sideexecvecannot gain privileges without breaking PAM helpers such asunix_chkpwd.setrlimit(RLIMIT_CORE, 0)to suppress core dumps for the locker.close_range(2)(kernel 5.9+) in the auth child to drop inherited file descriptors before invoking PAM; falls back to a manual loop on older kernels.
lockme mirrors waylock's privilege-separation model: the parent process
holds the Wayland connection and renders the lock surface, while a forked
child performs PAM authentication over a length-prefixed pipe. The
password buffer:
- has a fixed
1024-byte capacity rounded up to a page, - is allocated via
posix_memalignandmlock'd for its lifetime, - is excluded from core dumps via
madvise(MADV_DONTDUMP), - is zeroed via
explicit_bzeroon every clear (including after each failed authentication and after eachBackspace), - has its protections re-applied after
--fork-on-lock.
The auth child also re-applies best-effort mlockall(MCL_CURRENT) before
initializing PAM, because memory locks are not inherited across fork(2).
With --fork-on-lock, the background process additionally redirects
stdin/stdout/stderr to /dev/null to avoid SIGPIPE if the parent
shell is closed.
RLIMIT_MEMLOCK must be at least the password buffer size (one page).
Process-wide mlockall needs more headroom and can fail under normal
desktop mappings or restrictive limits. That best-effort failure is reported
only at --log-level debug; the locker continues with the password buffer's
own mandatory mlock still active.
See audit.md for the running security and performance review log, including the recent PAM, Matrix renderer, signal handling, and SHM sizing checks.
lockme performs authentication through PAM. The shipped default
pam.d/lockme is a minimal, auditable, distribution-independent chain:
auth optional pam_faildelay.so delay=2000000
auth required pam_unix.so nullok
account required pam_unix.so
This verifies a plain Unix password and applies a two-second failure delay without recording faillock tallies or locking the user out after mistyped passwords. That avoids the bad screen-locker failure mode where three incorrect attempts can block a later correct password for several minutes. Most Linux users authenticate this way and gain nothing from a larger PAM stack on their screen locker, so this is the default.
The default does NOT enable pam_systemd_home, GNOME Keyring or
KWallet auto-unlock, fingerprint readers, smartcards, or any other
auxiliary auth method. If you need any of those, install the full PAM
file instead:
nimble installPamFull
# or, without nimble:
sudo install -m 0644 pam.d/lockme.full /etc/pam.d/lockmeThe full file contains a single line, auth include system-auth, which
delegates authentication to the distribution's system-auth chain.
This is the same approach waylock and most other screen lockers ship
with. The trade-off is that lockme's effective auth surface becomes
whatever system-auth says it is. To audit it, read
/etc/pam.d/system-auth; edits there (for example a debugging
auth sufficient pam_permit.so line, or a pam_succeed_if clause that
bypasses checks for a group) silently affect lockme as well, and
lockme cannot defend against this.
For PAM debugging, run ./lockme --log-level debug --blank --dev-mode from
a terminal. Debug logging records PAM status codes and messages only; it
does not log password contents, password length, or prompts. In --dev-mode,
Esc exits without asking PAM, which keeps manual auth tests recoverable.
To revert to the default minimal chain at any time:
nimble installPam