Re: [RFC PATCH 3/4] livepatch: Add "replaceable" attribute to klp_patch

Yafang Shao
2026-04-07T09:46:08Z

On Tue, Apr 7, 2026 at 11:16 AM Yafang Shao <laoar.shao@gmail.com> wrote:
>
> On Tue, Apr 7, 2026 at 10:54 AM Song Liu <song@kernel.org> wrote:
> >
> > On Mon, Apr 6, 2026 at 2:12 PM Joe Lawrence <joe.lawrence@redhat.com> wrote:
> > [...]
> > > > > > - The regular livepatches are cumulative, have the replace flag; and
> > > > > > are replaceable.
> > > > > > - The occasional "off-band" livepatches do not have the replace flag,
> > > > > > and are not replaceable.
> > > > > >
> > > > > > With this setup, for systems with off-band livepatches loaded, we can
> > > > > > still release a cumulative livepatch to replace the previous cumulative
> > > > > > livepatch. Is this the expected use case?
> > > > >
> > > > > That matches our expected use case.
> > > >
> > > > If we really want to serve use cases like this, I think we can introduce
> > > > some replace tag concept: Each livepatch will have a tag, u32 number.
> > > > Newly loaded livepatch will only replace existing livepatch with the
> > > > same tag. We can even reuse the existing "bool replace" in klp_patch,
> > > > and make it u32: replace=0 means no replace; replace > 0 are the
> > > > replace tag.
> > > >
> > > > For current users of cumulative patches, all the livepatch will have the
> > > > same tag, say 1. For your use case, you can assign each user a
> > > > unique tag. Then all these users can do atomic upgrades of their
> > > > own livepatches.
> > > >
> > > > We may also need to check whether two livepatches of different tags
> > > > touch the same kernel function. When that happens, the later
> > > > livepatch should fail to load.
>
> That sounds like a viable solution. I'll look into it and see how we
> can implement it.

Does the following change look good to you ?

Subject: [PATCH] livepatch: Support scoped atomic replace using replace tags

Extend the replace attribute from a boolean to a u32 to act as a replace
tag. This introduces the following semantics:

replace = 0: Atomic replace is disabled. However, this patch remains
eligible to be superseded by others.
replace > 0: Enables tagged replace (default is 1). A newly loaded
livepatch will only replace existing patches that share the
same tag.

To maintain backward compatibility, a patch with replace == 0 does not
trigger an outgoing atomic replace, but remains eligible to be superseded
by any incoming patch with a valid replace tag.

Suggested-by: Song Liu <song@kernel.org>
Signed-off-by: Yafang Shao <laoar.shao@gmail.com>
---
.../livepatch/cumulative-patches.rst | 20 +++++++-----
Documentation/livepatch/livepatch.rst | 31 +++++++++++++------
include/linux/livepatch.h | 8 +++--
kernel/livepatch/core.c | 4 +++
scripts/livepatch/init.c | 6 +---
scripts/livepatch/klp-build | 11 +++++--
6 files changed, 53 insertions(+), 27 deletions(-)

diff --git a/Documentation/livepatch/cumulative-patches.rst
b/Documentation/livepatch/cumulative-patches.rst
index 1931f318976a..06e90dc5967c 100644
--- a/Documentation/livepatch/cumulative-patches.rst
+++ b/Documentation/livepatch/cumulative-patches.rst
@@ -12,23 +12,26 @@ modified the same function in different ways.

An elegant solution comes with the feature called "Atomic Replace". It allows
creation of so called "Cumulative Patches". They include all wanted changes
-from all older livepatches and completely replace them in one transition.
+from older livepatches with a matching tag and replace them in one transition.

Usage
-----

-The atomic replace can be enabled by setting "replace" flag in struct
klp_patch,
-for example::
+he atomic replace can be enabled by setting a non-zero value to the "replace"
+attribute in ``struct klp_patch``. This value acts as a **replace tag**,
+defining the scope of the replacement.
+
+For example::

static struct klp_patch patch = {
.mod = THIS_MODULE,
.objs = objs,
- .replace = true,
+ .replace = 1,
};

All processes are then migrated to use the code only from the new patch.
-Once the transition is finished, all older patches are automatically
-disabled.
+Once the transition is finished, all older patches with the same replace tag
+are automatically disabled. Patches with different tags remain active.

Ftrace handlers are transparently removed from functions that are no
longer modified by the new cumulative patch.
@@ -62,9 +65,10 @@ Limitations:
------------

- Once the operation finishes, there is no straightforward way
- to reverse it and restore the replaced patches atomically.
+ to reverse it and restore the replaced patches (with the same tag)
+ atomically.

- A good practice is to set .replace flag in any released livepatch.
+ A good practice is to set a consistent .replace tag in related livepatches.
Then re-adding an older livepatch is equivalent to downgrading
to that patch. This is safe as long as the livepatches do _not_ do
extra modifications in (un)patching callbacks or in the module_init()
diff --git a/Documentation/livepatch/livepatch.rst
b/Documentation/livepatch/livepatch.rst
index acb90164929e..1fc1543a22c3 100644
--- a/Documentation/livepatch/livepatch.rst
+++ b/Documentation/livepatch/livepatch.rst
@@ -347,15 +347,28 @@ to '0'.
5.3. Replacing
--------------

-All enabled patches might get replaced by a cumulative patch that
-has the .replace flag set.
-
-Once the new patch is enabled and the 'transition' finishes then
-all the functions (struct klp_func) associated with the replaced
-patches are removed from the corresponding struct klp_ops. Also
-the ftrace handler is unregistered and the struct klp_ops is
-freed when the related function is not modified by the new patch
-and func_stack list becomes empty.
+All currently enabled patches may be superseded by a cumulative patch that
+has the ``.replace`` attribute enabled. The behavior of the replacement
+depends on the value assigned to the replace tag:
+
+replace = 0
+ Atomic replace is disabled. However, this patch remains eligible to be
+ superseded by others.
+
+replace > 0
+ Enables tagged atomic replace. Once the new patch is enabled and the
+ transition finishes, the livepatching core identifies all existing
+ patches that share the same replace tag.
+
+Once the transition is complete, all functions (``struct klp_func``)
+associated with the matching replaced patches are removed from the
+corresponding ``struct klp_ops``. If a function is no longer modified by
+the new patch and its ``func_stack`` list becomes empty, the ftrace
+handler is unregistered and the ``struct klp_ops`` is freed.
+
+Patches with a different replace tag are not affected by this process
+and remain active. This allows for the independent management and
+stacking of multiple, non-conflicting livepatch sets.

See Documentation/livepatch/cumulative-patches.rst for more details.

diff --git a/include/linux/livepatch.h b/include/linux/livepatch.h
index ba9e3988c07c..417c67a17b99 100644
--- a/include/linux/livepatch.h
+++ b/include/linux/livepatch.h
@@ -123,7 +123,11 @@ struct klp_state {
* @mod: reference to the live patch module
* @objs: object entries for kernel objects to be patched
* @states: system states that can get modified
- * @replace: replace all actively used patches
+ * @replace: replace tag:
+ * = 0: Atomic replace is disabled; however, this patch remains
+ * eligible to be superseded by others.
+ * > 0: Atomic replace is enabled. Only existing patches with a
+ * matching replace tag will be superseded.
* @list: list node for global list of actively used patches
* @kobj: kobject for sysfs resources
* @obj_list: dynamic list of the object entries
@@ -137,7 +141,7 @@ struct klp_patch {
struct module *mod;
struct klp_object *objs;
struct klp_state *states;
- bool replace;
+ unsigned int replace;

/* internal */
struct list_head list;
diff --git a/kernel/livepatch/core.c b/kernel/livepatch/core.c
index 28d15ba58a26..e4e5c03b0724 100644
--- a/kernel/livepatch/core.c
+++ b/kernel/livepatch/core.c
@@ -793,6 +793,8 @@ void klp_free_replaced_patches_async(struct
klp_patch *new_patch)
klp_for_each_patch_safe(old_patch, tmp_patch) {
if (old_patch == new_patch)
return;
+ if (old_patch->replace && old_patch->replace !=
new_patch->replace)
+ continue;
klp_free_patch_async(old_patch);
}
}
@@ -1194,6 +1196,8 @@ void klp_unpatch_replaced_patches(struct
klp_patch *new_patch)
klp_for_each_patch(old_patch) {
if (old_patch == new_patch)
return;
+ if (old_patch->replace && old_patch->replace !=
new_patch->replace)
+ continue;

old_patch->enabled = false;
klp_unpatch_objects(old_patch);
diff --git a/scripts/livepatch/init.c b/scripts/livepatch/init.c
index f14d8c8fb35f..cd00e278a1d2 100644
--- a/scripts/livepatch/init.c
+++ b/scripts/livepatch/init.c
@@ -72,11 +72,7 @@ static int __init livepatch_mod_init(void)

/* TODO patch->states */

-#ifdef KLP_NO_REPLACE
- patch->replace = false;
-#else
- patch->replace = true;
-#endif
+ patch->replace = KLP_REPLACE_TAG;

return klp_enable_patch(patch);

diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build
index 7b82c7503c2b..9f6a7673304f 100755
--- a/scripts/livepatch/klp-build
+++ b/scripts/livepatch/klp-build
@@ -118,6 +118,7 @@ Options:
-j, --jobs=<jobs> Build jobs to run simultaneously
[default: $JOBS]
-o, --output=<file.ko> Output file [default: livepatch-<patch-name>.ko]
--no-replace Disable livepatch atomic replace
+ -t, --replace-tag=<tag> Set the atomic replace tag for this livepatch
-v, --verbose Pass V=1 to kernel/module builds

Advanced Options:
@@ -142,8 +143,8 @@ process_args() {
local long
local args

- short="hfj:o:vdS:T"
- long="help,show-first-changed,jobs:,output:,no-replace,verbose,debug,short-circuit:,keep-tmp"
+ short="hfj:o:t:vdS:T"
+ long="help,show-first-changed,jobs:,output:,no-replace,replace-tag:,verbose,debug,short-circuit:,keep-tmp"

args=$(getopt --options "$short" --longoptions "$long" -- "$@") || {
echo; usage; exit
@@ -176,6 +177,10 @@ process_args() {
REPLACE=0
shift
;;
+ -t | --replace-tag)
+ REPLACE="$2"
+ shift 2
+ ;;
-v | --verbose)
VERBOSE="V=1"
shift
@@ -759,7 +764,7 @@ build_patch_module() {

cflags=("-ffunction-sections")
cflags+=("-fdata-sections")
- [[ $REPLACE -eq 0 ]] && cflags+=("-DKLP_NO_REPLACE")
+ cflags+=("-DKLP_REPLACE_TAG=$REPLACE")

cmd=("make")
cmd+=("$VERBOSE")

--
Regards
Yafang