]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: sym: collect the symbols known to the dynamic linker
authorOlivier Houchard <ohouchard@haproxy.com>
Tue, 15 Sep 2026 13:07:53 +0000 (15:07 +0200)
committerOlivier Houchard <cognet@ci0.org>
Thu, 1 Oct 2026 15:06:31 +0000 (17:06 +0200)
The dynamic linker keeps .dynsym/.dynstr of every loaded object mapped for
relocation. Where dl_iterate_phdr() exists, walk each object's PT_DYNAMIC
to collect its exported function symbols from the memory image, plus its
address range from PT_LOAD. This works even when files are unreadable (e.g.
after chroot) and gives what dladdr() sees, but lock-free.

When an address falls past the nearest lower symbol, resolve_sym_name() now
prints "sym+off/size" instead of a bare offset, easier to feed to gdb. Add
"tune.disable-elf-symbols" to restore the previous output.

doc/configuration.txt
include/haproxy/global-t.h
src/cfgparse-global.c
src/sym.c

index a6cd50c7cdd6773e980044f2dba6191ad0330b6e..4cf633685ccaa65599b71881399b798476424678 100644 (file)
@@ -1914,6 +1914,7 @@ The following keywords are supported in the "global" section :
    - tune.cli.max-payload-size
    - tune.comp.maxlevel
    - tune.defaults.purge
+   - tune.disable-elf-symbols
    - tune.disable-fast-forward
    - tune.disable-zero-copy-forwarding
    - tune.epoll.mask-events
@@ -4341,6 +4342,15 @@ tune.defaults.purge [{ all | none | <element[,...]> }]
       tune.defaults.purge
       tune.defaults.purge proxies
 
+tune.disable-elf-symbols
+  Disables the in-process symbol table used to name functions in backtraces,
+  thread dumps and pointer dumps. It is built at startup from the symbols known
+  to the dynamic linker for the executable and all loaded libraries, and lets
+  more functions be named than dladdr() alone. When disabled, only dladdr() and
+  a small built-in list of functions are used, as in older versions. This
+  command is not meant for regular use, it will generally only be suggested by
+  developers along complex debugging sessions.
+
 tune.disable-fast-forward
   Disables the data fast-forwarding. It is a mechanism to optimize the data
   forwarding by passing data directly from a side to the other one without
index 239f11ba5663c214f2bbf1450c845abb2761f205..6ebfaffb1badb4be8fbc58a71029b6010e86a71e 100644 (file)
@@ -91,6 +91,7 @@
 
 /* subsystem-specific debugging options for tune.debug */
 #define GDBG_CPU_AFFINITY           (1U<< 0)
+#define GDBG_NO_ELF_SYMS            (1U<< 1)
 
 #define NO_ZERO_COPY_FWD             0x0001 /* Globally disable zero-copy FF */
 #define NO_ZERO_COPY_FWD_PT          0x0002 /* disable zero-copy FF for PT (recv & send are disabled automatically) */
index 64b640e22d7e7cda735e9cca2a738dc864434572..f09f865e70bd4525b6b8df35184601b0b78e2ae4 100644 (file)
@@ -1574,6 +1574,26 @@ static int cfg_parse_global_tune_forward_opts(char **args, int section_type,
 
 }
 
+/* Parser for tune options related to the symbol resolution used for
+ * backtraces.
+ */
+static int cfg_parse_global_tune_debug_opts(char **args, int section_type,
+                                           struct proxy *curpx, const struct proxy *defpx,
+                                           const char *file, int line, char **err)
+{
+       if (strcmp(args[0], "tune.disable-elf-symbols") == 0) {
+               if (too_many_args(0, args, err, NULL))
+                       return -1;
+               global.tune.debug |= GDBG_NO_ELF_SYMS;
+       }
+       else {
+               BUG_ON(1, "Triggered in cfg_parse_global_tune_debug_opts() by unsupported keyword.");
+               return -1;
+       }
+
+       return 0;
+}
+
 static int cfg_parse_global_unsupported_opts(char **args, int section_type,
                                             struct proxy *curpx, const struct proxy *defpx,
                                             const char *file, int line, char **err)
