MINOR: debug: add ability to dump loaded shared libraries
Many times core dumps reported by users who experience trouble are difficult to exploit due to missing system libraries. Sometimes, having just a list of loaded libraries and their respective addresses can already provide some hints about some problems. This patch makes a step in that direction by adding a new "show libs" command that will try to enumerate the list of object files that are loaded in memory, relying on the dynamic linker for this. It may also be used to detect that some foreign code embarks other undesired libs (e.g. some external Lua modules). At the moment it's only supported on glibc when USE_DL is set, but it's implemented in a way that ought to make it reasonably easy to be extended to other platforms.
This commit is contained in:
parent
3f3a56c9b0
commit
6ab7b21a11
@ -2623,6 +2623,19 @@ show info [typed|json] [desc] [float]
|
||||
$ echo "show info json" | socat /var/run/haproxy.sock stdio | \
|
||||
python -m json.tool
|
||||
|
||||
show libs
|
||||
Dump the list of loaded shared dynamic libraries and object files, on systems
|
||||
that support it. When available, for each shared object the range of virtual
|
||||
addresses will be indicated, the size and the path to the object. This can be
|
||||
used for example to try to estimate what library provides a function that
|
||||
appears in a dump. Note that on many systems, addresses will change upon each
|
||||
restart (address space randomization), so that this list would need to be
|
||||
retrieved upon startup if it is expected to be used to analyse a core file.
|
||||
This command may only be issued on sockets configured for levels "operator"
|
||||
or "admin". Note that the output format may vary between operating systems,
|
||||
architectures and even haproxy versions, and ought not to be relied on in
|
||||
scripts.
|
||||
|
||||
show map [[@<ver>] <map>]
|
||||
Dump info about map converters. Without argument, the list of all available
|
||||
maps is returned. If a <map> is specified, its contents are dumped. <map> is
|
||||
|
@ -250,6 +250,7 @@ typedef struct { } empty_t;
|
||||
/* dl_iterate_phdr() is available in GLIBC 2.2.4 and up. Let's round up to 2.3.x */
|
||||
#if defined(USE_DL) && defined(__GNU_LIBRARY__) && (__GLIBC__ > 2 || __GLIBC__ == 2 && __GLIBC_MINOR__ >= 3)
|
||||
#define HA_HAVE_DL_ITERATE_PHDR
|
||||
#define HA_HAVE_DUMP_LIBS
|
||||
#endif
|
||||
|
||||
/* malloc_trim() can be very convenient to reclaim unused memory especially
|
||||
|
@ -984,6 +984,7 @@ const void *resolve_sym_name(struct buffer *buf, const char *pfx, const void *ad
|
||||
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);
|
||||
|
||||
/* Note that this may result in opening libgcc() on first call, so it may need
|
||||
* to have been called once before chrooting.
|
||||
|
18
src/debug.c
18
src/debug.c
@ -305,6 +305,21 @@ static int cli_io_handler_show_threads(struct appctx *appctx)
|
||||
return 1;
|
||||
}
|
||||
|
||||
#if defined(HA_HAVE_DUMP_LIBS)
|
||||
/* parse a "show libs" command. It returns 1 if it emits anything otherwise zero. */
|
||||
static int debug_parse_cli_show_libs(char **args, char *payload, struct appctx *appctx, void *private)
|
||||
{
|
||||
if (!cli_has_level(appctx, ACCESS_LVL_OPER))
|
||||
return 1;
|
||||
|
||||
chunk_reset(&trash);
|
||||
if (dump_libs(&trash, 1))
|
||||
return cli_msg(appctx, LOG_INFO, trash.area);
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* dumps a state of all threads into the trash and on fd #2, then aborts. */
|
||||
void ha_panic()
|
||||
{
|
||||
@ -1204,6 +1219,9 @@ static struct cli_kw_list cli_kws = {{ },{
|
||||
{{ "debug", "dev", "sym", NULL }, "debug dev sym <addr> : resolve symbol address", debug_parse_cli_sym, NULL, NULL, NULL, ACCESS_EXPERT },
|
||||
{{ "debug", "dev", "tkill", NULL }, "debug dev tkill [thr] [sig] : send signal to thread", debug_parse_cli_tkill, NULL, NULL, NULL, ACCESS_EXPERT },
|
||||
{{ "debug", "dev", "write", NULL }, "debug dev write [size] : write that many bytes in return", debug_parse_cli_write, NULL, NULL, NULL, ACCESS_EXPERT },
|
||||
#if defined(HA_HAVE_DUMP_LIBS)
|
||||
{{ "show", "libs", NULL, NULL }, "show libs : show loaded object files and libraries", debug_parse_cli_show_libs, NULL, NULL },
|
||||
#endif
|
||||
{{ "show", "threads", NULL, NULL }, "show threads : show some threads debugging information", NULL, cli_io_handler_show_threads, NULL },
|
||||
{{},}
|
||||
}};
|
||||
|
90
src/tools.c
90
src/tools.c
@ -4983,6 +4983,96 @@ const void *resolve_sym_name(struct buffer *buf, const char *pfx, const void *ad
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* On systems where this is supported, let's provide a possibility to enumerate
|
||||
* the list of object files. The output is appended to a buffer initialized by
|
||||
* the caller, with one name per line. A trailing zero is always emitted if data
|
||||
* are written. Only real objects are dumped (executable and .so libs). The
|
||||
* function returns non-zero if it dumps anything. These functions do not make
|
||||
* use of the trash so that it is possible for the caller to call them with the
|
||||
* trash on input. The output format may be platform-specific but at least one
|
||||
* version must emit raw object file names when argument is zero.
|
||||
*/
|
||||
#if defined(HA_HAVE_DUMP_LIBS)
|
||||
# if defined(HA_HAVE_DL_ITERATE_PHDR)
|
||||
/* the private <data> we pass below is a dump context initialized like this */
|
||||
struct dl_dump_ctx {
|
||||
struct buffer *buf;
|
||||
int with_addr;
|
||||
};
|
||||
|
||||
static int dl_dump_libs_cb(struct dl_phdr_info *info, size_t size, void *data)
|
||||
{
|
||||
struct dl_dump_ctx *ctx = data;
|
||||
const char *fname;
|
||||
size_t p1, p2, beg, end;
|
||||
int idx;
|
||||
|
||||
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;
|
||||
|
||||
if (!ctx->with_addr)
|
||||
goto dump_name;
|
||||
|
||||
/* virtual addresses are relative to the load address and are per
|
||||
* pseudo-header, so we have to scan them all to find the furthest
|
||||
* one from the beginning. In this case we only dump entries if
|
||||
* they have at least one section.
|
||||
*/
|
||||
beg = ~0; end = 0;
|
||||
for (idx = 0; idx < info->dlpi_phnum; idx++) {
|
||||
if (!info->dlpi_phdr[idx].p_memsz)
|
||||
continue;
|
||||
p1 = info->dlpi_phdr[idx].p_vaddr;
|
||||
if (p1 < beg)
|
||||
beg = p1;
|
||||
p2 = p1 + info->dlpi_phdr[idx].p_memsz - 1;
|
||||
if (p2 > end)
|
||||
end = p2;
|
||||
}
|
||||
|
||||
if (!idx)
|
||||
goto leave;
|
||||
|
||||
chunk_appendf(ctx->buf, "0x%012llx-0x%012llx (0x%07llx) ",
|
||||
(ullong)info->dlpi_addr + beg,
|
||||
(ullong)info->dlpi_addr + end,
|
||||
(ullong)(end - beg + 1));
|
||||
dump_name:
|
||||
chunk_appendf(ctx->buf, "%s\n", fname);
|
||||
leave:
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* dumps lib names and optionally address ranges */
|
||||
int dump_libs(struct buffer *output, int with_addr)
|
||||
{
|
||||
struct dl_dump_ctx ctx = { .buf = output, .with_addr = with_addr };
|
||||
size_t old_data = output->data;
|
||||
|
||||
dl_iterate_phdr(dl_dump_libs_cb, &ctx);
|
||||
return output->data != old_data;
|
||||
}
|
||||
# else // no DL_ITERATE_PHDR
|
||||
# error "No dump_libs() function for this platform"
|
||||
# endif
|
||||
#else // no HA_HAVE_DUMP_LIBS
|
||||
|
||||
/* unsupported platform: do not dump anything */
|
||||
int dump_libs(struct buffer *output, int with_addr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif // HA_HAVE_DUMP_LIBS
|
||||
|
||||
/*
|
||||
* Allocate an array of unsigned int with <nums> as address from <str> string
|
||||
* made of integer separated by dot characters.
|
||||
|
Loading…
x
Reference in New Issue
Block a user