[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.