From patchwork Wed May 13 19:18:19 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Adhemerval Zanella Netto X-Patchwork-Id: 134936 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 3B9134BBC0A4 for ; Wed, 13 May 2026 19:23:49 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 3B9134BBC0A4 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=aKLE/nf/ X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-dy1-x1330.google.com (mail-dy1-x1330.google.com [IPv6:2607:f8b0:4864:20::1330]) by sourceware.org (Postfix) with ESMTPS id 430964BBC0A9 for ; Wed, 13 May 2026 19:20:23 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 430964BBC0A9 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 430964BBC0A9 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1330 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700023; cv=none; b=p04ROcypMWCmrfHQ5QcPdhsjGIFTjIEeZ9W3oYO7YCBLnpm9OtrNpD6LTpD8e0+7ulejijYPBzs6Ga511B/eByEYYPXoXpVzEE0rLP/H3KJCeArMEARZYhPyx7Vqf9/YkTlqIZrTFABVkqEzRGVbU2YEvQHFVfaSa5xFhY4S6oI= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700023; c=relaxed/simple; bh=JrZeNqbcdzedV071OwiW7M7B59BXs7C6Iq1c2ROINs0=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=pGCR41fxWvnjLj0KUVzArzjTc0Yj3VtiOxCJclLyX2tlD/lgglXjvkvqOlXwwHStrQ//7Z+0YAJL+xk9MUKfgn2Ep1dMiTe1JDD75hgEpAT8RtbXU8j5eGtiDczZCS4edGjJt0uinMW+XWGw19MU5+xEnaGLHFy+ETAfb9RO3x4= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=aKLE/nf/ DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 430964BBC0A9 Received: by mail-dy1-x1330.google.com with SMTP id 5a478bee46e88-2f7020a928eso10063762eec.1 for ; Wed, 13 May 2026 12:20:23 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1778700022; x=1779304822; darn=sourceware.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=M5rr7Nd4oxJnHJSYJEiSsHW+tV3F2URHrnfgJgX81GQ=; b=aKLE/nf/wM3dFITo3OmXbsH089EqK42uMKTq/7NF8Nj4R0u2X0qYhzm9jqbADuhCOt rFVFi8zP3bDzLT7XOUwOayzeC02ich5sh6z9HoHGWty25Mmaf+vmbc87cD039FASOWRD ch9OJeZiLR8YnFjm5GCBR2DmeM8xWNb9bZ010Lf2pWMa03XUkWH8VYO/Ns5wbhNyT9xO Sna1tlVuIIIVtLJ2gztYVKpdTEonqDE1kp3JTIntad8pv14HIF07/8Fe4gGEHWVdbhg3 vTf7ZGLD3ZYuy63lihSu639go4NM1Oe01VWXufYUVwhmhTBsQoyN6ydN9EL0s3T/0GBQ mhbg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778700022; x=1779304822; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=M5rr7Nd4oxJnHJSYJEiSsHW+tV3F2URHrnfgJgX81GQ=; b=Rfw3n+60/ozAouACW2Z+U/6T4NhIB9B9i1dlNzVgA2rGCH2Vb4edEdtwkCbAfjmXEl IkdwSTWJ90kL4Kswb02b6P+ZL7E7Wcq7Ps0orrtjl2vO/iv18qsLqxjZYfU+pe/rmT3O 8Cipo235CcGdYlL2ip7qOs8VkObU/UAA8b0zA+Dh0dv6bStSCKXBaBZGYq0FuJF9FjvI 7cqGMFN9BtC4XUW/FmcP4W+fvd9lVgVDj7RWiYTVA6nntSxPaKksBTu62odqQ2MTcPMk mDxu8iSsxNiQ2W/DDLlKgxtkLK0+oEg/c9uE069IPUrOuBlTkyE2OzlMrY2vyouIWAzT hINw== X-Gm-Message-State: AOJu0Yzu+oCcruTlWF/5VVPAqHEk2ze4XTqPBFqQt8EvOSdtl3z5Wdwc uFBzxlKCAktdVF0DutJlzBEM0F2X4BbcvyigOykYqzxpU7RRe6Eja0z2bOH7HbQdLa6qpudOad1 bsfmj X-Gm-Gg: Acq92OGo1wvYUQ/OUcWHo8bfG3YnnMz48Va+xM9Ow9bMI7ZzdGehP4jxE568siSzmkc lMXe7cRaerhndWGtFIa5xXsmd1JQiM3kNxnJXrpKlb98hbIKVSarKkhZ2/SfjOKbTJE5Ka6f0l0 rwRbSaKTtc+4RyXq4CP2KLUGhYFX3ze1w6o+UUV1tYm7Bt7WNqUnzQyod4BIKzeip0YDnBQLDkt tJdiOsJ8Jo86UXS07FhdiNRAjGxsvmLUPJThmHEzTpb8a1+X2oloWWkC14xnS21XMOQAlkRQLoY xgZ7oL7sjSdbj62D0Uua2gnKnp/iJzj9AkdAIOAoLwDBusmRhzkZNcd1XslJmC0DUvVpZ2qO8nZ UHKkXgaEYO81PR8UxOGURqsyobmukuTZ68k1GzULB1lrQ/pUZKzp7OjqnJqfIR3kVNawWoXy76s yR53st/KlFx417B8JfKdC1Ew6ypmotChXmtQM= X-Received: by 2002:a05:7300:5410:b0:2d1:9b35:4ed3 with SMTP id 5a478bee46e88-30119f5f603mr2930226eec.28.1778700021241; Wed, 13 May 2026 12:20:21 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c1:d905:1e50:98bf:e0d4:d27e]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-30294adddaasm117468eec.13.2026.05.13.12.20.19 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 12:20:20 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: "H . J . Lu" , Florian Weimer Subject: [PATCH 1/6] elf: Batch program-header reads in _dl_map_segments (oversight fix) Date: Wed, 13 May 2026 16:18:19 -0300 Message-ID: <20260513192013.2511422-2-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> References: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> MIME-Version: 1.0 X-Spam-Status: No, score=-12.1 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_NONE, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org The fix for BZ 26577 ("Fix stack overflow in _dl_map_object_from_fd with large e_phnum") removed the alloca for the program-header table and introduced a streaming iterator (dl_pt_load_iterator) so segments could be walked without staging the entire table on the stack. That patch batched reads correctly in _dl_map_object_scan_phdrs (the first walk, which collects PT_DYNAMIC/PT_TLS/PT_GNU_* metadata), but overlooked the second walk in _dl_map_segments: _dl_pt_load_iterator_next issued one pread64 per program header to find the next PT_LOAD entry. For an object with N program headers this added N redundant per-phdr syscalls on every dlopen / loader startup -- regardless of whether the table had already been read by open_verify into struct filebuf. Unify both walks behind a single batched helper, _dl_pt_load_iterator_phdr_at: - When the program header table fits in the bytes already read by open_verify into fbp->buf (the common case for nearly all shared objects), all phdr accesses are served from that buffer with no syscall at all. - Otherwise, up to FILEBUF_SIZE / sizeof(ElfW(Phdr)) program headers are read into fbp->buf with a single pread64; subsequent indices in the same window hit the buffer. Both _dl_map_object_scan_phdrs and _dl_pt_load_iterator_next now go through this helper, eliminating the separate batching logic in _dl_map_object_scan_phdrs. struct filebuf moves from dl-load.c to dl-load.h so the inline iterator in dl-map-segments.h can reach fbp->buf. The filebuf size is also bumped to ensure the cached fast path triggers for all observed binaries. A survey of an Ubuntu 24.04 installation (scanning /usr) shows: Candidate files : 465834 ELF files inspected : 11624 glibc-linked binaries : 10164 Minimum e_phnum : 5 Maximum e_phnum : 14 Average e_phnum : 11.37 Median e_phnum : 11.0 shows e_phnum capped at 14 (for instance gcc's cc1, lto1, perl, and gdb). The previous FILEBUF_SIZE of 832 on 64-bit fit only 13 program headers after the ELF header (64 + 13*56 = 792), so 64-bit binaries with 14 phdrs missed the cached path. FILEBUF_SIZE is bumped from 512/832 to 640/1024 (32-bit / 64-bit) -- enough for at least 16 program headers on either ABI, leaving headroom over the observed maximum. For a typical shared library where open_verify's initial read covers the program header table, this reduces _dl_map_segments from N preads to 0. For a worst-case e_phnum that does not fit in fbp->buf, reads drop from N to ceil(N / phdrs_per_buf) -- the same cost _dl_map_object_scan_phdrs already pays. No functional change. Tested on x86_64-linux-gnu, aaarch64-linux-gnu, and i686-linux-gnu. --- elf/dl-load.c | 285 +++++++++++++++++++++----------------------------- elf/dl-load.h | 100 ++++++++++++++---- 2 files changed, 201 insertions(+), 184 deletions(-) diff --git a/elf/dl-load.c b/elf/dl-load.c index f6e391a4689..d20e49d526a 100644 --- a/elf/dl-load.c +++ b/elf/dl-load.c @@ -34,31 +34,6 @@ #include #include -/* Type for the buffer we put the ELF header and hopefully the program - header. This buffer does not really have to be too large. In most - cases the program header follows the ELF header directly. If this - is not the case all bets are off and we can make the header - arbitrarily large and still won't get it read. This means the only - question is how large are the ELF and program header combined. The - ELF header 32-bit files is 52 bytes long and in 64-bit files is 64 - bytes long. Each program header entry is again 32 and 56 bytes - long respectively. I.e., even with a file which has 10 program - header entries we only have to read 372B/624B respectively. Add to - this a bit of margin for program notes and reading 512B and 832B - for 32-bit and 64-bit files respectively is enough. If this - heuristic should really fail for some file the code in - `_dl_map_object_from_fd' knows how to recover. */ -struct filebuf -{ - ssize_t len; -#if __WORDSIZE == 32 -# define FILEBUF_SIZE 512 -#else -# define FILEBUF_SIZE 832 -#endif - char buf[FILEBUF_SIZE] __attribute__ ((aligned (__alignof (ElfW(Ehdr))))); -}; - #include "dynamic-link.h" #include "get-dynamic-info.h" #include @@ -936,13 +911,18 @@ _dl_notify_new_object (int mode, Lmid_t nsid, struct link_map *l) } /* Initialize the PT_LOAD iterator IT for reading program headers from FD - at file offset PHOFF with PHNUM entries. Zeros all precomputed fields - so the caller's scan loop can fill them in. */ + at file offset PHOFF with PHNUM entries. FBP is used as scratch space + for batched program-header reads; if open_verify's initial read into + FBP->buf already covers the whole phdr table, the iterator runs + entirely from that buffer without any further pread. Zeros all + precomputed fields so the caller's scan loop can fill them in. */ static void -_dl_pt_load_iterator_init (struct dl_pt_load_iterator *it, - int fd, ElfW(Off) phoff, uint16_t phnum) +_dl_pt_load_iterator_init (struct dl_pt_load_iterator *it, int fd, + struct filebuf *fbp, ElfW(Off) phoff, + uint16_t phnum) { it->fd = fd; + it->fbp = fbp; it->phoff = phoff; it->phnum = phnum; it->idx = 0; @@ -952,161 +932,134 @@ _dl_pt_load_iterator_init (struct dl_pt_load_iterator *it, it->first_mapstart = 0; it->last_mapstart = 0; it->last_allocend = 0; + it->cached = (phoff + (ElfW(Off)) phnum * sizeof (ElfW(Phdr)) + <= (ElfW(Off)) fbp->len); + it->buf_base = 0; + it->buf_count = it->cached ? phnum : 0; } -/* Scan all program headers from IT->fd in chunks, using FBP->buf as a - scratch buffer. Fills in IT's precomputed PT_LOAD metadata and collects - segment attributes into L. Returns NULL on success, or an error message - string on failure; sets *ERRVALP to errno for I/O errors, 0 otherwise. */ +/* Scan all program headers from IT->fd, using the iterator's filebuf as a + scratch buffer for batched reads (skipped entirely if open_verify + already read the whole table). Fills in IT's precomputed PT_LOAD + metadata and collects segment attributes into L. Returns NULL on + success, or an error message string on failure; sets *ERRVALP to errno + for I/O errors, 0 otherwise. */ static const char * _dl_map_object_scan_phdrs (struct dl_pt_load_iterator *it, - struct filebuf *fbp, struct link_map *l, int mode, + struct link_map *l, int mode, unsigned int *stack_flagsp, bool *has_holesp, bool *empty_dynamicp, int *errvalp) { ElfW(Addr) prev_mapend = 0; - const ElfW(Half) phdrs_per_buf = sizeof (fbp->buf) / sizeof (ElfW(Phdr)); - ElfW(Phdr) *chunk = (ElfW(Phdr) *) fbp->buf; struct dl_machine_phdr_info minfo; elf_machine_phdr_info_init (&minfo); - /* Fast path: if all program headers fit within the bytes already read - into fbp->buf by open_verify, iterate them directly without any - additional pread syscalls. The slow path falls through to pread - in chunks (which overwrites fbp->buf, but the caller has already - saved the ELF header to a local copy). */ - const bool cached - = (it->phoff + (ElfW(Off)) it->phnum * sizeof (ElfW(Phdr)) - <= (ElfW(Off)) fbp->len); - - for (ElfW(Half) base = 0; base < it->phnum; ) + for (ElfW(Half) i = 0; i < it->phnum; i++) { - ElfW(Half) batch; - const ElfW(Phdr) *batch_ptr; - - if (__glibc_likely (cached)) + const ElfW(Phdr) *ph = _dl_pt_load_iterator_phdr_at (it, i); + if (__glibc_unlikely (ph == NULL)) { - batch = it->phnum; - batch_ptr = (const ElfW(Phdr) *) (fbp->buf + it->phoff); + *errvalp = errno; + return N_("cannot read file data"); } - else + elf_machine_phdr_collect (&minfo, ph); + switch (ph->p_type) { - batch = it->phnum - base; - if (batch > phdrs_per_buf) - batch = phdrs_per_buf; - size_t bytes = (size_t) batch * sizeof (ElfW(Phdr)); - ElfW(Off) off = it->phoff + (ElfW(Off)) base * sizeof (ElfW(Phdr)); - if (__pread64_nocancel (it->fd, chunk, bytes, off) != bytes) - { - *errvalp = errno; - return N_("cannot read file data"); - } - batch_ptr = chunk; - } - - for (ElfW(Half) i = 0; i < batch; i++) - { - const ElfW(Phdr) *ph = &batch_ptr[i]; - elf_machine_phdr_collect (&minfo, ph); - switch (ph->p_type) - { - case PT_LOAD: + case PT_LOAD: + { + if (__glibc_unlikely (((ph->p_vaddr - ph->p_offset) + & (it->pagesize - 1)) != 0)) { - if (__glibc_unlikely (((ph->p_vaddr - ph->p_offset) - & (it->pagesize - 1)) != 0)) - { - *errvalp = 0; - return N_("ELF load command address/offset not page-aligned"); - } - ElfW(Addr) mapstart = ALIGN_DOWN (ph->p_vaddr, it->pagesize); - ElfW(Addr) mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, - it->pagesize); - ElfW(Off) mapoff = ALIGN_DOWN (ph->p_offset, it->pagesize); - int prot = pf_to_prot (ph->p_flags); - if (powerof2 (ph->p_align) && ph->p_align > it->p_align_max) - it->p_align_max = ph->p_align; - it->p_align_max = _dl_map_segment_align (&(struct loadcmd) { - .mapstart = mapstart, - .mapend = mapend, - .mapoff = mapoff, - .prot = prot }, - it->p_align_max); - if (it->nloadcmds > 0 && prev_mapend != mapstart) - *has_holesp = true; - prev_mapend = mapend; - if (it->nloadcmds == 0) - it->first_mapstart = mapstart; - it->last_mapstart = mapstart; - it->last_allocend = ph->p_vaddr + ph->p_memsz; - it->nloadcmds++; + *errvalp = 0; + return N_("ELF load command address/offset not page-aligned"); } - break; + ElfW(Addr) mapstart = ALIGN_DOWN (ph->p_vaddr, it->pagesize); + ElfW(Addr) mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, + it->pagesize); + ElfW(Off) mapoff = ALIGN_DOWN (ph->p_offset, it->pagesize); + int prot = pf_to_prot (ph->p_flags); + if (powerof2 (ph->p_align) && ph->p_align > it->p_align_max) + it->p_align_max = ph->p_align; + it->p_align_max = _dl_map_segment_align (&(struct loadcmd) { + .mapstart = mapstart, + .mapend = mapend, + .mapoff = mapoff, + .prot = prot }, + it->p_align_max); + if (it->nloadcmds > 0 && prev_mapend != mapstart) + *has_holesp = true; + prev_mapend = mapend; + if (it->nloadcmds == 0) + it->first_mapstart = mapstart; + it->last_mapstart = mapstart; + it->last_allocend = ph->p_vaddr + ph->p_memsz; + it->nloadcmds++; + } + break; - /* These entries tell us where to find things once the file's - segments are mapped in. We record the addresses it says - verbatim, and later correct for the run-time load address. */ - case PT_DYNAMIC: - if (ph->p_filesz == 0) - *empty_dynamicp = true; /* Usually separate debuginfo. */ - else - { - /* Debuginfo only files from "objcopy --only-keep-debug" - contain a PT_DYNAMIC segment with p_filesz == 0. Skip - such a segment to avoid a crash later. */ - l->l_ld = (void *) ph->p_vaddr; - l->l_ldnum = ph->p_memsz / sizeof (ElfW(Dyn)); - l->l_ld_readonly = (ph->p_flags & PF_W) == 0; - } - break; - - case PT_PHDR: - l->l_phdr = (void *) ph->p_vaddr; - break; - - case PT_TLS: - if (ph->p_memsz == 0) - /* Nothing to do for an empty segment. */ - break; - - l->l_tls_blocksize = ph->p_memsz; - l->l_tls_align = ph->p_align; - if (ph->p_align == 0) - l->l_tls_firstbyte_offset = 0; - else - l->l_tls_firstbyte_offset = ph->p_vaddr & (ph->p_align - 1); - l->l_tls_initimage_size = ph->p_filesz; - /* Since we don't know the load address yet only store the - offset. We will adjust it later. */ - l->l_tls_initimage = (void *) ph->p_vaddr; - - /* l->l_tls_modid is assigned below, once there is no - possibility for failure. */ - - if (l->l_type != lt_library - && GL(dl_tls_dtv_slotinfo_list) == NULL) - { -#ifdef SHARED - /* We are loading the executable itself when the dynamic - linker was executed directly. The setup will happen - later. */ - assert (l->l_prev == NULL || (mode & __RTLD_AUDIT) != 0); -#else - assert (false && "TLS not initialized in static application"); -#endif - } - break; - - case PT_GNU_STACK: - *stack_flagsp = pf_to_prot (ph->p_flags); - break; - - case PT_GNU_RELRO: - l->l_relro_addr = ph->p_vaddr; - l->l_relro_size = ph->p_memsz; - break; + /* These entries tell us where to find things once the file's + segments are mapped in. We record the addresses it says + verbatim, and later correct for the run-time load address. */ + case PT_DYNAMIC: + if (ph->p_filesz == 0) + *empty_dynamicp = true; /* Usually separate debuginfo. */ + else + { + /* Debuginfo only files from "objcopy --only-keep-debug" + contain a PT_DYNAMIC segment with p_filesz == 0. Skip + such a segment to avoid a crash later. */ + l->l_ld = (void *) ph->p_vaddr; + l->l_ldnum = ph->p_memsz / sizeof (ElfW(Dyn)); + l->l_ld_readonly = (ph->p_flags & PF_W) == 0; } + break; + + case PT_PHDR: + l->l_phdr = (void *) ph->p_vaddr; + break; + + case PT_TLS: + if (ph->p_memsz == 0) + /* Nothing to do for an empty segment. */ + break; + + l->l_tls_blocksize = ph->p_memsz; + l->l_tls_align = ph->p_align; + if (ph->p_align == 0) + l->l_tls_firstbyte_offset = 0; + else + l->l_tls_firstbyte_offset = ph->p_vaddr & (ph->p_align - 1); + l->l_tls_initimage_size = ph->p_filesz; + /* Since we don't know the load address yet only store the + offset. We will adjust it later. */ + l->l_tls_initimage = (void *) ph->p_vaddr; + + /* l->l_tls_modid is assigned below, once there is no + possibility for failure. */ + + if (l->l_type != lt_library + && GL(dl_tls_dtv_slotinfo_list) == NULL) + { +#ifdef SHARED + /* We are loading the executable itself when the dynamic + linker was executed directly. The setup will happen + later. */ + assert (l->l_prev == NULL || (mode & __RTLD_AUDIT) != 0); +#else + assert (false && "TLS not initialized in static application"); +#endif + } + break; + + case PT_GNU_STACK: + *stack_flagsp = pf_to_prot (ph->p_flags); + break; + + case PT_GNU_RELRO: + l->l_relro_addr = ph->p_vaddr; + l->l_relro_size = ph->p_memsz; + break; } - base += batch; } if (__glibc_unlikely (elf_machine_reject_phdr_p (&minfo, l, it->fd))) @@ -1275,10 +1228,10 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd, bool has_holes; bool empty_dynamic = false; - _dl_pt_load_iterator_init (&it, fd, header.e_phoff, l->l_phnum); + _dl_pt_load_iterator_init (&it, fd, fbp, header.e_phoff, l->l_phnum); has_holes = false; - errstring = _dl_map_object_scan_phdrs (&it, fbp, l, mode, &stack_flags, + errstring = _dl_map_object_scan_phdrs (&it, l, mode, &stack_flags, &has_holes, &empty_dynamic, &errval); if (__glibc_unlikely (errstring != NULL)) goto lose; diff --git a/elf/dl-load.h b/elf/dl-load.h index e58028038c9..80ae5db4b3d 100644 --- a/elf/dl-load.h +++ b/elf/dl-load.h @@ -21,12 +21,41 @@ #define _DL_LOAD_H 1 #include +#include #include +#include #include #include #include +/* Type for the buffer we put the ELF header and hopefully the program + header. This buffer does not really have to be too large. In most + cases the program header follows the ELF header directly. If this + is not the case all bets are off and we can make the header + arbitrarily large and still won't get it read. This means the only + question is how large are the ELF and program header combined. The + ELF header for 32-bit files is 52 bytes long and for 64-bit files + 64 bytes long. Each program header entry is 32 and 56 bytes long + respectively. + + Size for at least 16 entries (with a little margin for program notes) + needs 52 + 16*32 = 564 bytes on 32-bit and 64 + 16*56 = 960 bytes on + 64-bit; round up to 640 and 1024 respectively. If this heuristic + should still fail for some file the code in + `_dl_map_object_from_fd' knows how to recover. */ +struct filebuf +{ + ssize_t len; +#if __WORDSIZE == 32 +# define FILEBUF_SIZE 640 +#else +# define FILEBUF_SIZE 1024 +#endif + char buf[FILEBUF_SIZE] __attribute__ ((aligned (__alignof (ElfW(Ehdr))))); +}; + + /* On some systems, no flag bits are given to specify file mapping. */ #ifndef MAP_FILE # define MAP_FILE 0 @@ -84,28 +113,65 @@ struct loadcmd }; -/* Iterator for PT_LOAD program header segments. It should be initialized - by _dl_pt_load_iterator_init once, then _dl_pt_load_iterator_next - repeatedly to walk each PT_LOAD segment without storing them all. - Segments are re-read one at a time via pread so that no large stack - buffer is needed for the program header table. */ +/* Iterator for program header segments. Initialize with + _dl_pt_load_iterator_init, then either walk PT_LOAD segments via + _dl_pt_load_iterator_next or do random access via + _dl_pt_load_iterator_phdr_at. A scratch buffer (fbp->buf) is used to + batch-read program headers; if the entire program header table was + already loaded by open_verify's initial read no pread is issued. */ struct dl_pt_load_iterator { int fd; /* File descriptor for pread. */ + struct filebuf *fbp; /* Scratch buffer for batched phdr reads. */ ElfW(Off) phoff; /* Program header table file offset. */ ElfW(Half) phnum; /* Total number of program headers. */ ElfW(Half) idx; /* Index of next header to read. */ + ElfW(Half) buf_base; /* Index of phdr at start of fbp->buf + (chunked mode only). */ + ElfW(Half) buf_count; /* Number of phdrs currently in fbp->buf. */ + bool cached; /* True iff entire phdr table is already + resident in fbp->buf from open_verify. */ ElfW(Addr) p_align_max; /* Maximum p_align over all PT_LOAD segments. */ ElfW(Addr) pagesize; /* System page size (GLRO(dl_pagesize)). */ - /* Fields below are precomputed by _dl_pt_load_iterator_init and - are intended for use by _dl_map_segments. */ + /* Fields below are precomputed by _dl_map_object_scan_phdrs and are + intended for use by _dl_map_segments. */ ElfW(Addr) first_mapstart; /* mapstart of the first PT_LOAD segment. */ ElfW(Addr) last_mapstart; /* mapstart of the last PT_LOAD segment. */ ElfW(Addr) last_allocend; /* allocend of the last PT_LOAD segment. */ size_t nloadcmds; /* Number of PT_LOAD segments found. */ }; +/* Return a pointer to the program header at INDEX. If the entire phdr + table is already cached in fbp->buf (from open_verify), it is served + directly with no syscall; otherwise a batch of up to FILEBUF_SIZE / + sizeof(ElfW(Phdr)) entries is read into fbp->buf via a single pread. + Subsequent calls within the same batch hit the buffer. Returns NULL on + read failure (errno set by pread). */ +static __always_inline const ElfW(Phdr) * +_dl_pt_load_iterator_phdr_at (struct dl_pt_load_iterator *it, ElfW(Half) idx) +{ + if (__glibc_likely (it->cached)) + return (const ElfW(Phdr) *) (it->fbp->buf + it->phoff) + idx; + + if (idx < it->buf_base || idx >= it->buf_base + it->buf_count) + { + const ElfW(Half) phdrs_per_buf + = sizeof (it->fbp->buf) / sizeof (ElfW(Phdr)); + ElfW(Half) batch = it->phnum - idx; + if (batch > phdrs_per_buf) + batch = phdrs_per_buf; + size_t bytes = (size_t) batch * sizeof (ElfW(Phdr)); + ElfW(Off) off = it->phoff + (ElfW(Off)) idx * sizeof (ElfW(Phdr)); + if (__pread64_nocancel (it->fd, it->fbp->buf, bytes, off) + != (ssize_t) bytes) + return NULL; + it->buf_base = idx; + it->buf_count = batch; + } + return (const ElfW(Phdr) *) it->fbp->buf + (idx - it->buf_base); +} + /* Advance iterator IT to the next PT_LOAD segment and fill C with its decoded load command. Returns true when a segment was found, false when the end of the program header table has been reached or a read @@ -115,21 +181,19 @@ _dl_pt_load_iterator_next (struct dl_pt_load_iterator *it, struct loadcmd *c) { while (it->idx < it->phnum) { - ElfW(Phdr) ph; - ElfW(Off) off = it->phoff + (ElfW(Off)) it->idx * sizeof ph; + const ElfW(Phdr) *ph = _dl_pt_load_iterator_phdr_at (it, it->idx); it->idx++; - if (__pread64_nocancel (it->fd, &ph, sizeof ph, off) - != (ssize_t) sizeof ph) + if (__glibc_unlikely (ph == NULL)) return false; - if (ph.p_type != PT_LOAD) + if (ph->p_type != PT_LOAD) continue; - c->mapstart = ALIGN_DOWN (ph.p_vaddr, it->pagesize); - c->mapend = ALIGN_UP (ph.p_vaddr + ph.p_filesz, it->pagesize); - c->dataend = ph.p_vaddr + ph.p_filesz; - c->allocend = ph.p_vaddr + ph.p_memsz; - c->mapoff = ALIGN_DOWN (ph.p_offset, it->pagesize); - c->prot = pf_to_prot (ph.p_flags); + c->mapstart = ALIGN_DOWN (ph->p_vaddr, it->pagesize); + c->mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, it->pagesize); + c->dataend = ph->p_vaddr + ph->p_filesz; + c->allocend = ph->p_vaddr + ph->p_memsz; + c->mapoff = ALIGN_DOWN (ph->p_offset, it->pagesize); + c->prot = pf_to_prot (ph->p_flags); c->mapalign = it->p_align_max; return true; } From patchwork Wed May 13 19:18:20 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Adhemerval Zanella Netto X-Patchwork-Id: 134935 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 93F954BBC0E1 for ; Wed, 13 May 2026 19:21:38 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 93F954BBC0E1 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=VNc1anbv X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-dy1-x1329.google.com (mail-dy1-x1329.google.com [IPv6:2607:f8b0:4864:20::1329]) by sourceware.org (Postfix) with ESMTPS id D14EB4BBC0B2 for ; Wed, 13 May 2026 19:20:24 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D14EB4BBC0B2 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org D14EB4BBC0B2 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1329 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700025; cv=none; b=OYgH7ExrfJevKwhDW31mSZXLxmWknRMSaXgQzoZYWzu7gMOokAclD4egafgP8n+Ga3oGRomoumEqydTQr9gqwS12ahB+skHHuvjO3vE8I+saOjgftNjU5KB9bugLw6oioj3YUECWgh704jmRkFDNGR651TvBCA4pDtnLrZMDQjs= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700025; c=relaxed/simple; bh=Z/oTZ6qyQdzRknYl6/EwuQfLmc436RcVdgI/KfP2YAo=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=hPXh1cRnN+g/4bn0VW21uvRXUwe7xu7nXd0tIhIRqsfktwMCqJdQ/DRCnopm7UNXxuI6oxaGawLJVTzWzAxksHglbjqC1do1qTOsYc5Xxlfz9Qh4Ts5TqKgA1a6A1SDBfvfUnPr/DkOd6wgjgw6j34xoUENL5R39o/qgJuab8BI= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=VNc1anbv DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D14EB4BBC0B2 Received: by mail-dy1-x1329.google.com with SMTP id 5a478bee46e88-3025d725a05so506363eec.1 for ; Wed, 13 May 2026 12:20:24 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1778700023; x=1779304823; darn=sourceware.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=VCzwbQK/CEEe/LB6LjdajGyJbPjab3ljxbShgbr9oQ4=; b=VNc1anbvhdQV2FDvL+JftANNjLUpxKjeOEIez0UIAa1BgLjjj8vCR+VBH28WsGPGQF XPkYDHHg1vAPU+y6o28v2DYKu0G/yf9re2WLE3h+rgCwPer5cIHbVBs4iOdJSetfaK6K x3S3fQYAZKE6svg//6rxK9XvZ7p0979+nBFuzueS5Oj68X5rrpytHfs5UmPa9L/ad1nc RtF5oKOfZqdxu9lIUFQF+7rm27plmmslZpIQWb9gkbPLjw3sWCveN4t5r6Hr8azwzD0T SHpchWO6w/UZFsZgyJKThpUjCg3Rzg66+AE4dMpL/YBwkcUhCfaQPu+VZdCNFPoHD5Mz qbXw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778700023; x=1779304823; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=VCzwbQK/CEEe/LB6LjdajGyJbPjab3ljxbShgbr9oQ4=; b=rdyrqsfni4mXRIYIsO4qyru1cant5+FeQed36jVyh3Ew1MQ/TRpziNGI9DxYdDeU2S vt+tPxHxhT89AQUGRvhBKHVkhQTiGEpe2P0q43cZfmVmyRmZF0YQXgSCl5OoNFhl7gRZ pCAHWktHsvlud0VV21UhdQv969qzoaEICIEMG7l0yXTHP8sPNbauwv5D9F8NLa1OTSkl pK27qlXBErqJ3ZvkjwxSZV+4devve4RId8krXeqQpxK1a9FE8YvF0Y3OP9P8IQnVjZCa hWzit2ogSOqsJY/9hSMhunR0UZm/zg6FBI4NvMQ3+uf9CC05CXtLFLuauHqn1OuUA+qO YFkw== X-Gm-Message-State: AOJu0YxXfd3sTRKsld95nTppMHFQWmI4uTdAtE0SPm8bhT4rdYFUVVEv 3LfiB9Zi+0FKMvmiLMWSZRmilDvGXAT9h6gLxBfyEAFyqEEGLZXlKxROMS621EMNHWBjv1HV5RH fPTsd X-Gm-Gg: Acq92OGZsttvvDzX7FEzKaUXnf2vlJmyVguq+fXte1bGzUFbujxI/xpCufUG0EN82IZ nJ458fJSRHHnfdpn6A0KWN5qIIomCykzK9bGnynSCxGglxLoncbB5kEqWG3t7lafgVAipoQzxwW QAX23LdtVvnci20afsK4DpbPe0eO413Lgg+gfvrtdN5b09fo85ufO4VxZA4vbcvzGcZTZBqT7f8 Q8zJl7HYjAvXSnh4JDjXD2Qb9NQSEpXzRv/3r6eF9GadMhOaNO6xcn3RsO76Znszd9nwDbkVStU 5jiupq3kPFijgQZ1aB18xn7y/0Oqo4jvSd/eRCqt+CB6+9p6eEqQaeXYPo1X+PhljFeTViSCDsF NCX5qxGJC+XGiQmXEM+6j3T0CbMyhbjg360q4ql7MitWGoab3CqYhcKLaIp3MRGe9EyUoHSjOai cTDV+liHHL0Fy8o1Uh5wSSiyY8bok/TOWO5rxcauLvTGaWqT7hqPpLFxs4 X-Received: by 2002:a05:7300:cc0c:b0:2f2:6dde:df53 with SMTP id 5a478bee46e88-3011a16dd89mr2849036eec.17.1778700023123; Wed, 13 May 2026 12:20:23 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c1:d905:1e50:98bf:e0d4:d27e]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-30294adddaasm117468eec.13.2026.05.13.12.20.21 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 12:20:22 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: "H . J . Lu" , Florian Weimer Subject: [PATCH 2/6] elf: Add dl_scratch_buffer, a loader-side scratch buffer Date: Wed, 13 May 2026 16:18:20 -0300 Message-ID: <20260513192013.2511422-3-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> References: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> MIME-Version: 1.0 X-Spam-Status: No, score=-12.2 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_NONE, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org 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. - 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 : 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 + . */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +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 + . */ + +/* This is the loader-side analogue of . It exists + because the loader has two constraints that 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 +#include +#include + +/* 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 */ From patchwork Wed May 13 19:18:21 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Adhemerval Zanella Netto X-Patchwork-Id: 134933 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id E85DA4BBC0D2 for ; Wed, 13 May 2026 19:21:28 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org E85DA4BBC0D2 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=zCdVkMR+ X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-dy1-x1335.google.com (mail-dy1-x1335.google.com [IPv6:2607:f8b0:4864:20::1335]) by sourceware.org (Postfix) with ESMTPS id 74AB54BBC0B4 for ; Wed, 13 May 2026 19:20:26 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 74AB54BBC0B4 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 74AB54BBC0B4 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1335 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700026; cv=none; b=X5c8RZPHf5sRM7G9GJVrlPcX6hIrNIdXR3J0CpUcpn+O8dZ/TEwUhQTqkqj5TLzBPr9rIp2vmlm+io8kZ3jTUk2m8OidayX7mXnD492nAs5CMPcBFRx+0XedX2wwZ+mUj+xfhNQfRUriKLHlcF5ThsvcgNBd2y6pBb610yV0Z5w= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700026; c=relaxed/simple; bh=zS3/XRi5n1KdK+7k7iMj16obRoPF5reiCUyCWL6bB1Y=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=LLw7M7WHWCXX+/QEsnzb2NGR4yrmuFnnGQcq0W8moTObTekuKBT5FFdSMTIzKfKHEy+9KSMvDXRq0UkypJ3D5BaASRBhjgQ6dtPOlW2p1hwR2J4xzXojJKsscWoIcB8CcFd6SkWxM7nCdPK7Yw5R27X38FAHGCLkwWVIIiM7f6Q= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=zCdVkMR+ DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 74AB54BBC0B4 Received: by mail-dy1-x1335.google.com with SMTP id 5a478bee46e88-2f0d3e07e30so17083313eec.0 for ; Wed, 13 May 2026 12:20:26 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1778700025; x=1779304825; darn=sourceware.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=mmQPomtIrcx56OwzDlaaRyoOM/wd8uOmytOW3nMwBgQ=; b=zCdVkMR+fegGIqkiCJTpPYjOFMQRv5pAVhH+qizZUoBVxZrQQKtIYfOCfj2SpX7Dfh Ydz0sgY3unVHDFGoCWKMQMZm2hVgTM7fi91ORE5U/QZAO7T9U1hpEAs3EHN0+BscJW8E 1U+cPf9RzljOw2HbYLyi2ZfnrgOW2q246Ro8L0qkkYMqz1+2GpF0FbHq70Af5QZrE1eO 4aEYpO4zE/YimZPStRJrMEB/xpJ3oso0Hf3VtVqFKtTMSRYKFaPw2f7GSmrI8tHOXmZ2 uHUqGYueoLOAVxcYTxJTIyQsg3mAFGnCpplk2w/f0R0c6LVLUPvNa+k43vMz2rT8U3l/ mhfg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778700025; x=1779304825; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=mmQPomtIrcx56OwzDlaaRyoOM/wd8uOmytOW3nMwBgQ=; b=KIrRPjkh+mtIypMp3lCwPfEorrnioWAX0ASaHQsLQQ0iFL0CPmTcdjKm5AM8nTXknK mFXn9ohyCX0H5wbzbO8oxCtD01mh9ftH4b/mavS56x3W5JCvhlyPsLMMIdy/luToxpSJ zFf9bgkDMIqqJBEf/I++P1G1LtrKb6HFCvS2ZzR7/PlIT8B5NOy3tP5WynyDWgSGv0W5 bw4XGv4+3PuYYI+ZF3vKmpoJM4oDGFt6vyOOnlzX/zTFizIwB7kZ96UMD7LDEn+zkDZQ iy9KbuLLYhWj/cHqKoOtLCQnSPoJ1he6X0xeQPgAIqm22ffJROW9f8q08KPY32L3C1DS iyeg== X-Gm-Message-State: AOJu0Yz9AVDt/okw1yviKwDgI0mN4hSnAvZ2okb8PtxXp2Lus3ZIuMGg ScsbOkkDUdFFQ12+dYqMx/GpmlOrDBOyMRjXij6FgyMtS9e2aw/Cg7HYaPGKuIx7U6JWXnMllUI 7a9Rq X-Gm-Gg: Acq92OH3wRAdAuK7AhXCC9Xk2SbEfX4Md1BE8cP6SErH50ebQyDrRKguFOI9Ow/91+O Olm9VZ0ni3AezF9b9Tcd3ynb1BgOS74ZSQ0L/qoOIr6BNk7jnzjUvCs9cpT8M6/zpirF4QrbqVn jdjdFOmPQ6fUm6NvjzOwwEpqx7Ewf+n5tXaBgAwmVXwrp+LJVKyoSBjWGnSR3yDSC5uDL7Cgky4 yCsf7UZYu18CmR7+qldv28QNhbGJYqf/04JRiqrkVUYgaVTsbsEZu3y3/WTnR5oplRdm8/SoPmO IfisPj4ddg/mdGCEUYbtXt9Jrj+sIzGbnR//3hIzNutEBxzOzS7+EbnxGp70Ai3uLHfTeNtOo45 lAZRazzo5KdmBSG2wTc6sbDgSGn6J06BiEYdQvkN3q5TjMoJW0dY7lUH/eS3Bks0Fb47iK1uohR CZM9SjFIiXimfVLcqv5E/rCUn8CLOXrS+6z/9e35xsWfn2Sg== X-Received: by 2002:a05:693c:3004:b0:2f1:fbbb:e321 with SMTP id 5a478bee46e88-30116e97909mr3198986eec.6.1778700024842; Wed, 13 May 2026 12:20:24 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c1:d905:1e50:98bf:e0d4:d27e]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-30294adddaasm117468eec.13.2026.05.13.12.20.23 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 12:20:24 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: "H . J . Lu" , Florian Weimer Subject: [PATCH 3/6] elf: Replace alloca/VLA with dl_scratch_buffer in dl-load.c Date: Wed, 13 May 2026 16:18:21 -0300 Message-ID: <20260513192013.2511422-4-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> References: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> MIME-Version: 1.0 X-Spam-Status: No, score=-12.3 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, PROLO_LEO1, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_NONE, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org is_trusted_path_normalize, print_search_path, and open_path used alloca or a VLA to hold a path scratch buffer sized by user-controlled inputs (an RPATH directory length, or max_dirnamelen + max_capstrlen + namelen). On the worst case that consumes up to PATH_MAX bytes of stack per call, which can overflow a PTHREAD_STACK_MIN-sized stack mid-dlopen when combined with the loader's other on-stack scratch (struct filebuf, etc.). Replace those allocations with dl_scratch_buffer. As a small cleanup, print_search_path now takes the scratch buffer from its caller (open_path's buffer is already large enough -- max_dirnamelen + max_capstrlen + namelen with namelen >= 1 covers the max_dirnamelen + max_capstrlen + 1 print_search_path requires), so LD_DEBUG=libs no longer pays for an extra allocation per open_path invocation. A new test elf/tst-dl-path-buf exercises the relevant paths -- dlopen via DT_RPATH, open_path failure cleanup, dlopen with an over-long name, dlopen from a PTHREAD_STACK_MIN thread. Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu. --- elf/Makefile | 13 +++ elf/dl-load.c | 43 ++++++-- elf/tst-dl-path-buf-mod.c | 23 ++++ elf/tst-dl-path-buf.c | 214 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 11 deletions(-) create mode 100644 elf/tst-dl-path-buf-mod.c create mode 100644 elf/tst-dl-path-buf.c diff --git a/elf/Makefile b/elf/Makefile index 67bcd7f072d..66502a92ae8 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -410,6 +410,7 @@ tests += \ tst-debug1 \ tst-deep1 \ tst-dl-is_dso \ + tst-dl-path-buf \ tst-dlclose-lazy \ tst-dlmodcount \ tst-dlmopen-dlerror \ @@ -703,6 +704,7 @@ ifeq (yes,$(build-shared)) ifneq ($(PERL),no) tests-special += \ $(objpfx)noload-mem.out \ + $(objpfx)tst-dl-path-buf-mem.out \ $(objpfx)tst-leaks1-mem.out \ # tests-special endif @@ -912,6 +914,7 @@ modules-names += \ tst-deep1mod1 \ tst-deep1mod2 \ tst-deep1mod3 \ + tst-dl-path-buf-mod \ tst-dl_find_object-mod1 \ tst-dl_find_object-mod2 \ tst-dl_find_object-mod3 \ @@ -2271,6 +2274,16 @@ CFLAGS-tst-dlopenrpath.c += -DPFX=\"$(objpfx)\" LDFLAGS-tst-dlopenrpathmod.so += -Wl,-rpath,\$$ORIGIN/test-subdir $(objpfx)tst-dlopenrpath.out: $(objpfx)firstobj.so +$(objpfx)tst-dl-path-buf: $(objpfx)tst-dl-path-buf-mod.so $(shared-thread-library) +LDFLAGS-tst-dl-path-buf += -Wl,-rpath,\$$ORIGIN/tst-dl-path-buf-subdir +tst-dl-path-buf-TUNABLES = glibc.mem.decorate_maps=1 +tst-dl-path-buf-ENV = MALLOC_TRACE=$(objpfx)tst-dl-path-buf.mtrace \ + LD_PRELOAD=$(common-objpfx)/malloc/libc_malloc_debug.so + +$(objpfx)tst-dl-path-buf-mem.out: $(objpfx)tst-dl-path-buf.out + $(common-objpfx)malloc/mtrace $(objpfx)tst-dl-path-buf.mtrace > $@; \ + $(evaluate-test) + $(objpfx)tst-deep1mod2.so: $(objpfx)tst-deep1mod3.so $(objpfx)tst-deep1: $(objpfx)tst-deep1mod1.so $(objpfx)tst-deep1.out: $(objpfx)tst-deep1mod2.so diff --git a/elf/dl-load.c b/elf/dl-load.c index d20e49d526a..fc5d9961ef4 100644 --- a/elf/dl-load.c +++ b/elf/dl-load.c @@ -33,6 +33,7 @@ #include #include #include +#include #include "dynamic-link.h" #include "get-dynamic-info.h" @@ -96,7 +97,9 @@ is_trusted_path_normalize (const char *path, size_t len) if (len == 0) return false; - char *npath = (char *) alloca (len + 2); + struct dl_scratch_buffer scratch = dl_scratch_buffer_init (); + dl_scratch_buffer_allocate (&scratch, len + 2, 0); + char *npath = scratch.data; char *wnp = npath; while (*path != '\0') { @@ -131,19 +134,24 @@ is_trusted_path_normalize (const char *path, size_t len) if (wnp == npath || wnp[-1] != '/') *wnp++ = '/'; + bool result = false; const char *trun = system_dirs; for (size_t idx = 0; idx < nsystem_dirs_len; ++idx) { if (wnp - npath >= system_dirs_len[idx] && memcmp (trun, npath, system_dirs_len[idx]) == 0) - /* Found it. */ - return true; + { + /* Found it. */ + result = true; + break; + } trun += system_dirs_len[idx] + 1; } - return false; + dl_scratch_buffer_free (&scratch); + return result; } /* Given a substring starting at INPUT, just after the DST '$' start @@ -1470,12 +1478,17 @@ cannot enable executable stack as shared object requires"); return l; } -/* Print search path. */ +/* Print search path. BUF is a scratch buffer provided by the caller; + it must be large enough to hold the longest "" plus + a trailing NUL byte -- i.e. at least + max_dirnamelen + max_capstrlen + 1 bytes. open_path's path buffer + (max_dirnamelen + max_capstrlen + namelen, namelen >= 1) is reused + here so that enabling LD_DEBUG=libs does not require an extra mmap + per call. */ static void print_search_path (struct r_search_path_elem **list, - const char *what, const char *name) + const char *what, const char *name, char *buf) { - char buf[max_dirnamelen + max_capstrlen]; int first = 1; _dl_debug_printf (" search path="); @@ -1725,7 +1738,6 @@ open_path (const char *name, size_t namelen, int mode, bool *found_other_class) { struct r_search_path_elem **dirs = sps->dirs; - char *buf; int fd = -1; const char *current_what = NULL; int any = 0; @@ -1735,7 +1747,10 @@ open_path (const char *name, size_t namelen, int mode, given on the command line when rtld is run directly. */ return -1; - buf = alloca (max_dirnamelen + max_capstrlen + namelen); + size_t bufsize = max_dirnamelen + max_capstrlen + namelen; + struct dl_scratch_buffer scratch = dl_scratch_buffer_init (); + dl_scratch_buffer_allocate (&scratch, bufsize, 0); + char *buf = scratch.data; do { struct r_search_path_elem *this_dir = *dirs; @@ -1750,7 +1765,7 @@ open_path (const char *name, size_t namelen, int mode, && current_what != this_dir->what) { current_what = this_dir->what; - print_search_path (dirs, current_what, this_dir->where); + print_search_path (dirs, current_what, this_dir->where, buf); } edp = (char *) __mempcpy (buf, this_dir->dirname, this_dir->dirnamelen); @@ -1838,6 +1853,7 @@ open_path (const char *name, size_t namelen, int mode, if (*realname != NULL) { memcpy (*realname, buf, buflen); + dl_scratch_buffer_free (&scratch); return fd; } else @@ -1845,6 +1861,7 @@ open_path (const char *name, size_t namelen, int mode, /* No memory for the name, we certainly won't be able to load and link it. */ __close_nocancel (fd); + dl_scratch_buffer_free (&scratch); return -1; } } @@ -1854,7 +1871,10 @@ open_path (const char *name, size_t namelen, int mode, directory (for instance, if the component is a existing file meaning essentially that the pathname is invalid - ENOTDIR). */ if (here_any && errno != ENOENT && errno != EACCES && errno != ENOTDIR) - return -1; + { + dl_scratch_buffer_free (&scratch); + return -1; + } /* Remember whether we found anything. */ any |= here_any; @@ -1875,6 +1895,7 @@ open_path (const char *name, size_t namelen, int mode, sps->dirs = (void *) -1; } + dl_scratch_buffer_free (&scratch); return -1; } diff --git a/elf/tst-dl-path-buf-mod.c b/elf/tst-dl-path-buf-mod.c new file mode 100644 index 00000000000..5642ea985a8 --- /dev/null +++ b/elf/tst-dl-path-buf-mod.c @@ -0,0 +1,23 @@ +/* Trivial DSO used by tst-dl-path-buf. + 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 + . */ + +unsigned int +tst_dl_path_buf_mod_value (void) +{ + return 0xaabbccddu; +} diff --git a/elf/tst-dl-path-buf.c b/elf/tst-dl-path-buf.c new file mode 100644 index 00000000000..29ef8e757c1 --- /dev/null +++ b/elf/tst-dl-path-buf.c @@ -0,0 +1,214 @@ +/* Exercise the mmap-backed path scratch buffer in elf/dl-load.c. + 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 + . */ + +/* open_path()'s scratch buffer comes from dl_scratch_buffer, which uses + anonymous mmap while __minimal_malloc is active (during loader startup) + and libc malloc afterwards. Mappings from the mmap backend are tagged + with the VMA name " glibc: loader scratch". This test exercises the + relevant code paths -- search via DT_RPATH, open_path failure cleanup, + dlopen with an over-long name, dlopen from a minimal-stack thread, + and per-backend leak checks -- to verify each path properly + alloc/frees the scratch buffer. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* Must match LDFLAGS-tst-dl-path-buf in elf/Makefile. The test binary's + DT_RPATH resolves to $ORIGIN of the binary plus this subdirectory. */ +#define MOD_SUBDIR "tst-dl-path-buf-subdir" +#define MOD_NAME "tst-dl-path-buf-mod.so" + +/* Tag installed by _dl_scratch_buffer_allocate via __set_vma_name. Mappings + in /proc/self/maps annotated with this string belong to a live scratch + buffer; after a successful dlopen/dlclose cycle there must be zero of + them. */ +#define SCRATCH_VMA_TAG "[anon: glibc: loader scratch]" + +/* Open MOD_NAME via DT_RPATH. Returns the handle; the caller closes it. */ +static void +dlopen_module (void) +{ + void *h = xdlopen (MOD_NAME, RTLD_NOW | RTLD_LOCAL); + unsigned int (*fn) (void) = xdlsym (h, "tst_dl_path_buf_mod_value"); + TEST_COMPARE (fn (), 0xaabbccddu); + xdlclose (h); +} + +/* Subtest 1: basic dlopen/dlclose via DT_RPATH search. */ +static void +test_basic (void) +{ + dlopen_module (); +} + +/* Subtest 2: non-existent name: open_path is exercised on every search list + (DT_RPATH then cache then __rtld_search_dirs), failing each time. Each + failure path must release its scratch buffer. */ +static void +test_nonexistent (void) +{ + void *h = dlopen ("tst-dl-path-buf-does-not-exist.so", + RTLD_NOW | RTLD_LOCAL); + TEST_VERIFY (h == NULL); +} + +/* Subtest 3: a name whose resolved length far exceeds PATH_MAX cannot refer + to a real file: open_path will allocate a large scratch buffer, build + candidate paths, and have every open() return ENAMETOOLONG. dlopen must + therefore fail cleanly (and without leaking the scratch buffer on the + failure paths). */ +static void +test_overlong_name (void) +{ + char *huge = xmalloc (PATH_MAX + 64); + memset (huge, 'a', PATH_MAX + 32); + memcpy (huge + PATH_MAX, ".so", 4); + + void *h = dlopen (huge, RTLD_NOW | RTLD_LOCAL); + TEST_VERIFY (h == NULL); + + free (huge); +} + +/* Count anonymous mappings in /proc/self/maps annotated with SCRATCH_VMA_TAG. + Used by the mmap-backend leak subtest below. */ +static unsigned int +count_scratch_mappings (void) +{ + FILE *f = xfopen ("/proc/self/maps", "r"); + unsigned int n = 0; + char *line = NULL; + size_t line_len = 0; + while (xgetline (&line, &line_len, f)) + if (strstr (line, SCRATCH_VMA_TAG) != NULL) + ++n; + free (line); + xfclose (f); + return n; +} + +/* Subtest 4a (mmap backend). dl_scratch_buffer's mmap backend is used while + __rtld_malloc_is_complete returns false -- that window covers the entire + loader-startup phase, during which the loader resolves the test binary's + DT_NEEDED dependencies via open_path() (and so allocates and frees scratch + buffers). */ +static void +test_no_leak_mmap (void) +{ + if (!support_set_vma_name_supported ()) + { + printf ("info: skipping mmap-backend leak subtest:" + " kernel does not support PR_SET_VMA_ANON_NAME\n"); + return; + } + + unsigned int residual = count_scratch_mappings (); + if (residual != 0) + FAIL_EXIT1 ("%u leaked loader scratch mapping(s) survived loader" + " startup -- _dl_scratch_buffer_free's mmap backend" + " is broken", residual); +} + +/* Subtest 4b (malloc backend). Once libc malloc is active, + dl_scratch_buffer_allocate routes through malloc and the mapping VMA tag is + no longer used. Drive enough dlopen success+failure cycles to exercise + every path in dl_scratch_buffer_free. */ +static void +test_no_leak_malloc (void) +{ + mtrace (); + + enum { iterations = 10 }; + for (unsigned int i = 0; i < iterations; ++i) + { + dlopen_module (); + void *nh = dlopen ("tst-dl-path-buf-does-not-exist.so", + RTLD_NOW | RTLD_LOCAL); + TEST_VERIFY (nh == NULL); + } +} + +/* Subtest 5. Run the success path from a PTHREAD_STACK_MIN thread. */ +static void * +minstack_thread (void *closure) +{ + dlopen_module (); + void *nh = dlopen ("tst-dl-path-buf-does-not-exist.so", + RTLD_NOW | RTLD_LOCAL); + TEST_VERIFY (nh == NULL); + return NULL; +} + +static void +test_minstack (void) +{ + pthread_attr_t attr; + xpthread_attr_init (&attr); + xpthread_attr_setstacksize (&attr, PTHREAD_STACK_MIN); + pthread_t thr = xpthread_create (&attr, minstack_thread, NULL); + xpthread_join (thr); + xpthread_attr_destroy (&attr); +} + +static int +do_test (void) +{ + support_need_proc ("/proc/self/maps is read for the leak subtest."); + + char *subdir = xasprintf ("%s/elf/" MOD_SUBDIR, support_objdir_root); + xmkdirp (subdir, 0777); + add_temp_file (subdir); + + char *src = xasprintf ("%s/elf/" MOD_NAME, support_objdir_root); + char *dst = xasprintf ("%s/" MOD_NAME, subdir); + support_copy_file (src, dst); + add_temp_file (dst); + + /* Check the mmap backend's startup behavior first, before any + subtest can perturb /proc/self/maps with its own allocations. */ + test_no_leak_mmap (); + + test_basic (); + test_nonexistent (); + test_overlong_name (); + test_no_leak_malloc (); + test_minstack (); + + free (src); + free (dst); + free (subdir); + return 0; +} + +#include From patchwork Wed May 13 19:18:22 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Adhemerval Zanella Netto X-Patchwork-Id: 134937 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 9421C4BBC0B4 for ; Wed, 13 May 2026 19:24:10 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 9421C4BBC0B4 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=YCVEHNJy X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-dy1-x1335.google.com (mail-dy1-x1335.google.com [IPv6:2607:f8b0:4864:20::1335]) by sourceware.org (Postfix) with ESMTPS id A72104BBC0BA for ; Wed, 13 May 2026 19:20:28 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org A72104BBC0BA Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org A72104BBC0BA Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1335 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700028; cv=none; b=MRpdfp4UWsDImiRCWg7vRuwxm6XH9p/yaduPTQ6ShrxE3JF/3lXJ8HyhTB+xcqPFBz4Rz5swps94LYiu9FMMa5w5HTD5bjNH2hLihpeGGD6TdLiW0EqKN1E7YLMy/NcqkQk5ZpjQXoPkCv4PvCg7YYNuLJroxl0KN+3l1/s3kcA= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700028; c=relaxed/simple; bh=utrVeH60YvCPkiUn4RSoCluF5yIBwhYxXk6//a6mT+Y=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=n0UlfMKHoKjo+95TqLYAPFJDLxgMyN72ADMzqozhCA5WI8NwXsCQALHBXTpD9nNEosbpvtrJ8sfShMqIOkkeNub6Tpgn9/EV7B9kBYkmhsrsHxLEuLF1FNg/xU23svFpPIOog+619YZMrT4cpXsqJHjwpDPNnp1KBKuQbxB5j1M= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=YCVEHNJy DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org A72104BBC0BA Received: by mail-dy1-x1335.google.com with SMTP id 5a478bee46e88-3025d725a05so506526eec.1 for ; Wed, 13 May 2026 12:20:28 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1778700027; x=1779304827; darn=sourceware.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=NrJiofXS4dXSn5FXZFONvUkQDCT1LGzxE0Wrb81eI48=; b=YCVEHNJyv91PIK1TX/3Sj7AjUhf1v2rE/w9EJHM7nM68sDm4ft7DrsvvqO4NqlyOPu aVC2AxyHPxiwwFw2czckfJoRIMTo9Bxx6jf48CRQzqojgJlaXP9rbaZYkvU64Gv5rkCh y5fQs7N/K5krpDZ4JnjlUI6s///hiKsjAn6//4Phh13TVRypm4pW2yDVffzCE5NdDhI1 SrpAYBAUqBzaE8GY3GIbuYiEwleS6wZPmoqVjO4PfQfhGT95lBSFgO/M0v5MM+NWkDB2 u1E/igXgEvO0ISMlXy42PEQZBFSptxK89KnH7TGbYHNGC2ra/ZSZME8JQC2UtR6Jfygf ymaw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778700027; x=1779304827; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=NrJiofXS4dXSn5FXZFONvUkQDCT1LGzxE0Wrb81eI48=; b=dqjFpFnemOEmZFgpFAAp3c4aWgJqwfsoVk4WdZa4yDONqfO30wiDokXv47AQVKzN8o YjKOy32+m6HTmGZrIOeSGHp0NwSCofNxcOfa2uisK+b7AJkT3bId8GnBvR2MDb2wCZD8 R3OCo2lQO6/rPQiK4LPm8mC1Avat04I1ZkG4eHk4gZONTQKSLuUnDIwbKUZbdtzUtzeq H9DWfZEhJ+v+olskKLiq0p26J7Zg0nnErVMR9Cdz+dY3SJfWwwtsjGb+9q+HS5kn4InT 25JLn1c63huB/gvZnEK7W47cQAe2H5mTg8t4RGYbIK9McDbs3FB+3iiSZ1zxem55pRoL NJ9Q== X-Gm-Message-State: AOJu0Yy6SpzuvvARGgMVC9nfaKuPBXu0N9/7E85nTvOWbRr/jv6ex4nc /7vNah5coiapm7j/XzO65mhXEQUspRwfHZi97ThXM7kdNTbyUIlPOEEJMIY0CWonmVdtCaRS67e ryeUB X-Gm-Gg: Acq92OHpMFGQ94gZx/yZ1xl2dfw/byBp9JEI5Qj1JIVjOQeuM/AF+wjj8jCUhS+KFJ4 CzVZk9HU2I6DZBaOMS8uxVbF3gWzTxbpVUUO3AOiyS63Syh/35h/wjLrRNaFSrLXW8xoMZhy+QY KHLjlOx5AmZ5q0u6AkFgQqwo0ScY4UTJs1fyjx+s3w5K+Y3UuSNdeO2hUKV+LisIBAQ0Ju9BeNM qHT4IWPMKWxHVhRejZ0cdCxIar69g+kDAylF16IQksUQyBJezksBh0NkRHmQMg9Wkqp0s0BBnOD LIfM+QV1p8BmWkUz9Oca6lafXvzUVv00vxn8Cu4h9GbVO1Sku5hRHL9BQZvkIW0qAFjHR1EPdla hdHbO4KBt6uiWmgG5cxw0sNbb9fRqIMHEn26icmHAVMWOhWGPqQsI4vKR288EiVaTVvVKbxaTmD shuAPtuyZO9qejNh/WvRZO8/1zH0uI+AtmM6WQ+nnGXWs0TA== X-Received: by 2002:a05:7301:1690:b0:2d8:7302:d21 with SMTP id 5a478bee46e88-3011a16dd66mr3028684eec.16.1778700026567; Wed, 13 May 2026 12:20:26 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c1:d905:1e50:98bf:e0d4:d27e]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-30294adddaasm117468eec.13.2026.05.13.12.20.25 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 12:20:26 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: "H . J . Lu" , Florian Weimer Subject: [PATCH 4/6] elf: Replace alloca with dl_scratch_buffer in _dl_load_cache_lookup Date: Wed, 13 May 2026 16:18:22 -0300 Message-ID: <20260513192013.2511422-5-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> References: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_NONE, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org The alloca added by commit ccdb048d ("Fix recursive dlopen") to snapshot the matched cache entry before __strdup runs through interposable malloc is sized by best_len, which can reach PATH_MAX. On PTHREAD_STACK_MIN threads that's enough to overflow the stack mid-dlopen. Use dl_scratch_buffer with DL_SCRATCH_NO_MALLOC: short entries stay in the 256-byte inline area, longer ones spill to anonymous mmap rather than to interposable malloc. The recursive-dlopen invariant is preserved. New container test elf/tst-dl-cache-long-path constructs a ~3.4 KB deep directory, populates ld.so.cache with that entry, and dlopens from a PTHREAD_STACK_MIN thread under deliberate stack pressure; reliably SIGSEGVs against the alloca-based code and passes with the fix. Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu. --- elf/Makefile | 5 + elf/dl-cache.c | 14 ++- elf/tst-dl-cache-long-path.c | 173 +++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 elf/tst-dl-cache-long-path.c diff --git a/elf/Makefile b/elf/Makefile index 66502a92ae8..239d146566d 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -293,6 +293,7 @@ CRT-tst-tls1-static-non-pie := $(csu-objpfx)crt1.o tst-tls1-static-non-pie-no-pie = yes tests-container := \ + tst-dl-cache-long-path \ tst-ldconfig-bad-aux-cache \ tst-ldconfig-ld_so_conf-update \ # tests-container @@ -2866,6 +2867,10 @@ LDFLAGS-tst-dlopen-nodelete-reloc-mod17.so = -Wl,--no-as-needed $(objpfx)tst-ldconfig-ld_so_conf-update.out: $(objpfx)tst-ldconfig-ld-mod.so +# Reuses the trivial module already built for tst-dl-path-buf. +$(objpfx)tst-dl-cache-long-path: $(shared-thread-library) +$(objpfx)tst-dl-cache-long-path.out: $(objpfx)tst-dl-path-buf-mod.so + LDFLAGS-tst-filterobj-flt.so = -Wl,--filter=$(objpfx)tst-filterobj-filtee.so $(objpfx)tst-filterobj: $(objpfx)tst-filterobj-flt.so $(objpfx)tst-filterobj.out: $(objpfx)tst-filterobj-filtee.so diff --git a/elf/dl-cache.c b/elf/dl-cache.c index 9458ffae2a6..c1de93f2041 100644 --- a/elf/dl-cache.c +++ b/elf/dl-cache.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include <_itoa.h> #include @@ -490,12 +491,15 @@ _dl_load_cache_lookup (const char *name) /* The double copy is *required* since malloc may be interposed and call dlopen itself whose completion would unmap the data we are accessing. Therefore we must make the copy of the - mapping data without using malloc. */ - char *temp; + mapping data without using malloc. The DL_SCRATCH_NO_MALLOC + forces any spill to anonymous mmap rather than the malloc. */ + struct dl_scratch_buffer scratch = dl_scratch_buffer_init (); size_t best_len = strlen (best) + 1; - temp = alloca (best_len); - memcpy (temp, best, best_len); - return __strdup (temp); + dl_scratch_buffer_allocate (&scratch, best_len, DL_SCRATCH_NO_MALLOC); + memcpy (scratch.data, best, best_len); + char *result = __strdup (scratch.data); + dl_scratch_buffer_free (&scratch); + return result; } #ifndef MAP_COPY diff --git a/elf/tst-dl-cache-long-path.c b/elf/tst-dl-cache-long-path.c new file mode 100644 index 00000000000..34d8ebabe25 --- /dev/null +++ b/elf/tst-dl-cache-long-path.c @@ -0,0 +1,173 @@ +/* Test dlopen through ld.so.cache with a cache entry longer than the + dl_scratch_buffer inline area. + 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 + . */ + +/* This test populates the cache with a single library sitting in a directory + whose absolute path is far larger than the dl_scratch_buffer inline area + (256 bytes) and most of the way to PATH_MAX; the loader's cache lookup + therefore exercises the anonymous-mmap spill. The dlopen is also repeated + from a PTHREAD_STACK_MIN thread to demonstrate that the path no longer + consumes the too much caller's stack. */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +/* ldconfig only indexes filenames starting with "lib", so the module is + deployed under the lib-prefixed name (MOD_DEPLOYED) in the deep directory + and dlopened by that name. */ +#define MOD_BUILT "tst-dl-path-buf-mod.so" +#define MOD_DEPLOYED "libtst-dl-path-buf-mod.so" +#define MOD_SYMBOL "tst_dl_path_buf_mod_value" +#define MOD_EXPECTED 0xaabbccddu + +/* Final absolute path of the deep directory holding the module; filled in by + setup (). Kept around so dlopen_module can sanity-print it on failure. */ +static char *deep_dir; + +static void +run_ldconfig (void *x) +{ + char *prog = xasprintf ("%s/ldconfig", support_install_rootsbindir); + char *args[] = { prog, NULL }; + execv (args[0], args); + FAIL_EXIT1 ("execv (%s): %m", prog); +} + +/* Build /tst-dl-cache-long-path/d.../d.../d... with several long components, + totalling well past dl_scratch_buffer's inline area + (DL_SCRATCH_BUFFER_INLINE_SIZE = 256 bytes) and close to PATH_MAX. */ +static char * +build_deep_directory (void) +{ + enum { component_len = 250, components = 15 }; + /* 14 * (1 + 240) = 3374 bytes of nesting, plus the base. */ + char component[component_len + 1]; + memset (component, 'd', component_len); + component[component_len] = '\0'; + + const char *base = "/tst-dl-cache-long-path"; + size_t cap = strlen (base) + components * (1 + component_len) + 1; + char *path = xmalloc (cap); + strcpy (path, base); + xmkdirp (path, 0777); + add_temp_file (path); + + for (int i = 0; i < components; ++i) + { + strcat (path, "/"); + strcat (path, component); + xmkdirp (path, 0777); + add_temp_file (path); + } + return path; +} + +static void +do_prepare (int argc, char **argv) +{ + deep_dir = build_deep_directory (); + TEST_VERIFY (strlen (deep_dir) > 256); + + char *src = xasprintf ("%s/elf/" MOD_BUILT, support_objdir_root); + char *dst = xasprintf ("%s/" MOD_DEPLOYED, deep_dir); + support_copy_file (src, dst); + add_temp_file (dst); + free (src); + free (dst); + + char *conf = xasprintf ("%s/ld.so.conf", support_sysconfdir_prefix); + FILE *fp = xfopen (conf, "w"); + fprintf (fp, "%s\n", deep_dir); + xfclose (fp); + free (conf); + + xmkdirp ("/var/cache/ldconfig", 0777); + struct support_capture_subprocess r + = support_capture_subprocess (run_ldconfig, NULL); + support_capture_subprocess_check (&r, "ldconfig", 0, sc_allow_none); + support_capture_subprocess_free (&r); +} +#define PREPARE do_prepare + +static void +__attribute_noinline__ +dlopen_via_cache (volatile char *pressure) +{ + if (pressure != NULL) + (void) *pressure; + + void *h = xdlopen (MOD_DEPLOYED, RTLD_NOW | RTLD_LOCAL); + unsigned int (*fn) (void) = xdlsym (h, MOD_SYMBOL); + TEST_COMPARE (fn (), MOD_EXPECTED); + xdlclose (h); +} + +/* Reduce the stack budget available to the dlopen call chain by + STACK_PRESSURE bytes. */ +enum { STACK_PRESSURE = 5 * 1024 }; + +static void +__attribute_noinline__ +dlopen_via_cache_under_pressure (void) +{ + char filler[STACK_PRESSURE]; + dlopen_via_cache (&filler[0]); +} + +static void * +minstack_thread (void *closure) +{ + dlopen_via_cache_under_pressure (); + return NULL; +} + +static int +do_test (void) +{ + /* Sanity: from the main thread (no stack pressure needed). */ + dlopen_via_cache (NULL); + + /* The motivating scenario: from a PTHREAD_STACK_MIN thread. Before + _dl_load_cache_lookup was converted to dl_scratch_buffer this would + have alloca'd ~3 KB mid-dlopen and risked overflowing. */ + pthread_attr_t attr; + xpthread_attr_init (&attr); + xpthread_attr_setstacksize (&attr, PTHREAD_STACK_MIN); + pthread_t thr = xpthread_create (&attr, minstack_thread, NULL); + xpthread_join (thr); + xpthread_attr_destroy (&attr); + + free (deep_dir); + return 0; +} + +#include From patchwork Wed May 13 19:18:23 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Adhemerval Zanella Netto X-Patchwork-Id: 134934 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 0F34E4BBC0BF for ; Wed, 13 May 2026 19:21:36 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 0F34E4BBC0BF Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=EqakrhYk X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-dy1-x1330.google.com (mail-dy1-x1330.google.com [IPv6:2607:f8b0:4864:20::1330]) by sourceware.org (Postfix) with ESMTPS id D7A774BBC0B9 for ; Wed, 13 May 2026 19:20:29 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org D7A774BBC0B9 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org D7A774BBC0B9 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1330 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700030; cv=none; b=AdRTyg226kNH+bpWQh/wdwRLsUR3Iap2IuVOAkZLLvomXZyjkXdv7j6gKisKG4jsz2ozLiwmPLK27KVWA0yoih8udAobFlshMh8eG0GeHeMdFudGiu1PccIwd8F8jd3s03jJBxSr85bfcdcLnHsDmqDCl5/G8ySDoOIVqOU51/Q= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700030; c=relaxed/simple; bh=M17PiH4vCBYzbnNyfVOYWymOg01GmS6Yq1tseiy2FwQ=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=bHTWMah2B2V09PluLV63HOnebzrhcLdeRtNNq986FzGaqubfCsmshA09jkFgS586YLQqxeuG9nvX4SxvFeSgXOSMcqglF9IdQ7iUyzna32UdHrE5ft1rPEDkJm4NkQ7HKDHGFa2MNAuAwzXOSRU+2KE+7DKxgAjtZcNwlOwa65w= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=EqakrhYk DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org D7A774BBC0B9 Received: by mail-dy1-x1330.google.com with SMTP id 5a478bee46e88-2ff5472f263so3419784eec.1 for ; Wed, 13 May 2026 12:20:29 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1778700029; x=1779304829; darn=sourceware.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=AE80fjQ6fHH7D9cuqvj7RscJCetF2OuLw4hGvf58CEE=; b=EqakrhYkGj4FSuxg4DWFd6Z/jQQvk0s7HAx3f4AfX1aNACgY1B+4pRV+QUpG5naAf0 EerWMCrLsVdaAw+M5aIcubeLFQBGl41tLDAlMoEPtnxG0lyNSSLXRSj3UJNNIzKFGh55 zMKwAXEY2viibL5l7plJXXfnMKtoZGnsUCupOCbwlKG1Xa5XcfXsNWsGxcpHVvqn7GI/ 0xRhfsHzL9qXmjtIf/kV5pQxa9ShLFTUJKhe2IboJIGQ2mAxQDtCHOP2RrY2HMt2Igno PWCo2dLkxYW48GPeQ+PjAvQZ5J7V3/EGSkPvLGkq93fuQP0W5z3l4uIUuL1aHQ+DFWPU HUvw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778700029; x=1779304829; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=AE80fjQ6fHH7D9cuqvj7RscJCetF2OuLw4hGvf58CEE=; b=Lp9A04JL9/MkpCjPRqbXc5LYv5Zbu2HGkFOGFeomS2FoKUINhvYF5C2upEaL3hCCdj +WfwGvbAcX7JB6/AOj0aDh5Dx71XUwra/DLPGbUXS89dgCy3c+vA/bxcvWYgVeQUBunI uzvDLscd1NSSYFdEZaS+cpqcqpUYZJyrqs2tmHFqZYf9QsY3BrvxOdZq+tVyOEQI5h61 RV1qTTfF7JznRbJ5IbZXw8hG7gf7/0w+KieeXuqIfR+O1ZFsnZ4nHVAoLPIrC8TRNcwA H0Ui8LjvE9tJK6MQRSeLP7uTPuCKZsWk+ucnJx4rXiautJ2VV6dnYf+yQRcckM7E7rh+ VWvQ== X-Gm-Message-State: AOJu0YyVPeZ3MgAYKNohPWBNQl8QwDj+CFWBBqcSwr0GaDzCxIzWvUOW SSArTsjvfRaZc4w2xFB2LBAoGUnG8NM1hJDEuf6KvViuFgn14K+dxHENHUXD2j/uqIl+jUaAsvo gzKu6 X-Gm-Gg: Acq92OFr4T2DNlYcLMBJVJ+umFOnC+pZVHgLuvNKjIkYQ4wYISX+WM8G3YGgGIFd+cU Ml4QoJ00Ytvt1eLFBsGawxEpIW+JAQhH6QEmbTT38Pkg9sWekHZmVgteJsx31rnGJaIupZpS8uM wvkv+2yvPPR0MghVRvW1iLrxH63Z/BgzYEbZpqAlHRqMvCVatmlUeIn3TIt7/Zhh1ld0l1SKBe6 kaIu+0FiMZ+KActsHmCOBEWOj7OkPZjpPPkBdIR6C/xZHAIXlaifK9B9WVh3CC/VDVMK6JDoFM9 yBFqXXp+r/dv9OrWBX+t60TDIhqJqlkiZg6mwboPvXqJAgMp+3P4Rr3XlXv/e2mPPe21g/xt2Aw dCbE4FXL+QjQOxoscGp83QZUk9BFejs3wzXpURM+FPJsaysD5itMbBf/59RmCsg1Oe5mvg9WndP oENO44FUmAzNeyZfX2y13+5uzfjSaPluDrv/g/k10QQ84EuQ== X-Received: by 2002:a05:7300:572a:b0:2e6:fe90:27ad with SMTP id 5a478bee46e88-30116e95830mr2719785eec.7.1778700028525; Wed, 13 May 2026 12:20:28 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c1:d905:1e50:98bf:e0d4:d27e]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-30294adddaasm117468eec.13.2026.05.13.12.20.26 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 12:20:27 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: "H . J . Lu" , Florian Weimer Subject: [PATCH 5/6] elf: Use dl_scratch_buffer for DST expansion in _dl_map_object_deps Date: Wed, 13 May 2026 16:18:23 -0300 Message-ID: <20260513192013.2511422-6-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> References: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, PROLO_LEO1, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_NONE, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org The expand_dst macro in _dl_map_object_deps performs an unbounded alloca via DL_DST_REQUIRED, which scales with the link map's l_origin length plus the count of dynamic-string tokens in the input string. When a DT_NEEDED entry carries several DSTs and the link map sits in a deep directory, the resulting allocation grows to several kilobytes -- enough to overflow a PTHREAD_STACK_MIN thread that calls dlopen. Convert the macro to a static function that draws from a caller- owned dl_scratch_buffer, so oversized expansions land on the heap (or anonymous mmap during early startup) instead of the stack. The scratch buffer is reused across DT_NEEDED, DT_AUXILIARY, and DT_FILTER entries of the same map and freed once dependency expansion completes. A new regression test, tst-dst-needed-minstack, builds a wrapper library that inherits a five-DST SONAME from a leaf module, deploys it under a deep temporary directory, and dlopens it from a PTHREAD_STACK_MIN thread. Without the fix the dlopen overflows the thread stack and crashes; with the fix the dlopen returns cleanly (with or without a successful load). Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu. --- elf/Makefile | 9 +++ elf/dl-deps.c | 114 +++++++++++++++++++------------- elf/tst-dst-needed-leaf-mod.c | 20 ++++++ elf/tst-dst-needed-minstack.c | 120 ++++++++++++++++++++++++++++++++++ elf/tst-dst-needed-wrap-mod.c | 21 ++++++ 5 files changed, 238 insertions(+), 46 deletions(-) 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 diff --git a/elf/Makefile b/elf/Makefile index 239d146566d..443e29493c2 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -434,6 +434,7 @@ tests += \ tst-dlopenfail-2 \ tst-dlopenrpath \ tst-dlsym-error \ + tst-dst-needed-minstack \ tst-filterobj \ tst-filterobj-dlopen \ tst-glibc-hwcaps \ @@ -946,6 +947,8 @@ modules-names += \ tst-dlopenfailmod3 \ tst-dlopenfailnodelmod \ tst-dlopenrpathmod \ + tst-dst-needed-leaf-mod \ + tst-dst-needed-wrap-mod \ tst-filterobj-aux \ tst-filterobj-filtee \ tst-filterobj-flt \ @@ -2871,6 +2874,12 @@ $(objpfx)tst-ldconfig-ld_so_conf-update.out: $(objpfx)tst-ldconfig-ld-mod.so $(objpfx)tst-dl-cache-long-path: $(shared-thread-library) $(objpfx)tst-dl-cache-long-path.out: $(objpfx)tst-dl-path-buf-mod.so +LDFLAGS-tst-dst-needed-leaf-mod.so = \ + -Wl,-soname,\$$ORIGIN/\$$ORIGIN/\$$ORIGIN/\$$ORIGIN/\$$ORIGIN/leaf.so +$(objpfx)tst-dst-needed-wrap-mod.so: $(objpfx)tst-dst-needed-leaf-mod.so +$(objpfx)tst-dst-needed-minstack: $(shared-thread-library) +$(objpfx)tst-dst-needed-minstack.out: $(objpfx)tst-dst-needed-wrap-mod.so + LDFLAGS-tst-filterobj-flt.so = -Wl,--filter=$(objpfx)tst-filterobj-filtee.so $(objpfx)tst-filterobj: $(objpfx)tst-filterobj-flt.so $(objpfx)tst-filterobj.out: $(objpfx)tst-filterobj-filtee.so diff --git a/elf/dl-deps.c b/elf/dl-deps.c index 4c363180ce7..446163b55c8 100644 --- a/elf/dl-deps.c +++ b/elf/dl-deps.c @@ -30,6 +30,7 @@ #include #include +#include /* Whether an shared object references one or more auxiliary objects is signaled by the AUXTAG entry in l_info. */ @@ -80,47 +81,35 @@ struct list }; -/* Macro to expand DST. It is an macro since we use `alloca'. */ -#define expand_dst(l, str, fatal) \ - ({ \ - const char *__str = (str); \ - const char *__result = __str; \ - size_t __dst_cnt = _dl_dst_count (__str); \ - \ - if (__dst_cnt != 0) \ - { \ - char *__newp; \ - \ - /* DST must not appear in SUID/SGID programs. */ \ - if (__libc_enable_secure) \ - _dl_signal_error (0, __str, NULL, N_("\ -DST not allowed in SUID/SGID programs")); \ - \ - __newp = (char *) alloca (DL_DST_REQUIRED (l, __str, strlen (__str), \ - __dst_cnt)); \ - \ - __result = _dl_dst_substitute (l, __str, __newp); \ - \ - if (*__result == '\0') \ - { \ - /* The replacement for the DST is not known. We can't \ - processed. */ \ - if (fatal) \ - _dl_signal_error (0, __str, NULL, N_("\ -empty dynamic string token substitution")); \ - else \ - { \ - /* This is for DT_AUXILIARY. */ \ - if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS)) \ - _dl_debug_printf (N_("\ -cannot load auxiliary `%s' because of empty dynamic string token " \ - "substitution\n"), __str); \ - continue; \ - } \ - } \ - } \ - \ - __result; }) +/* Expand the dynamic-string-tokens ($ORIGIN / $LIB / $PLATFORM) in INPUT + using L's context. Returns the expanded string -- a pointer either into + SCRATCH->data (when expansion was needed) or back at INPUT (when no DSTs + were present, so no allocation happened). Returns NULL when a DST was + present but could not be resolved. + + SCRATCH must be an init'd dl_scratch_buffer the caller will release once + the returned string is no longer needed. This function never returns when + called for a SUID/SGID that contains DSTs: it raises a loader error. */ +static const char * +expand_dst (struct link_map *l, const char *input, + struct dl_scratch_buffer *scratch) +{ + size_t dst_cnt = _dl_dst_count (input); + if (dst_cnt == 0) + return input; + + /* DST must not appear in SUID/SGID programs. */ + if (__libc_enable_secure) + _dl_signal_error (0, input, NULL, N_("\ +DST not allowed in SUID/SGID programs")); + + size_t total = DL_DST_REQUIRED (l, input, strlen (input), dst_cnt); + dl_scratch_buffer_allocate (scratch, total + 1, 0); + const char *result = _dl_dst_substitute (l, input, scratch->data); + if (*result == '\0') + return NULL; + return result; +} static void preload (struct list *known, unsigned int *nlist, struct link_map *map) @@ -224,12 +213,26 @@ _dl_map_object_deps (struct link_map *map, /* Map in the needed object. */ struct link_map *dep; - /* Recognize DSTs. */ - name = expand_dst (l, strtab + d->d_un.d_val, 0); + /* Recognize DSTs. Empty substitution for DT_NEEDED is + non-fatal: log and skip this entry. */ + struct dl_scratch_buffer scratch + = dl_scratch_buffer_init (); + name = expand_dst (l, strtab + d->d_un.d_val, &scratch); + if (__glibc_unlikely (name == NULL)) + { + if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS)) + _dl_debug_printf (N_("\ +cannot load auxiliary `%s' because of empty dynamic string token " + "substitution\n"), + strtab + d->d_un.d_val); + dl_scratch_buffer_free (&scratch); + continue; + } /* Store the tag in the argument structure. */ args.name = name; int err = _dl_catch_exception (&exception, openaux, &args); + dl_scratch_buffer_free (&scratch); if (__glibc_unlikely (exception.errstring != NULL)) { if (err) @@ -267,9 +270,25 @@ _dl_map_object_deps (struct link_map *map, { struct list *newp; - /* Recognize DSTs. */ - name = expand_dst (l, strtab + d->d_un.d_val, - d->d_tag == DT_AUXILIARY); + /* Recognize DSTs. DT_AUXILIARY is fatal on unresolved + DST; DT_FILTER is non-fatal and is skipped. */ + struct dl_scratch_buffer scratch + = dl_scratch_buffer_init (); + name = expand_dst (l, strtab + d->d_un.d_val, &scratch); + if (__glibc_unlikely (name == NULL)) + { + dl_scratch_buffer_free (&scratch); + if (d->d_tag == DT_AUXILIARY) + _dl_signal_error (0, strtab + d->d_un.d_val, NULL, + N_("empty dynamic string token " + "substitution")); + if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS)) + _dl_debug_printf (N_("\ +cannot load auxiliary `%s' because of empty dynamic string token " + "substitution\n"), + strtab + d->d_un.d_val); + continue; + } /* Store the tag in the argument structure. */ args.name = name; @@ -285,6 +304,9 @@ _dl_map_object_deps (struct link_map *map, object is not available. For filter objects the dependency must be available. */ int err = _dl_catch_exception (&exception, openaux, &args); + /* NAME is consumed by openaux above; release the DST + scratch buffer regardless of outcome. */ + dl_scratch_buffer_free (&scratch); if (__glibc_unlikely (exception.errstring != NULL)) { if (d->d_tag == DT_AUXILIARY) diff --git a/elf/tst-dst-needed-leaf-mod.c b/elf/tst-dst-needed-leaf-mod.c new file mode 100644 index 00000000000..30d756be399 --- /dev/null +++ b/elf/tst-dst-needed-leaf-mod.c @@ -0,0 +1,20 @@ +/* Leaf DSO whose SONAME contains several DST tokens. Used by + tst-dst-needed-minstack via tst-dst-needed-wrap-mod.so. + 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 + . */ + +int tst_dst_needed_leaf_dummy; diff --git a/elf/tst-dst-needed-minstack.c b/elf/tst-dst-needed-minstack.c new file mode 100644 index 00000000000..ab6f0aef796 --- /dev/null +++ b/elf/tst-dst-needed-minstack.c @@ -0,0 +1,120 @@ +/* Test that dlopen of a library whose DT_NEEDED string carries + several dynamic-string tokens does not overflow a + PTHREAD_STACK_MIN-sized thread. + + 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 + . */ + +/* The leaf library is linked with a SONAME containing five $DST tokens; the + wrapper library links against the leaf so its DT_NEEDED inherits that + string. This test deploys the wrapper in a ~3 KB deep directory (so the + wrapper's l_origin matches), then dlopens it from a PTHREAD_STACK_MIN + thread. The leaf is not actually reachable through the (impossible) + expanded path, so the dlopen is expected to fail -- the regression + assertion is that the failure occurs without a stack overflow. */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define WRAP_MOD "tst-dst-needed-wrap-mod.so" + +/* Set by do_prepare; the absolute path of the wrapper as deployed in the + deep directory. */ +static char *deep_wrap_path; + +/* Build // and return the final path; intermediates + are registered with add_temp_file so cleanup is automatic. */ +static char * +build_deep_directory (void) +{ + enum { component_len = 240, components = 14 }; + char component[component_len + 1]; + memset (component, 'd', component_len); + component[component_len] = '\0'; + + char *base = support_create_temp_directory ("tst-dst-needed-"); + size_t cap = strlen (base) + components * (1 + component_len) + 1; + char *path = xmalloc (cap); + strcpy (path, base); + free (base); + + for (int i = 0; i < components; ++i) + { + strcat (path, "/"); + strcat (path, component); + xmkdirp (path, 0777); + add_temp_file (path); + } + return path; +} + +static void +do_prepare (int argc, char **argv) +{ + char *deep_dir = build_deep_directory (); + /* Deep enough that l_origin alone is well over PTHREAD_STACK_MIN. */ + TEST_VERIFY (strlen (deep_dir) > 256); + + char *src = xasprintf ("%s/elf/" WRAP_MOD, support_objdir_root); + deep_wrap_path = xasprintf ("%s/" WRAP_MOD, deep_dir); + support_copy_file (src, deep_wrap_path); + add_temp_file (deep_wrap_path); + free (src); + free (deep_dir); +} +#define PREPARE do_prepare + +static void * +minstack_thread (void *closure __attribute__ ((unused))) +{ + /* Trigger DT_NEEDED expansion on the deep wrapper. The leaf's five-$DST + SONAME, expanded against the wrapper's deep l_origin, produces a buffer + of several KB inside _dl_map_object_deps. We do not care whether the + leaf is actually findable -- the test passes if and only if the dlopen + returns without a stack overflow. */ + void *h = dlopen (deep_wrap_path, RTLD_NOW); + TEST_VERIFY_EXIT (h == NULL); + return NULL; +} + +static int +do_test (void) +{ + pthread_attr_t attr; + xpthread_attr_init (&attr); + xpthread_attr_setstacksize (&attr, PTHREAD_STACK_MIN); + pthread_t thr = xpthread_create (&attr, minstack_thread, NULL); + xpthread_join (thr); + xpthread_attr_destroy (&attr); + + free (deep_wrap_path); + return 0; +} + +#include diff --git a/elf/tst-dst-needed-wrap-mod.c b/elf/tst-dst-needed-wrap-mod.c new file mode 100644 index 00000000000..d6b95d8b119 --- /dev/null +++ b/elf/tst-dst-needed-wrap-mod.c @@ -0,0 +1,21 @@ +/* Wrapper DSO whose DT_NEEDED string inherits the DST-laden SONAME + of tst-dst-needed-leaf-mod.so. Loading this from a + PTHREAD_STACK_MIN thread is what tst-dst-needed-minstack exercises. + 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 + . */ + +int tst_dst_needed_wrap_dummy; From patchwork Wed May 13 19:18:24 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Adhemerval Zanella Netto X-Patchwork-Id: 134938 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id DEF8F4BBC0BF for ; Wed, 13 May 2026 19:25:24 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org DEF8F4BBC0BF Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=RQ9Tsl1H X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-dy1-x1331.google.com (mail-dy1-x1331.google.com [IPv6:2607:f8b0:4864:20::1331]) by sourceware.org (Postfix) with ESMTPS id 031B04BBC0BC for ; Wed, 13 May 2026 19:20:32 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 031B04BBC0BC Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=linaro.org Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=linaro.org ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 031B04BBC0BC Authentication-Results: sourceware.org; arc=none smtp.remote-ip=2607:f8b0:4864:20::1331 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700032; cv=none; b=HeHMN4UdA5KwcDMDf8xO/K3E+dXV0w8SNuOX1aQYNgEFP4WZ/8Sbw00S9vh1wu9KxSBdFteMdI3UEtx0FbJqL7CzUnCuv+ZwqIE4NQwl/XT1IqOnaQ3XbtFgGPrr2Om2kmXnDVvVwoe9+rtOeNbsol5KwSh05Vy5wwV5cpvflh8= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1778700032; c=relaxed/simple; bh=uvaDETSS3euf8arnm6pWYo3hjGlYZzDnRJpJJViHu98=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=VWlenFxWhmXrI4+zKgcY7JpumMqvGCN/0jUgT57fofRX9TXw/o3RqC3puP1eLArY3svndZ6cAFKM1C7zZ5IFqdUg2cgWaj0GlNcWJkulLqNrhyeUmcOjbImPro1gRqWlnJ2ocwI2k8hkIE+FS9RjjJowkVi1KxK+3nEr24qNn/E= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=linaro.org header.i=@linaro.org header.a=rsa-sha256 header.s=google header.b=RQ9Tsl1H DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 031B04BBC0BC Received: by mail-dy1-x1331.google.com with SMTP id 5a478bee46e88-2ee990e8597so12178149eec.1 for ; Wed, 13 May 2026 12:20:31 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linaro.org; s=google; t=1778700031; x=1779304831; darn=sourceware.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=Vhho/gZFp981bmeXKIHxWNn+DL+YjehQZFLKcpfnJRI=; b=RQ9Tsl1Hgi4+shSJ3tbJ2WH7SOBHcpRCDLVEArL+74SGsI3Ub78s7YeG+WhLzFuD6A +Hfgw8k0a01rtz7kyNIcjhvvRTNor9t9X+ynFfav+NidFKDFXhdQQBoy715GA9I8SKtJ aPmesyez5SvABiDBCKUzuMaWMRyjJ5nDRv6ccCIKiWZcAIhbJEopzY1uvQqhVIufVYqP UwT8S8pcW9hvECCChYL8q33qxNBKzmS1HghQPgsRmUmxVyi8D+RlQZYK2/ibNX4i5fGl ZAMItc3s57FK4d08Wbz7+Uhv5SnFGgPhoHYJx4gATbGMfYd7a6noFtXMubKqC/rczh8u XrSw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778700031; x=1779304831; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=Vhho/gZFp981bmeXKIHxWNn+DL+YjehQZFLKcpfnJRI=; b=cIlcab2rA3A7Ba5c28+7xnyz5nEWt7vX4kIa5DaOcKoIwkTfHpm2Rmhb7O52NofmMf oRDIn/s4i2GO6TvU46Wrm/obc5+8aHg0bMY2f73iB7u4y2A4SSLstglzSrMds6FQau3o i5pHSwP3DbLNL4XsL2mSGSO4N9Nm7OB7Cly0vBx38rmMoN9aJn/M53CUwunCVQrGMDzH c8tyOxaHZVjTXIy8zBgMJkfPyINvDbF79UjmF8LbTTu0w4/MYZbq+iv2HOGpbrb9kGIP xds5ER3VudA36P2eJT5ho5a+HO4LigV41RrSm52xeCsBQqXSJ53nqa5S7sj+c8kVNVeA I6Hg== X-Gm-Message-State: AOJu0Yyd5XN4Lq5sduEoZ+Axh3Us2rdYhPasPWagW2rv8vQcdfpW83q0 wQRjhoXdnkdabOuK/9C07YTmZHEMi/fo3J5BtyPTckb7p+ZGrzvjMOv8kXrLRURAirnmdGtY7NR PyW6E X-Gm-Gg: Acq92OGQ1SMLq1weN6Dl196eL7mkekNPkp4YXeX4azCdWJ/yhQxm8et1FM8OP77b6j4 2ZOQ8rDaLAZCm5IBQmxIWrJtl4mDjuXiSJYFn+rbtW4eh0CwCtPtRIWjkUZc1Ps39hxSMPUc5fm TgJ9YiYSENU72D3K1YdKyPoHIyqIJUKwp9EcAeqsydN73VLMmYaYNsq48As10docaUiyIHn8xes ILTVLLjgSpRtHucl6KASEckgl/4/ktdMd8EhIIk2ivxTcYzZIKnUlycVeeAC+C/oxU+T+wiE0fL Ry5v/hi106n/orbXRKKyCEJelyCJ1TL5Hjutbe8PU7azIrwl1C03rKikqHS1VQsKzPVucybV5dw 7MpFJeXmkNHr+MwQv+USqxa6TzbgVjqnIt9vGNpKrx0fz9ADAS2kbjkn1RLJdrLNtScuS0tl2/Q eaG6s0j7ulP9jYnThJKgCZWvgjM+UtgUiz/CFpYd3VBsK/+g== X-Received: by 2002:a05:693c:3004:b0:2f2:1b3c:d832 with SMTP id 5a478bee46e88-30116e96f9cmr3560851eec.5.1778700030302; Wed, 13 May 2026 12:20:30 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c1:d905:1e50:98bf:e0d4:d27e]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-30294adddaasm117468eec.13.2026.05.13.12.20.28 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 12:20:29 -0700 (PDT) From: Adhemerval Zanella To: libc-alpha@sourceware.org Cc: "H . J . Lu" , Florian Weimer Subject: [PATCH 6/6] elf: Use dl_scratch_buffer for LD_LIBRARY_PATH copy in _dl_init_paths Date: Wed, 13 May 2026 16:18:24 -0300 Message-ID: <20260513192013.2511422-7-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> References: <20260513192013.2511422-1-adhemerval.zanella@linaro.org> MIME-Version: 1.0 X-Spam-Status: No, score=-12.4 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_NONE, TXREP shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libc-alpha-bounces~patchwork=sourceware.org@sourceware.org _dl_init_paths used strdupa to make a mutable copy of LD_LIBRARY_PATH for fillin_rpath to tokenize. The env block is attacker-controllable and Linux allows individual variables up to MAX_ARG_STRLEN (32 * PAGE_SIZE = 128 KB), so the strdupa can push tens of KB onto the loader's startup stack on top of the env block that already sits on the initial stack. With a reduced RLIMIT_STACK the doubled copy overflows before main () is reached. Replace the strdupa with a dl_scratch_buffer: short paths stay in the 256-byte inline area, longer ones spill to anonymous mmap (malloc is not yet available during _dl_init_paths). Two follow-on changes make the new scratch lifetime safe against _dl_signal_error: * Count entries directly off the const LD_LIBRARY_PATH and allocate __rtld_env_path_list.dirs *before* the scratch is live. That way the larger of the two heap allocations the loader controls signals its OOM with no scratch to leak. * Convert fillin_rpath to return bool instead of calling _dl_signal_error internally on per-entry malloc failure. Its only caller in the LLP path now frees the scratch first and then signals the error from a clean state. decompose_rpath, the other caller, is updated symmetrically. This also fixes a pre-existing leak in fillin_rpath's OOM path, where the to_free heap copy from expand_dynamic_string_token was not released before the _dl_signal_error. Checked on x86_64-linux-gnu, aarch64-linux-gnu, and i686-linux-gnu. --- elf/Makefile | 3 ++ elf/dl-load.c | 53 ++++++++++++++++----- elf/tst-dl-llp-stack.c | 106 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 elf/tst-dl-llp-stack.c diff --git a/elf/Makefile b/elf/Makefile index 443e29493c2..2a90eaaeb30 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -411,6 +411,7 @@ tests += \ tst-debug1 \ tst-deep1 \ tst-dl-is_dso \ + tst-dl-llp-stack \ tst-dl-path-buf \ tst-dlclose-lazy \ tst-dlmodcount \ @@ -2789,6 +2790,8 @@ generated += tst-bz26577-mod.so $(objpfx)tst-bz26577-minstack: $(shared-thread-library) $(objpfx)tst-bz26577-minstack.out: $(objpfx)tst-bz26577-mod.so +tst-dl-llp-stack-ARGS = -- $(host-test-program-cmd) + $(objpfx)tst-unwind-ctor: $(objpfx)tst-unwind-ctor-lib.so LDLIBS-tst-unwind-ctor += $(libunwind) LDFLAGS-tst-unwind-ctor-lib.so = -Wl,--unresolved-symbols=ignore-all diff --git a/elf/dl-load.c b/elf/dl-load.c index fc5d9961ef4..7e64efe97cd 100644 --- a/elf/dl-load.c +++ b/elf/dl-load.c @@ -422,7 +422,10 @@ struct r_search_path_struct __rtld_search_dirs attribute_relro; static size_t max_dirnamelen; -static struct r_search_path_elem ** +/* Tokenize RPATH (in place) and populate RESULT with one entry per non-empty + directory. Returns false if a per-entry allocation fails,leaving the + caller responsible for signaling any error. */ +static bool fillin_rpath (char *rpath, struct r_search_path_elem **result, const char *sep, const char *what, const char *where, struct link_map *l) { @@ -490,8 +493,10 @@ fillin_rpath (char *rpath, struct r_search_path_elem **result, const char *sep, malloc (sizeof (*dirp) + ncapstr * sizeof (enum r_dir_status) + where_len + len + 1); if (dirp == NULL) - _dl_signal_error (ENOMEM, NULL, NULL, - N_("cannot create cache for search path")); + { + free (to_free); + return false; + } dirp->dirname = ((char *) dirp + sizeof (*dirp) + ncapstr * sizeof (enum r_dir_status)); @@ -528,7 +533,7 @@ fillin_rpath (char *rpath, struct r_search_path_elem **result, const char *sep, /* Terminate the array. */ result[nelems] = NULL; - return result; + return true; } @@ -609,7 +614,13 @@ decompose_rpath (struct r_search_path_struct *sps, _dl_signal_error (ENOMEM, NULL, NULL, errstring); } - fillin_rpath (copy, result, ":", what, where, l); + if (!fillin_rpath (copy, result, ":", what, where, l)) + { + free (copy); + free (result); + errstring = N_("cannot create cache for search path"); + goto signal_error; + } /* Free the copied RPATH string. `fillin_rpath' make own copies if necessary. */ @@ -782,12 +793,13 @@ _dl_init_paths (const char *llp, const char *source, if (llp != NULL && *llp != '\0') { - char *llp_tmp = strdupa (llp); - - /* Decompose the LD_LIBRARY_PATH contents. First determine how many - elements it has. */ + /* Count entries directly off the const LD_LIBRARY_PATH so the + search-path dirs array can be allocated before the scratch buffer is + live; that way an OOM on either of the two heap allocations the + loader controls (the dirs array or the per-entry malloc inside + fillin_rpath) is signalled after the scratch has been released. */ size_t nllp = 1; - for (const char *cp = llp_tmp; *cp != '\0'; ++cp) + for (const char *cp = llp; *cp != '\0'; ++cp) if (*cp == ':' || *cp == ';') ++nllp; @@ -799,8 +811,25 @@ _dl_init_paths (const char *llp, const char *source, goto signal_error; } - (void) fillin_rpath (llp_tmp, __rtld_env_path_list.dirs, ":;", - source, NULL, l); + /* fillin_rpath needs a mutable copy because __strsep punches NULs + into it as it tokenizes. */ + size_t llp_len = strlen (llp); + struct dl_scratch_buffer scratch = dl_scratch_buffer_init (); + dl_scratch_buffer_allocate (&scratch, llp_len + 1, 0); + char *llp_tmp = memcpy (scratch.data, llp, llp_len + 1); + + bool ok = fillin_rpath (llp_tmp, __rtld_env_path_list.dirs, ":;", + source, NULL, l); + + dl_scratch_buffer_free (&scratch); + + if (!ok) + { + free (__rtld_env_path_list.dirs); + __rtld_env_path_list.dirs = NULL; + errstring = N_("cannot create cache for search path"); + goto signal_error; + } if (__rtld_env_path_list.dirs[0] == NULL) { diff --git a/elf/tst-dl-llp-stack.c b/elf/tst-dl-llp-stack.c new file mode 100644 index 00000000000..c05ea7ec275 --- /dev/null +++ b/elf/tst-dl-llp-stack.c @@ -0,0 +1,106 @@ +/* Test that a long LD_LIBRARY_PATH does not overflow loader startup + stack. + 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 + . */ + +/* This test reduces RLIMIT_STACK to a value that just covers regular + loader startup, builds an envp with only a long LD_LIBRARY_PATH + (~64 KB of synthetic entries). */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static int restart; +#define CMDLINE_OPTIONS \ + { "restart", no_argument, &restart, 1 }, + +/* 16 entries × 4000 bytes ≈ 64 KB of synthetic LD_LIBRARY_PATH. */ +enum { llp_entry_len = 4000, llp_entries = 16 }; + +/* Stack rlimit for the child. Tuned so that regular loader startup + (envp on the initial stack + a few KB of loader frames) fits. */ +enum { stack_limit_kb = 128 }; + +/* Build a "LD_LIBRARY_PATH=" string with llp_entries synthetic + colon-separated entries each llp_entry_len bytes long. */ +static char * +build_llp_env (void) +{ + size_t junk_len = (size_t) llp_entries * (1 + llp_entry_len); + char *env = xmalloc (strlen ("LD_LIBRARY_PATH=") + junk_len + 1); + char *p = stpcpy (env, "LD_LIBRARY_PATH="); + for (int i = 0; i < llp_entries; i++) + { + *p++ = ':'; + *p++ = '/'; + memset (p, 'd', llp_entry_len - 1); + p += llp_entry_len - 1; + } + *p = '\0'; + return env; +} + +static int +do_test (int argc, char *argv[]) +{ + if (restart) + return 0; + + TEST_VERIFY_EXIT (argc == 2 || argc == 5); + const char *test_binary = argv[argc - 1]; + + char *llp_env = build_llp_env (); + char *envp[] = { llp_env, NULL }; + + /* Reduce the stack rlimit; the posix_spawn'd child inherits it. */ + struct rlimit rl_save, rl_small; + TEST_VERIFY_EXIT (getrlimit (RLIMIT_STACK, &rl_save) == 0); + rl_small.rlim_cur = (rlim_t) stack_limit_kb * 1024; + rl_small.rlim_max = rl_save.rlim_max; + TEST_VERIFY_EXIT (setrlimit (RLIMIT_STACK, &rl_small) == 0); + + char *child_argv[] = { + (char *) test_binary, + (char *) "--direct", + (char *) "--restart", + NULL + }; + struct support_capture_subprocess proc + = support_capture_subprogram (test_binary, child_argv, envp); + + TEST_VERIFY_EXIT (setrlimit (RLIMIT_STACK, &rl_save) == 0); + + if (WIFSIGNALED (proc.status)) + FAIL_EXIT1 ("child killed by signal %d", WTERMSIG (proc.status)); + TEST_VERIFY_EXIT (WIFEXITED (proc.status)); + TEST_COMPARE (WEXITSTATUS (proc.status), 0); + + support_capture_subprocess_free (&proc); + free (llp_env); + return 0; +} + +#define TEST_FUNCTION_ARGV do_test +#include