@@ -1976,6 +1996,7 @@ static struct cfg_kw_list cfg_kws = {ILH, {
        { CFG_GLOBAL, "tune.chksize", cfg_parse_global_unsupported_opts },
        { CFG_GLOBAL, "tune.comp.maxlevel", cfg_parse_global_tune_opts },
        { CFG_GLOBAL, "tune.defaults.purge", cfg_parse_global_tune_opts },
+       { CFG_GLOBAL, "tune.disable-elf-symbols", cfg_parse_global_tune_debug_opts },
        { CFG_GLOBAL, "tune.disable-fast-forward", cfg_parse_global_tune_forward_opts },
        { CFG_GLOBAL, "tune.disable-zero-copy-forwarding", cfg_parse_global_tune_forward_opts },
        { CFG_GLOBAL, "tune.fd.tables", cfg_parse_global_tune_opts, KWF_EXPERIMENTAL },
index 3bf0eb15082f45f93a2c144f97648dd9ff3ef58e..e4a2b39c3e22c6cc3bfe18cd3feed7f6f501355b 100644 (file)
--- a/src/sym.c
+++ b/src/sym.c
  * This maintains a table of function symbols (address, size, name, owning
  * object) sorted by address, so that backtraces and pointer dumps can name
  * the functions they hit. It is built at boot time
- * from whatever is available; for now only the well-known entry points of
- * sym_known_fcts[] are fed into it.
+ * from the well-known entry points of sym_known_fcts[] and, where the dynamic
+ * linker exposes dl_iterate_phdr(), from the exported symbols of the
+ * executable and of every loaded shared library, read straight from their
+ * memory image (what dladdr() sees, but lock-free and usable in a chroot).
  *
  * Lookups (sym_resolve()) are lock-free and async-signal-safe: they only read
  * an immutable snapshot, replaced only while no thread is running and
@@ -217,9 +219,191 @@ static void add_known(struct sym_build *b, const struct sym_obj *obj)
                              sym_known_fcts[i].name, obj);
 }
 
