Бывает так, что серверные Linux-системы неделями или даже месяцами работают без перезагрузки. Переносить нагрузку с одного сервера на другие, устанавливать обновления, проверять, что после перезагрузки железо работает верно, запускать заново все необходимое ПО — это далеко не всегда просто, быстро и дешево.

Но как быть, если нужно устранить критические уязвимости или другие серьезные ошибки в ядре Linux?

Об одном из способов сделать это без перезагрузки системы и пойдет речь в этой статье.

Меня зовут Евгений, я работаю в компании YADRO. Задачами, связанными с разработкой и анализом компонентов ядра Linux, я занимаюсь уже около 15 лет.

Я расскажу, что такое livepatching (далее для удобства чтения будем использовать русскоязычную кальку), как такая технология появилась и как она применяется. А также покажу на примере, как можно самому сделать лайвпатч, чтобы исправить реальный баг в ядре Ubuntu 22.04 Server.

У статьи будет продолжение. Там подробно разберем, как именно лайвпатчи заменяют одни функции ядра Linux на другие, а также узнаем, на что нужно обращать внимание, если потребуется использовать все это в продакшене.

В основном речь пойдет о системах с архитектурой x86 и RISC-V (хотя технология работает и для ARM64, PowerPC, s390x).

Ошибки, ошибки кругом

Как и в любом ПО, в ядре Linux есть ошибки, и их там много. Бывают и уязвимости: в рассылке Linux CVE Announce каждый месяц публикуют десятки и сотни проблем в безопасности ядра Linux, многие из которых могут иметь серьезные последствия. Например, только за август 2024 там было опубликовано более 200 (!) уязвимостей с CVSS от 7.0 и выше.

При этом для многих серверных систем известная поговорка «Time is money» давно превратилась в «Uptime is money» (uptime — время, которое система работает без перезагрузки). Так что очень хочется исправлять ошибки в ядре Linux, не прерывая надолго работу системы. Лайвпатчи — один из способов это сделать.

«Точечные» обновления

Лайвпатчи могут пригодиться, если обновлять ядро Linux полностью долго и дорого, а в ядре при этом надо что-то поправить «точечно»: изменить от одной до нескольких сотен функций ядра. 

А если нужно изменить много функций: тысячи и больше? Например, перейти от stable-версии v6.6.40 к v6.6.70 или еще дальше — к v6.12.30. Лайвпатчи тут, скорее всего, не подойдут:

  • Не каждое исправление в исходном коде ядра Linux можно превратить в лайвпатч и применять в работающей системе.

  • Чем больше исправлений приносится в ядро лайвпатчем, тем больше вероятность, что эти исправления не удастся применить. 

Подробнее об этом поговорим во второй части статьи, когда будем глубже разбираться, как работают такие обновления. 

А вот для относительно небольших исправлений в ядре Linux лайвпатчи могут оказаться очень ценными.

Лайвпатч-обновления для ядра Linux поставляют многие компании: RedHat, SUSE, Canonical, Oracle, Cloud Linux (TuxCare / KernelCare) и другие. Как правило, — в рамках платной подписки, хотя у Canonical есть бесплатный вариант для небольшого числа машин с Ubuntu.

Разумеется, специалисты из этих компаний сами решают, какие исправления включить в очередное лайвпатч-обновление и когда его выпускать. Например, RedHat и Canonical обычно выпускают их раз в месяц–полтора или реже.

А как быть, если вы собираете и поддерживаете свой вариант Linux? Или вам нужны исправления не только security-багов, но и других проблем, критичных для вас и ваших клиентов? Что делать, если исправление бага вам нужно уже «позавчера», а не через месяц, когда компания-поставщик лайвпатчей выпустит очередную версию?

Сделать лайвпатч самостоятельно!

Есть мнение, что лайвпатчи могут делать только сверхквалифицированные специалисты, которых не так много, и все они, разумеется, работают в компаниях, продающих подписку на лайвпатч-обновления. На мой взгляд, это не так.

Готовить лайвпатчи не очень просто, но и не запредельно сложно. Вам будет достаточно хорошего знания языка С, опыта работы с компонентами ядра Linux и понимания типичных подводных камней. О последних поговорим в продолжении этой статьи, которое выйдет на следующей неделе.

Немного истории

Технологии «точечных» обновлений ядра Linux, не требующих перезагрузки, известны уже давно. Как минимум, - с 2008 года, когда появилась система Ksplice, впоследствии приобретенная компанией Oracle.

