Index ¦ Archives ¦ Atom

Kernel livepatching on Fedora part 1

Introduction

This blog post uses Fedora 40 and the 6.10 kernel. The focus is to get the technology working on a 'real' system via a series of experiments. Serial experiments help build-up the knowledge, experience, tooling etc. to handle real-world use-cases.

Kernel livepatching is a nice technology that doesn't see heavy usage outside of some specific (read, expensive) use cases. The technology is simple enough to understand: A function is marked for replacement and the kernel attempts to replace all references to it. The kernel can do that since it schedules all tasks, and in the pauses between tasks (while context switching) the kernel can modify the task environment while the task is effectively "frozen".

I hope to explore in this blog series the more unusual ways the technology can be used to:

  • test changes

  • debug problems

  • address security issues (specifically, unusual ways to "fix" security issues, like disabling syscalls)

Preparation

Install the following generic dependencies:

dnf -y install git make gcc wget tar

Install Linux kernel headers:

dnf -y install kernel-devel

Sanity test

Before creating a new livepatch module it is a good idea that the setup can compile a known-good livepatching sample. It is highly recommended to create a separate user to run the kernel Makefiles, as they have had bugs (a long time ago) that would delete the root filesystem (i.e. a classic rm -rf / hanging around in a makefile). I run these commands in a VM, so the precaution isn't as relevant.

To create a new user and switch to it, run:

adduser build
su - build
gpasswd -a build wheel

To enable passwordless sudo on the user, run visudo and uncomment the following line:

## Same thing without a password
%wheel  ALL=(ALL)       NOPASSWD: ALL

Create a directory for the sanity test:

mkdir -p sanity-test
cd sanity-test
wget https://raw.githubusercontent.com/torvalds/linux/master/samples/livepatch/livepatch-sample.c

Create a Makefile:

obj-m = livepatch-sample.o

Compile the livepatch sample:

make -C/lib/modules/$(uname -r)/build M=$(pwd) modules

The compilation will output something similar to the following:

make -C/lib/modules/$(uname -r)/build M=$(pwd) modules
make: Entering directory '/usr/src/kernels/6.10.4-200.fc40.x86_64'
  CC [M]  /home/build/sanity-test/livepatch-sample.o
  MODPOST /home/build/sanity-test/Module.symvers
  CC [M]  /home/build/sanity-test/livepatch-sample.mod.o
  LD [M]  /home/build/sanity-test/livepatch-sample.ko
  BTF [M] /home/build/sanity-test/livepatch-sample.ko
Skipping BTF generation for /home/build/sanity-test/livepatch-sample.ko due to unavailability of vmlinux
make: Leaving directory '/usr/src/kernels/6.10.4-200.fc40.x86_64'

There will be a number of files generated, we're only interested in the livepatch-sample.ko file:

[build@fedora sanity-test]$ ls
livepatch-sample.c  livepatch-sample.ko

Insert the module into the running kernel with:

sudo insmod livepatch-sample.ko

On Fedora 40 this will result in the following message in the dmesg -w output:

[ 1693.302006] livepatch_sample: loading out-of-tree module taints kernel.
[ 1693.302985] livepatch_sample: tainting kernel with TAINT_LIVEPATCH
[ 1693.303560] livepatch_sample: module verification failed: signature and/or required key missing - tainting kernel
[ 1693.305566] livepatch: enabling patch 'livepatch_sample'
[ 1693.311902] livepatch: 'livepatch_sample': starting patching transition
[ 1694.920570] livepatch: 'livepatch_sample': patching complete

Configuring kernel module signatures is a separate topic. The important thing is the last line: "patching complete" which indicates that all references to the function have been replaced. We can check that the livepatch has been applied by running:

cat /proc/cmdline 
this has been live patched

To disable the patch, we can run:

echo "0" > /sys/kernel/livepatch/livepatch_sample/enabled