+#if defined(HA_HAVE_DL_ITERATE_PHDR)
+
+#include <elf.h>
+
+/* native ELF symbol-type accessor, derived from the toolchain since glibc
+ * doesn't provide *_NATIVE convenience macros.
+ */
+#if __WORDSIZE == 64
+# define SYM_ST_TYPE(i)  ELF64_ST_TYPE(i)
+#else
+# define SYM_ST_TYPE(i)  ELF32_ST_TYPE(i)
+#endif
+
+/*
+ * Collects the exported symbols of the object as provided by the dynamic
+ * linker.
+ */
+static void collect_dynsym_mem(struct sym_build *b, const struct dl_phdr_info *info,
+                              const struct sym_obj *obj)
+{
+       const ElfW(Dyn) *dyn = NULL;
+       const ElfW(Sym) *sym = NULL;
+       const char *str = NULL;
+       const ElfW(Word) *hash = NULL;
+       const uint32_t *gnu = NULL;
+       unsigned long base = info->dlpi_addr;
+       size_t strsz = 0, nsyms = 0, i;
+
+       for (i = 0; i < info->dlpi_phnum; i++) {
+               if (info->dlpi_phdr[i].p_type == PT_DYNAMIC) {
+                       dyn = (const ElfW(Dyn) *)(base + info->dlpi_phdr[i].p_vaddr);
+                       break;
+               }
+       }
+       if (!dyn)
+               return;
+
+       for (; dyn->d_tag != DT_NULL; dyn++) {
+               unsigned long ptr = dyn->d_un.d_ptr;
+
+               /* glibc relocates these pointers in place, other loaders leave
+                * the link-time vaddr which is then below the load address.
+                */
+               if (ptr < base)
+                       ptr += base;
+
+               switch (dyn->d_tag) {
+               case DT_SYMTAB:   sym  = (const ElfW(Sym) *)ptr; break;
+               case DT_STRTAB:   str  = (const char *)ptr; break;
+               case DT_STRSZ:    strsz = dyn->d_un.d_val; break;
+               case DT_HASH:     hash = (const ElfW(Word) *)ptr; break;
+               case DT_GNU_HASH: gnu  = (const uint32_t *)ptr; break;
+               }
+       }
+       if (!sym || !str || !strsz)
+               return;
+
+       if (hash) {
+               nsyms = hash[1]; /* nchain */
+       }
+       else if (gnu) {
+               uint32_t nbuckets = gnu[0], symoff = gnu[1], bloomsz = gnu[2];
+               const uint32_t *buckets = (const uint32_t *)((const ElfW(Addr) *)(gnu + 4) + bloomsz);
+               const uint32_t *chains = buckets + nbuckets;
+               uint32_t last = 0;
+
+               for (i = 0; i < nbuckets; i++)
+                       if (buckets[i] > last)
+                               last = buckets[i];
+               if (last >= symoff) {
+                       /* walk the last chain to its terminator */
+                       while (!(chains[last - symoff] & 1))
+                               last++;
+                       nsyms = last + 1;
+               }
+       }
+       else if ((const char *)sym < str) {
+               nsyms = (size_t)(str - (const char *)sym) / sizeof(*sym);
+       }
+
+       for (i = 0; i < nsyms && !b->oom; i++) {
+               unsigned char type = SYM_ST_TYPE(sym[i].st_info);
+
+               if (type != STT_FUNC && type != STT_GNU_IFUNC)
+                       continue;
+               if (sym[i].st_shndx == SHN_UNDEF || sym[i].st_shndx >= SHN_LORESERVE)
+                       continue;
+               if (!sym[i].st_value || sym[i].st_name >= strsz || !str[sym[i].st_name])
+                       continue;
+
+               build_add_sym(b, base + sym[i].st_value, sym[i].st_size, str + sym[i].st_name, obj);
+       }
+}
+
+/* dl_iterate_phdr() callback: collects each loaded object into the builder.
+ * Exported symbols come from the memory image, and the well-known entry
+ * points of sym_known_fcts[] are always added for the executable. Returns
+ * non-zero (stopping the iteration) on OOM.
+ */
+static int phdr_cb(struct dl_phdr_info *info, size_t size, void *data)
+{
+       struct sym_build *b = data;
+       unsigned long beg = ~0UL, end = 0;
+       const char *name = info->dlpi_name;
+       struct sym_obj *obj;
+       unsigned int before;
+       int is_exe = 0;
+       int idx, seen = 0;
+
+       if (!name || !*name) {
+               /* the main executable has an empty name */
+               name = get_exec_path();
+               is_exe = 1;
+       }
+
+       /* compute the runtime address range from the PT_LOAD segments */
+       for (idx = 0; idx < info->dlpi_phnum; idx++) {
+               unsigned long p1, p2;
+
+               if (info->dlpi_phdr[idx].p_type != PT_LOAD ||
+                   !info->dlpi_phdr[idx].p_memsz)
+                       continue;
+               seen = 1;
+               p1 = info->dlpi_phdr[idx].p_vaddr;
+               p2 = p1 + info->dlpi_phdr[idx].p_memsz;
+               if (p1 < beg)
+                       beg = p1;
+               if (p2 > end)
+                       end = p2;
+       }
+       if (!seen)
+               return 0;
+
+       obj = calloc(1, sizeof(*obj));
+       if (!obj) {
+               b->oom = 1;
+               return 1;
+       }
+       obj->base = info->dlpi_addr;
+       obj->low = info->dlpi_addr + beg;
+       obj->high = info->dlpi_addr + end;
+       obj->is_exe = is_exe;
+       obj->file = name ? strdup(name) : NULL; /* NULL tolerated; only used for the lib: prefix */
+
+       before = b->n;
+
+       /* the memory image first, then the well-known functions (sizeless) as
+        * a last resort: the first collected wins dedup (radix sort is stable).
+        */
+       collect_dynsym_mem(b, info, obj);
+       if (is_exe)
+               add_known(b, obj);
+
+       if (b->oom)
+               goto fail;
+
+       if (b->n == before) {
+               /* no symbol at all: drop this object */
+               free((void *)obj->file);
+               free(obj);
+               return 0;
+       }
+
+       if (build_keep_obj(b, obj) != 0)
+               goto fail;
+       return 0;
+
+ fail:
+       free((void *)obj->file);
+       free(obj);
+       return 1;
+}
+
+#endif /* HA_HAVE_DL_ITERATE_PHDR */
+
 /* collects everything known about the loaded objects */
 static void collect_all(struct sym_build *b)
 {
+#if defined(HA_HAVE_DL_ITERATE_PHDR)
+       dl_iterate_phdr(phdr_cb, b);
+#else
+       /* without the dynamic linker's help, only the well-known functions,
+        * into an object describing the executable whose extent is unknown,
+        * so any address may be attributed to it.
+        */
        struct sym_obj *obj;
 
        obj = calloc(1, sizeof(*obj));
@@ -234,6 +418,7 @@ static void collect_all(struct sym_build *b)
                return;
        }
        add_known(b, obj);
