commit ede8d94d154157d269b18f3601440ac576c1f96a Author: Florian Weimer Date: Mon May 16 18:41:43 2022 +0200 csu: Implement and use _dl_early_allocate during static startup This implements mmap fallback for a brk failure during TLS allocation. scripts/tls-elf-edit.py is updated to support the new patching method. The script no longer requires that in the input object is of ET_DYN type. Reviewed-by: Adhemerval Zanella (cherry picked from commit f787e138aa0bf677bf74fa2a08595c446292f3d7) Conflicts: elf/Makefile (missing ld.so static execve backport upstream) sysdeps/generic/ldsodefs.h (missing ld.so dependency sorting optimization upstream) diff --git a/csu/libc-tls.c b/csu/libc-tls.c index d83e69f6257ae981..738f59f46b62c31c 100644 --- a/csu/libc-tls.c +++ b/csu/libc-tls.c @@ -145,11 +145,16 @@ __libc_setup_tls (void) _dl_allocate_tls_storage (in elf/dl-tls.c) does using __libc_memalign and dl_tls_static_align. */ tcb_offset = roundup (memsz + GLRO(dl_tls_static_surplus), max_align); - tlsblock = __sbrk (tcb_offset + TLS_INIT_TCB_SIZE + max_align); + tlsblock = _dl_early_allocate (tcb_offset + TLS_INIT_TCB_SIZE + max_align); + if (tlsblock == NULL) + _startup_fatal ("Fatal glibc error: Cannot allocate TLS block\n"); #elif TLS_DTV_AT_TP tcb_offset = roundup (TLS_INIT_TCB_SIZE, align ?: 1); - tlsblock = __sbrk (tcb_offset + memsz + max_align - + TLS_PRE_TCB_SIZE + GLRO(dl_tls_static_surplus)); + tlsblock = _dl_early_allocate (tcb_offset + memsz + max_align + + TLS_PRE_TCB_SIZE + + GLRO(dl_tls_static_surplus)); + if (tlsblock == NULL) + _startup_fatal ("Fatal glibc error: Cannot allocate TLS block\n"); tlsblock += TLS_PRE_TCB_SIZE; #else /* In case a model with a different layout for the TCB and DTV diff --git a/elf/Makefile b/elf/Makefile index 6423ebbdd7708a14..ea1512549be3f628 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -33,6 +33,7 @@ routines = \ $(all-dl-routines) \ dl-addr \ dl-addr-obj \ + dl-early_allocate \ dl-error \ dl-iteratephdr \ dl-libc \ @@ -104,6 +105,7 @@ all-dl-routines = $(dl-routines) $(sysdep-dl-routines) # But they are absent from the shared libc, because that code is in ld.so. elide-routines.os = \ $(all-dl-routines) \ + dl-early_allocate \ dl-exception \ dl-origin \ dl-reloc-static-pie \ @@ -264,6 +266,7 @@ tests-static-normal := \ tst-linkall-static \ tst-single_threaded-pthread-static \ tst-single_threaded-static \ + tst-tls-allocation-failure-static \ tst-tlsalign-extern-static \ tst-tlsalign-static \ # tests-static-normal @@ -1101,6 +1104,10 @@ $(objpfx)tst-glibcelf.out: tst-glibcelf.py elf.h $(..)/scripts/glibcelf.py \ --cc="$(CC) $(patsubst -DMODULE_NAME=%,-DMODULE_NAME=testsuite,$(CPPFLAGS))" \ < /dev/null > $@ 2>&1; $(evaluate-test) +ifeq ($(run-built-tests),yes) +tests-special += $(objpfx)tst-tls-allocation-failure-static-patched.out +endif + # The test requires shared _and_ PIE because the executable # unit test driver must be able to link with the shared object # that is going to eventually go into an installed DSO. @@ -2637,3 +2644,15 @@ $(objpfx)tst-ro-dynamic-mod.so: $(objpfx)tst-ro-dynamic-mod.os \ $(objpfx)tst-ro-dynamic-mod.os $(objpfx)tst-rtld-run-static.out: $(objpfx)/ldconfig + +$(objpfx)tst-tls-allocation-failure-static-patched: \ + $(objpfx)tst-tls-allocation-failure-static $(..)scripts/tst-elf-edit.py + cp $< $@ + $(PYTHON) $(..)scripts/tst-elf-edit.py --maximize-tls-size $@ + +$(objpfx)tst-tls-allocation-failure-static-patched.out: \ + $(objpfx)tst-tls-allocation-failure-static-patched + $< > $@ 2>&1; echo "status: $$?" >> $@ + grep -q '^Fatal glibc error: Cannot allocate TLS block$$' $@ \ + && grep -q '^status: 127$$' $@; \ + $(evaluate-test) diff --git a/elf/dl-early_allocate.c b/elf/dl-early_allocate.c new file mode 100644 index 0000000000000000..61677aaa0364c209 --- /dev/null +++ b/elf/dl-early_allocate.c @@ -0,0 +1,30 @@ +/* Early memory allocation for the dynamic loader. Generic version. + Copyright (C) 2022 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 + +void * +_dl_early_allocate (size_t size) +{ + void *result = __sbrk (size); + if (result == (void *) -1) + result = NULL; + return result; +} diff --git a/elf/tst-tls-allocation-failure-static.c b/elf/tst-tls-allocation-failure-static.c new file mode 100644 index 0000000000000000..8de831b2469ba390 --- /dev/null +++ b/elf/tst-tls-allocation-failure-static.c @@ -0,0 +1,31 @@ +/* Base for test program with impossiblyh large PT_TLS segment. + Copyright (C) 2022 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 test actual binary is patched using scripts/tst-elf-edit.py + --maximize-tls-size, and this introduces the expected test + allocation failure due to an excessive PT_LS p_memsz value. + + Patching the binary is required because on some 64-bit targets, TLS + relocations can only cover a 32-bit range, and glibc-internal TLS + variables such as errno end up outside that range. */ + +int +main (void) +{ + return 0; +} diff --git a/scripts/tst-elf-edit.py b/scripts/tst-elf-edit.py new file mode 100644 index 0000000000000000..0e19ce1e7392f3ca --- /dev/null +++ b/scripts/tst-elf-edit.py @@ -0,0 +1,226 @@ +#!/usr/bin/python3 +# ELF editor for load align tests. +# Copyright (C) 2022 Free Software Foundation, Inc. +# Copyright The GNU Toolchain Authors. +# 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 +# . + +import argparse +import os +import sys +import struct + +EI_NIDENT=16 + +EI_MAG0=0 +ELFMAG0=b'\x7f' +EI_MAG1=1 +ELFMAG1=b'E' +EI_MAG2=2 +ELFMAG2=b'L' +EI_MAG3=3 +ELFMAG3=b'F' + +EI_CLASS=4 +ELFCLASSNONE=b'0' +ELFCLASS32=b'\x01' +ELFCLASS64=b'\x02' + +EI_DATA=5 +ELFDATA2LSB=b'\x01' +ELFDATA2MSB=b'\x02' + +ET_EXEC=2 +ET_DYN=3 + +PT_LOAD=1 +PT_TLS=7 + +def elf_types_fmts(e_ident): + endian = '<' if e_ident[EI_DATA] == ELFDATA2LSB else '>' + addr = 'I' if e_ident[EI_CLASS] == ELFCLASS32 else 'Q' + off = 'I' if e_ident[EI_CLASS] == ELFCLASS32 else 'Q' + return (endian, addr, off) + +class Elf_Ehdr: + def __init__(self, e_ident): + endian, addr, off = elf_types_fmts(e_ident) + self.fmt = '{0}HHI{1}{2}{2}IHHHHHH'.format(endian, addr, off) + self.len = struct.calcsize(self.fmt) + + def read(self, f): + buf = f.read(self.len) + if not buf: + error('{}: header too small'.format(f.name)) + data = struct.unpack(self.fmt, buf) + self.e_type = data[0] + self.e_machine = data[1] + self.e_version = data[2] + self.e_entry = data[3] + self.e_phoff = data[4] + self.e_shoff = data[5] + self.e_flags = data[6] + self.e_ehsize = data[7] + self.e_phentsize= data[8] + self.e_phnum = data[9] + self.e_shstrndx = data[10] + + +class Elf_Phdr: + def __init__(self, e_ident): + endian, addr, off = elf_types_fmts(e_ident) + self.ei_class = e_ident[EI_CLASS] + if self.ei_class == ELFCLASS32: + self.fmt = '{0}I{2}{1}{1}IIII'.format(endian, addr, off) + else: + self.fmt = '{0}II{2}{1}{1}QQQ'.format(endian, addr, off) + self.len = struct.calcsize(self.fmt) + + def read(self, f): + buf = f.read(self.len) + if len(buf) < self.len: + error('{}: program header too small'.format(f.name)) + data = struct.unpack(self.fmt, buf) + if self.ei_class == ELFCLASS32: + self.p_type = data[0] + self.p_offset = data[1] + self.p_vaddr = data[2] + self.p_paddr = data[3] + self.p_filesz = data[4] + self.p_memsz = data[5] + self.p_flags = data[6] + self.p_align = data[7] + else: + self.p_type = data[0] + self.p_flags = data[1] + self.p_offset = data[2] + self.p_vaddr = data[3] + self.p_paddr = data[4] + self.p_filesz = data[5] + self.p_memsz = data[6] + self.p_align = data[7] + + def write(self, f): + if self.ei_class == ELFCLASS32: + data = struct.pack(self.fmt, + self.p_type, + self.p_offset, + self.p_vaddr, + self.p_paddr, + self.p_filesz, + self.p_memsz, + self.p_flags, + self.p_align) + else: + data = struct.pack(self.fmt, + self.p_type, + self.p_flags, + self.p_offset, + self.p_vaddr, + self.p_paddr, + self.p_filesz, + self.p_memsz, + self.p_align) + f.write(data) + + +def error(msg): + print(msg, file=sys.stderr) + sys.exit(1) + + +def elf_edit_align(phdr, align): + if align == 'half': + phdr.p_align = phdr.p_align >> 1 + else: + phdr.p_align = int(align) + +def elf_edit_maximize_tls_size(phdr, elfclass): + if elfclass == ELFCLASS32: + # It is possible that the kernel can allocate half of the + # address space, so use something larger. + phdr.p_memsz = 0xfff00000 + else: + phdr.p_memsz = 1 << 63 + +def elf_edit(f, opts): + ei_nident_fmt = 'c' * EI_NIDENT + ei_nident_len = struct.calcsize(ei_nident_fmt) + + data = f.read(ei_nident_len) + if len(data) < ei_nident_len: + error('{}: e_nident too small'.format(f.name)) + e_ident = struct.unpack(ei_nident_fmt, data) + + if e_ident[EI_MAG0] != ELFMAG0 \ + or e_ident[EI_MAG1] != ELFMAG1 \ + or e_ident[EI_MAG2] != ELFMAG2 \ + or e_ident[EI_MAG3] != ELFMAG3: + error('{}: bad ELF header'.format(f.name)) + + if e_ident[EI_CLASS] != ELFCLASS32 \ + and e_ident[EI_CLASS] != ELFCLASS64: + error('{}: unsupported ELF class: {}'.format(f.name, e_ident[EI_CLASS])) + + if e_ident[EI_DATA] != ELFDATA2LSB \ + and e_ident[EI_DATA] != ELFDATA2MSB: \ + error('{}: unsupported ELF data: {}'.format(f.name, e_ident[EI_DATA])) + + ehdr = Elf_Ehdr(e_ident) + ehdr.read(f) + if ehdr.e_type not in (ET_EXEC, ET_DYN): + error('{}: not an executable or shared library'.format(f.name)) + + phdr = Elf_Phdr(e_ident) + maximize_tls_size_done = False + for i in range(0, ehdr.e_phnum): + f.seek(ehdr.e_phoff + i * phdr.len) + phdr.read(f) + if phdr.p_type == PT_LOAD and opts.align is not None: + elf_edit_align(phdr, opts.align) + f.seek(ehdr.e_phoff + i * phdr.len) + phdr.write(f) + break + if phdr.p_type == PT_TLS and opts.maximize_tls_size: + elf_edit_maximize_tls_size(phdr, e_ident[EI_CLASS]) + f.seek(ehdr.e_phoff + i * phdr.len) + phdr.write(f) + maximize_tls_size_done = True + break + + if opts.maximize_tls_size and not maximize_tls_size_done: + error('{}: TLS maximum size was not updated'.format(f.name)) + +def get_parser(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('-a', dest='align', + help='How to set the LOAD alignment') + parser.add_argument('--maximize-tls-size', action='store_true', + help='Set maximum PT_TLS size') + parser.add_argument('output', + help='ELF file to edit') + return parser + + +def main(argv): + parser = get_parser() + opts = parser.parse_args(argv) + with open(opts.output, 'r+b') as fout: + elf_edit(fout, opts) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h index a38de94bf7ea8e93..87ad2f3f4d89eb7d 100644 --- a/sysdeps/generic/ldsodefs.h +++ b/sysdeps/generic/ldsodefs.h @@ -1238,6 +1238,11 @@ extern struct link_map * _dl_get_dl_main_map (void) /* Initialize the DSO sort algorithm to use. */ extern void _dl_sort_maps_init (void) attribute_hidden; +/* Perform early memory allocation, avoding a TCB dependency. + Terminate the process if allocation fails. May attempt to use + brk. */ +void *_dl_early_allocate (size_t size) attribute_hidden; + /* Initialization of libpthread for statically linked applications. If libpthread is not linked in, this is an empty function. */ void __pthread_initialize_minimal (void) weak_function; diff --git a/sysdeps/unix/sysv/linux/dl-early_allocate.c b/sysdeps/unix/sysv/linux/dl-early_allocate.c new file mode 100644 index 0000000000000000..52c538e85afa8522 --- /dev/null +++ b/sysdeps/unix/sysv/linux/dl-early_allocate.c @@ -0,0 +1,82 @@ +/* Early memory allocation for the dynamic loader. Generic version. + Copyright (C) 2022 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 + . */ + +/* Mark symbols hidden in static PIE for early self relocation to work. */ +#if BUILD_PIE_DEFAULT +# pragma GCC visibility push(hidden) +#endif +#include + +#include +#include +#include +#include +#include + +#include +#include + +/* Defined in brk.c. */ +extern void *__curbrk; + +void * +_dl_early_allocate (size_t size) +{ + void *result; + + if (__curbrk != NULL) + /* If the break has been initialized, brk must have run before, + so just call it once more. */ + { + result = __sbrk (size); + if (result == (void *) -1) + result = NULL; + } + else + { + /* If brk has not been invoked, there is no need to update + __curbrk. The first call to brk will take care of that. */ + void *previous = __brk_call (0); + result = __brk_call (previous + size); + if (result == previous) + result = NULL; + else + result = previous; + } + + /* If brk fails, fall back to mmap. This can happen due to + unfortunate ASLR layout decisions and kernel bugs, particularly + for static PIE. */ + if (result == NULL) + { + long int ret; + int prot = PROT_READ | PROT_WRITE; + int flags = MAP_PRIVATE | MAP_ANONYMOUS; +#ifdef __NR_mmap2 + ret = MMAP_CALL_INTERNAL (mmap2, 0, size, prot, flags, -1, 0); +#else + ret = MMAP_CALL_INTERNAL (mmap, 0, size, prot, flags, -1, 0); +#endif + if (INTERNAL_SYSCALL_ERROR_P (ret)) + result = NULL; + else + result = (void *) ret; + } + + return result; +}