The output of dmesg -w can again be checked to see the progres of the unpatching operation:

[ 1693.311902] livepatch: 'livepatch_sample': starting patching transition
[ 1694.920570] livepatch: 'livepatch_sample': patching complete
[ 1999.451428] livepatch: 'livepatch_sample': starting unpatching transition
[ 2000.945583] livepatch: 'livepatch_sample': unpatching complete

Now, remove the module from the kernel:

rmmod livepatch_sample

The cmdline can again be checked to see that it is back to the original value:

cat /proc/cmdline
BOOT_IMAGE=(hd0,gpt2)/vmlinuz-6.10.4-200.fc40.x86_64 root=UUID=356cb170-4518-42db-b02a-ad6530cf8956 ro console=tty0 rd_NO_PLYMOUTH console=ttyS0,115200

First livepatch

Let's see if we can patch something a bit more exciting, like the output of cat /proc/cpuinfo.

For that, a copy of the original function is required. The easiest way to obtain a copy of the kernel .src.rpm package is to navigate to https://koji.fedoraproject.org and search for the "kernel" package, or to use dnf directly:

dnf download --source kernel

Depending on how unreliable the local mirrror is, it might be better to use koji. For the system I am using the correct kernel .src.rpm package is:

wget https://kojipkgs.fedoraproject.org//packages/kernel/6.10.4/200.fc40/src/kernel-6.10.4-200.fc40.src.rpm

Check the output of rpm -qa | grep kernel to find the correct srpm version to download. Unpack the package with:

rpm -Uvh kernel-6.10.4-200.fc40.src.rpm

Untar the kernel source in rpmbuild/SOURCES:

tar -xvf linux-6.10.4.tar.xz

The file we are looking for is for the x86 architecture:

/home/build/rpmbuild/SOURCES/linux-6.10.4/arch/x86/kernel/cpu/proc.c

Let's replace a function that seems to be easy to replace on x86_64: show_cpuinfo. Dealing with references is a chore, so the function has been modified to remove some features (displaying CPU bugs, displaying power management). The whole file would look similar to this:

// SPDX-License-Identifier: GPL-2.0

#include <linux/module.h>
#include <linux/livepatch.h>
#include <linux/seq_file.h>

// See linux-6.10.4/arch/x86/kernel/cpu/proc.c for detailed licensing information
static void show_cpuinfo_core(struct seq_file *m, struct cpuinfo_x86 *c,
                              unsigned int cpu)
{
        seq_printf(m, "physical id\t: %d\n", c->topo.pkg_id);
        seq_printf(m, "siblings\t: %d\n",
                   cpumask_weight(topology_core_cpumask(cpu)));
        seq_printf(m, "core id\t\t: %d\n", c->topo.core_id);
        seq_printf(m, "cpu cores\t: %d\n", c->booted_cores);
        seq_printf(m, "apicid\t\t: %d\n", c->topo.apicid);
        seq_printf(m, "initial apicid\t: %d\n", c->topo.initial_apicid);
}

static void show_cpuinfo_misc(struct seq_file *m, struct cpuinfo_x86 *c)
{
        seq_printf(m,
                   "fpu\t\t: yes\n"
                   "fpu_exception\t: yes\n"
                   "cpuid level\t: %d\n"
                   "wp\t\t: yes\n",
                   c->cpuid_level);
}

