Skip to content

Latest commit

 

History

43 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lkps — live kernel patch service

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.

The Status page


The honest version first

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.

What the scheduled run actually does

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.

When it runs

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 default

The Schedule page

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

Install

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" lkps

then 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 --check

kpatch on Arch

The 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 yourself

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

Getting to a patchable kernel

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 it

Three ways to run a different kernel

They 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 kernel

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

Switching kernel without a reboot

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

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

The Kernels page

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.

Everything from the program

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 line

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.

Watching a build

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.

Choosing what goes in the kernel

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

The Build page

Any group can be added to any profile:

lkps build --strategy localmod --with net,gpu

Groups 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 changed

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

The Kernel Options page

What live patching costs you

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.

What cannot be turned off

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.

Architecture

  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.

What it will not do

  • 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 remove refuses 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 make as root. Compiles run as the lkps account.
  • Fill your disk. Builds are refused below a configurable free-space floor, and installs are refused if /boot cannot take the result.
  • Install a kernel with no way to boot it. grub.cfg is regenerated, syntax-checked, and confirmed to contain an entry for the new kernel before the install is allowed to stand.

Configuration

/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

Development

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 offscreen

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

Layout

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

Recovering

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 1 and lkps 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/.

About

A system daemon that builds livepatch-capable Linux kernels from upstream source and keeps the running one patched — command line, GTK3 interface, and a weekly timer that never reboots you

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages