elf: Make string tunables startup-only

Message ID 20260617193627.1570884-1-adhemerval.zanella@linaro.org (mailing list archive)
State New
Headers
Series elf: Make string tunables startup-only |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Test passed
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_check--master-arm success Test passed

Commit Message

Adhemerval Zanella June 17, 2026, 7:35 p.m. UTC
  String tunable values reference 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 references are only safe while no
application code has run.  Until now this was an undocumented convention:
every string tunable happened to be consumed by init_cpu_features during
early startup.

Make the lifetime explicit and enforced without copying the value or
allocating any memory.  Add __tunable_seal_strings, which drops every
string tunable reference (and marks each string tunable sealed) once early
startup is complete, and have __tunable_get_val report a fatal error when
a sealed string tunable is read.

The seal is applied after the only string tunable consumer
(init_cpu_features, run from DL_PLATFORM_INIT before dl_main, or from
ARCH_INIT_CPU_FEATURES in __libc_start_main) but before any user code can
run.  In particular it precedes the relocation phase, where IFUNC
resolvers fire, and the constructors run later from _dl_init; it also
precedes RELRO, which freezes the tunable list.

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.
---
 csu/libc-start.c               |  4 ++
 elf/Makefile                   |  1 +
 elf/dl-tunable-types.h         |  3 ++
 elf/dl-tunables.c              | 29 +++++++++++++
 elf/dl-tunables.h              |  2 +
 elf/rtld.c                     |  4 ++
 elf/tst-tunables-seal-static.c |  1 +
 elf/tst-tunables-seal.c        | 75 ++++++++++++++++++++++++++++++++++
 sysdeps/aarch64/Makefile       |  3 ++
 sysdeps/loongarch/Makefile     |  3 ++
 sysdeps/powerpc/Makefile       |  3 ++
 sysdeps/s390/Makefile          |  3 ++
 sysdeps/x86/Makefile           | 13 ++++++
 13 files changed, 144 insertions(+)
 create mode 100644 elf/tst-tunables-seal-static.c
 create mode 100644 elf/tst-tunables-seal.c
  

Comments

Andreas Schwab June 18, 2026, 7:59 a.m. UTC | #1
On Jun 17 2026, Adhemerval Zanella wrote:

> @@ -460,6 +461,12 @@ __tunable_get_val (tunable_id_t id, void *valp, tunable_callback_t callback)
>  	}
>      case TUNABLE_TYPE_STRING:
>  	{
> +	  /* String tunables reference the environment block and are only
> +	     valid during early startup; once sealed they must not be read. */
> +	  if (__glibc_unlikely (cur->sealed))
> +	    _dl_fatal_printf ("Inconsistency detected: trying to read the %s "
> +			      "string tunable after process initialization\n",
> +			      cur->name);

Why do you need the sealed member?  Wouldn't it be enough to check for
NULL in strval?
  
Adhemerval Zanella June 18, 2026, noon UTC | #2
On 18/06/26 04:59, Andreas Schwab wrote:
> On Jun 17 2026, Adhemerval Zanella wrote:
> 
>> @@ -460,6 +461,12 @@ __tunable_get_val (tunable_id_t id, void *valp, tunable_callback_t callback)
>>  	}
>>      case TUNABLE_TYPE_STRING:
>>  	{
>> +	  /* String tunables reference the environment block and are only
>> +	     valid during early startup; once sealed they must not be read. */
>> +	  if (__glibc_unlikely (cur->sealed))
>> +	    _dl_fatal_printf ("Inconsistency detected: trying to read the %s "
>> +			      "string tunable after process initialization\n",
>> +			      cur->name);
> 
> Why do you need the sealed member?  Wouldn't it be enough to check for
> NULL in strval?
> 

It would prevent to use a TUNABLE_GET{_FULL} without first checking TUNABLE_IS_INITIALIZED.
I can change all TUNABLE_GET{_FULL} usage to do so, but I don't see this much as 
improvement. The internal ABI is already a bit clunky.
  

Patch

diff --git a/csu/libc-start.c b/csu/libc-start.c
index 1614e932c29..0d4dbe771c0 100644
--- a/csu/libc-start.c
+++ b/csu/libc-start.c
@@ -271,6 +271,10 @@  LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
 
   ARCH_INIT_CPU_FEATURES ();
 
+  /* Seal the string tunables now, before any user code runs, after this their
+     values can no longer alias the application-owned environment block.  */
+  __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 65144282941..476f6a05b2a 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -566,6 +566,7 @@  tests-internal += \
   tst-tls6 \
   tst-tls7 \
   tst-tls8 \
+  tst-tunables-seal \
   unload \
   unload2 \
   # tests-internal
diff --git a/elf/dl-tunable-types.h b/elf/dl-tunable-types.h
index f421c01c260..2f2b25ec898 100644
--- a/elf/dl-tunable-types.h
+++ b/elf/dl-tunable-types.h
@@ -68,6 +68,9 @@  struct _tunable
   /* Compatibility elements.  */
   const char env_alias[TUNABLE_ALIAS_MAX]; /* The compatibility environment
 					   variable name.  */
+  /* Set once a string tunable must no longer be read (since it points to
+     writable memory in the process).  */
+  bool sealed;
 };
 
 typedef struct _tunable tunable_t;
