A system daemon that builds livepatch-capable Linux kernels from upstream source, installs them alongside your distribution's kernel, and — on a schedule you choose — applies new stable-release fixes to the running kernel without a reboot where that is possible, and builds a new kernel where it is not.
It is the companion to systemMonoger: that tool's Kernel page reports that live patching is unavailable on a stock Arch kernel. This one is what fixes that.
Live patching is not a version upgrade. It replaces individual functions in the kernel that is running right now. That is what makes it useful, and what bounds it:
| Can be live-patched | Cannot be live-patched |
|---|---|
| A fix inside a function body | A change to a data structure's layout |
| A security fix in a driver or subsystem | Code that already ran at boot |
| Most stable point-release fixes | Moving from 7.2 to 7.3 |
So 7.2.3 → 7.2.4 is often applicable live. 7.2 → 7.3 never is. When a
release cannot be applied live, the only route is a new kernel and a reboot —
and this service will build and install one, but it will never reboot your
machine.
On a stock Arch kernel, nothing can be patched at all. Arch ships its
kernels without CONFIG_LIVEPATCH, so the machinery is simply absent. The
first thing this service does for you is build a kernel that has it. You boot
that once, manually, and from then on Mondays can be reboot-free.
Building live patches needs kpatch, which is not in Arch's repositories.
It comes from the AUR. Without it, kernel builds still work and the weekly job
still keeps you current — it just always takes the build-and-reboot route.
fetch the list of releases in the tracked series (a few kB; nothing downloaded)
│
├─ nothing newer ──────────────────────────────────► done
│
└─ 7.2.4 is out
│
├─ try to build a live patch for 7.2.3 → 7.2.4
│ succeeded ──► load it into the running kernel
│ the fix is live. No reboot. Done.
│
└─ kpatch says these changes cannot be applied live
└─► build 7.2.4, install it alongside your existing kernels,
mark a reboot as pending, and stop there.
If the machine was off when the run was due, it happens shortly after the machine next boots rather than being skipped. It never wakes the machine, and never reboots it.
The default is every Monday at 02:00, which is a guess about somebody else's week. Change it on the Schedule page, or from the command line:
lkps schedule # what is set now
lkps schedule set --interval weekly --day Sat --time 03:30
lkps schedule set --interval daily --time 05:00
lkps schedule set --interval monthly --day 1 --time 04:00
lkps schedule reset # back to the defaultThe schedule is written as a systemd drop-in under
/etc/systemd/system/lkps-update.timer.d/; the packaged unit is never edited,
so reset is one file being deleted and an upgrade cannot clobber your choice.
Nothing is written until systemd-analyze has confirmed it parses — an
OnCalendar systemd rejects does not fail at write time, it fails silently
when the timer next loads, and the only symptom is that updates stop.
A random delay (15 minutes by default) is added after the chosen time so a fleet of machines does not all start cloning at the same instant.
The same controls are on the Kernel page of systemMonoger, where the service is detected automatically.
cd ~/Projects/liveKernelPatchService
sudo ./install.sh --add-me--add-me puts you in the lkps group. This only affects reading. Without
it you get:
Not permitted to read the service status at /run/lkps/lkps.sock
and the status readouts are empty — but build, install, update, remove,
set-default and revert all still work, because they authenticate through
polkit and run as root, and root is not subject to the socket's group
permission. If you have already installed without it:
sudo gpasswd -a "$USER" lkpsthen log out and back in, or newgrp lkps for the current shell.
The installer creates an unprivileged lkps system account, installs
everything root-owned, enables the daemon and the weekly timer, and then
verifies its own work — file ownership, socket permissions, that the daemon is
running and that lkps status answers. Re-check any time:
sudo ./install.sh --checkThe AUR kpatch package is incomplete for this purpose. Its PKGBUILD removes
the whole kmod directory from the build to work around the deprecated core
module failing to compile — and that takes kmod/patch with it, which is the
module template kpatch-build compiles every patch into, livepatch path
included. So kpatch-build installs, runs, prepares the kernel tree, compiles
for a minute, and then dies with cp: cannot stat '/usr/share/kpatch/patch'.
lkps status checks for the template and says so before any of that happens,
and the service can put it right itself:
lkps repair-kpatch # uses the sources your package manager kept
lkps repair-kpatch --download # if there are none on this machine
lkps repair-kpatch --source <tarball> # or name one yourselfor the Install the missing kpatch files button that appears on the Status page when this is what is wrong.
Doing it by hand is worth avoiding: kmod/patch/kpatch.h is a symlink to
../core/kpatch.h, so copying the directory alone leaves a link pointing at
nothing — the file lists fine and kpatch-build, which copies with -L, stops
on it. Copying again over the result then fails with not writing through
dangling symlink. repair-kpatch resolves links, builds the template beside
its destination and swaps it in, so a failure never leaves a half-populated
directory. A download is verified against the checksum your own package manager
recorded, when a PKGBUILD is there to compare with.
The daemon runs with PrivateTmp=yes, so anything under /tmp is invisible to
it — a copy extracted there has to be named with --source. ProtectHome is
read-only rather than yes for this one reason: the release tarball lives
under ~/.cache, and a daemon that cannot see it reports the sources missing
while they sit in plain view. Writing to home remains impossible.
lkps status # explains exactly what is missing
lkps build # 25–40 min, ~9 GB; runs at low priority
# reboot, choosing the new kernel at the boot menu
lkps status # should now say live patching is available
lkps set-default 7.2.3-lkps # once you are happy with itThey get called "no reboot" interchangeably. They are not the same thing.
| interruption | what it gets you | |
|---|---|---|
| Live patch | none at all | function-level fixes, one point release at a time |
Hand-over (kexec) |
seconds; every process stops | a whole new kernel, any version, skipping firmware |
| Reboot | a minute | the same as a hand-over, and it always works |
A live patch is the only one where nothing stops, and it is the most limited: it replaces functions in the kernel already running, so a release that changes a data structure or boot-time code cannot be delivered that way at all.
A hand-over is the honest answer to "compile a new kernel and load it without rebooting" — the machine does not reboot, but it does restart. Your session ends and unsaved work is lost. What it saves is the firmware, the POST and the boot menu.
lkps kexec # is it possible here, and what is staged
lkps kexec 7.2.3-lkps # stage it; nothing happens yet
lkps kexec --now # hand over
lkps kexec --cancel # drop the staged kernelStaging and jumping are separate on purpose: staging proves the kernel and
initramfs are readable and acceptable, and is reversible. The jump goes through
systemctl kexec rather than kexec -e, so filesystems are unmounted and
services stopped first — going straight from a mounted root is how a filesystem
gets corrupted. A kernel with no initramfs is refused rather than staged, since
it would hang looking for a root filesystem instead of failing cleanly.
It needs kexec-tools (sudo pacman -S kexec-tools); the kernel side is
already on in kernels this service builds.
Where live patching is available, the Kernels page offers Switch to this live on a selected kernel, and the command line has the same thing:
lkps switch 7.2.5This does not boot another kernel. It compiles the source difference between
the running release and the target and redirects the affected functions inside
the kernel already in memory. uname -r keeps reporting the release the kernel
was built as; the Status page reports the version it is effectively at.
Parallel compiles are capped against available memory, not just core count. A
kernel built for live patching carries full debug information and each compile
wants well over a gigabyte, so one job per CPU can run a machine out of memory
— the kernel kills a compiler and the build fails with gcc: fatal error: Killed signal terminated program cc1, which says nothing about memory. When
that happens anyway it is reported as a killed build, not as a verdict on the
patch, and build.jobs can be lowered by hand.
It is not fast. Applying a finished patch takes milliseconds — that is the part people mean when they say live patching is instant. Building one is a different job: kpatch compiles the kernel twice, once clean and once patched, and compares every object to find what changed. That costs roughly what a kernel build costs, 30 to 60 minutes at full CPU, and it runs at low priority so the machine stays usable. What live patching saves you is the reboot, not the time.
It only goes forward, and only within one series — across a series the diff is most of the kernel and kpatch cannot build it. And it lasts until the next reboot, because a patch lives in memory. The button greys itself out with the reason in its tooltip for each of those cases, and the service refuses them independently rather than trusting the button.
A kernel that is built and installed but not the one running is offered on the
Status page with a button that makes it the default — nothing is rebuilt,
it is already there and in the boot menu. lkps kernels says the same thing at
the end of its listing.
The build is not made the default boot entry. A kernel that has never booted should not be what boots automatically; you pick it once, then promote it deliberately.
Nothing this service needs has to be arranged in a terminal. The Requirements
page — and lkps doctor — surveys what is missing and offers the repair for
each:
lkps doctor # what is needed, what is missing, and why each matters
lkps doctor --fix # apply every repair that is offered| Missing | What it does about it |
|---|---|
build tools, pahole, kexec-tools |
installs them with the system package manager |
| kpatch not installed | builds the project's own release archive into /usr/local |
| kpatch installed but incomplete | puts back the files the AUR package leaves out |
not in the lkps group |
adds the login accounts, and says a new group needs a re-login |
| the weekly timer off | enables it — lkps timer enable / disable |
| a boot menu listing kernels that are gone | rewrites it — lkps boot-menu |
An optional item says so rather than looking broken: a machine without
kexec-tools is not misconfigured, it just cannot do one of the three things
here.
Two things are deliberately not automated. Package removal is never offered. And kpatch is built from the release archive the project publishes, with its checksum checked, rather than by driving an AUR helper — installing from the AUR means building whatever a recipe says, and there is no careful way to do that on someone's behalf.
| Command | What it does |
|---|---|
lkps status |
Kernel, patch capability, pending reboot, last run |
lkps capability |
Why live patching does or does not work here |
lkps check |
Ask upstream whether there is a newer release (no root; --refresh to skip the cached answer) |
lkps build [version] |
Build and install a kernel (--strategy picks a profile, --with adds driver groups) |
lkps profiles |
How much of the kernel each profile builds, and what each driver group covers |
lkps update |
Run the Monday cycle now |
lkps kernels |
What is installed, and what built it |
lkps patches |
Live patches currently loaded |
lkps doctor [--fix] |
What the service needs, and repair whatever is missing |
lkps timer enable|disable |
Turn the scheduled update on or off |
lkps boot-menu |
Rewrite the boot menu from what is installed |
lkps kexec <release> |
Stage a kernel and hand over to it, skipping the firmware |
lkps schedule |
When the automatic update runs; set and reset change it |
lkps switch <version> |
Carry the running kernel to another point release without rebooting |
lkps revert <name> |
Remove a live patch immediately |
lkps set-default <release> |
Make a kernel the default boot entry |
lkps remove <release> |
Delete an installed kernel |
lkps log -f |
Watch a running build |
lkps options |
Every kernel option, with the locked ones marked |
lkps options set CONFIG_X n |
Turn one option off (or y / m) |
lkps options reset |
Forget every option change |
lkps trim |
Build only what this system is using (--full to undo) |
lkps config get -v |
Every setting, explained |
Commands that change something re-run themselves under pkexec; lkps check
does not, because it changes nothing — group membership is enough, and it
falls back to asking for authentication only if you are not in the group.
Ctrl-C during
a build detaches rather than killing it — rejoin with lkps log -f.
There is also a GTK3 interface: lkps-gui, or Kernel Patch Service in your
application menu.
Starting a build or an update opens a progress window showing the current phase, a percentage, elapsed time and an estimate of what is left, with the live output behind an expander.
The percentage is real rather than decorative. Phase boundaries come from the daemon's own log lines, and inside the compile — which is most of the hour — make prints one line per object file, so counted lines measure actual work. What is not known on a first build is how many object files there will be, so the first run shows a live count and an indeterminate bar, records the total when it finishes, and every build after that shows a true percentage. No estimate is offered until there is something to base one on.
Closing the window does not stop the build. Continue in background hides it, and Actions → Show Build Progress brings it back.
Two levels, on the Build and Kernel Options pages or from the command line.
The profile decides the starting point. Both extremes are wrong most of the time: everything your distribution builds takes an hour and a half and fills 25 GB with drivers for hardware that does not exist, while only the modules loaded at this instant means the USB stick you plug in next week has no driver. The profiles in between start from what this machine is using and add back whole subsystems that something can reasonably be plugged into.
lkps profiles # the table, with what each one adds
lkps build --strategy portable # the default
lkps config set build.strategy broad # change it for good| Profile | What it builds | Roughly |
|---|---|---|
full |
Everything your distribution builds | 60–90 min, 25 GB |
broad |
This machine + network, hot-plug, filesystems, disks, cameras, graphics | 35–50 min, 11 GB |
portable |
This machine + network, hot-plug, filesystems | 25–40 min, 9 GB |
localmod |
Only the modules loaded right now | 10–20 min, 3 GB |
Any group can be added to any profile:
lkps build --strategy localmod --with net,gpuGroups are net, removable, fs, storage, media and gpu. They are
worked out from the source tree being built — every symbol is attributed to
the directory of the Kconfig file that defines it — so they stay correct
across kernel releases rather than being a hardcoded list that rots. The value
each option gets comes from this system's own kernel configuration, so a
group can only restore coverage your distribution already ships; it never
invents a driver. What is deliberately left out is listed in
lkpsd/profiles.py, with a reason on each line: amateur radio, ATM, CAN bus,
ARCnet, USB device-mode controllers, cluster filesystems.
lkps trim is still there as the shortcut to the two extremes (localmod,
or --full).
Whatever the profile, a set of options is always restored afterwards: the things userspace loads on demand — the loop device, the device mapper, dm-crypt, FUSE, overlayfs, squashfs, zram, tun, user namespaces. A reduction keeps what is loaded at the moment it runs, and nothing mounts an ISO or opens an encrypted container during a build, so these are dropped every time. Their absence surfaces days later as an error that names no kernel option at all. A build that would lose one of them is refused, unless you turned it off yourself.
Individual options are applied on top:
lkps options BTRFS # search
lkps options set CONFIG_BTRFS_FS n # turn it off
lkps options --only changed # what you have changedChoices live in /etc/lkps/overrides.config, separate from the generated
configuration, so they survive every rebuild and every kernel version change.
Nothing takes effect until the next build.
kpatch-build refuses to work on a kernel built with any of four options, and checks for them before doing anything:
| Option | |
|---|---|
CONFIG_X86_KERNEL_IBT |
Indirect Branch Tracking, a control-flow integrity feature |
CONFIG_DEBUG_INFO_SPLIT |
debug information in a form kpatch cannot read |
CONFIG_GCC_PLUGIN_LATENT_ENTROPY |
changes code kpatch has to compare |
CONFIG_GCC_PLUGIN_RANDSTRUCT |
makes two builds incomparable |
Arch enables the first. Kernels built here therefore turn it off, which is a
real reduction in hardening — you cannot have both Indirect Branch Tracking
and kpatch live patching. The build log says so, lkps status lists it as a
note, and a configuration that still carries one of these is refused before the
compile starts rather than after.
If that trade is not one you want, update.mode = build-only gets you new
kernels on a schedule with the distribution's hardening intact; you just reboot
into them.
Options this machine provably needs are locked, and the set is worked out from the hardware rather than hardcoded — a fixed list would be wrong on someone else's machine, which is exactly where being wrong is unrecoverable.
$ lkps options set CONFIG_EXT4_FS n
error: CONFIG_EXT4_FS cannot be turned off.
This machine needs it — your root filesystem is ext4.
A kernel built without it would not boot on this machine, and the only way
back would be the boot menu.
Locked because of this machine: the root and /boot filesystem drivers, the
storage driver reaching the root disk, and UEFI support if it booted that way.
Locked everywhere: live patching's requirements, systemd's documented
requirements, and the handful of options without which nothing reaches
userspace. Turning a locked option on is always allowed — only turning one
off is refused.
lkps / lkps-gui lkpsd (root, systemd service)
─────────────── ────────────────────────────
no privileges owns one job slot at a time
read status over the socket ──────► authorises by SO_PEERCRED
write commands via pkexec ──────► builds as the unprivileged 'lkps' user
installs alongside, never over
The daemon exists because the work takes an hour. A build cannot be a request/response call: you must be able to start one, close the window, and rejoin it later. It writes every job to a log and fans the output out to whoever is watching.
Authorisation comes from the kernel, not the client. The socket is
root:lkps 0660, so group members can read status; every command that changes
anything additionally requires the peer's real uid to be 0, which clients reach
through polkit.
- Reboot your machine. Ever, for any reason.
- Replace the kernel you are running. New kernels are added under their own release name; a build whose name would collide with the running kernel is refused before it starts.
- Remove the running kernel.
lkps removerefuses it outright. - Make an unbooted kernel the default, unless you ask.
- Leave a half-applied patch. A patch that does not finish transitioning is reverted, and the kernel is left exactly as it was.
- Run
makeas root. Compiles run as thelkpsaccount. - Fill your disk. Builds are refused below a configurable free-space floor,
and installs are refused if
/bootcannot take the result. - Install a kernel with no way to boot it.
grub.cfgis regenerated, syntax-checked, and confirmed to contain an entry for the new kernel before the install is allowed to stand.
/etc/lkps/config.toml, or lkps config set <name> <value>:
| Setting | Default | Notes |
|---|---|---|
source.series |
auto |
Follows the running kernel, e.g. 7.2 |
build.strategy |
portable |
One of full, broad, portable, localmod — see lkps profiles |
build.localversion |
-lkps |
Keeps the kernel's name distinct from the distribution's |
install.set_default |
false |
Whether installing also changes the default boot entry |
install.keep_kernels |
3 |
Older build artefacts are pruned beyond this |
update.mode |
livepatch-then-build |
Also livepatch-only, build-only, off |
limits.min_free_build_gb |
30 |
Builds are refused below this |
python3 tools/servicetest.py # starts a real daemon in a sandbox
python3 tools/sourcetest.py # git handling against a real repo in a temp dir
python3 tools/kerneltest.py # config derivation and bootloader entry matching
python3 tools/guitest.py # renders every GUI page offscreenservicetest.py runs unprivileged with every path redirected into a temporary
directory — including BUILDS_DIR, /boot and the module root, which are
absolute constants and so have to be redirected by name. Its main job is the authorisation boundary: run as an ordinary user,
every write command must be refused, every read command must answer, and
check -- which changes nothing and therefore needs no password -- must be
allowed through. It also covers malformed requests and the job manager.
sourcetest.py prepares the same source tree three times in a row. That is not
padding: a bug here only appeared on the second run, because every git query
on the tree ran as root while the tree belonged to the build account, git
refused it, and capture() turned the refusal into an empty string.
guitest.py builds each page twice — once with no service running, once with a
full status payload — because the window has to render when the daemon is
absent or the user is not in the group.
lkpsd/ the privileged daemon
safety.py atomic writes, locking, audit log, rollback guard
runner.py no-shell execution, allowlisted, streams output
config.py schema-driven settings with validators
state.py what is built, installed, patched
profiles.py how much of the kernel to build, derived from the tree
schedule.py when the automatic update runs; a systemd drop-in
source.py git: cheap remote checks, shallow tag fetches
kconfig.py deriving a livepatch-capable configuration
build.py the compile
install.py /boot, modules, initramfs, bootloader
livepatch.py kpatch integration, loading, transition handling
update.py the Monday cycle
ipc.py socket protocol, peer credentials
daemon.py job manager and command dispatch
lkpscli/ the command line client
lkpsgui/ the GTK3 client
data/ systemd units, polkit policy, desktop entry, launchers
Everything this service adds is additive, so recovery is mostly "boot something else":
- The new kernel does not boot. Pick your distribution's kernel at the boot
menu — it is still there and still the default unless you changed that. Then
lkps remove <release>. - A live patch misbehaves.
lkps revert <name>restores the original code immediately. A reboot also clears every live patch. - A build fills the disk. Artefacts live under
/var/lib/lkps/builds;lkps config set install.keep_kernels 1andlkps prune. - You want it all gone.
sudo ./uninstall.sh --purge. Installed kernels are deliberately left alone; remove those yourself.
Everything the service has done is in /var/log/lkps/audit.log, and each job's
full output is in /var/log/lkps/jobs/.




