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
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
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?
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.
@@ -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
@@ -566,6 +566,7 @@ tests-internal += \
tst-tls6 \
tst-tls7 \
tst-tls8 \
+ tst-tunables-seal \
unload \
unload2 \
# tests-internal
@@ -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;
@@ -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)
@@ -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
@@ -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. */
new file mode 100644
@@ -0,0 +1 @@
+#include "tst-tunables-seal.c"
new file mode 100644
@@ -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>
@@ -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
@@ -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)
@@ -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
@@ -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)
@@ -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