[PATCH v2 1/5] elf: Add dl_scratch_buffer, a loader-side scratch buffer
H.J. Lu
hjl.tools@gmail.com
Fri May 15 21:56:53 GMT 2026
On Sat, May 16, 2026 at 2:22 AM Adhemerval Zanella
<adhemerval.zanella@linaro.org> wrote:
>
> Several loader code paths need a short-lived scratch buffer sized
> by attacker-influenced inputs (RPATH entries, ld.so.cache strings,
> etc.). The available primitives are all unsuitable:
>
> - alloca is unbounded and can overflow PTHREAD_STACK_MIN stacks.
>
> - <scratch_buffer.h> is unaware of __minimal_malloc: a malloc'd
> spill freed during early loader startup silently leaks because
> __minimal_free only releases the most-recent allocation.
>
> - A few paths cannot route through the interposable malloc at
> all -- ld.so.cache lookup in particular, because an interposed
> user malloc may recursively call dlopen and __munmap the cache
> mapping mid-copy (commit ccdb048d, "Fix recursive dlopen").
>
> Add a loader-side analogue of <scratch_buffer.h>: a 256-byte inline
> area for the common case, with spill to malloc by default or to
> anonymous mmap when __minimal_malloc is active or the caller passes
> DL_SCRATCH_NO_MALLOC. Mmap spills are tagged " glibc: loader
> scratch" via __set_vma_name for /proc/self/maps visibility. On OOM
> dl_scratch_buffer_allocate raises a loader error via _dl_signal_error
> and does not return. The one-shot contract (no second allocate
> without an intervening free) is enforced by an assertion in
> _dl_scratch_buffer_allocate.
>
> No functional change in this commit; consumers are added separately.
>
> Reviewed-by: H.J. Lu <hjl.tools@gmail.com>
> ---
> elf/Makefile | 1 +
> elf/dl-scratch-buffer.c | 90 +++++++++++++++++++++++++
> elf/dl-scratch-buffer.h | 145 ++++++++++++++++++++++++++++++++++++++++
> 3 files changed, 236 insertions(+)
> create mode 100644 elf/dl-scratch-buffer.c
> create mode 100644 elf/dl-scratch-buffer.h
>
> diff --git a/elf/Makefile b/elf/Makefile
> index f4d22c15991..67bcd7f072d 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -78,6 +78,7 @@ dl-routines = \
> dl-reloc \
> dl-runtime \
> dl-scope \
> + dl-scratch-buffer \
> dl-setup_hash \
> dl-sort-maps \
> dl-thread_gscope_wait \
> diff --git a/elf/dl-scratch-buffer.c b/elf/dl-scratch-buffer.c
> new file mode 100644
> index 00000000000..eb836017159
> --- /dev/null
> +++ b/elf/dl-scratch-buffer.c
> @@ -0,0 +1,90 @@
> +/* Loader-internal scratch buffer.
> + Copyright (C) 2026 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +#include <dl-scratch-buffer.h>
> +
> +#include <assert.h>
> +#include <errno.h>
> +#include <ldsodefs.h>
> +#include <libc-pointer-arith.h>
> +#include <libintl.h>
> +#include <setvmaname.h>
> +#include <stdlib.h>
> +#include <sys/mman.h>
> +
> +void
> +_dl_scratch_buffer_allocate (struct dl_scratch_buffer *b, size_t size,
> + unsigned int flags)
> +{
> + /* Enforce the one-shot contract. */
> + assert (b->backend == DL_SCRATCH_INLINE);
> +
> + bool use_malloc = !(flags & DL_SCRATCH_NO_MALLOC);
> +#ifdef SHARED
> + /* While __minimal_malloc is the active allocator, __minimal_free
> + only releases the most-recent block; route through mmap instead so
> + dl_scratch_buffer_free can symmetrically release the spill. */
> + if (!__rtld_malloc_is_complete ())
> + use_malloc = false;
> +#endif
> +
> + if (use_malloc)
> + {
> + void *p = malloc (size);
> + if (__glibc_unlikely (p == NULL))
> + _dl_signal_error (ENOMEM, NULL, NULL,
> + N_("cannot allocate loader scratch buffer"));
> + b->data = p;
> + b->size = size;
> + b->backend = DL_SCRATCH_MALLOC;
> + return;
> + }
> +
> + size_t map_size = ALIGN_UP (size, GLRO(dl_pagesize));
> + void *p = __mmap (NULL, map_size, PROT_READ | PROT_WRITE,
> + MAP_ANON | MAP_PRIVATE, -1, 0);
> + if (__glibc_unlikely (p == MAP_FAILED))
> + _dl_signal_error (ENOMEM, NULL, NULL,
> + N_("cannot allocate loader scratch buffer"));
> + __set_vma_name (p, map_size, " glibc: loader scratch");
> + b->data = p;
> + b->size = map_size;
> + b->backend = DL_SCRATCH_MMAP;
> +}
> +rtld_hidden_def (_dl_scratch_buffer_allocate)
> +
> +void
> +_dl_scratch_buffer_free (struct dl_scratch_buffer *b)
> +{
> + switch (b->backend)
> + {
> + case DL_SCRATCH_MALLOC:
> + free (b->data);
> + break;
> + case DL_SCRATCH_MMAP:
> + __munmap (b->data, b->size);
> + break;
> + case DL_SCRATCH_INLINE:
> + /* Unreachable in normal use; guarded by the inline wrapper. */
> + break;
> + }
> + b->data = b->inline_data;
> + b->size = sizeof b->inline_data;
> + b->backend = DL_SCRATCH_INLINE;
> +}
> +rtld_hidden_def (_dl_scratch_buffer_free)
> diff --git a/elf/dl-scratch-buffer.h b/elf/dl-scratch-buffer.h
> new file mode 100644
> index 00000000000..b6b0b690aee
> --- /dev/null
> +++ b/elf/dl-scratch-buffer.h
> @@ -0,0 +1,145 @@
> +/* Loader-internal scratch buffer.
> + Copyright (C) 2026 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public
> + License as published by the Free Software Foundation; either
> + version 2.1 of the License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; if not, see
> + <https://www.gnu.org/licenses/>. */
> +
> +/* This is the loader-side analogue of <scratch_buffer.h>. It exists
> + because the loader has two constraints that <scratch_buffer.h> does
> + not address:
> +
> + 1. While the active allocator is __minimal_malloc (early startup,
> + before __rtld_malloc_init_real has switched in libc's malloc),
> + __minimal_free only releases the most-recent allocation -- a
> + malloc'd spill would silently leak.
> +
> + 2. Some loader code paths cannot route a spill through the
> + interposable malloc at all because the user malloc may
> + recursively re-enter the loader and invalidate state we are
> + copying from (the canonical example is _dl_load_cache_lookup
> + copying out of the file-backed ld.so.cache mapping).
> +
> + The buffer starts in a stack-resident inline area; if the caller
> + needs more bytes, the spill is to anonymous mmap (always safe,
> + tagged for /proc/self/maps visibility) or to malloc (cheaper, only
> + chosen when both the active allocator is real malloc and the
> + caller does not pass DL_SCRATCH_NO_MALLOC).
> +
> + Typical usage:
> +
> + struct dl_scratch_buffer scratch = dl_scratch_buffer_init ();
> + dl_scratch_buffer_allocate (&scratch, needed, 0);
> + ... use scratch.data ...
> + dl_scratch_buffer_free (&scratch);
> +
> + The interface is one-shot: every consumer knows the required size
> + upfront and calls dl_scratch_buffer_allocate exactly once, so there
> + is no incremental-growth model. A second allocate without an
> + intervening free is a programming error and is checked by an
> + assertion in _dl_scratch_buffer_allocate.
> +
> + On allocation failure dl_scratch_buffer_allocate does not return;
> + it raises a loader ENOMEM via _dl_signal_error. Callers may
> + therefore treat scratch.data as valid after a successful return. */
> +
> +#ifndef _DL_SCRATCH_BUFFER_H
> +#define _DL_SCRATCH_BUFFER_H 1
> +
> +#include <stdbool.h>
> +#include <stddef.h>
> +#include <sys/cdefs.h>
> +
> +/* Size of the inline area. Tuned to cover typical ld.so.cache
> + entries (well under 256 bytes) so that the common case stays
> + entirely on-stack with no syscall and no malloc. */
> +enum { DL_SCRATCH_BUFFER_INLINE_SIZE = 256 };
> +
> +enum dl_scratch_backend
> +{
> + DL_SCRATCH_INLINE,
> + DL_SCRATCH_MMAP,
> + DL_SCRATCH_MALLOC,
> +};
> +
> +struct dl_scratch_buffer
> +{
> + void *data;
> + size_t size;
> + enum dl_scratch_backend backend;
> + char inline_data[DL_SCRATCH_BUFFER_INLINE_SIZE]
> + __attribute__ ((aligned (__alignof__ (max_align_t))));
> +};
> +
> +enum
> +{
> + /* Forbid the malloc backend for spill allocations -- the spill must
> + come from anonymous mmap so that interposed user malloc cannot
> + recursively re-enter the loader and invalidate state the caller
> + is copying from. See _dl_load_cache_lookup. */
> + DL_SCRATCH_NO_MALLOC = 1 << 0,
> +};
> +
> +/* Return a freshly-initialized scratch buffer suitable for use as a
> + stack-resident initializer. */
> +static __always_inline __attribute_warn_unused_result__
> +struct dl_scratch_buffer
> +dl_scratch_buffer_init (void)
> +{
> + return (struct dl_scratch_buffer) {
> + .data = NULL,
> + .size = sizeof ((struct dl_scratch_buffer *) 0)->inline_data,
> + .backend = DL_SCRATCH_INLINE,
> + };
> +}
> +
> +extern void _dl_scratch_buffer_allocate (struct dl_scratch_buffer *b,
> + size_t size, unsigned int flags)
> + __nonnull ((1)) attribute_hidden;
> +rtld_hidden_proto (_dl_scratch_buffer_allocate)
> +
> +extern void _dl_scratch_buffer_free (struct dl_scratch_buffer *b)
> + __nonnull ((1)) attribute_hidden;
> +rtld_hidden_proto (_dl_scratch_buffer_free)
> +
> +/* Ensure B->data points to a buffer of at least SIZE bytes; updates
> + B->size and B->backend accordingly. Intended to be called exactly
> + once per buffer lifetime (callers know the required size upfront --
> + there is no incremental growth model). Raises a loader ENOMEM
> + error via _dl_signal_error on failure -- does not return NULL. */
> +static __always_inline __nonnull ((1)) void
> +dl_scratch_buffer_allocate (struct dl_scratch_buffer *b, size_t size,
> + unsigned int flags)
> +{
> + /* First call after dl_scratch_buffer_init: point .data at the
> + caller's inline area now that its address is in scope. */
> + if (__glibc_unlikely (b->data == NULL))
> + b->data = b->inline_data;
> + if (__glibc_likely (size <= b->size))
> + return;
> + _dl_scratch_buffer_allocate (b, size, flags);
> +}
> +
> +/* Release any out-of-line allocation held by B and restore the
> + inline state. Safe to call multiple times (and on an already-freed
> + or freshly-initialized buffer). */
> +static __always_inline __nonnull ((1)) void
> +dl_scratch_buffer_free (struct dl_scratch_buffer *b)
> +{
> + if (__glibc_likely (b->backend == DL_SCRATCH_INLINE))
> + return;
> + _dl_scratch_buffer_free (b);
> +}
> +
> +#endif /* dl-scratch-buffer.h */
> --
> 2.43.0
>
LGTM.
Reviewed-by: H.J. Lu <hjl.tools@gmail.com>
Thanks.
--
H.J.
More information about the Libc-alpha
mailing list