| Message ID | 20260513192013.2511422-3-adhemerval.zanella@linaro.org |
|---|---|
| State | New |
| Headers | show |
| Series | elf: Bound loader stack usage from user-controllable inputs | expand |
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 >
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.
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 --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 */
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