В 2013–2014 годах RedHat и SUSE создали свои инструменты для лайвпатчинга — Kpatch и kGraft. Примерно в то же время Cloud Linux выпустила систему KernelCare. В отличие от Kpatch и kGraft, KernelCare — проприетарная система, код механизма применения патчей там закрыт.

Разработчики из RedHat и SUSE быстро поняли, что нужна поддержка лайвпатчинга непосредственно в mainline-ядре Linux. Они объединили усилия, и уже в 2015 году первая реализация подсистемы «livepatch» вошла в версию 4.0 ядра Linux. На тот момент реализация из mainline-ядра сильно уступала по функциональности Kpatch и kGraft: например, там не поддерживалась атомарная замена лайвпатч-обновлений.

Но уже в версии ядра 5.1 (2019) в mainline появилось все, что было в Kpatch и kGraft, и даже больше. Тогда стало возможным в полной мере использовать лайвпатчинг из mainline-ядра в продакшене.

Патчим то, что важно

В первый раз я столкнулся с лайвпатчами в 2015 году, еще до начала работы в YADRO. Передо мной и моими коллегами встала такая задача. Клиенты компании, где я тогда работал, неохотно ставили обновления для нашего варианта Linux, так как перезагрузка системы означала длительный простой, что приводило к потере времени и денег. При этом время от времени в ядре этого варианта Linux выявлялись неприятные для клиентов ошибки: падения, снижение производительности, уязвимости.

И да, безопасность тогда «продавалась» не очень хорошо. Убедить людей поставить исправления для той или иной security-дыры было не всегда просто. А вот если где-то на клиентских серверах в определенных сценариях ядро начинало падать или сильно тормозить — клиенты сами нас торопили, просили сделать «заплатку» как можно скорее.

В итоге мы пришли к принципу «патчим то, что важно». За 6 лет, пока я участвовал в подготовке лайвпатчей, лишь около 30% исправленных таким образом багов в ядре были уязвимостями. Остальные 70% — важные проблемы со стабильностью и производительностью, которые проявлялись на системах наших клиентов.

Как ни странно, такой подход к выбору, что исправлять лайвпатчами, похоже, не очень популярен. Крупные компании, поставляющие исправления для ядра Linux в серверных системах, в основном, ориентируются на исправления уязвимостей. Так делают RedHat, Canonical, Oracle, Cloud Linux (TuxCare / KernelCare)

SUSE — одни из немногих, кто предлагает лайвпатчи не только для security-исправлений.

Livepatch-обновления

С точки зрения пользователя (в данном случае человека, отвечающего за обновления данной Linux-системы) лайвпатч — это просто модуль ядра Linux, в котором содержится как минимум следующее: 

  • исправленный код одной или более функций из vmlinux и/или из модулей ядра,

  • метаданные, указывающие, как применить эти исправления к соответствующим компонентам ядра Linux. 

Чтобы применить лайвпатч-обновление, нужно загрузить этот модуль ядра, например, с помощью insmod или modprobe, а затем активировать его, как правило, через sysfs. 

При этом старый код ядра Linux (тот, который хотим обновить) и новый (тот, что в лайвпатч-модуле) сосуществуют в памяти системы. Это дает возможность при необходимости отменить лайвпатч-обновление в runtime: деактивировать его через sysfs, а затем выгрузить лайвпатч-модуль.

Важный момент: если используются средства для работы с лайвпатчами из mainline-ядра, то при активации и деактивации лайвпатч-обновлений работающие процессы не нужно завершать или даже останавливать на сколько-нибудь существенное время.

Примечания:

  • У TuxCare / KernelCare лайвпатчи поставляются в другом, проприетарном, формате, но мы его рассматривать не будем.

  • Для некоторых архитектур, например RISC-V, при активации и деактивации патча вызывается stop_machine(), то есть работающие процессы при этом все-таки останавливаются на некоторое время. Начиная с версии 6.16 ядра Linux, для RISC-V stop_machine() уже, вероятно, не будет использоваться в таких ситуациях.

Готовим livepatch

Довольно абстрактных рассуждений. Давайте посмотрим на примере, как подготовить и использовать лайвпатч для реальной проблемы в ядре 6.8.0-41-generic в Ubuntu Server 24.04, x86-64.