diff --git a/elf/dl-tunables.c b/elf/dl-tunables.c
index 5c65e8b4583..91238349506 100644
--- a/elf/dl-tunables.c
+++ b/elf/dl-tunables.c
@@ -23,6 +23,7 @@ 
 # pragma GCC visibility push(hidden)
 #endif
 #include <startup.h>
+#include <assert.h>
 #include <stdint.h>
 #include <stdbool.h>
 #include <unistd.h>
@@ -460,6 +461,12 @@  __tunable_get_val (tunable_id_t id, void *valp, tunable_callback_t callback)
 	}
     case TUNABLE_TYPE_STRING:
 	{
+	  /* String tunables reference the environment block and are only
+	     valid during early startup; once sealed they must not be read. */
+	  if (__glibc_unlikely (cur->sealed))
+	    _dl_fatal_printf ("Inconsistency detected: trying to read the %s "
+			      "string tunable after process initialization\n",
+			      cur->name);
 	  *((const struct tunable_str_t **) valp) = &cur->val.strval;
 	  break;
 	}
@@ -472,3 +479,25 @@  __tunable_get_val (tunable_id_t id, void *valp, tunable_callback_t callback)
 }
 
 rtld_hidden_def (__tunable_get_val)
+
+/* String tunable values reference 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 references are only safe while
+   no application code has run.
+   Drop the references so a later __tunable_get_val 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 };
+      cur->sealed = true;
+    }
+}
+rtld_hidden_def (__tunable_seal_strings)
diff --git a/elf/dl-tunables.h b/elf/dl-tunables.h
index 45aeed47bc0..14b4c781bc7 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 ebb5446681a..5231c114ace 100644
--- a/elf/rtld.c
+++ b/elf/rtld.c
@@ -2251,6 +2251,10 @@  dl_main (const ElfW(Phdr) *phdr,
   /* If we are profiling we also must do lazy relocation.  */
   GLRO(dl_lazy) |= consider_profiling;
 
+  /* Seal the string tunables now, before any user code runs, after this their
+     values can no longer alias the application-owned environment block.  */
+  __tunable_seal_strings ();
+
   /* If libc.so has been loaded, relocate it early, after the dynamic
      loader itself.  The initial self-relocation of ld.so should be
      sufficient for IFUNC resolvers in libc.so.  */
diff --git a/elf/tst-tunables-seal-static.c b/elf/tst-tunables-seal-static.c
new file mode 100644
index 00000000000..143f972fdc1
--- /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 00000000000..e81236872bf
--- /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,
+		       "trying to read the " TST_SEAL_TUNABLE_FULLNAME
+		       " string tunable after process initialization")
+	       != NULL);
+
+  support_capture_subprocess_free (&result);
+  return 0;
+#endif
+}
+
+#include <support/test-driver.c>
diff --git a/sysdeps/aarch64/Makefile b/sysdeps/aarch64/Makefile
index 52ac85a75db..fc26750bd12 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=name
+tst-tunables-seal-ENV = GLIBC_TUNABLES=glibc.cpu.name=generic
+
 tests += \
   tst-vpcs \
   # tests
diff --git a/sysdeps/loongarch/Makefile b/sysdeps/loongarch/Makefile
index b00c090faaa..4e5618ddab9 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-ENV = GLIBC_TUNABLES=glibc.cpu.hwcaps=-LASX
 endif
 
 ifeq ($(subdir),csu)
diff --git a/sysdeps/powerpc/Makefile b/sysdeps/powerpc/Makefile
index 5cdb64f29ba..5a26d5990eb 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-ENV = GLIBC_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 481e8347925..644214d4955 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-ENV = GLIBC_TUNABLES=glibc.cpu.hwcaps=z13
 endif
 
 ifeq ($(subdir),string)
diff --git a/sysdeps/x86/Makefile b/sysdeps/x86/Makefile
index b16f4a34049..73705fcce18 100644
--- a/sysdeps/x86/Makefile
+++ b/sysdeps/x86/Makefile
@@ -29,6 +29,14 @@  tests += \
   tst-get-cpu-features-static \
   tst-hwcap-tunables \
 # tests
+
+tests-internal += \
+  tst-tunables-seal-static \
+# tests-internal
+
+tests-static += \
+  tst-tunables-seal-static \
+# tests-static
 ifneq (no,$(have-test-mtls-descriptor))
 tests += \
   tst-gnu2-tls2-x86-noxsave \
@@ -100,6 +108,11 @@  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
+CFLAGS-tst-tunables-seal-static.c += -DTST_SEAL_TUNABLE_NAME=hwcaps
+tst-tunables-seal-ENV = GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2
+tst-tunables-seal-static-ENV = $(tst-tunables-seal-ENV)
+
 CFLAGS-tst-gnu2-tls2.c += -msse2
 CFLAGS-tst-gnu2-tls2mod0.c += -msse2 -mtune=haswell
 CFLAGS-tst-gnu2-tls2mod1.c += -msse2 -mtune=haswell