#### Toolchain options.
CC = cc
LD = $(CC)
+NM = nm
+OBJCOPY = objcopy
+READELF = readelf
+XZ = xz
#### Default optimizations
# Those are integrated early in the list of CFLAGS, and may be overridden by
USE_MATH USE_DEVICEATLAS USE_51DEGREES \
USE_WURFL USE_OBSOLETE_LINKER USE_PRCTL USE_PROCCTL \
USE_THREAD_DUMP USE_EVPORTS USE_QUIC USE_PROMEX \
- USE_MEMORY_PROFILING USE_SHM_OPEN \
+ USE_MEMORY_PROFILING USE_SHM_OPEN USE_MINIDEBUG \
USE_STATIC_PCRE USE_STATIC_PCRE2 \
USE_PCRE USE_PCRE_JIT USE_PCRE2 USE_PCRE2_JIT \
USE_QUIC_OPENSSL_COMPAT USE_KTLS
#### Extra objects to be built and integrated (used only for development)
EXTRA_OBJS =
+# A compressed table of the symbols which are not exported (mostly the static
+# functions) may be stored into the executable's .gnu_debugdata section, so
+# that backtraces still name them once the executable is stripped or when its
+# file is unreachable. This is the "MiniDebugInfo" format that gdb reads too,
+# so gdb benefits from it as well. It is produced after the link using nm,
+# objcopy and xz, hence it is enabled by default only when the three are
+# available, together with the readelf that scripts/minidbg.sh needs, and the
+# compiler produces ELF objects, as the placeholders are ELF-specific. That
+# script appends the table to the program just linked, which moves nothing, so
+# it needs neither reserved room nor a second link.
+ifeq ($(origin USE_MINIDEBUG),undefined)
+USE_MINIDEBUG := $(if $(and $(shell command -v $(NM)),$(shell command -v $(OBJCOPY)),$(shell command -v $(READELF)),$(shell command -v $(XZ)),$(shell $(CC) -dM -E -xc - </dev/null 2>/dev/null | grep -F __ELF__)),1,)
+endif
+
# This variable collects all USE_* values except those set to "implicit". This
# is used to report a list of all flags which were used to build this version.
# Do not assign anything to it.
src/xz_crc64.o
endif
+ifneq ($(USE_MINIDEBUG:0=),)
+ MINIDEBUG_TOOL = $(SHELL) scripts/minidbg.sh
+endif
+
HATERM_OBJS += $(OBJS) src/haterm_init.o src/hbuf.o
HALOAD_OBJS += $(OBJS) src/haload_init.o src/haload.o src/hbuf.o
.build_opts:
endif # non-empty target
+# Extracts the symbols which are not in the dynamic symbol table, builds a
+# symbols-only ELF file out of them, compresses it and appends it to the program
+# just linked, where minidbg.sh points its .gnu_debugdata placeholders at it.
+# Only the last step can leave the program half-written, so it is the only one
+# whose failure is fatal; not being able to produce the table just leaves the
+# program without one, as happens when the tools do not know the target.
+define make_minidebug
+ $(call qinfo, MINIDBG)if ! ( \
+ $(NM) -D $@ --format=posix --defined-only | awk '{ print $$1 }' | sort > $@.dsyms && \
+ $(NM) $@ --format=posix --defined-only | awk '$$2 == "T" || $$2 == "t" { print $$1 }' | sort > $@.fsyms && \
+ comm -13 $@.dsyms $@.fsyms > $@.ksyms && \
+ $(OBJCOPY) --only-keep-debug $@ $@.dbg && \
+ $(OBJCOPY) -S --remove-section .gdb_index --remove-section .comment --keep-symbols=$@.ksyms $@.dbg $@.mini && \
+ $(XZ) -f --check=crc64 $@.mini \
+ ); then \
+ echo "$@: cannot build the symbol table, see the error above; the program is" >&2; \
+ echo "$@: left without one. Cross-builds need NM, OBJCOPY and READELF pointed at" >&2; \
+ echo "$@: the target's tools, and USE_MINIDEBUG=0 skips this step altogether." >&2; \
+ elif ! READELF='$(READELF)' $(MINIDEBUG_TOOL) $@ $@.mini.xz; then \
+ rm -f $@ $@.dsyms $@.fsyms $@.ksyms $@.dbg $@.mini $@.mini.xz; \
+ exit 1; \
+ fi; \
+ rm -f $@.dsyms $@.fsyms $@.ksyms $@.dbg $@.mini $@.mini.xz
+endef
+
haproxy: $(OPTIONS_OBJS) $(OBJS)
$(cmd_LD) $(ARCH_FLAGS) $(LDFLAGS) -o $@ $^ $(LDOPTS)
+ifneq ($(USE_MINIDEBUG:0=),)
+ $(make_minidebug)
+endif
haterm: $(OPTIONS_OBJS) $(HATERM_OBJS)
$(cmd_LD) $(ARCH_FLAGS) $(LDFLAGS) -o $@ $^ $(LDOPTS)
+ifneq ($(USE_MINIDEBUG:0=),)
+ $(make_minidebug)
+endif
haload: $(OPTIONS_OBJS) $(HALOAD_OBJS)
$(cmd_LD) $(ARCH_FLAGS) $(LDFLAGS) -o $@ $^ $(LDOPTS)
+ifneq ($(USE_MINIDEBUG:0=),)
+ $(make_minidebug)
+endif
objsize: haproxy
$(Q)objdump -t $^|grep ' g '|grep -F '.text'|awk '{print $$5 FS $$6}'|sort
clean:
$(Q)rm -f *.[oas] src/*.[oas] haproxy haterm test .build_opts .build_opts.new
+ $(Q)for p in haproxy haterm haload; do rm -f $$p.dsyms $$p.fsyms $$p.ksyms $$p.dbg $$p.mini $$p.mini.xz; done
$(Q)for dir in . src dev/* admin/* addons/* include/* doc; do rm -f $$dir/*~ $$dir/*.rej $$dir/core; done
$(Q)rm -f haproxy-$(VERSION).tar.gz haproxy-$(VERSION)$(SUBVERS)$(EXTRAVERSION).tar.gz
$(Q)rm -f haproxy-$(VERSION) haproxy-$(VERSION)$(SUBVERS)$(EXTRAVERSION) nohup.out gmon.out
--- /dev/null
+#!/bin/sh
+#
+# Stores a compressed symbol table into an already linked ELF file.
+#
+# Copyright (C) 2026 HAProxy Technologies
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version
+# 2 of the License, or (at your option) any later version.
+#
+# The table describes the file it is stored in, so it can only be produced once
+# that file is linked, and storing it must not move anything. The table is
+# therefore appended at the end, the .gnu_debugdata section header reserved at
+# link time is pointed at it so that gdb finds it by name, and the note segment
+# reserved at link time is turned into a loadable one covering it, so that it is
+# also readable from memory alone. The address is picked past everything the
+# file already maps.
+#
+# usage: minidbg.sh <elf> <table>
+
+: "${READELF:=readelf}"
+
+elf="$1"
+pay="$2"
+# The loader maps the segment with mmap(), which needs its file offset and its
+# address to be congruent modulo the page size of the machine that runs it, not
+# of the one that builds it. Aligning both to the largest page size in use (64 kB
+# on arm64 and ppc64) satisfies every smaller one as well, at the cost of at most
+# that much padding once.
+PAGE=65536
+
+die() { echo "minidbg: $1" >&2; exit 1; }
+
+[ $# = 2 ] || die "usage: $0 <elf> <table>"
+
+hdr=$(LC_ALL=C "$READELF" -hW "$elf") || die "cannot read $elf"
+case "$hdr" in
+ *ELF64*) w=8 ;;
+ *ELF32*) w=4 ;;
+ *) die "unknown ELF class" ;;
+esac
+case "$hdr" in
+ *"big endian"*) be=1 ;;
+ *) be=0 ;;
+esac
+# an empty value would silently be taken as zero by the arithmetic below, so
+# everything read from the listings is checked before being used
+num() {
+ case "$2" in
+ '' | *[!0-9]*) die "cannot read the $1 of $elf from $READELF" ;;
+ esac
+}
+
+phoff=$(echo "$hdr" | awk '/Start of program header/ { print $(NF-3) }')
+phentsize=$(echo "$hdr" | awk '/Size of program header/ { print $(NF-1) }')
+shoff=$(echo "$hdr" | awk '/Start of section header/ { print $(NF-3) }')
+shentsize=$(echo "$hdr" | awk '/Size of section header/ { print $(NF-1) }')
+num "program header offset" "$phoff"
+num "program header size" "$phentsize"
+num "section header offset" "$shoff"
+num "section header size" "$shentsize"
+
+# the two placeholders: the section gives the name, the note gives the segment.
+# Their index in the listing is not their index in the table, so the section is
+# located by the number readelf prints, and the note by where it starts.
+secs=$(LC_ALL=C "$READELF" -SW "$elf") || die "cannot read the sections of $elf"
+sec=$(echo "$secs" | awk '{ for (i = 1; i < NF; i++) if ($i == ".gnu_debugdata") {
+ n = (i == 2) ? $1 : $(i - 1); gsub(/[][]/, "", n); print n; exit } }')
+[ -n "$sec" ] || die "no .gnu_debugdata section in $elf, was it reserved at link time?"
+num "section index" "$sec"
+note=$(echo "$secs" | awk '{ for (i = 1; i < NF; i++) if ($i == ".note.hapdbg") {
+ o = $(i + 3); z = $(i + 4); sub(/^0*/, "", o); sub(/^0*/, "", z)
+ print o " " z; exit } }')
+[ -n "$note" ] || die "no .note.hapdbg section in $elf, was it reserved at link time?"
+
+# the note's own segment is the one that starts where the note does and holds
+# nothing else, so that turning it into a loadable one loses no other note
+segs=$(LC_ALL=C "$READELF" -lW "$elf") || die "cannot read the segments of $elf"
+seg=$(echo "$segs" | awk -v want="$note" '
+ $2 ~ /^0x/ && $3 ~ /^0x/ && $4 ~ /^0x/ {
+ if ($1 == "NOTE") {
+ o = $2; z = $5; sub(/^0x0*/, "", o); sub(/^0x0*/, "", z)
+ if (o " " z == want) { print idx; exit }
+ }
+ idx++
+ }')
+[ -n "$seg" ] || die "the note of $elf has no segment of its own"
+num "segment index" "$seg"
+
+# the table is mapped past everything the file already maps
+maxva=0
+for e in $(echo "$segs" | awk '$1 == "LOAD" && $3 ~ /^0x/ { print $3 "+" $6 }'); do
+ e=$(( $e ))
+ [ $e -gt $maxva ] && maxva=$e
+done
+[ $maxva -gt 0 ] || die "$elf has no loadable segment"
+
+paylen=$(wc -c < "$pay") || die "cannot read $pay"
+[ "$paylen" -gt 0 ] || die "$pay is empty"
+elflen=$(wc -c < "$elf")
+off=$(( (elflen + PAGE - 1) / PAGE * PAGE ))
+vaddr=$(( (maxva + PAGE - 1) / PAGE * PAGE ))
+
+# writes <size> bytes of <value> at <offset>, in the file's byte order
+wr() {
+ _o=$1; _s=$2; _v=$3; _i=0; _e=''
+ while [ $_i -lt $_s ]; do
+ _b=$(printf '\\0%03o' $(( (_v >> (8 * _i)) & 255 )))
+ if [ "$be" = 1 ]; then _e="$_b$_e"; else _e="$_e$_b"; fi
+ _i=$(( _i + 1 ))
+ done
+ printf '%b' "$_e" | dd of="$elf" bs=1 seek=$_o conv=notrunc 2>/dev/null ||
+ die "cannot patch $elf"
+}
+
+# append at a page-aligned end, so that offset and address stay congruent
+dd if=/dev/zero bs=1 count=$(( off - elflen )) 2>/dev/null >> "$elf" || die "cannot pad $elf"
+cat "$pay" >> "$elf" || die "cannot append the table to $elf"
+
+if [ $w = 8 ]; then
+ sh_type=4; sh_flags=8; sh_addr=16; sh_off=24; sh_size=32; sh_align=48
+ ph_type=0; ph_flags=4; ph_off=8; ph_va=16; ph_pa=24; ph_fsz=32; ph_msz=40; ph_align=48
+else
+ sh_type=4; sh_flags=8; sh_addr=12; sh_off=16; sh_size=20; sh_align=32
+ ph_type=0; ph_off=4; ph_va=8; ph_pa=12; ph_fsz=16; ph_msz=20; ph_flags=24; ph_align=28
+fi
+
+s=$(( shoff + sec * shentsize ))
+wr $(( s + sh_type )) 4 1 # SHT_PROGBITS
+wr $(( s + sh_flags )) $w 2 # SHF_ALLOC
+wr $(( s + sh_addr )) $w $vaddr
+wr $(( s + sh_off )) $w $off
+wr $(( s + sh_size )) $w $paylen
+wr $(( s + sh_align )) $w 16
+
+p=$(( phoff + seg * phentsize ))
+wr $(( p + ph_type )) 4 1 # PT_LOAD
+wr $(( p + ph_flags )) 4 4 # PF_R
+wr $(( p + ph_off )) $w $off
+wr $(( p + ph_va )) $w $vaddr
+wr $(( p + ph_pa )) $w $vaddr
+wr $(( p + ph_fsz )) $w $paylen
+wr $(( p + ph_msz )) $w $paylen
+wr $(( p + ph_align )) $w $PAGE
* 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).
- * It also carries the helpers needed to collect the symbols of an ELF image
- * available in memory, which the next commits use to read the symbol tables
- * found in the objects themselves and in their files.
+ * The compressed symbol tables that objects may embed in a loadable
+ * .gnu_debugdata section (the MiniDebugInfo format that gdb also reads) are
+ * decompressed and parsed as well, which brings the local/static functions
+ * and works even on a stripped or unreadable file.
*
* Lookups (sym_resolve()) are lock-free and async-signal-safe: they only read
* an immutable snapshot, replaced only while no thread is running and
struct sym_build {
struct sym_entry *syms; /* growing array of collected symbols */
unsigned int n, alloc;
+ void **bufs; /* memory blocks to release at the end of the build */
+ unsigned int nbufs, balloc;
+ const unsigned char *debugdata; /* compressed table of the object being collected */
+ size_t debugdata_size;
struct sym_obj **objs; /* objects created this build, handed to the snapshot */
unsigned int nobjs, oalloc;
int oom; /* set on any allocation failure */
#include <elf.h>
#include <link.h>
+#include <import/xz.h>
+/* Largest decompressed symbol table we accept, and largest LZMA2 dictionary we
+ * let the decoder allocate for it.
+ */
+#define SYM_DEBUGDATA_MAX (64 * 1024 * 1024)
+#define SYM_DEBUGDATA_DICT_MAX (64 * 1024 * 1024)
+
+#ifdef USE_MINIDEBUG
+/* Placeholders for this program's own table, filled in by scripts/minidbg.sh
+ * after the link: the section gives it the name gdb looks for, and the note,
+ * whose alignment makes linkers give it a segment of its own, gives it the
+ * segment entry that is turned into a loadable one.
+ */
+__attribute__((section(".gnu_debugdata"), aligned(16), used))
+static const unsigned char sym_debugdata_room[16];
+
+__attribute__((section(".note.hapdbg"), aligned(16), used))
+static const struct {
+ uint32_t namesz, descsz, type;
+ char name[8];
+ uint32_t desc;
+} sym_debugdata_note = { 7, 4, 1, "HAPDBG", 0 };
+#endif
/* native ELF class / endianness / symbol-type accessor, derived from the
* toolchain since glibc doesn't provide *_NATIVE convenience macros.
/* validates an ELF header for the native class/endianness and a sane section
* header table that fits in the <sz>-byte mapping. Returns 1 if usable.
*/
-static __maybe_unused int elf_hdr_ok(const ElfW(Ehdr) *eh, size_t sz)
+static int elf_hdr_ok(const ElfW(Ehdr) *eh, size_t sz)
{
if (!in_map(eh, sizeof(*eh), eh, sz))
return 0;
+/* records a memory block to release when the build finishes */
+static int build_keep_buf(struct sym_build *b, void *buf)
+{
+ if (b->nbufs >= b->balloc) {
+ unsigned int na = b->balloc ? b->balloc * 2 : 8;
+ void **nb = realloc(b->bufs, na * sizeof(*nb));
-static __maybe_unused void collect_syms(struct sym_build *b, const void *map, size_t sz,
+ if (!nb) {
+ b->oom = 1;
+ return -1;
+ }
+ b->bufs = nb;
+ b->balloc = na;
+ }
+ b->bufs[b->nbufs++] = buf;
+ return 0;
+}
+
+static void collect_syms(struct sym_build *b, const void *map, size_t sz,
const ElfW(Shdr) *symsh, const ElfW(Shdr) *strsh,
unsigned long base, const struct sym_obj *obj)
{
-static __maybe_unused void elf_find_symtab(const ElfW(Ehdr) *eh,
+static void elf_find_symtab(const ElfW(Ehdr) *eh,
const ElfW(Shdr) **symtab, const ElfW(Shdr) **strtab)
{
const ElfW(Shdr) *sh = (const ElfW(Shdr) *)((const char *)eh + eh->e_shoff);
}
}
+/* Decompresses the xz-compressed ELF image
+ */
+#if !defined(HA_HAVE_DL_ITERATE_PHDR)
+/* the loader callback is its only caller here; the file parsing which also
+ * needs it comes next, and is itself kept compiled without the API.
+ */
+__maybe_unused
+#endif
+static void collect_debugdata(struct sym_build *b, const struct sym_obj *obj,
+ const unsigned char *blob, size_t size)
+{
+ static const unsigned char magic[6] = { 0xfd, '7', 'z', 'X', 'Z', 0x00 };
+ static int crc_done;
+ const ElfW(Shdr) *symtab, *strtab;
+ const ElfW(Ehdr) *eh;
+ struct xz_dec *dec;
+ struct xz_buf xb;
+ unsigned char *out, *new;
+ size_t outsz = 1024 * 1024;
+ enum xz_ret ret;
+
+ /* room left empty by a build without the required tools, or not a
+ * compressed table at all.
+ */
+ if (size < sizeof(magic) || memcmp(blob, magic, sizeof(magic)) != 0)
+ return;
+
+ if (!crc_done) {
+ xz_crc32_init();
+ xz_crc64_init();
+ crc_done = 1;
+ }
+
+ dec = xz_dec_init(XZ_DYNALLOC, SYM_DEBUGDATA_DICT_MAX);
+ if (!dec)
+ return;
+
+ out = malloc(outsz);
+ if (!out)
+ goto end;
+
+ memset(&xb, 0, sizeof(xb));
+ xb.in = blob;
+ xb.in_size = size;
+ xb.out = out;
+ xb.out_size = outsz;
+
+ while ((ret = xz_dec_run(dec, &xb)) == XZ_OK) {
+ /* only a full output buffer is worth retrying, anything else
+ * means the stream is truncated.
+ */
+ if (xb.out_pos < xb.out_size || outsz >= SYM_DEBUGDATA_MAX)
+ goto end;
+ new = realloc(out, outsz * 2);
+ if (!new)
+ goto end;
+ out = new;
+ outsz *= 2;
+ xb.out = out;
+ xb.out_size = outsz;
+ }
+
+ if (ret != XZ_STREAM_END)
+ goto end;
+
+ eh = (const ElfW(Ehdr) *)out;
+ if (!elf_hdr_ok(eh, xb.out_pos))
+ goto end;
+
+ elf_find_symtab(eh, &symtab, &strtab);
+ if (!symtab || build_keep_buf(b, out) != 0)
+ goto end;
+
+ collect_syms(b, out, xb.out_pos, symtab, strtab, obj->base, obj);
+ out = NULL; /* now owned by the builder */
+ end:
+ free(out);
+ xz_dec_end(dec);
+}
#if defined(HA_HAVE_DL_ITERATE_PHDR)
+/* A compressed symbol table stored into an object by the build sits alone in a
+ * segment of its own (see scripts/minidbg.sh), which is how it is recognised
+ * below: nothing has to be exported for it, and the same applies to any other
+ * object carrying one. Being in a segment is what makes it readable from memory
+ * alone once the file is stripped, replaced or unreachable.
+ */
+static const unsigned char sym_xz_magic[6] = { 0xfd, '7', 'z', 'X', 'Z', 0x00 };
+
/*
* Collects the exported symbols of the object as provided by the dynamic
* linker.
unsigned long base = info->dlpi_addr;
size_t strsz = 0, nsyms = 0, i;
+ b->debugdata = NULL;
+ b->debugdata_size = 0;
+
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);
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;
+ if (type != STT_FUNC && type != STT_GNU_IFUNC)
+ continue;
+
build_add_sym(b, base + sym[i].st_value, sym[i].st_size, str + sym[i].st_name, obj);
}
}
+/* Returns the end address of the loaded segment covering <addr> in the object
+ * described by <info>, or 0 when it is covered by none.
+ */
+static unsigned long seg_end(const struct dl_phdr_info *info, unsigned long addr)
+{
+ unsigned long beg;
+ int idx;
+
+ for (idx = 0; idx < info->dlpi_phnum; idx++) {
+ if (info->dlpi_phdr[idx].p_type != PT_LOAD)
+ continue;
+ beg = info->dlpi_addr + info->dlpi_phdr[idx].p_vaddr;
+ if (addr >= beg && addr < beg + info->dlpi_phdr[idx].p_memsz)
+ return beg + info->dlpi_phdr[idx].p_memsz;
+ }
+ return 0;
+}
+
/* 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.
+ * Local symbols come from the file's .symtab (or separate debug file) when
+ * readable, exported ones always 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)
{
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).
+ /* the embedded table first, so that it wins dedup over the memory
+ * image (radix sort is stable), then the well-known functions
+ * (sizeless) as a last resort.
*/
collect_dynsym_mem(b, info, obj);
+
+ /* a table stored by the build is covered by a segment of its own, whose
+ * contents identify it.
+ */
+ for (idx = 0; !b->debugdata && idx < info->dlpi_phnum; idx++) {
+ const unsigned char *p;
+
+ if (info->dlpi_phdr[idx].p_type != PT_LOAD ||
+ !(info->dlpi_phdr[idx].p_flags & PF_R) ||
+ info->dlpi_phdr[idx].p_filesz < sizeof(sym_xz_magic))
+ continue;
+ p = (const unsigned char *)(info->dlpi_addr + info->dlpi_phdr[idx].p_vaddr);
+ if (memcmp(p, sym_xz_magic, sizeof(sym_xz_magic)) == 0) {
+ b->debugdata = p;
+ b->debugdata_size = info->dlpi_phdr[idx].p_filesz;
+ }
+ }
+ /* stay within the mapping no matter what the segment advertises */
+ if (b->debugdata) {
+ unsigned long end = seg_end(info, (unsigned long)b->debugdata);
+
+ if (!end)
+ b->debugdata_size = 0;
+ else if (end - (unsigned long)b->debugdata < b->debugdata_size)
+ b->debugdata_size = end - (unsigned long)b->debugdata;
+ }
+
+ if (b->debugdata && b->debugdata_size)
+ collect_debugdata(b, obj, b->debugdata, b->debugdata_size);
+
if (is_exe)
add_known(b, obj);
published = 1;
leave:
+ for (i = 0; i < b.nbufs; i++)
+ free(b.bufs[i]);
+ free(b.bufs);
+
/* on success the objects are referenced by the published symbols and
* survive with the snapshot; on failure free them.
*/