[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