MINOR: debug: read all libs in memory when set-dumpable=libs

When "set-dumpable" is set to "libs", in addition to marking the process
dumpable, haproxy also reads the binary and shared objects into memory as
a tar archive in a page-aligned location so that these files are easily
extractable from a future core dump. The goal here is to always have
access to the exact same binary and libs as those which caused the core
to happen. It's indeed very frequent to miss some of these, or to get
mismatching files due to a local update that didn't experience a reload,
or to get those of a host system instead of the container.

The in-memory tar file presents everything under a directory called
"core-%d" where %d corresponds to the PID of the worker process. In
order to ease the finding of these data in the core dump, the memory
area is contiguous and surrounded by PROT_NONE pages so that it appears
in its own segment in the core file. The total size used by this is a
few tens of MB, which is not a problem on large systems.
This commit is contained in:
Willy Tarreau 2026-03-18 10:47:16 +01:00
parent 6152a4eef5
commit e1738b665d
7 changed files with 115 additions and 2 deletions

View File

@ -3148,7 +3148,25 @@ server-state-file <file>
configuration. See also "server-state-base" and "show servers state",
"load-server-state-from-file" and "server-state-file-name"
set-dumpable [ on | off ]
set-dumpable [ on | off | libs ]
This option helps choose the core dump behavior in case of process crash.
Available options are:
- on this enables core dumping at the process level if it was
previously disabled.
- off this disables a previously enabled core dumping.
- libs this enables core dumping with an embedded copy of the binaries and
libraries that are required for debugging. This may be requested by
developers. In this case haproxy will try to load the libraries it
depends on into memory and keep them preciously. If the process
crashes, they will be dumped into the core so there is no need for
retrieving them from the file system anymore and no risk that they
do not match the core. This takes a few megabytes to a few tens of
megabytes of additional RAM, so it is better not to use it on small
systems.
This option is better left disabled by default and enabled only upon a
developer's request. By default it is disabled. Without argument, it defaults
to "on". If it has been enabled, it may still be forcibly disabled by prefixing

View File

@ -79,7 +79,7 @@
#define GTUNE_DISABLE_H2_WEBSOCKET (1<<21)
#define GTUNE_DISABLE_ACTIVE_CLOSE (1<<22)
#define GTUNE_QUICK_EXIT (1<<23)
/* (1<<24) unused */
#define GTUNE_COLLECT_LIBS (1<<24)
/* (1<<25) unused */
#define GTUNE_USE_FAST_FWD (1<<26)
#define GTUNE_LISTENER_MQ_FAIR (1<<27)

View File

@ -58,6 +58,10 @@ extern int devnullfd;
extern int fileless_mode;
extern struct cfgfile fileless_cfg;
/* storage for collected libs */
extern void *lib_storage;
extern size_t lib_size;
struct proxy;
struct server;
int main(int argc, char **argv);

View File

@ -1153,6 +1153,7 @@ const char *get_exec_path(void);
void *get_sym_curr_addr(const char *name);
void *get_sym_next_addr(const char *name);
int dump_libs(struct buffer *output, int with_addr);
void collect_libs(void);
/* Note that this may result in opening libgcc() on first call, so it may need
* to have been called once before chrooting.

View File

@ -97,6 +97,8 @@ int cfg_parse_global(const char *file, int linenum, char **args, int kwm)
}
if (!*args[1] || strcmp(args[1], "on") == 0)
global.tune.options |= GTUNE_SET_DUMPABLE;
else if (strcmp(args[1], "libs") == 0)
global.tune.options |= GTUNE_SET_DUMPABLE | GTUNE_COLLECT_LIBS;
else if (strcmp(args[1], "off") == 0)
global.tune.options &= ~GTUNE_SET_DUMPABLE;
else {

View File

@ -271,6 +271,10 @@ unsigned int tainted = 0;
unsigned int experimental_directives_allowed = 0;
unsigned int deprecated_directives_allowed = 0;
/* mapped storage for collected libs */
void *lib_storage = NULL;
size_t lib_size = 0;
int check_kw_experimental(struct cfg_keyword *kw, const char *file, int linenum,
char **errmsg)
{
@ -2516,6 +2520,10 @@ static void step_init_2(int argc, char** argv)
chunk_appendf(&trash, "TARGET='%s'", pm_target_opts);
post_mortem_add_component("haproxy", haproxy_version, cc, cflags, opts, argv[0]);
if ((global.tune.options & (GTUNE_SET_DUMPABLE | GTUNE_COLLECT_LIBS)) ==
(GTUNE_SET_DUMPABLE | GTUNE_COLLECT_LIBS))
collect_libs();
}
/* This is a third part of the late init sequence, where we register signals for

View File

@ -6031,6 +6031,81 @@ int dump_libs(struct buffer *output, int with_addr)
dl_iterate_phdr(dl_dump_libs_cb, &ctx);
return output->data != old_data;
}
/* the private <data> we pass below is a dump context initialized like this */
struct dl_collect_ctx {
char *storage;
size_t size;
char *prefix;
int pos;
};
static int dl_collect_libs_cb(struct dl_phdr_info *info, size_t size, void *data)
{
struct dl_collect_ctx *ctx = data;
const char *fname;
if (!info || !info->dlpi_name)
goto leave;
if (!*info->dlpi_name)
fname = get_exec_path();
else if (strchr(info->dlpi_name, '/'))
fname = info->dlpi_name;
else
/* else it's a VDSO or similar and we're not interested */
goto leave;
load_file_into_tar(&ctx->storage, &ctx->size, ctx->prefix, fname, NULL, "haproxy-libs-dump");
leave:
/* increment the object's number */
ctx->pos++;
return 0;
}
/* dumps lib names and optionally address ranges */
void collect_libs(void)
{
struct dl_collect_ctx ctx = { .storage = NULL, .size = 0, .pos = 0 };
ulong pagesize = sysconf(_SC_PAGESIZE);
char dir_name[16];
size_t new_size;
void *page;
/* prepend a directory named after the starting pid */
snprintf(dir_name, sizeof(dir_name), "core-%u", getpid());
ctx.prefix = dir_name;
/* callbacks will (re-)allocate ctx->storage */
dl_iterate_phdr(dl_collect_libs_cb, &ctx);
/* now that the archive is complete, we need to close it by appending
* two empty 512B blocks. We'll also place it aligned in an isolated
* mapped area so that it uses its own segment in a core dump for
* easier locating. In order to do this, we'll allocate two extra
* pages and will punch holes around.
*/
new_size = (ctx.size + 2*512 + 2*pagesize + pagesize - 1) & -pagesize;
page = mmap(NULL, new_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page != MAP_FAILED) {
/* punch holes around that won't go into the core */
mprotect(page, pagesize, PROT_NONE);
mprotect(page + new_size - pagesize, pagesize, PROT_NONE);
new_size -= 2*pagesize;
page += pagesize;
/* copy and make read-only */
memcpy(page, ctx.storage, ctx.size);
mprotect(page, lib_size, PROT_READ);
vma_set_name(page, new_size, "archive", "boot-libs");
lib_storage = page;
lib_size = new_size;
}
/* don't need the temporary storage anymore */
ha_free(&ctx.storage);
}
# else // no DL_ITERATE_PHDR
# error "No dump_libs() function for this platform"
# endif
@ -6042,6 +6117,11 @@ int dump_libs(struct buffer *output, int with_addr)
return 0;
}
/* unsupported platform: do not collect anything */
void collect_libs(void)
{
}
#endif // HA_HAVE_DUMP_LIBS
/*