[RFC PATCH v4 3/4] livepatch: Implement replace set for scoped atomic replace
Yafang Shao <[email protected]>
| Newsgroups | org.kernel.vger.live-patching |
|---|---|
| Message-ID | <[email protected]> |
The current bool replace flag is too coarse: it is either all or nothing. A livepatch with .replace=true replaces ALL existing livepatches, which is safe but inflexible. There is no way to have multiple independent livepatch sets coexist on the same system. Replace it with a more flexible model using two new fields in struct klp_patch: - provides: an unsigned int id identifying the patch replace set. By default (provides=0), any livepatch replaces any other livepatch. - obsoletes: an optional array of unsigned int ids specifying additional provides ids to be replaced. This allows a new patch to explicitly obsolete patches from different replace sets. A new livepatch atomically replaces any existing livepatch whose provides id matches either: 1. The new patch provides id (same replace set), or 2. Any id in the new patch obsoletes list The klp-build script is updated with -p/--provides and -r/--obsoletes options. The obsoletes list automatically includes the provides id and deduplicates entries. Input validation rejects malformed values at build time. Two helper functions are introduced: - klp_patch_replaceable(): checks if an old patch should be replaced by a new patch. - klp_has_function_conflict(): rejects loading a livepatch that would modify a function already patched by a livepatch with a different provides id. Suggested-by: Song Liu <[email protected]> Suggested-by: Joe Lawrence <[email protected]> Suggested-by: Petr Mladek <[email protected]> Co-developed-by: Petr Mladek <[email protected]> Signed-off-by: Petr Mladek <[email protected]> Signed-off-by: Yafang Shao <[email protected]> --- .../ABI/removed/sysfs-kernel-livepatch | 7 ++ .../ABI/testing/sysfs-kernel-livepatch | 22 ++++- .../livepatch/cumulative-patches.rst | 93 +++++++++++++------ Documentation/livepatch/livepatch.rst | 23 +++-- include/linux/livepatch.h | 7 +- kernel/livepatch/core.c | 69 ++++++++++++-- kernel/livepatch/core.h | 1 + kernel/livepatch/state.c | 56 +++++++++-- kernel/livepatch/transition.c | 11 ++- scripts/livepatch/init.c | 71 +++++++++++++- scripts/livepatch/klp-build | 87 +++++++++++++++-- 11 files changed, 368 insertions(+), 79 deletions(-) create mode 100644 Documentation/ABI/removed/sysfs-kernel-livepatch diff --git a/Documentation/ABI/removed/sysfs-kernel-livepatch b/Documentation/ABI/removed/sysfs-kernel-livepatch new file mode 100644 index 000000000000..87d57ba27326 --- /dev/null +++ b/Documentation/ABI/removed/sysfs-kernel-livepatch @@ -0,0 +1,7 @@ +What: /sys/kernel/livepatch/<patch>/replace +Date: Jun 2024 +KernelVersion: 6.11.0 +Contact: [email protected] +Description: + An attribute which indicates whether the patch supports + atomic-replace. diff --git a/Documentation/ABI/testing/sysfs-kernel-livepatch b/Documentation/ABI/testing/sysfs-kernel-livepatch index 3c3f36b32b57..2588f676deb1 100644 --- a/Documentation/ABI/testing/sysfs-kernel-livepatch +++ b/Documentation/ABI/testing/sysfs-kernel-livepatch @@ -47,13 +47,25 @@ Description: disabled when the feature is used. See Documentation/livepatch/livepatch.rst for more information. -What: /sys/kernel/livepatch/<patch>/replace -Date: Jun 2024 -KernelVersion: 6.11.0 +What: /sys/kernel/livepatch/<patch>/provides +Date: Jun 2026 +KernelVersion: 7.4.0 Contact: [email protected] Description: - An attribute which indicates whether the patch supports - atomic-replace. + An attribute to show the provides id of this livepatch. + Only one active livepatch per provides id is allowed. + +What: /sys/kernel/livepatch/<patch>/obsoletes +Date: Jun 2026 +KernelVersion: 7.4.0 +Contact: [email protected] +Description: + An attribute to show the obsoletes ids of this livepatch. + The obsoletes ids are a comma-separated list of provides + ids that this patch obsoletes. When this livepatch is + loaded, any existing livepatch whose provides id matches + either this patch's provides id or any id in the obsoletes + list will be atomically replaced. What: /sys/kernel/livepatch/<patch>/stack_order Date: Jan 2025 diff --git a/Documentation/livepatch/cumulative-patches.rst b/Documentation/livepatch/cumulative-patches.rst index 1931f318976a..04352ae3f0d0 100644 --- a/Documentation/livepatch/cumulative-patches.rst +++ b/Documentation/livepatch/cumulative-patches.rst @@ -2,33 +2,66 @@ Atomic Replace & Cumulative Patches =================================== -There might be dependencies between livepatches. If multiple patches need -to do different changes to the same function(s) then we need to define -an order in which the patches will be installed. And function implementations -from any newer livepatch must be done on top of the older ones. - -This might become a maintenance nightmare. Especially when more patches -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. - -Usage ------ - -The atomic replace can be enabled by setting "replace" flag in struct klp_patch, -for example:: - - static struct klp_patch patch = { - .mod = THIS_MODULE, - .objs = objs, - .replace = true, - }; - -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. +Livepatches are used to fix kernel bugs. New fixes need to be added over time. +The fixes might be independent, but they might also depend on each other. This +brings a challenge of how to keep the livepatched system safe and consistent. + +Part of the solution is the "Atomic Replace" feature, which allows the kernel to +atomically replace an existing livepatch with another one. These newer +livepatches are designed as "Cumulative Patches". They include all wanted +changes from all older livepatches and completely replace them in one +transition. + +The second part of the solution is the newly introduced ``provides`` and +``obsoletes`` fields in ``struct klp_patch``, which allow the installation of +multiple livepatches in parallel. A livepatch will atomically replace any +already installed livepatch whose ``provides`` id matches either the new +patch's ``provides`` id or any id in the new patch's ``obsoletes`` list. +This might be used to fix independent problems separately, for example, the +livepatches might be prepared by separate teams focusing on particular +functionality or a subsystem. + +It should be emphasized that the preferred and most secure way is to always use +the default ``provides = 0``. In this mode, any livepatch replaces any other +livepatch, preventing any unexpected interactions between incompatible +livepatches. + +Provides and Obsoletes +----------------------- + +The ``provides`` field in ``struct klp_patch`` is an unsigned integer that +identifies the livepatch's replace set. By default, it is 0. + +The ``obsoletes`` field is an optional array of unsigned integers that +specifies additional ``provides`` ids to be replaced when this patch is +loaded. By default, it includes the patch's own ``provides`` id, ensuring +that a new patch always replaces any existing patch with the same +``provides`` id. + +For example:: + + static struct klp_patch patch = { + .mod = THIS_MODULE, + .objs = objs, + .provides = 0, + }; + +Any ``provides`` value might be associated with a set of livepatched symbols, +callbacks, shadow variables, and state IDs. By definition, there can only ever +be one active livepatch for a given ``provides`` id. + +On the contrary, livepatches with a different ``provides`` id must not +modify the same function, or use the state with the same ID. Any attempt to +load an incompatible livepatch will be rejected by the kernel. + +Atomic Replace +-------------- + +A livepatch with a given ``provides`` id is replaced by another livepatch +with the same ``provides`` id, or whose ``obsoletes`` list includes that id. +All processes are migrated to use the code only from the new patch. Once +the transition is finished, the older patch is disabled. Patches with a +different ``provides`` id are not affected and remain active. Ftrace handlers are transparently removed from functions that are no longer modified by the new cumulative patch. @@ -64,7 +97,11 @@ Limitations: - Once the operation finishes, there is no straightforward way to reverse it and restore the replaced patches atomically. - A good practice is to set .replace flag in any released livepatch. + A good practice is to use only one (default) ``provides`` id. It + makes sure that there always will be only one enabled livepatch + on the system. The consistency model will ensure a safe update + between two versions. It prevents potential problems with installing + two livepatches doing incompatible functional changes. 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..73635f9ddd91 100644 --- a/Documentation/livepatch/livepatch.rst +++ b/Documentation/livepatch/livepatch.rst @@ -347,15 +347,20 @@ 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. +There can be only one active livepatch for a given ``provides`` id. +A new livepatch atomically replaces any existing livepatch whose +``provides`` id matches either the new patch's ``provides`` id or +any id in the new patch's ``obsoletes`` list. + +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 ``provides`` id 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..854ed6230b96 100644 --- a/include/linux/livepatch.h +++ b/include/linux/livepatch.h @@ -123,7 +123,8 @@ 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 + * @provides: only one active livepatch per id + * @obsoletes: replace given livepatch id(s) * @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 +138,9 @@ struct klp_patch { struct module *mod; struct klp_object *objs; struct klp_state *states; - bool replace; + unsigned int provides; + unsigned int *obsoletes; + unsigned int nr_obsoletes; /* internal */ struct list_head list; diff --git a/kernel/livepatch/core.c b/kernel/livepatch/core.c index 5ec30909d382..bd37ed99814e 100644 --- a/kernel/livepatch/core.c +++ b/kernel/livepatch/core.c @@ -350,7 +350,8 @@ int klp_apply_section_relocs(struct module *pmod, Elf_Shdr *sechdrs, * /sys/kernel/livepatch/<patch>/enabled * /sys/kernel/livepatch/<patch>/transition * /sys/kernel/livepatch/<patch>/force - * /sys/kernel/livepatch/<patch>/replace + * /sys/kernel/livepatch/<patch>/provides + * /sys/kernel/livepatch/<patch>/obsoletes * /sys/kernel/livepatch/<patch>/stack_order * /sys/kernel/livepatch/<patch>/<object> * /sys/kernel/livepatch/<patch>/<object>/patched @@ -448,13 +449,32 @@ static ssize_t force_store(struct kobject *kobj, struct kobj_attribute *attr, return count; } -static ssize_t replace_show(struct kobject *kobj, +static ssize_t provides_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { struct klp_patch *patch; patch = container_of(kobj, struct klp_patch, kobj); - return sysfs_emit(buf, "%d\n", patch->replace); + return sysfs_emit(buf, "%u\n", patch->provides); +} + +static ssize_t obsoletes_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct klp_patch *patch; + unsigned int i; + int len = 0; + + patch = container_of(kobj, struct klp_patch, kobj); + if (!patch->obsoletes || !patch->nr_obsoletes) + return sysfs_emit(buf, "\n"); + + for (i = 0; i < patch->nr_obsoletes; i++) + len += sysfs_emit_at(buf, len, "%u%s", patch->obsoletes[i], + i < patch->nr_obsoletes - 1 ? "," : ""); + + len += sysfs_emit_at(buf, len, "\n"); + return len; } static ssize_t stack_order_show(struct kobject *kobj, @@ -481,13 +501,15 @@ static ssize_t stack_order_show(struct kobject *kobj, static struct kobj_attribute enabled_kobj_attr = __ATTR_RW(enabled); static struct kobj_attribute transition_kobj_attr = __ATTR_RO(transition); static struct kobj_attribute force_kobj_attr = __ATTR_WO(force); -static struct kobj_attribute replace_kobj_attr = __ATTR_RO(replace); +static struct kobj_attribute provides_kobj_attr = __ATTR_RO(provides); +static struct kobj_attribute obsoletes_kobj_attr = __ATTR_RO(obsoletes); static struct kobj_attribute stack_order_kobj_attr = __ATTR_RO(stack_order); static struct attribute *klp_patch_attrs[] = { &enabled_kobj_attr.attr, &transition_kobj_attr.attr, &force_kobj_attr.attr, - &replace_kobj_attr.attr, + &provides_kobj_attr.attr, + &obsoletes_kobj_attr.attr, &stack_order_kobj_attr.attr, NULL }; @@ -520,6 +542,28 @@ static void klp_init_func_early(struct klp_object *obj, static void klp_init_object_early(struct klp_patch *patch, struct klp_object *obj); +/* + * Check if old_patch should be replaced by new_patch: + * 1. Same provides ID + * 2. Old provides ID is in new patch's obsoletes list + */ +bool klp_patch_replaceable(struct klp_patch *old_patch, struct klp_patch *new_patch) +{ + unsigned int i; + + if (old_patch->provides == new_patch->provides) + return true; + + if (!new_patch->obsoletes) + return false; + + for (i = 0; i < new_patch->nr_obsoletes; i++) { + if (new_patch->obsoletes[i] == old_patch->provides) + return true; + } + return false; +} + static struct klp_object *klp_alloc_object_dynamic(const char *name, struct klp_patch *patch) { @@ -618,6 +662,9 @@ static int klp_add_nops(struct klp_patch *patch) struct klp_object *old_obj; klp_for_each_patch(old_patch) { + if (!klp_patch_replaceable(old_patch, patch)) + continue; + klp_for_each_object(old_patch, old_obj) { int err; @@ -793,6 +840,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 (!klp_patch_replaceable(old_patch, new_patch)) + continue; klp_free_patch_async(old_patch); } } @@ -985,11 +1034,9 @@ static int klp_init_patch(struct klp_patch *patch) if (ret) return ret; - if (patch->replace) { - ret = klp_add_nops(patch); - if (ret) - return ret; - } + ret = klp_add_nops(patch); + if (ret) + return ret; klp_for_each_object(patch, obj) { ret = klp_init_object(patch, obj); @@ -1204,6 +1251,8 @@ void klp_unpatch_replaced_patches(struct klp_patch *new_patch) klp_for_each_patch(old_patch) { if (old_patch == new_patch) return; + if (!klp_patch_replaceable(old_patch, new_patch)) + continue; old_patch->enabled = false; klp_unpatch_objects(old_patch); diff --git a/kernel/livepatch/core.h b/kernel/livepatch/core.h index 361a0917a03f..558ce7b70754 100644 --- a/kernel/livepatch/core.h +++ b/kernel/livepatch/core.h @@ -18,6 +18,7 @@ void klp_free_replaced_patches_async(struct klp_patch *new_patch); void klp_unpatch_replaced_patches(struct klp_patch *new_patch); void klp_discard_nops(struct klp_patch *new_patch); struct klp_func *klp_find_func(struct klp_object *obj, struct klp_func *func); +bool klp_patch_replaceable(struct klp_patch *old_patch, struct klp_patch *new_patch); static inline bool klp_is_object_loaded(struct klp_object *obj) { diff --git a/kernel/livepatch/state.c b/kernel/livepatch/state.c index 2565d039ade0..f388c9ba20f7 100644 --- a/kernel/livepatch/state.c +++ b/kernel/livepatch/state.c @@ -85,24 +85,58 @@ EXPORT_SYMBOL_GPL(klp_get_prev_state); /* Check if the patch is able to deal with the existing system state. */ static bool klp_is_state_compatible(struct klp_patch *patch, + struct klp_patch *old_patch, struct klp_state *old_state) { struct klp_state *state; state = klp_get_state(patch, old_state->id); + if (klp_patch_replaceable(old_patch, patch)) { + /* + * If the new livepatch will replace the old one, it must + * handle all already modified states (cumulative patch). + */ + if (!state) + return false; + return state->version >= old_state->version; - /* A cumulative livepatch must handle all already modified states. */ - if (!state) - return !patch->replace; + } - return state->version >= old_state->version; + /* + * Two livepatches with a different "provides" must _not_ use + * the same "state->id. + */ + return !state; } /* - * Check that the new livepatch will not break the existing system states. - * Cumulative patches must handle all already modified states. - * Non-cumulative patches can touch already modified states. + * Refuse loading a livepatch which would want to modify a + * function which is already livepatched with the livepatch + * with another "provides". */ +static bool klp_has_function_conflict(struct klp_patch *patch, + struct klp_patch *old_patch) +{ + struct klp_object *obj, *old_obj; + struct klp_func *func; + + klp_for_each_object(patch, obj) { + klp_for_each_object(old_patch, old_obj) { + if (!!obj->name != !!old_obj->name) + continue; + if (obj->name && strcmp(obj->name, old_obj->name)) + continue; + + klp_for_each_func(obj, func) { + if (klp_find_func(old_obj, func)) + return true; + } + } + } + return false; +} + +/* Check that the new livepatch will not break the existing system states. */ bool klp_is_patch_compatible(struct klp_patch *patch) { struct klp_patch *old_patch; @@ -110,9 +144,15 @@ bool klp_is_patch_compatible(struct klp_patch *patch) klp_for_each_patch(old_patch) { klp_for_each_state(old_patch, old_state) { - if (!klp_is_state_compatible(patch, old_state)) + if (!klp_is_state_compatible(patch, old_patch, old_state)) return false; } + + if (klp_patch_replaceable(old_patch, patch)) + continue; + + if (klp_has_function_conflict(patch, old_patch)) + return false; } return true; diff --git a/kernel/livepatch/transition.c b/kernel/livepatch/transition.c index 2351a19ac2a9..15dbc13d0341 100644 --- a/kernel/livepatch/transition.c +++ b/kernel/livepatch/transition.c @@ -89,7 +89,7 @@ static void klp_complete_transition(void) klp_transition_patch->mod->name, klp_target_state == KLP_TRANSITION_PATCHED ? "patching" : "unpatching"); - if (klp_transition_patch->replace && klp_target_state == KLP_TRANSITION_PATCHED) { + if (klp_target_state == KLP_TRANSITION_PATCHED) { klp_unpatch_replaced_patches(klp_transition_patch); klp_discard_nops(klp_transition_patch); } @@ -498,7 +498,7 @@ void klp_try_complete_transition(void) */ if (!patch->enabled) klp_free_patch_async(patch); - else if (patch->replace) + else klp_free_replaced_patches_async(patch); } @@ -720,11 +720,12 @@ void klp_force_transition(void) klp_update_patch_state(idle_task(cpu)); /* Set forced flag for patches being removed. */ - if (klp_target_state == KLP_TRANSITION_UNPATCHED) + if (klp_target_state == KLP_TRANSITION_UNPATCHED) { klp_transition_patch->forced = true; - else if (klp_transition_patch->replace) { + } else { klp_for_each_patch(patch) { - if (patch != klp_transition_patch) + if (patch != klp_transition_patch && + klp_patch_replaceable(patch, klp_transition_patch)) patch->forced = true; } } diff --git a/scripts/livepatch/init.c b/scripts/livepatch/init.c index f14d8c8fb35f..a875dd228cea 100644 --- a/scripts/livepatch/init.c +++ b/scripts/livepatch/init.c @@ -8,6 +8,7 @@ #include <linux/kernel.h> #include <linux/slab.h> #include <linux/livepatch.h> +#include <linux/string.h> static struct klp_patch *patch; @@ -72,10 +73,73 @@ static int __init livepatch_mod_init(void) /* TODO patch->states */ -#ifdef KLP_NO_REPLACE - patch->replace = false; +#ifdef KLP_PROVIDES + patch->provides = KLP_PROVIDES; #else - patch->replace = true; + patch->provides = 0; +#endif + +#ifdef KLP_OBSOLETES + /* + * Parse KLP_OBSOLETES string (format: "0,1,2" or "0" or empty) + * and convert to unsigned int array for patch->obsoletes + * + * Note: KLP_OBSOLETES is always defined and includes at least the + * provides ID, ensuring the patch replaces old patches with same ID. + */ + { + unsigned int *obs_array; + unsigned int count = 1; + char *obsoletes_str; + char *token, *str; + int i = 0; + + for (str = (char *)KLP_OBSOLETES; *str; str++) { + if (*str == ',') + count++; + } + + obsoletes_str = kstrdup(KLP_OBSOLETES, GFP_KERNEL); + if (!obsoletes_str) { + ret = -ENOMEM; + goto err_free_objs; + } + + obs_array = kmalloc_array(count, sizeof(unsigned int), GFP_KERNEL); + if (!obs_array) { + kfree(obsoletes_str); + ret = -ENOMEM; + goto err_free_objs; + } + + str = obsoletes_str; + while ((token = strsep(&str, ",")) != NULL) { + unsigned int val; + + ret = kstrtouint(token, 10, &val); + if (ret) { + kfree(obsoletes_str); + kfree(obs_array); + goto err_free_objs; + } + obs_array[i++] = val; + } + + patch->obsoletes = obs_array; + patch->nr_obsoletes = i; + + if (i > 0) + pr_info("obsoletes patch ids: %s\n", KLP_OBSOLETES); + + kfree(obsoletes_str); + } +#else + /* + * This should never happen as obsoletes always includes provides ID. + * But keep this as a safety fallback. + */ + patch->obsoletes = NULL; + patch->nr_obsoletes = 0; #endif return klp_enable_patch(patch); @@ -96,6 +160,7 @@ static void __exit livepatch_mod_exit(void) kfree(obj->funcs); kfree(patch->objs); + kfree(patch->obsoletes); kfree(patch); } diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build index c4a7acf8edc3..974578328733 100755 --- a/scripts/livepatch/klp-build +++ b/scripts/livepatch/klp-build @@ -21,7 +21,8 @@ shopt -s lastpipe unset DEBUG_CLONE DIFF_CHECKSUM SKIP_CLEANUP VERBOSE XTRACE -REPLACE=1 +PROVIDES=0 +OBSOLETES="" SHORT_CIRCUIT=0 JOBS="$(getconf _NPROCESSORS_ONLN)" shopt -o xtrace | grep -q 'on' && XTRACE=1 @@ -132,7 +133,8 @@ Options: -f, --show-first-changed Show address of first changed instruction -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 + -p, --provides=<id> Set the provides id for this livepatch + -r, --obsoletes=<ids> Set the obsoletes ids array (e.g., "[0,1,2]") -v, --verbose Pass V=1 to kernel/module builds Advanced Options: @@ -147,7 +149,6 @@ Advanced Options: EOF } - usage() { __usage >&2 } @@ -159,8 +160,8 @@ process_args() { local args local patch - short="hfj:o:vdS:T" - long="help,show-first-changed,jobs:,output:,no-replace,verbose,debug,short-circuit:,keep-tmp" + short="hfj:o:p:r:vdS:T" + long="help,show-first-changed,jobs:,output:,provides:,obsoletes:,verbose,debug,short-circuit:,keep-tmp" args=$(getopt --options "$short" --longoptions "$long" -- "$@") || { echo; usage; exit @@ -189,9 +190,26 @@ process_args() { NAME="$(module_name_string "$NAME")" shift 2 ;; - --no-replace) - REPLACE=0 - shift + -p | --provides) + PROVIDES="$2" + shift 2 + ;; + -r | --obsoletes) + OBSOLETES="$2" + local obs_val="$2" + [[ "$obs_val" != \[*\] ]] && die "obsoletes must be enclosed in brackets, e.g., '[0,1,2]'" + local obs_check="${obs_val//[\[\] ]/}" + [[ -z "$obs_check" ]] && shift 2 && continue + [[ "$obs_check" == ,* || "$obs_check" == *, || "$obs_check" == *,,* ]] && \ + die "obsoletes has invalid comma usage: '$obs_val'" + local IFS=',' + local obs_elem + for obs_elem in $obs_check; do + [[ ! "$obs_elem" =~ ^[0-9]+$ ]] && \ + die "obsoletes contains invalid value '$obs_elem': only non-negative integers allowed" + done + unset IFS + shift 2 ;; -v | --verbose) VERBOSE=1 @@ -235,6 +253,47 @@ process_args() { exit 1 fi + # Always add provides ID to obsoletes (even if provides=0) + # This ensures the new patch replaces old patches with the same provides ID + if [[ -n "$OBSOLETES" && "$OBSOLETES" != "[]" ]]; then + local obsoletes_clean="${OBSOLETES//[\[\] ]/}" + local IFS=',' + local -a obs_array=() + local obs_id + local provides_added=0 + local already_exists + + # Parse existing obsoletes values and remove duplicates + for obs_id in $obsoletes_clean; do + if [[ -n "$obs_id" ]]; then + already_exists=0 + for existing in "${obs_array[@]}"; do + if [[ "$existing" -eq "$obs_id" ]]; then + already_exists=1 + break + fi + done + + if [[ "$already_exists" -eq 0 ]]; then + obs_array+=("$obs_id") + if [[ "$obs_id" -eq "$PROVIDES" ]]; then + provides_added=1 + fi + fi + fi + done + + if [[ "$provides_added" -eq 0 ]]; then + obs_array+=("$PROVIDES") + fi + + local IFS=',' + OBSOLETES="[${obs_array[*]}]" + unset IFS + elif [[ -z "$OBSOLETES" || "$OBSOLETES" == "[]" ]]; then + OBSOLETES="[$PROVIDES]" + fi + KEEP_TMP="$keep_tmp" PATCHES=("$@") @@ -847,7 +906,17 @@ build_patch_module() { cflags=("-ffunction-sections") cflags+=("-fdata-sections") - [[ $REPLACE -eq 0 ]] && cflags+=("-DKLP_NO_REPLACE") + cflags+=("-DKLP_PROVIDES=$PROVIDES") + + # Process OBSOLETES: remove brackets and spaces, convert to comma-separated string + # Input: "[0, 1, 2]" or "[]" or "" -> Output: "0,1,2" or empty + if [[ -n "$OBSOLETES" && "$OBSOLETES" != "[]" ]]; then + # Remove '[', ']', and spaces, keep commas + local obsoletes_clean="${OBSOLETES//[\[\] ]/}" + # Escape quotes properly for C string macro + cflags+=("-DKLP_OBSOLETES=\\\"$obsoletes_clean\\\"") + [[ -v VERBOSE ]] && echo " KLP_OBSOLETES=$obsoletes_clean (from OBSOLETES=$OBSOLETES, PROVIDES=$PROVIDES)" >&2 + fi cmd=("make") if [[ -v VERBOSE ]]; then -- 2.52.0