LIBPULP(7)
LIBPULP π (that's the LIBPULP(7) page bestie, section 7, "Libpulp Overview")
β¨ NAME β¨
Libpulp β Userspace Live Patching π©Ήβ¨
πΈ INTRODUCTION πΈ
Okay so picture this: Libpulp is a framework that makes live patching of userspace processes happen. Translation? A process that's already running can get modified without you having to restart the whole entire application. No downtime, no "sorry we're closed", no nothing. π
Libpulp is two things at once, and she's serving both:
- a library, which hands live patching capabilities to processes that are already running, and
- a collection of tools that actually go out there and perform the live patching operations.
It's giving glow-up mid-flight, fr fr. π¦
π₯ REQUIREMENTS AND RESTRICTIONS π₯
Not gonna lie bestie, there's a vibe check here. All of the following requirements and restrictions have to be met for live patching operations to work. Read this part, I'm begging you. π
Process Startup
Live patches cannot be applied to every process in the system β that's just not how any of this works. For a process to even be eligible for live patching, it has to dynamically load libpulp.so, which is the runtime support for live patching operations.
Normally, dynamically linked libraries that a program needs get loaded into memory automatically during process initialization. But β and this is the plot twist β live patching operations aren't called by the program itself, they're called from an external tool. So there's no real dependency there, which means libpulp.so might be completely missing from the DT_NEEDED entries (peep ld.so(8) for that whole situation).
To bridge that gap: processes should be started with
LD_PRELOAD=libpulp.so
That's the move. π
Target Libraries Rebuilding
Not every library in the system is eligible for live patching either β only the ones that have been previously prepared to be. We call those target libraries. π
Making a target library does not require changes to its source code. However, it does require rebuilding the library with patchable function entries (see -fpatchable-function-entry in gcc(1)). So like, no source edits, but a rebuild is non-negotiable. π
Patch Granularity
Live patches cannot be applied to arbitrarily tiny little bits of a program. When a live patch gets applied, it replaces entire functions at a time. Whole functions. Nothing smaller. That's the smallest unit of chaos available, bestie. π
Function visibility
Live patches cannot be applied to every function in a process either. Two conditions, both mandatory:
- First, only functions belonging to shared libraries can be live patched. Functions in the application itself, or functions linked from static libraries, are simply not eligible.
- Secondly, only externally visible (
GLOBALorWEAK) functions can be targets of a live patch.
If your function is out here being shy and invisible, she's not getting patched. Periodt. π
Process Permissions
Libpulp uses ptrace to attach to target processes, which means only the owner of a process (or root) can apply a live patch to it. Sorry, you can't just patch your friend's stuff. π«
Moreover, on systems that have Linux Security Modules (LSM) enabled, some extra convincing might be required (see Ptrace access mode checking in ptrace(2)). Sometimes you gotta flirt with the security policy a little. π
π οΈ TOOLS π οΈ
Creating and applying live patches is achieved with the following tools. Meet the squad:
ulp_post(1)β Converts the sequences of one-bytenopsat the entry of patchable functions into multi-bytenops.ulp_packer(1)β Creates a live patch metadata file based on the live patch description file that it takes as argument. That metadata file can later be used byulp_trigger(1)to actually apply the live patch to a running process. For more info about the description file format, go look at the METADATA section below. πulp_reverse(1)β Opens the metadata file passed as argument and creates a new metadata file that can be used to reverse a previously applied live patch. Undo button, but make it Unix. β©οΈulp_dump(1)β Parses the metadata file passed as argument and prints its contents in human-readable format. Show me everything, no secrets. πulp_trigger(1)β Applies the live patch described by the metadata file received as argument to the process with the specifiedpid.ulp_check(1)β Checks if the live patch referred to by the metadata file has already been applied to the process with the specifiedpid. The "are we there yet" of tools. β¨
π«Ά ANATOMY OF LIVE PATCHES π«Ά
A live patch is very simple, and honestly? Iconic for that. She's composed of exactly two things:
- a dynamic shared object (DSO) containing replacement functions, and
- a metadata file describing which target library and which functions those replacements are meant for.
The DSO is no different than a regular shared library β it's built from regular source code into a shared object, nothing special. The metadata is described below. π§
π METADATA π
A metadata file describes a live patch. It contains three pieces of information:
Path to live patch DSO
Live patching operates on a function-by-function basis (see Patch Granularity). These functions are provided in a dynamic shared object file. The absolute path to this file is recorded in the metadata. π
Path to the target library DSO
Each live patch contains replacement functions for a specific library. The absolute path to the in-disk location of the library is recorded in the metadata.
Libpulp compares this path against the memory mappings of the target process, as provided by procfs(5). Which means β get this β even if the library is removed from disk between the starting of the process and the application of the live patch, the comparison still works. Resilient queen. π
List of replacement functions
Libpulp needs a correlation between the original functions in the target library and the replacement functions provided by the live patch. This correlation is recorded in the metadata file as a list of pairs of functions. Matchmaking, but for functions. π
Description file format
The metadata file is created based on a description file. The description file is rather simple, okay, here's the layout:
- The first line contains the absolute path to the live patch DSO.
- The second line starts with the
@character, immediately followed by the absolute path to the target library DSO. - Subsequent lines, when not preceded by the
#character, provide the list of replacement functions, where each line starts with the name of the original function, followed by a colon, then by the name of the replacement function. - Finally, subsequent lines that do start with
#specify the offsets that local (not-exported) variables in the target library have from the beginning of the library load location, as well as the offsets of references to those variables in the live patch object. Each of these lines is composed of four items separated by colons:- the name of the variable in the target library;
- the name of a reference to it in the live patch object;
- the offset of the library variable within the library DSO;
- the offset of the reference variable within the live patch DSO.
These offsets are used by Libpulp to enable access to local variables from the live patch. π
For example, a live patch to the math library could have a description file that looked like the following snippet (paths may differ across distributions):
/usr/lib64/livepatches/libm_livepatch_20210514.so
@/lib64/libm.so.6
hypot:hypot_v2
gamma:new_gamma
atan:atan_new
#narenas:ulpr_narenas:00000000001c2720:0000000000004020
#main_arena:ulpr_main_arena:00000000001c3a00:0000000000004060
β οΈ Notice, however, that even though the paths mentioned above refer to files in storage, the patches are not applied to the files themselves. Rather, they are applied to running processes that have loaded these files. See ulp_trigger(1). Don't patch the file, bestie, patch the process. π
to be continuedβ¦ π
β¨ EXAMPLES β¨
okay bestie, this is the part where it all comes together π everything below is a full worked example of how you actually use Libpulp, start to finish, no skips.
First things first: you gotta make a live patchable library, and compile it the right way or it simply will not patch. β¨
πΈ Library source πΈ
#include <stdlib.h>
#include <string.h>
char *
proverb(void)
{
int selection;
char *result;
char *proverbs[] = {
"A picture is worth a thousand words",
"Actions speak louder than words",
"An apple a day keeps the doctor away",
"Birds of a feather flock together",
"Do not judge a book by its cover",
"Never look a gift horse in the mouth",
"Practice makes perfect",
"Slow and steady wins the race",
"There is no place like home",
"Too many cooks spoil the broth"
};
selection = rand() % (sizeof(proverbs) / sizeof(char *));
result = strdup(proverbs[selection]);
return result;
}
So remember how the Target Libraries Rebuilding section said it: to be live patchable, a target library has to be built with patchable function entries. That's the whole game. Non-negotiable, fr fr. Other than that, you can optionally post-process it with ulp_post(1):
$ gcc library.c -o library.so \
-shared -fPIC \
-fpatchable-function-entry=24,22
$ ulp_post library.so
Next up, a program that uses the library π¦
π Program source π
#include <stdio.h>
#include <unistd.h>
char *proverb(void);
int
main(void)
{
char buffer[128];
printf("%d\n", getpid());
while (fgets(buffer, sizeof(buffer), stdin))
printf("%s\n", proverb());
return 0;
}
The tea here: applications themselves do not require rebuilds π but for the sake of completeness (and because we love a full picture), here's how you'd build an application and link it to a library sitting in a non-default location:
$ gcc program.c -L$PWD -lrary -Wl,-rpath=$PWD -o program
Once it starts up, the program prints its own PID β and you're gonna need that PID later in this example, so don't lose it π. Also, hitting ENTER makes the program call into the library, and the library replies with a message:
$ LD_PRELOAD=libpulp.so ./program
libpulp loaded...
12345
<ENTER>
Birds of a feather flock together
<ENTER>
An apple a day keeps the doctor away
(and so on...)
Now, remember how a live patch can only replace entire functions? (go back and reread Patch Granularity if you need to, no shame π
). So the live patch source below gives you a reimplementation of the proverb function, and it gets a different name so nothing clashes:
π Live patch source π
#include <string.h>
char *
proverb_v2(void)
{
return strdup("All good things must come to an end");
}
Live patches get built just like shared libraries β and yes, that means you need the -shared option, don't forget it:
$ gcc livepatch.c -shared -fPIC -o livepatch.so
okay so, next thing: a live patch is not just the object you built above. It also needs a metadata file, which is how Libpulp knows which library the live patch is for, and it also gives you the correlation between the original functions and the replaced ones. And a metadata file is built out of a description file. So:
π Description file π
/absolute/path/to/livepatch.so
@/absolute/path/to/library.so
proverb:proverb_v2
Converting description β‘οΈ metadata is a ulp_packer(1) job:
$ ulp_packer livepatch.dsc -o livepatch.ulp
And finally, ulp_trigger(1) is what you use to connect to the target process and actually apply the live patch. Notice how the PID gets specified, with the -p option:
$ ulp_trigger -p 12345 livepatch.ulp
Wrapping up π«Ά the target process is now live patched, and it should behave totally differently when ENTER gets hit in its controlling terminal:
(...)
<ENTER>
All good things must come to an end
<ENTER>
All good things must come to an end
iconic. we ate. no cap. π
π SEE ALSO π
go read these next, bestie, they're the whole supporting cast π
ptrace(2)ulp_packer(1)ulp_reverse(1)ulp_dump(1)ulp_post(1)ulp_trigger(1)ulp_check(1)
and that's the whole page, babes π this was LIBPULP(7) β section 7 of the manual, and if you made it this far you're genuinely built different π go patch something, live your main character era, and remember: the vibe changes, the facts never do β¨π§ββοΈ
π¦ where this page comes from
- debian trixie: libpulp-tools 0.2.8-1 Β· LGPL-2.1-or-later
- opensuse-leap 16.0: libpulp-tools 0.3.17-160000.2.1 Β· LGPL-2.1-or-later
- opensuse-leap 16.0: libpulp-tools 0.3.17-160000.2.1 Β· LGPL-2.1-or-later
- Copyright (C) 2021 SUSE Software Solutions GmbH
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms