[glibc] elf: Make string tunables startup-only
Adhemerval Zanella via Glibc-cvs <[email protected]>
| Newsgroups | gmane.comp.lib.glibc.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=3923b80921aa2aeac2c3312e9231ca892ccbf4b0 commit 3923b80921aa2aeac2c3312e9231ca892ccbf4b0 Author: Adhemerval Zanella <[email protected]> Date: Wed Jul 29 17:18:47 2026 -0300 elf: Make string tunables startup-only A string tunable value usually references the GLIBC_TUNABLES (or alias) environment string, which lives in the environment block the kernel places on the initial stack. That memory is owned by the application, which may overwrite it (e.g. setproctitle), so the reference is only safe while no application code has run (a value coming from the system-wide tunables cache is a copy instead, but the rule is applied uniformly). This patch make the lifetime explicit and enforced without copying the value or allocating any memory by adding __tunable_seal_strings, which drops every string tunable reference once early startup is complete. The seal is applied after the only string tunable consumer and before any code outside of the startup sequence runs. Checked on aarch64-linux-gnu and x86_64-linux-gnu. I also run the elf tests on powerpc64le-linux-gnu, loongarch64-linux-gnuf64, and s390x-linux-gnu. Diff: --- csu/libc-start.c | 4 +++ elf/Makefile | 5 +++ elf/dl-tunables.c | 43 ++++++++++++++++++++++++ elf/dl-tunables.h | 2 ++ elf/rtld.c | 6 ++++ elf/tst-tunables-seal-static.c | 1 + elf/tst-tunables-seal.c | 75 ++++++++++++++++++++++++++++++++++++++++++ manual/README.tunables | 16 +++++++++ sysdeps/aarch64/Makefile | 3 ++ sysdeps/loongarch/Makefile | 3 ++ sysdeps/powerpc/Makefile | 3 ++ sysdeps/s390/Makefile | 3 ++ sysdeps/x86/Makefile | 3 ++ 13 files changed, 167 insertions(+) diff --git a/csu/libc-start.c b/csu/libc-start.c index ec42f23981..b6fc8597a1 100644 --- a/csu/libc-start.c +++ b/csu/libc-start.c @@ -268,6 +268,10 @@ LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL), ARCH_INIT_CPU_FEATURES (); + /* Every string tunable has been consumed by ARCH_INIT_CPU_FEATURES, before + any code outside of libc startup runs. */ + __tunable_seal_strings (); + /* Do static-pie self relocation for the non-IRELATIVE part after tunables and cpu features are set up. IFUNC entries are deferred until after the TCB and the stack-protector canary are usable, so that an instrumented diff --git a/elf/Makefile b/elf/Makefile index dbc6cfec7f..c5bf1f38cb 100644 --- a/elf/Makefile +++ b/elf/Makefile @@ -290,6 +290,7 @@ tests-static-internal := \ tst-tls1-static-non-pie \ tst-tunables \ tst-tunables-enable_secure \ + tst-tunables-seal-static \ # tests-static-internal ifeq (yesyes,$(have-gcc-ifunc)$(have-ssp)) @@ -579,6 +580,7 @@ tests-internal += \ tst-tls6 \ tst-tls7 \ tst-tls8 \ + tst-tunables-seal \ unload \ unload2 \ # tests-internal @@ -3176,6 +3178,9 @@ $(objpfx)tst-glibc-hwcaps-cache.out: $(objpfx)tst-glibc-hwcaps tst-tunables-ARGS = -- $(host-test-program-cmd) tst-tunables-enable_secure-ARGS = -- $(host-test-program-cmd) +CFLAGS-tst-tunables-seal-static.c += $(CFLAGS-tst-tunables-seal.c) +tst-tunables-seal-static-TUNABLES += $(tst-tunables-seal-TUNABLES) + $(objpfx)list-tunables.out: tst-rtld-list-tunables.sh $(objpfx)ld.so $(SHELL) $< $(objpfx)ld.so '$(test-wrapper-env)' \ '$(run_program_env)' > $(objpfx)/tst-rtld-list-tunables.out diff --git a/elf/dl-tunables.c b/elf/dl-tunables.c index 54d1296bff..ee4a1aab31 100644 --- a/elf/dl-tunables.c +++ b/elf/dl-tunables.c @@ -65,6 +65,17 @@ get_next_env (char **envp, char **name, char **val, char ***prev_envp) return NULL; } +/* Set by __tunable_seal_strings once the values of the string tunables are + no longer valid. */ +static bool tunables_strings_sealed attribute_relro; + +static void __attribute__ ((noreturn)) +tunable_sealed_error (const tunable_t *cur, const char *action) +{ + _dl_fatal_printf ("Fatal glibc error: %s: string tunable %s after " + "process initialization\n", cur->name, action); +} + static void do_tunable_update_val (tunable_t *cur, const tunable_val_t *valp, const tunable_num_t *minp, @@ -78,6 +89,8 @@ do_tunable_update_val (tunable_t *cur, const tunable_val_t *valp, switch (cur->type.type_code) { case TUNABLE_TYPE_STRING: + if (__glibc_unlikely (tunables_strings_sealed)) + tunable_sealed_error (cur, "set"); cur->val.strval = valp->strval; cur->initialized = true; return; @@ -629,6 +642,10 @@ __tunable_get_val (tunable_id_t id, void *valp, tunable_callback_t callback) } case TUNABLE_TYPE_STRING: { + /* String tunable values are only valid during early startup; once + sealed they must not be read. */ + if (__glibc_unlikely (tunables_strings_sealed)) + tunable_sealed_error (cur, "read"); *((const struct tunable_str_t **) valp) = &cur->val.strval; break; } @@ -641,3 +658,29 @@ __tunable_get_val (tunable_id_t id, void *valp, tunable_callback_t callback) } rtld_hidden_def (__tunable_get_val) + +/* A string tunable value usually references the GLIBC_TUNABLES (or alias) + environment string, which lives in the environment block the kernel places + on the initial stack. That memory is owned by the application, which may + overwrite it (e.g. setproctitle), so the reference is only safe while no + application code has run. + A value coming from the system-wide cache is a private copy instead, but + it is sealed as well so that the lifetime rule does not depend on where + the value came from. + Drop the references so that a later access triggers a fatal error. */ +void +__tunable_seal_strings (void) +{ + for (int i = 0; i < tunables_list_size; i++) + { + tunable_t *cur = &tunable_list[i]; + + if (cur->type.type_code != TUNABLE_TYPE_STRING) + continue; + + cur->val.strval = (struct tunable_str_t) { NULL, 0 }; + } + + tunables_strings_sealed = true; +} +rtld_hidden_def (__tunable_seal_strings) diff --git a/elf/dl-tunables.h b/elf/dl-tunables.h index 3f34329614..bc703553c1 100644 --- a/elf/dl-tunables.h +++ b/elf/dl-tunables.h @@ -54,12 +54,14 @@ extern void __tunable_get_val (tunable_id_t, void *, tunable_callback_t); extern void __tunable_set_val (tunable_id_t, tunable_val_t *, tunable_num_t *, tunable_num_t *); extern void __tunable_get_default (tunable_id_t id, void *valp); +extern void __tunable_seal_strings (void); rtld_hidden_proto (__tunables_init) rtld_hidden_proto (__tunables_print) rtld_hidden_proto (__tunable_is_initialized) rtld_hidden_proto (__tunable_get_val) rtld_hidden_proto (__tunable_set_val) rtld_hidden_proto (__tunable_get_default) +rtld_hidden_proto (__tunable_seal_strings) /* Define TUNABLE_GET and TUNABLE_SET in short form if TOP_NAMESPACE and TUNABLE_NAMESPACE are defined. This is useful shorthand to get and set diff --git a/elf/rtld.c b/elf/rtld.c index fc053df858..b37c650631 100644 --- a/elf/rtld.c +++ b/elf/rtld.c @@ -1671,6 +1671,12 @@ dl_main (const ElfW(Phdr) *phdr, _dl_handle_execstack_tunable (); + /* Every string tunable has been consumed by now (init_cpu_features runs + from DL_PLATFORM_INIT, before dl_main). It precedes any code outside + the dynamic loader (the audit modules and IFUNC resolvers), and + RELRO. */ + __tunable_seal_strings (); + /* If the current libname is different from the SONAME, add the latter as well. */ { diff --git a/elf/tst-tunables-seal-static.c b/elf/tst-tunables-seal-static.c new file mode 100644 index 0000000000..143f972fdc --- /dev/null +++ b/elf/tst-tunables-seal-static.c @@ -0,0 +1 @@ +#include "tst-tunables-seal.c" diff --git a/elf/tst-tunables-seal.c b/elf/tst-tunables-seal.c new file mode 100644 index 0000000000..b0494fbd8e --- /dev/null +++ b/elf/tst-tunables-seal.c @@ -0,0 +1,75 @@ +/* Verify that string tunables are sealed after early startup. + Copyright (C) 2026 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + <https://www.gnu.org/licenses/>. */ + +/* This generic test is parameterized by TST_SEAL_TUNABLE_NAME, the name of + a glibc.cpu string tunable for the target ABI. */ + +#include <stdlib.h> +#include <support/check.h> + +#ifdef TST_SEAL_TUNABLE_NAME + +# include <string.h> +# include <unistd.h> +# include <support/capture_subprocess.h> + +# define TUNABLE_NAMESPACE cpu +# include <elf/dl-tunables.h> + +# define STRINGIFY(x) STRINGIFY1 (x) +# define STRINGIFY1(x) #x + +/* The full internal name of the tunable, e.g. "glibc.cpu.hwcaps", as + embedded in the fatal error message. */ +# define TST_SEAL_TUNABLE_FULLNAME \ + STRINGIFY (TOP_NAMESPACE) "." STRINGIFY (TUNABLE_NAMESPACE) "." \ + STRINGIFY (TST_SEAL_TUNABLE_NAME) + +static void +read_sealed_tunable (void *closure) +{ + TUNABLE_GET (TST_SEAL_TUNABLE_NAME, struct tunable_str_t *, NULL); + /* Not reached: the read above is a fatal error. Exit successfully so + that a missing diagnostic is caught as an unexpected exit status. */ + _exit (EXIT_SUCCESS); +} + +#endif /* TST_SEAL_TUNABLE_NAME */ + +static int +do_test (void) +{ +#ifndef TST_SEAL_TUNABLE_NAME + FAIL_UNSUPPORTED ("the target ABI has no glibc.cpu string tunable"); +#else + struct support_capture_subprocess result + = support_capture_subprocess (read_sealed_tunable, NULL); + + support_capture_subprocess_check (&result, "tst-tunables-seal", 127, + sc_allow_stderr); + TEST_VERIFY (strstr (result.err.buffer, + "Fatal glibc error: " TST_SEAL_TUNABLE_FULLNAME + ": string tunable read after process initialization") + != NULL); + + support_capture_subprocess_free (&result); + return 0; +#endif +} + +#include <support/test-driver.c> diff --git a/manual/README.tunables b/manual/README.tunables index 594879397b..0c191e1809 100644 --- a/manual/README.tunables +++ b/manual/README.tunables @@ -132,6 +132,22 @@ The tunable list is set as read-only after the dynamic linker relocates itself, so setting tunable values must be limited only to tunables within the dynamic linker, that too before relocation. +STRING TUNABLES ARE STARTUP-ONLY +-------------------------------- + +The value of a STRING tunable usually points into the GLIBC_TUNABLES (or +alias) environment variable, which lives in the environment block the kernel +places on the initial stack. That memory is owned by the application, which +may overwrite it (e.g. setproctitle), so the value is only valid while no +code outside of startup has run. + +The values are therefore sealed by __tunable_seal_strings, called from +dl_main for dynamically linked programs and from __libc_start_main for +static ones. Both calls happen after init_cpu_features, the only consumer +of string tunables, and before audit modules, IFUNC resolvers or +constructors can run. Reading or setting a string tunable after that point +is a glibc internal error and terminates the process. + FUTURE WORK ----------- diff --git a/sysdeps/aarch64/Makefile b/sysdeps/aarch64/Makefile index 52ac85a75d..87ce51c821 100644 --- a/sysdeps/aarch64/Makefile +++ b/sysdeps/aarch64/Makefile @@ -50,6 +50,9 @@ tests-internal += \ tst-ifunc-arg-4 \ # tests-internal +CFLAGS-tst-tunables-seal.c += -DTST_SEAL_TUNABLE_NAME=hwcaps +tst-tunables-seal-TUNABLES += glibc.cpu.hwcaps=-midr + tests += \ tst-vpcs \ # tests diff --git a/sysdeps/loongarch/Makefile b/sysdeps/loongarch/Makefile index b00c090faa..5818755f05 100644 --- a/sysdeps/loongarch/Makefile +++ b/sysdeps/loongarch/Makefile @@ -19,6 +19,9 @@ sysdep-dl-routines += \ gen-as-const-headers += \ dl-link.sym \ # gen-as-const-headers + +CFLAGS-tst-tunables-seal.c += -DTST_SEAL_TUNABLE_NAME=hwcaps +tst-tunables-seal-TUNABLES += glibc.cpu.hwcaps=-LASX endif ifeq ($(subdir),csu) diff --git a/sysdeps/powerpc/Makefile b/sysdeps/powerpc/Makefile index 5cdb64f29b..fadd45807f 100644 --- a/sysdeps/powerpc/Makefile +++ b/sysdeps/powerpc/Makefile @@ -17,6 +17,9 @@ $(objpfx)tst-tlsopt-powerpc: $(objpfx)mod-tlsopt-powerpc.so tests-static += tst-cache-ppc-static tests-internal += tst-cache-ppc-static +CFLAGS-tst-tunables-seal.c += -DTST_SEAL_TUNABLE_NAME=hwcaps +tst-tunables-seal-TUNABLES += glibc.cpu.hwcaps=-arch_3_1 + ifeq (yes,$(build-shared)) modules-names += mod-cache-ppc tests += tst-cache-ppc tst-cache-ppc-static-dlopen diff --git a/sysdeps/s390/Makefile b/sysdeps/s390/Makefile index 481e834792..309425c7e1 100644 --- a/sysdeps/s390/Makefile +++ b/sysdeps/s390/Makefile @@ -165,6 +165,9 @@ tst-dl-runtime-resolve-audit-ENV = $(env-audit) tst-dl-runtime-profile-noaudit-ENV = $(env-profile) tst-dl-runtime-profile-audit-ENV = $(env-profile) $(env-audit) endif + +CFLAGS-tst-tunables-seal.c += -DTST_SEAL_TUNABLE_NAME=hwcaps +tst-tunables-seal-TUNABLES += glibc.cpu.hwcaps=z13 endif ifeq ($(subdir),string) diff --git a/sysdeps/x86/Makefile b/sysdeps/x86/Makefile index b4434deb0c..1c64e224ad 100644 --- a/sysdeps/x86/Makefile +++ b/sysdeps/x86/Makefile @@ -111,6 +111,9 @@ tst-ifunc-isa-2-ENV = GLIBC_TUNABLES=glibc.cpu.hwcaps=-SSE4_2,-AVX,-AVX2,-AVX512 tst-ifunc-isa-2-static-ENV = $(tst-ifunc-isa-2-ENV) tst-hwcap-tunables-ARGS = -- $(host-test-program-cmd) +CFLAGS-tst-tunables-seal.c += -DTST_SEAL_TUNABLE_NAME=hwcaps +tst-tunables-seal-TUNABLES += glibc.cpu.hwcaps=-AVX2 + CFLAGS-tst-gnu2-tls2.c += -msse2 CFLAGS-tst-gnu2-tls2mod0.c += -msse2 -mtune=haswell CFLAGS-tst-gnu2-tls2mod1.c += -msse2 -mtune=haswell