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.
This commit is contained in:
Adhemerval Zanella
2026-08-24 13:49:12 -03:00
parent 53ec26f173
commit 3923b80921
13 changed files with 167 additions and 0 deletions
+4
View File
@@ -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
+5
View File
@@ -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
+43
View File
@@ -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)
+2
View File
@@ -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
+6
View File
@@ -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. */
{
+1
View File
@@ -0,0 +1 @@
#include "tst-tunables-seal.c"
+75
View File
@@ -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>
+16
View File
@@ -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
-----------
+3
View File
@@ -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
+3
View File
@@ -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)
+3
View File
@@ -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
+3
View File
@@ -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)
+3
View File
@@ -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