[PATCH v2 0/5] elf: Replace alloca/VLA with dl_scratch_buffer in dl-load.c
Adhemerval Zanella
adhemerval.zanella@linaro.org
Fri May 15 18:21:46 GMT 2026
This series is a follow-up to the BZ #26577 work, where the fix removed
the two big VLAs/allocas in _dl_map_object_from_fd (the phdr table and
the loadcmds VLA). The loader still contained several unbounded allocas
and strdupa()s in code paths reachable from ordinary dlopen() or from
loader startup with user-controllable environment.
The series introduces a small loader-side scratch buffer abstraction
and uses it to replace the remaining unbounded alloca/VLA/strdupa
sites. The scratch buffer keeps a 256-byte inline area on the caller
stack for the common case and spills larger allocations to malloc, or
to anonymous mmap while __minimal_malloc is still the active allocator
(so the very early loader paths -- _dl_init_paths,
_dl_load_cache_lookup called via recursive dlopen, etc. -- still
work).
After the series, the loader's dlopen()/startup path no longer grows
the stack proportionally to LD_LIBRARY_PATH length, DT_NEEDED DST
count, l_origin depth, RPATH/RUNPATH entry length, or cached path
length.
Per-commit stack reduction
--------------------------
Synthetic worst case Worst case in the
(formula upper bound) regression test Commit
~ 128 KB ~ 64 KB elf: Use dl_scratch_buffer
(MAX_ARG_STRLEN per for LD_LIBRARY_PATH copy
env entry) in _dl_init_paths
~ 2.4 MB ~ 17 KB elf: Use dl_scratch_buffer
(dst_cnt * max( for DST expansion in
l_origin, $LIB, _dl_map_object_deps
$PLATFORM); pathological
~580 DSTs * PATH_MAX)
4 KB ~ 3.4 KB elf: Replace alloca with
(best_len <= dl_scratch_buffer in
PATH_MAX) _dl_load_cache_lookup
~ 12 KB not measurable elf: Replace alloca/VLA
(PATH_MAX per (transient with dl_scratch_buffer
call site, several per-iteration in dl-load.c
on stack during one alloca released
dlopen call chain) before next sample)
The synthetic worst case is what the formula in the loader can be
made to produce given a maximally adversarial input. The test
column is what the new regression test included with that commit
actually triggers on a default x86_64 build.
Cumulative effect on the two workloads where massif can cleanly
capture a steady-state peak (origin/master baseline -> series HEAD,
x86_64, valgrind --tool=massif --stacks=yes):
DST-laden DT_NEEDED workload (tst-dst-needed-minstack):
peak mem_stacks_B 22,208 B -> 7,208 B (-15 KB)
Long LD_LIBRARY_PATH workload (tst-dl-llp-stack):
peak mem_stacks_B 18,280 B -> 7,128 B (-11 KB)
Scratch buffer safety against _dl_signal_error
----------------------------------------------
The scratch buffer's MMAP/MALLOC backing must be released on every
exit path between dl_scratch_buffer_allocate and
dl_scratch_buffer_free. The hazard is that _dl_signal_error fired
between the two -- and caught by an outer _dl_catch_exception --
would skip the matching free and leak the mapping. Every use site
in the series has been audited against this:
* dl_scratch_buffer_allocate only sets b->backend to MMAP/MALLOC
after the underlying mmap/malloc returns. If the allocation
itself fails, _dl_signal_error fires while b->backend is still
INLINE; there is nothing to free, so no leak is possible.
* dl-cache.c _dl_load_cache_lookup: only memcpy + __strdup run
between allocate and free; __strdup returns NULL on OOM, never
signals.
* dl-deps.c expand_dst: an __libc_enable_secure check short-
circuits with _dl_signal_error before allocate runs. After
allocate succeeds, _dl_dst_substitute can only call
is_trusted_path_normalize through the check_for_trusted path,
which is gated on __libc_enable_secure -- already known false
by construction -- so the sub-call is unreachable.
* dl-deps.c _dl_map_object_deps DT_NEEDED / DT_AUXILIARY /
DT_FILTER paths: openaux is wrapped in _dl_catch_exception, so
any signal_error from the actual dependency open is caught and
the matching free runs. DT_AUXILIARY's own signal_error on
empty DST substitution is placed after the scratch free.
* dl-load.c is_trusted_path_normalize and open_path: between the
matching allocate and free, only pure string manipulation,
open_verify (which returns -1 on error, never signals), stat,
and malloc-returning-NULL run. None can _dl_signal_error.
* dl-load.c _dl_init_paths (LD_LIBRARY_PATH copy): the two heap
allocations the loader controls -- the search-path dirs array
and fillin_rpath's per-entry mallocs -- are now signalled with
the scratch in a known state. The dirs array is allocated
before the scratch is live (its OOM signals with no scratch
to leak), and fillin_rpath has been changed to return bool
instead of signalling internally; the caller frees the scratch
first and then signals the error from a clean state. This
also fixes a pre-existing leak in fillin_rpath where the
per-iteration to_free heap copy was not released before the
_dl_signal_error.
Tested on x86_64-linux-gnu, aarch64-linux-gnu, and i686-linux-gnu.
I also cross-built and run the elf tests on powerpc64le, s390x,
risc64, and loongarch64.
--
Changes from v1:
* Scale envp/rlim tst-dl-llp-stack.c by PAGE_SIZE (to possible handle
Linaro CI regression).
* Fixed build issues on Hurd.
* Improved some comments.
Adhemerval Zanella (5):
elf: Add dl_scratch_buffer, a loader-side scratch buffer
elf: Replace alloca/VLA with dl_scratch_buffer in dl-load.c
elf: Replace alloca with dl_scratch_buffer in _dl_load_cache_lookup
elf: Use dl_scratch_buffer for DST expansion in _dl_map_object_deps
elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths
elf/Makefile | 31 +++++
elf/dl-cache.c | 14 ++-
elf/dl-deps.c | 114 ++++++++++-------
elf/dl-load.c | 104 ++++++++++++----
elf/dl-scratch-buffer.c | 90 ++++++++++++++
elf/dl-scratch-buffer.h | 145 ++++++++++++++++++++++
elf/tst-dl-cache-long-path.c | 179 +++++++++++++++++++++++++++
elf/tst-dl-llp-stack.c | 112 +++++++++++++++++
elf/tst-dl-path-buf-mod.c | 23 ++++
elf/tst-dl-path-buf.c | 225 ++++++++++++++++++++++++++++++++++
elf/tst-dst-needed-leaf-mod.c | 20 +++
elf/tst-dst-needed-minstack.c | 127 +++++++++++++++++++
elf/tst-dst-needed-wrap-mod.c | 21 ++++
13 files changed, 1131 insertions(+), 74 deletions(-)
create mode 100644 elf/dl-scratch-buffer.c
create mode 100644 elf/dl-scratch-buffer.h
create mode 100644 elf/tst-dl-cache-long-path.c
create mode 100644 elf/tst-dl-llp-stack.c
create mode 100644 elf/tst-dl-path-buf-mod.c
create mode 100644 elf/tst-dl-path-buf.c
create mode 100644 elf/tst-dst-needed-leaf-mod.c
create mode 100644 elf/tst-dst-needed-minstack.c
create mode 100644 elf/tst-dst-needed-wrap-mod.c
--
2.43.0
More information about the Libc-alpha
mailing list