+#endif
 }
 
 /* release a snapshot and every object it references; <t> may be NULL. Must
@@ -269,6 +454,9 @@ static void sym_build(void)
        unsigned int i, out = 0;
        int published = 0;
 
+       if (global.tune.debug & GDBG_NO_ELF_SYMS)
+               return;
+
        /* the table is built at boot, while the process is still starting and
         * no thread is running yet, so the build needs no locking. An init
         * phase may load more objects with dlopen() and build again to
@@ -611,6 +799,7 @@ const void *resolve_sym_name(struct buffer *buf, const char *pfx, const void *ad
        const char *fname, *p;
 #endif
        struct sym_lookup sl;
+       int have_near = 0;
        size_t dist, best_dist;
        int i, best_idx;
 
@@ -636,14 +825,22 @@ const void *resolve_sym_name(struct buffer *buf, const char *pfx, const void *ad
        if (!best_dist)
                goto use_array;
 
-       /* Then try the symbol table (sym.c). Lookups are lock-free and
-        * async-signal-safe. Like dladdr(), a symbol is only considered
-        * resolved (non-NULL return) when the address is really within it:
-        * callers use this to tell code pointers from arbitrary values.
+       /* First try our own ELF symbol tables. Unlike dladdr(), these also
+        * contain local/static functions (from .symtab) and work on static
+        * builds. They're lock-free and async-signal-safe. Like dladdr(), a
+        * symbol is only considered resolved (non-NULL return) when the
+        * address is really within it: callers use this to tell code pointers
+        * from arbitrary values. When only the nearest lower symbol is known,
+        * the dladdr() path below gets the priority (it may know a data symbol
+        * or the object), and the nearest symbol is only used where the old
+        * output would have been a bare offset.
         */
-       if (sym_resolve(addr, &sl) && sl.name && sl.inside) {
-               dump_sym_lookup(buf, &sl, addr, 1);
-               return sl.addr;
+       if (sym_resolve(addr, &sl) && sl.name) {
+               if (sl.inside) {
+                       dump_sym_lookup(buf, &sl, addr, 1);
+                       return sl.addr;
+               }
+               have_near = 1;
        }
 
 #if (defined(__ELF__) && !defined(__linux__)) || defined(USE_DL)
@@ -739,15 +936,26 @@ const void *resolve_sym_name(struct buffer *buf, const char *pfx, const void *ad
                return dli.dli_saddr;
        }
        else if (dli_main.dli_fbase != dli.dli_fbase) {
-               /* unresolved symbol from a known library, report relative offset */
-               chunk_appendf(buf, "+%#lx", (long)(addr - dli.dli_fbase));
+               /* unresolved symbol from a known library, report the nearest
+                * symbol if known, otherwise the relative offset.
+                */
+               if (have_near)
+                       dump_sym_lookup(buf, &sl, addr, 0);
+               else
+                       chunk_appendf(buf, "+%#lx", (long)(addr - dli.dli_fbase));
                return NULL;
        }
 #endif /* __ELF__ && !__linux__ || USE_DL */
  use_array:
        /* either exact match from the array, or unresolved symbol for which we
-        * may have a close match. Otherwise we report an offset relative to main.
+        * may have a close match, in the array or in the ELF tables. Otherwise
+        * we report an offset relative to main.
         */
+       if (have_near && (best_idx < 0 || (size_t)(addr - sl.addr) < best_dist)) {
+               dump_sym_lookup(buf, &sl, addr, 1);
+               return NULL;
+       }
+
        if (best_idx >= 0) {
                chunk_appendf(buf, "%s", sym_known_fcts[best_idx].name);
                if (best_dist)