static int patched_show_cpuinfo(struct seq_file *m, void *v)
{
        struct cpuinfo_x86 *c = v;
        unsigned int cpu;

        cpu = c->cpu_index;
        seq_printf(m, "processor\t: %u\n"
                   "vendor_id\t: %s\n"
                   "cpu family\t: %s\n"
                   "model\t\t: %s\n"
                   "model name\t: %s\n",
                   cpu,
                   "Anderson Robotics",
                   "Sentient Rocks",
                   "REDACTED",
                   "Standard Lightning-infused rock MK2");

        if (c->x86_stepping || c->cpuid_level >= 0)
                seq_printf(m, "stepping\t: %d\n", c->x86_stepping);
        else
                seq_puts(m, "stepping\t: unknown\n");
        if (c->microcode)
                seq_printf(m, "microcode\t: 0x%x\n", c->microcode);

        /* Cache size */
        if (c->x86_cache_size)
                seq_printf(m, "cache size\t: %u KB\n", c->x86_cache_size);

        show_cpuinfo_core(m, c, cpu);
        show_cpuinfo_misc(m, c);

        seq_printf(m, "\nbogomips\t: %lu.%02lu\n",
                   c->loops_per_jiffy/(500000/HZ),
                   (c->loops_per_jiffy/(5000/HZ)) % 100);

        if (c->x86_tlbsize > 0)
                seq_printf(m, "TLB size\t: %d 4K pages\n", c->x86_tlbsize);
        seq_printf(m, "clflush size\t: %u\n", c->x86_clflush_size);
        seq_printf(m, "cache_alignment\t: %d\n", c->x86_cache_alignment);
        seq_printf(m, "address sizes\t: %u bits physical, %u bits virtual\n",
                   c->x86_phys_bits, c->x86_virt_bits);

        seq_puts(m, "\n\n");

        return 0;
}

static struct klp_func lfuncs[] = {
        {
                .old_name = "show_cpuinfo",
                .new_func = patched_show_cpuinfo,
        }, { }
};

static struct klp_object lobjs[] = {
        {
                .funcs = lfuncs,
        }, { }
};

// The .replace = true indicates that loading this patch replaces
// the previously loaded patch
// See https://docs.kernel.org/livepatch/cumulative-patches.html
// Hence, the highlander references

static struct klp_patch lpatch = {
        .mod = THIS_MODULE,
        .objs = lobjs,
        .replace = true,
};

static int linit(void)
{
        pr_info("Adding highlander livepatch...");
        return klp_enable_patch(&lpatch);
}

static void lexit(void)
{
        pr_info("Removing highlander livepatch...");
}

module_init(linit);
module_exit(lexit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");

Save the above file as highlander.c Modify Makefile to contain:

obj-m=highlander.o

And compile the module with:

make -C/lib/modules/$(uname -r)/build M=$(pwd) modules

Load the module with:

sudo insmod highlander.ko

You should now be able to see the following when running cat /proc/cpuinfo:

cat /proc/cpuinfo | head -n 25
processor       : 0
vendor_id       : Anderson Robotics
cpu family      : Sentient Rocks
model           : REDACTED
model name      : Standard Lightning-infused rock MK2
stepping        : 1
microcode       : 0xa404102
cache size      : 512 KB
physical id     : 0
siblings        : 1
core id         : 0
cpu cores       : 1
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 16
wp              : yes

bogomips        : 5789.09
TLB size        : 1024 4K pages
clflush size    : 64
cache_alignment : 64
address sizes   : 48 bits physical, 48 bits virtual

Conclusion of part 1

There's more that could be explored here but I am time-limited today.

The two important take-aways are:

1) Resolving references to functions is non-trivial. You might have to copy-paste a lot of code which is non-ideal.

2) Not all functions can be patched. Check /proc/kallsyms, if the function is there, it can be patched. If the function you wish to replace is called by a function from kallsyms, (i.e. it is likely inlined) replace the kallsyms function and modify the function so that it calls the function you couldn't patch otherwise. For example:

[ 5011.921889] livepatch: symbol 'show_cpuinfo_misc' not found in symbol table

While it's not possible to directly patch show_cpuinfo_misc, show_cpuinfo can be patched and the call to the function show_cpuinfo_misc replaced with a different function.

References

https://docs.kernel.org/livepatch/livepatch.html

LFD420 from the Linux Foundation or Jerry Cooperstein's books are a good reference.

© Bruno Henc. Built using Pelican. Theme by Giulio Fidente on github.