diff mbox series

[2/6] elf: Add dl_scratch_buffer, a loader-side scratch buffer

Message ID 20260513192013.2511422-3-adhemerval.zanella@linaro.org
State New
Headers show
Series elf: Bound loader stack usage from user-controllable inputs | expand

Commit Message

Adhemerval Zanella Netto May 13, 2026, 7:18 p.m. UTC
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_grow raises a loader error via _dl_signal_error
and does not return.

No functional change in this commit; consumers are added separately.
---
 elf/Makefile            |   1 +
 elf/dl-scratch-buffer.c |  86 ++++++++++++++++++++++++
 elf/dl-scratch-buffer.h | 142 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 229 insertions(+)
 create mode 100644 elf/dl-scratch-buffer.c
 create mode 100644 elf/dl-scratch-buffer.h

Comments

H.J. Lu May 13, 2026, 9:18 p.m. UTC | #1
On Thu, May 14, 2026 at 3:20 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_grow raises a loader error via _dl_signal_error
> and does not return.

Is it possible to share 2 implementations?

> No functional change in this commit; consumers are added separately.
> ---
>  elf/Makefile            |   1 +
>  elf/dl-scratch-buffer.c |  86 ++++++++++++++++++++++++
>  elf/dl-scratch-buffer.h | 142 ++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 229 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..a2cbcf163ba
> --- /dev/null
> +++ b/elf/dl-scratch-buffer.c
> @@ -0,0 +1,86 @@
> +/* 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 <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)
> +{
> +  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..bc5d2e4898c
> --- /dev/null
> +++ b/elf/dl-scratch-buffer.h
> @@ -0,0 +1,142 @@
> +/* 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.  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
>
Adhemerval Zanella Netto May 14, 2026, 12:04 p.m. UTC | #2
On 13/05/26 18:18, H.J. Lu wrote:
> On Thu, May 14, 2026 at 3:20 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_grow raises a loader error via _dl_signal_error
>> and does not return.
> 
> Is it possible to share 2 implementations?
The dl_scratch_buffer just hide the machinery required to check if
libc malloc is active, otherwise allocate using mmap/munmap.  The only
usage where mmap is always required is one where it explicitly avoids
malloc to work on interposable ones.

We can open-code each of one, but this makes slight more simple.  I
also tried to evaluate a way to tied this scratch buffer with the
exception mechanism, so wen _dl_signal_error is thrown it would
be automatically deallocate.  But I couldn't think in a simple way
to accomplish it, we are essentially emulating a C++ exception handling
using setjmp/longjmp it is complex for a reason.
H.J. Lu May 15, 2026, 2 a.m. UTC | #3
On Thu, May 14, 2026 at 3:20 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_grow raises a loader error via _dl_signal_error
> and does not return.
>
> No functional change in this commit; consumers are added separately.
> ---
>  elf/Makefile            |   1 +
>  elf/dl-scratch-buffer.c |  86 ++++++++++++++++++++++++
>  elf/dl-scratch-buffer.h | 142 ++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 229 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..a2cbcf163ba
> --- /dev/null
> +++ b/elf/dl-scratch-buffer.c
> @@ -0,0 +1,86 @@
> +/* 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 <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)
> +{
> +  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..bc5d2e4898c
> --- /dev/null
> +++ b/elf/dl-scratch-buffer.h
> @@ -0,0 +1,142 @@
> +/* 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.  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.
diff mbox series

Patch

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..a2cbcf163ba
--- /dev/null
+++ b/elf/dl-scratch-buffer.c
@@ -0,0 +1,86 @@ 
+/* 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 <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)
+{
+  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..bc5d2e4898c
--- /dev/null
+++ b/elf/dl-scratch-buffer.h
@@ -0,0 +1,142 @@ 
+/* 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.  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 */