Лучше не экспериментировать на рабочей машине, а установить эту ОС в QEMU VM или в другую виртуальную машину. Для этого можно использовать официальные ISO-образы Ubuntu Server 24.04.

Если после установки в гостевой системе будет другая версия ядра, то пакеты с 6.8.0-41-generic можно установить отдельно:

Ядро 6.8.0-41-generic для Ubuntu 24.04 было выпущено в августе 2024. В этом ядре есть баг, найденный с помощью системы Syzkaller в мае 2024.

Исправление для этого бага было добавлено в mainline-ядро в том же месяце: 

diff --git a/net/ipv4/netfilter/nf_tproxy_ipv4.c b/net/ipv4/netfilter/nf_tproxy_ipv4.c
index 69e33179960430..73e66a088e25eb 100644
--- a/net/ipv4/netfilter/nf_tproxy_ipv4.c
+++ b/net/ipv4/netfilter/nf_tproxy_ipv4.c
@@ -58,6 +58,8 @@ __be32 nf_tproxy_laddr4(struct sk_buff *skb, __be32 user_laddr, be32 daddr)

              	laddr = 0;
              	indev = in_dev_get_rcu(skb->dev);
+           	if (!indev)
+                             	return daddr;

              	in_dev_for_each_ifa_rcu(ifa, indev) {
                                	if (ifa->ifa_flags & IFA_F_SECONDARY)

Системе Syzkaller удалось построить программу на языке C, с помощью которой легко воспроизвести проблему.

Исходный код этой программы можно сохранить как repro.c, собрать бинарник ("gcc -o repro repro.c") и запустить под root. При запуске от имени обычного пользователя может не сработать.

У меня в экспериментах система, работающая в QEMU VM, после запуска repro почти сразу падала с NULL pointer dereference в функции nf_tproxy_laddr4() из модуля nf_tproxy_ipv4.ko:

[   54.253461] BUG: kernel NULL pointer dereference, address: 0000000000000010
[   54.254236] #PF: supervisor read access in kernel mode
[   54.254831] #PF: error_code(0x0000) - not-present page
[   54.255411] PGD 103240067 P4D 103240067 PUD 101fed067 PMD 0
[   54.256039] Oops: 0000 [#1] PREEMPT SMP NOPTI
[   54.256572] CPU: 1 PID: 1029 Comm: repro Not tainted 6.8.0-41-generic #41-Ubuntu
[   54.257419] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.15.0-1 04/01/2014
[   54.258369] RIP: 0010:nf_tproxy_laddr4+0x1a/0x50 [nf_tproxy_ipv4]
...
[   54.308357] Kernel panic - not syncing: Fatal exception in interrupt

Гостевая система становилась неработоспособной, помогала только перезагрузка. 

Давайте теперь подготовим лайвпатч, исправляющий этот баг. 

Для этого воспользуемся средствами для работы с лайвпатчами из ядра Linux и open source инструментами kpatch tools. В ядре 6.8.0-41-generic на x86-64 поддержка лайвпатчей уже включена: CONFIG_LIVEPATCH=y

Само по себе исправление простое: меняется только одна функция ядра, nf_tproxy_laddr4(), а при замене в runtime этой функции на исправленную ничего плохого случиться не должно. Это актуально не для всех исправлений, но тут нам повезло — исходный патч дорабатывать не нужно.

Kpatch tools — не единственный набор инструментов для сборки лайвпатчей. Сейчас идет разработка klp-build, который, вероятно, со временем войдет в mainline-ядро и будет стандартом де-факто. Но на момент написания этой статьи klp-build и необходимые для него изменения в objtool — еще в стадии разработки. Так что — воспользуемся более старыми, несколько более медленными, но надежными и проверенными временем kpatch tools.

Окружение для сборки

Если хочется попробовать лайвпатч-исправление уже сейчас, готовый лайвпатч-модуль можно найти  в этом архиве. Также там находится программа-reproducer и файлы, которые использовались для сборки: исходный код и config-файл указанного ядра Ubuntu и vmlinux с отладочной информацией. Пароль для скачивания: livepatch2025. 

Желательно, чтобы версия компилятора в окружении для сборки лайвпатчей была как можно ближе к той, что использовались для сборки соответствующей версии ядра.

Cборку лайвпатча для этого примера я делал в контейнере, созданном по такому dockerfile:

FROM	ubuntu:24.04
RUN	echo "deb-src http://archive.ubuntu.com/ubuntu/ noble main restricted" >> /etc/apt/sources.list && \
	echo "deb-src http://archive.ubuntu.com/ubuntu/ noble-updates main restricted" >> /etc/apt/sources.list && \
	apt-get update -y && \
	ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime && \
	DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata && \
	apt-get install -y \
    	build-essential \
    	elfutils \
    	fakeroot \
    	devscripts \
    	dpkg-dev \
    	shellcheck \
    	kmod \
    	mc \
    	libssl-dev \
    	libelf-dev \
    	make \
    	zstd && \
	apt-get -y build-dep linux && \
	apt-get install -y linux-headers-6.8.0-41-generic && \
	apt-get remove -y gcc-13 gcc-13-base cpp-13 gcc-13-x86-64-linux-gnu cpp-13-x86-64-linux-gnu libgcc-13-dev libobjc-13-dev g++-13-x86-64-linux-gnu g++-13 && \
	apt-get install -y \
    	gcc-13='13.2.0-23ubuntu4' \
    	gcc-13-base='13.2.0-23ubuntu4' \
    	cpp-13='13.2.0-23ubuntu4' \
    	gcc-13-x86-64-linux-gnu='13.2.0-23ubuntu4' \
    	cpp-13-x86-64-linux-gnu='13.2.0-23ubuntu4' \
    	libgcc-13-dev='13.2.0-23ubuntu4' && \
	ln -s /usr/bin/gcc-13 /usr/bin/gcc

Нужную версию GCC (13.2.0-23ubuntu4 для ядра 6.8.0-41-generic) пришлось там указать явно, поскольку сейчас у Ubuntu 24.04 по умолчанию ставится более новая версия.

Процесс сборки

Для сборки лайвпатча нам понадобится следующее:

  • Git repo c kpatch tools.

  • Исходное исправление бага (source patch): netfilter-tproxy-fix.patch

  • Config, который использовался для сборки ядра 6.8.0-41-generic. Можно взять из пакета linux-modules-6.8.0-41-generic_6.8.0-41.41_amd64.deb или из работающей системы с таким ядром (/boot/config-6.8.0-41-generic).

В config-файле нужно очистить значения параметров CONFIG_SYSTEM_TRUSTED_KEYS и CONFIG_SYSTEM_REVOCATION_KEYS, иначе сборка livepatch-модуля может не пройти:

-CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"
+CONFIG_SYSTEM_TRUSTED_KEYS=""
...
-CONFIG_SYSTEM_REVOCATION_KEYS="debian/canonical-revoked-certs.pem"
+CONFIG_SYSTEM_REVOCATION_KEYS=""
  • Набор исходников ядра 6.8.0-41-generic. Его можно получить, например, из пакета linux-source-6.8.0_6.8.0-41.41_all.deb (usr/src/linux-source-6.8.0/linux-source-6.8.0.tar.bz2) или из git repo

В начале linux-source-6.8.0/Makefile нужно подправить компоненты версии ядра, чтобы они соответствовали целевой версии 6.8.0-41-generic, иначе ядро может отказаться грузить лайвпатч-модуль:

VERSION = 6
PATCHLEVEL = 8
SUBLEVEL = 0
EXTRAVERSION = -41-generic
  • vmlinux с отладочной информацией. kpatch tools используют эту информацию при подготовке метаданных для лайвпатч-модуля. Отладочная информация для модуля nf_tproxy_ipv4 для сборки не требуется.

vmlinux можно найти в dbgsym-пакете (usr/lib/debug/boot/vmlinux-6.8.0-41-generic).

Допустим, все вышеперечисленное находится в каталоге, который смонтирован в /work/livepatch/ в контейнере со сборочным окружением:

root@b936e748a113:/work/livepatch# ls -l
...
drwxrwxr-x 12 ubuntu ubuntu  	 4096 Sep  8  2024 kpatch.git
-rw-r--r--  1 ubuntu ubuntu	   287442 Aug  2 14:15 config-6.8.0-41-generic
drwxrwxr-x 27   2001   2501  	 4096 Sep 14 21:15 linux-source-6.8.0
-rw-r--r--  1 ubuntu ubuntu 357001696 Aug  2 14:15 linux-source-6.8.0.tar.bz2
-rw-rw-r--  1 ubuntu ubuntu  	 1838 Sep 14 20:37 netfilter-tproxy-fix.patch
-rw-r--r--  1 ubuntu ubuntu 410746952 Aug  2 14:15 vmlinux-6.8.0-41-generic

У некоторых дистрибутивов Linux пакеты с kpatch tools уже есть в репозиториях. Но там иногда бывают сильно устаревшие версии, так что я бы рекомендовал собрать из исходников: 

root@b936e748a113:~# cd /work/livepatch/kpatch.git/
root@b936e748a113:/work/livepatch/kpatch.git# make -C kpatch-build

А теперь используем kpatch-build, чтобы собрать livepatch-модуль. В параметрах указываем путь к исходникам ядра, к config-файлу ядра, к vmlinux с debug info и к исходному патчу. 

root@b936e748a113:/work/livepatch# /work/livepatch/kpatch.git/kpatch-build/kpatch-build \
  -s ./linux-source-6.8.0 \
  -c ./config-6.8.0-41-generic \
  -v ./vmlinux-6.8.0-41-generic \
  ./netfilter-tproxy-fix.patch
 
Using source directory at /work/livepatch/linux-source-6.8.0
Testing patch file(s)
Reading special section data
Building original source
Building patched source
Extracting new and modified ELF sections
nf_tproxy_ipv4.o: changed function: nf_tproxy_laddr4
Patched objects: net/ipv4/netfilter/nf_tproxy_ipv4.ko
Building patch module: livepatch-netfilter-tproxy-fix.ko
modprobe: FATAL: could not get modversions of /root/.kpatch/tmp/patch/livepatch-netfilter-tproxy-fix.ko: Invalid argument
SUCCESS

Ошибки modprobe здесь можно игнорировать. 

Что же происходило при сборке?

kpatch-build собрал ядро из указанных исходников дважды: без патча netfilter-tproxy-fix.patch и затем с патчем. При этом собрал так, чтобы каждая функция попала в отдельную ELF-секцию (опция компилятора — -ffunction-sections). Так проще сравнивать, что изменилось в ядре на бинарном уровне в результате применения патча: какие функции изменены, какие добавлены и т. п. Аналогично для глобальных и некоторых других переменных (-fdata-sections).

Затем kpatch-build выделил только новые и измененные функции в пропатченном ядре. В данном случае на бинарном уровне изменилась только функция nf_tproxy_laddr4 из net/ipv4/netfilter/nf_tproxy_ipv4.ko. Вот как выглядит начало исправленного варианта для этой функции:

 Disassembly of section .text.nf_tproxy_laddr4:
 ...
 0000000000000010 <nf_tproxy_laddr4>:
   10: call   15 <__fentry__>
   15: push   %rbp
   16: mov	%esi,%eax
   18: mov	%rsp,%rbp
   1b: test   %esi,%esi
+  1d: jne	46 <nf_tproxy_laddr4+0x36>
   1f: mov	0x10(%rdi),%rax
   23: mov	0x3e0(%rax),%rax
+  2a: test   %rax,%rax  // if (!indev) ...
+  2d: je 	52 <nf_tproxy_laddr4+0x42>
   2f: mov	0x10(%rax),%rax
   33: test   %rax,%rax
 ...

Дальше kpatch-build скопировал из пропатченного ядра бинарный код всех новых и измененных функций и добавил метаданные, которые нужны для применения такого обновления в runtime. Все это было объединено в модуль ядра, с использованием «обвязки» для лайвпатчей. При этом выполнялись и некоторые другие операции, например корреляция статических локальных переменных в старых и измененных функциях, а также корреляция оптимизированных компилятором вариантов функций. Но тут в детали вдаваться мы не будем.

В результате получился livepatch-модуль с нужным исправлением: livepatch-netfilter-tproxy-fix.ko

Такой процесс сборки может быть не очень быстрым: например, на моей машине подготовка такого лайвпатча заняла примерно два с половиной часа. Большая часть этого времени идет на сборку ядра без патча. А вот если после сборки сохранить build tree (/work/livepatch/linux-source-6.8.0) и использовать его при последующих сборках как дерево исходников ядра, то время подготовки лайвпатча сокращается до нескольких минут! Уже лучше. 

Проверка лайвпатча

Теперь давайте скопируем собранный модуль livepatch-netfilter-tproxy-fix.ko на целевую машину и попробуем применить это лайвпатч-обновление.

Важный момент. Если в целевой системе уже используются какие-то лайвпатчи от других вендоров (патчи от Canonical, обновления KernelCare и пр.), их лучше выгрузить, прежде чем экспериментировать со своими.

Для управления лайвпатчами бывает удобно использовать скрипт kpatch, но в данном случае обойдемся без него, чтобы было понятнее, как все работает.

Загрузим подготовленный нами лайвпатч-модуль обычным способом:

root@ubuntu-x64:/var/tmp# insmod ./livepatch-netfilter-tproxy-fix.ko

Если при загрузке ошибок не было, то в sysfs должен появиться каталог для этого лайвпатча:

root@ubuntu-x64:/var/tmp# ls -l /sys/kernel/livepatch/livepatch_netfilter_tproxy_fix/
total 0
-rw-r--r-- 1 root root 4096 Jun  3 19:56 enabled
--w------- 1 root root 4096 Jun  3 19:56 force
drwxr-xr-x 3 root root	  0 Jun  3 19:56 nf_tproxy_ipv4
-r--r--r-- 1 root root 4096 Jun  3 19:56 transition

Обычно livepatch автоматически активируется при загрузке такого модуля. Статус можно посмотреть в файле enabled (1 — патч активирован, включен, 0 — выключен):

root@ubuntu-x64:/var/tmp# cat /sys/kernel/livepatch/livepatch_netfilter_tproxy_fix/enabled
1

Если после загрузки лайвпатч почему-то не включился автоматически, можно попытаться активировать его явно: 

echo 1 > /sys/kernel/livepatch/livepatch_netfilter_tproxy_fix/enabled 

В dmesg при этом будут сообщения о том, что лайвпатч загружен и включен и что ядро получило TAINT_LIVEPATCH:

[10356.456901] livepatch_netfilter_tproxy_fix: loading out-of-tree module taints kernel.
[10356.456927] livepatch_netfilter_tproxy_fix: tainting kernel with TAINT_LIVEPATCH
[10356.456933] livepatch_netfilter_tproxy_fix: module verification failed: signature and/or required key missing - tainting kernel
[10356.458553] livepatch: enabling patch 'livepatch_netfilter_tproxy_fix'
[10356.458841] livepatch: 'livepatch_netfilter_tproxy_fix': starting patching transition
[10356.522778] livepatch: 'livepatch_netfilter_tproxy_fix': patching complete

Кстати, если модуль ядра nf_tproxy_ipv4, в котором мы меняем функцию nf_tproxy_laddr4(), не загружен на момент активации лайвпатча, — нестрашно. Когда nf_tproxy_ipv4 будет загружаться, Linux автоматически применит к нему все активные на тот момент лайвпатчи. В dmesg при этом будет сообщение такого вида:

livepatch: applying patch 'livepatch_netfilter_tproxy_fix' to loading module 'nf_tproxy_ipv4' 

Попробуем теперь запустить ту же самую программу-reproduсer, которая раньше приводила к падению системы:

root@ubuntu-x64:/var/tmp# ./repro

В dmesg теперь появляется несколько сообщений вида «netlink: 'repro': attribute type 4 has an invalid length», но ядро уже не падает и система продолжает работать! Тот баг, который раньше приводил к неработоспособности системы, ей больше не страшен.

Правда, если почему-то понадобится систему перезагрузить, то после этого она снова будет уязвима: модуль с патчем не загрузится автоматически при старте. Если нужно, чтобы лайвпатчи применялись при загрузке системы, можно, например, воспользоваться сервисом наподобие такого и скриптом kpatch из того же git repo.

При необходимости лайвпатчи можно отключить в runtime. После этого ядро снова будет работать по-старому, без исправлений соответствующих ошибок:

root@ubuntu-x64:/var/tmp# echo 0 > /sys/kernel/livepatch/livepatch_netfilter_tproxy_fix/enabled
 
root@ubuntu-x64:/var/tmp# ls -l /sys/kernel/livepatch/
total 0
 
root@ubuntu-x64:/var/tmp# dmesg | tail
...
[26216.555446] livepatch: 'livepatch_netfilter_tproxy_fix': starting unpatching transition
[26216.647453] livepatch: 'livepatch_netfilter_tproxy_fix': unpatching complete

Модуль после этого нужно выгрузить:

root@ubuntu-x64:/var/tmp# rmmod livepatch_netfilter_tproxy_fix 

Стоит заметить, что лайвпатч-модуль, как и обычные модули ядра Linux, собирается для конкретной версии ядра. Если нужно сделать лайвпатчи для нескольких версий, для каждой потребуется собрать свой модуль.

Накопительные обновления и «атомарная» замена лайвпатчей

Может возникнуть вопрос: а как быть, если у вас несколько исправлений для данной версии ядра? Делать для каждого исправления отдельный лайвпатч-модуль, собрать их в один модуль или как-то еще сгруппировать? А если вы эту коллекцию фиксов захотите обновить или изменить какие-то из них?

 Есть разные варианты, но в моей практике хорошо себя показали так называемые накопительные (cumulative) обновления. Их, кстати, рекомендуют и мейнтейнеры livepatch-подсистемы в ядре.

В такой схеме все необходимые исправления объединяются в один source patch, по которому затем и собирается лайвпатч-модуль. Этому объединенному патчу можно присвоить какую-нибудь версию. Например:

Если затем вам понадобится заменить исправление CVE-2024-43888.patch на CVE-2024-43888-02.patch, добавить фикс для бага BUG-42001, а патч для BUG-12382 окажется ненужным, то можно сделать новую версию набора патчей и, соответственно, лайвпатча:

По умолчанию лайвпатч при загрузке заменяет собой все лайвпатчи, что были в системе до этого.

Допустим, была загружена первая версия нашего набора исправлений. Заменим ее на вторую.

root@ubuntu-x64:/var/tmp# ls -l /sys/kernel/livepatch
total 0
drwxr-xr-x 4 root root 0 Jun  3 21:40 livepatch_v1
 
root@ubuntu-x64:/var/tmp# insmod livepatch-v2.ko
root@ubuntu-x64:/var/tmp# ls -l /sys/kernel/livepatch
total 0
drwxr-xr-x 4 root root 0 Jun  3 21:42 livepatch_v2
 
root@ubuntu-x64:/var/tmp# rmmod livepatch-p01

Начиная с версии 5.1, ядро Linux поддерживает «атомарную замену» лайвпатчей (atomic replacement). Что это значит?

Это значит, что возможны только два варианта:

  • livepatch_v2 будет загружен и активирован (включен) успешно — тогда все процессы в системе будут использовать только функции из v2, но не из v1.

  • При загрузке livepatch_v2 происходит ошибка — тогда все процессы в системе продолжат использовать только функции из v1, но не v2.

Это и есть «атомарность»: livepatch заменяется на другой как единое целое — или полностью, или никак. Не будет ситуаций, когда какой-то процесс в системе использует часть функций из v1, а часть — из v2.

Накопительные лайвпатчи и их атомарные обновления сильно упрощают поддержку исправлений для ядра.

Downgrade для накопительных патчей, то есть замена v2 на v1 в нашем случае, делается аналогично: загружаем livepatch-v1.ko, и он заменяет собой v2 автоматически.

Правда, если лайвпатчи используют специальные callback-функции для выполнения тех или иных операций при активации или деактивации, то лучше не полагаться на atomic replacement. Вместо этого лучше явно деактивировать и выгрузить v2, а затем загрузить v1:

root@ubuntu-x64:/var/tmp# echo 0 > /sys/kernel/livepatch/livepatch_v2/enabled
root@ubuntu-x64:/var/tmp# rmmod livepatch_v2
root@ubuntu-x64:/var/tmp# insmod livepatch-v1.ko 

Это связано с тем, что такие callback-функции вызываются только для того лайвпатча, который загружаем (v1 в нашем случае).  Для v2 они вызваны не будут. Если эти callback-функции, скажем, освобождают ресурсы, которые использовались в v2, то при замене на v1 этого освобождения не произойдет — будет утечка ресурсов. Подробнее об этом можно почитать тут.

Если нужно, чтобы лайвпатч не заменял все остальные при загрузке, это можно отключить при сборке с помощью опции -R (--non-replace) для kpatch-build. Может быть полезно, если лайвпатч делается для экспериментов, отладки и пр.

Модель согласованности (consistency model)

В примере с ошибкой в nf_tproxy_ipv4.ko лайвпатч заменил ровно одну функцию, nf_tproxy_laddr4(), на ее исправленный вариант. Это прекрасно, но в реальных ситуациях так бывает редко: обычно нужно менять несколько функций. Некоторые из них могут быть связаны друг с другом и ожидать друг от друга определенного поведения.

Это особенно актуально, если используются «накопительные» обновления, о которых говорилось выше. В моей практике количество функций, которые заменялись в одном таком лайвпатче, доходило до 300–400. За время, в течение которого поддерживалась данная версия ядра, для нее в лайвпатчах накапливалось достаточно много исправлений. 

Чтобы лайвпатчи не устроили в системе полный хаос, нужны определенные меры предосторожности.

Например, нельзя патчить функции, которые в данный момент выполняются. О причинах поговорим во второй части статьи, а пока просто запомним, что такое ограничение есть.

Как проверить, выполняет ли сейчас данный процесс функцию foo()? Например, можно посмотреть call stack:

[<ffffffff808926a8>] __schedule+0x242/0x5c2
[<ffffffff80892a6e>] schedule+0x46/0xd2
[<ffffffff808984c4>] foo+0x80/0x152 [my_driver]
...
[<ffffffff80035da4>] kthread+0xbc/0xce
[<ffffffff80003b64>] ret_from_exception+0x0/0x16

Естественно, для этого ядро Linux должно уметь получать хорошие, годные надежные (reliable) stack traces для процессов — такие, где, как минимум, ни один stack frame не пропущен. Обеспечить это бывает непросто. 

Для x86 такая функциональность есть, для RISC-V — вплоть до версии ядра 6.15 — пока нет. Для ARM reliable stacktraces, возможно, скоро будут поддерживаться, c Sframe unwinder или без него.

Итак, допустим, что патчится несколько функций ядра, которые могут быть связаны между собой. Нужно обеспечить, чтобы каждый процесс в системе (точнее, каждый task / thread, с точки зрения ядра Linux) использовал либо только старые варианты этих функций, либо только новые, пропатченные, но не смесь тех и других.

За это отвечает модель согласованности. При этом переход на новые функции происходит так:

  • Процессы в системе переводятся на использование пропатченных функций ядра по очереди. Во время применения лайвпатча может быть так, что один процесс видит только пропатченные функции ядра, а другой — только старые, в зависимости от значения поля patch_state в task_struct.

  • Если поддерживаются reliable stack traces, то механизм применения лайвпатчей проверяет stack traces для процессов, которые в данный момент спят. Если у процесса в call trace нет функций, которые хотим пропатчить, то переводим его на использование новых функций. Для оставшихся - повторяем такую проверку несколько раз.

  • Если процесс выходит из ядра, тоже переключаем его на новые функции. Если не выходит 15 секунд или дольше, livepatch-подсистема ядра отправляет ему специальный сигнал.

  • А если процесс никогда не выходит из ядра и постоянно работает в тех функциях, которые хотим пропатчить? Тут общего решения нет, нужно разбираться в каждом конкретном случае. В коде некоторых kernel threads есть вызовы klp_update_patch_state(), во время которых такие kernel threads можно переключить на пропатченные функции или обратно на старые. Но такое есть не везде, так что, вероятно, придется искать, можно ли исправить баг каким-то другим способом. Не факт, что получится, но попытаться стоит.

Итак, теперь вы знаете, что такое лайвпатчи и представляете, как их готовить и для чего использовать. Но как именно они работают и что у них внутри? Об этом поговорим в продолжении статьи.

Чтобы не пропустить вторую часть, добавляйте текст в закладки и подписывайтесь на блог YADRO.

Продолжение следует.

Комментарии (3)


  1. MountainGoat
    17.07.2025 12:46

    Расскажите, пожалуйста, а где вообще в норме нужно обновление ядра без перезагрузки? Я думаю, что там, где требуется непрерывность обслуживания, всегда стоит несколько машин и распределение нагрузки между ними. Поэтому машины по очереди перезагрузить как раз проще простого, всё должно само отработать. А там, где нет такой возможности, например на дроне в полёте, просто не надо ничего вообще патчить во время работы.

    Единственная ситуация, которая мне вообще воображается - это когда пытаются из одной шкуры сделать десять шапок и потом раздать их со старого компьютера секретарши, наречённого ныне "бесперебойной серверной"


    1. euspectre Автор
      17.07.2025 12:46

      Да, один из use case был - системы у хостеров и т.п. Их владельцы как раз любят выжимать из серверов всё и не очень любят простои.

      Не единственный вариант, но достаточно распространённый.


    1. Lx6g1ZG1
      17.07.2025 12:46

      а где вообще в норме нужно обновление ядра без перезагрузки?

      Например нода гипервизора виртуализации. Чтобы не мигрировать виртуалки на другие ноды если конечно они (виртуалки) не в HA кластере