[v6,4/4] Add system-wide tunables: Filters

Message ID xnzf47s084.fsf@greed.delorie.com (mailing list archive)
State New
Headers
Series Add system-wide tunables |

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-arm success Test passed
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Test passed

Commit Message

DJ Delorie March 17, 2026, 1:39 a.m. UTC
  Add support for [proc:*] syntax where * matches /proc/self/exe
(fallback: argv[0] unless AT_SECURE).  Tunables after such a
line are limited to matching processes.

Note that this filter is reset when including a file or at
end of file.

If the filename starts with a slash (example: [proc:/bin/foo]) the
full path must match.  If not (example: [proc:foo]) the basename is
matched.

Add support for filtering out AT_SECURE or non-AT_SECURE binaries:

  $glibc.only-for.unsecure-binaries=1
  @glibc.only-for.secure-binaries=1
---
 csu/libc-start.c                        |   2 +-
 elf/Makefile                            |   4 +
 elf/cache.c                             |   3 +-
 elf/dl-tunables.c                       |  80 +++++++++++++++-
 elf/dl-tunables.h                       |   2 +-
 elf/ldconfig-parse.c                    |   6 +-
 elf/ldconfig.c                          |   3 +
 elf/tst-tunconf1.c                      |  36 +++++++
 elf/tst-tunconf1.root/etc/tunables.conf |  15 +++
 elf/tst-tunconf1.root/ldconfig.run      |   0
 elf/tst-tunconf1.root/postclean.req     |   0
 elf/tunconf.c                           | 122 +++++++++++++++++++++++-
 elf/tunconf.h                           |   3 +
 sysdeps/mach/hurd/dl-sysdep.c           |   2 +-
 sysdeps/unix/sysv/linux/dl-sysdep.c     |   2 +-
 15 files changed, 265 insertions(+), 15 deletions(-)
 create mode 100644 elf/tst-tunconf1.c
 create mode 100644 elf/tst-tunconf1.root/etc/tunables.conf
 create mode 100644 elf/tst-tunconf1.root/ldconfig.run
 create mode 100644 elf/tst-tunconf1.root/postclean.req
  

Comments

Yury Khrustalev March 25, 2026, 12:10 p.m. UTC | #1
Hello DJ,

On Mon, Mar 16, 2026 at 09:39:39PM -0400, DJ Delorie wrote:
> 
> Add support for [proc:*] syntax where * matches /proc/self/exe
> (fallback: argv[0] unless AT_SECURE).  Tunables after such a
> line are limited to matching processes.
> 
> Note that this filter is reset when including a file or at
> end of file.
> 
> If the filename starts with a slash (example: [proc:/bin/foo]) the
> full path must match.  If not (example: [proc:foo]) the basename is
> matched.
> 
> Add support for filtering out AT_SECURE or non-AT_SECURE binaries:
> 
>   $glibc.only-for.unsecure-binaries=1
>   @glibc.only-for.secure-binaries=1

We probably need a bit information about /etc/tunables.conf in the
manual.

> 
> ...
>
> diff --git a/csu/libc-start.c b/csu/libc-start.c
> index 1c58561bce..ae36170045 100644
> --- a/csu/libc-start.c
> +++ b/csu/libc-start.c
> @@ -264,7 +264,7 @@ LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
>    _dl_aux_init (auxvec);
>  # endif
>  
> -  __tunables_init (__environ);
> +  __tunables_init (__environ, argv);

Nit: For some deeply intrinsic reason I prefer argv to go first.

>  
>    ARCH_INIT_CPU_FEATURES ();
>  
> diff --git a/elf/Makefile b/elf/Makefile
> index 5398fe0d2c..4fbd03cefc 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -323,6 +323,7 @@ tests-internal := \
>    $(tests-static-internal) \
>    tst-tls1 \
>    tst-tls_tp_offset \
> +  tst-tunconf1 \
>    # tests-internal
>  
>  tests-static := $(tests-static-normal) $(tests-static-internal)
> @@ -333,6 +334,8 @@ tests-static += \
>    tst-tls9-static \
>    # tests-static
>  
> +tst-tunconf1-ENV = GLIBC_TUNABLES=glibc.malloc.tcache_count=5

If I add glibc.malloc.tcache_max=42 to this line, the test fails.
It looks like the environment variable overrides tunables.conf,
is this intentional?

In this test we probably should have the same tunable in both
environment and tunables.conf, and the test should check that
whatever has higher priority applies.

> 
> ...
> 
> diff --git a/elf/dl-tunables.c b/elf/dl-tunables.c
> index 29fcd4504b..acee6eb872 100644
> --- a/elf/dl-tunables.c
> +++ b/elf/dl-tunables.c
> @@ -291,13 +291,22 @@ parse_tunables (const char *valstring)
>     ENV_ALIAS to find values.  Later we will also use the tunable names to find
>     values.  */
>  void
> -__tunables_init (char **envp)
> +__tunables_init (char **envp, char **argv)
> 
> ...
> 
> +
> +	  /* Apply selected filter, if any.  */
> +	  switch (tec->flags & TUNCONF_FLAG_FILTER) {
> +	  case TUNCONF_FILTER_PERPROC:
> +	    /* Perform one-time calculations that aren't needed if we
> +	       don't use this filter.  */
> +	    if (prog_name_len == -1)
> +	      {
> +		ssize_t n = readlink ("/proc/self/exe",
> +				      exebuf, sizeof (exebuf) - 1);

Do we have to do this inside the for loop?

Thanks,
Yury
  
DJ Delorie March 25, 2026, 6:36 p.m. UTC | #2
Yury Khrustalev <yury.khrustalev@arm.com> writes:
>>   $glibc.only-for.unsecure-binaries=1
>>   @glibc.only-for.secure-binaries=1
>
> We probably need a bit information about /etc/tunables.conf in the
> manual.

Agreed.  Writing up all the documentation is the next task on my list,
but I'm trying to avoid this patch rotting for a couple more years while
waiting for every little feature to be haggled over, reviewed, and
committed.  This patch set is step 1 ;-)

>> -  __tunables_init (__environ);
>> +  __tunables_init (__environ, argv);
>
> Nit: For some deeply intrinsic reason I prefer argv to go first.

I won't put in argc even if you ask ;-)

>> +tst-tunconf1-ENV = GLIBC_TUNABLES=glibc.malloc.tcache_count=5
>
> If I add glibc.malloc.tcache_max=42 to this line, the test fails.
> It looks like the environment variable overrides tunables.conf,
> is this intentional?

Yes!  At this point, all the security implementation is missing (aside
from AT_SECURE, because that was easy).  The "problem" with the security
model I chose is that each tunable needs some inherent logic about
what's "more secure".  For numeric values, is a bigger one more secure,
or a smaller one?  What about hwcaps?  Is adding a hwcap more secure, or
removing one?  Does it depend on which hwcap?

I put the syntax in because it affects the file format and I didn't want
to have to change that later.  I might end up skipping the "more secure"
implementation and just do the "override or not" options, but even the,
what does "do not override" mean for hwcaps?  Is it per-cap or the whole
string?

Either way, it (like the docs and man pages) was intentionally left as a
"next step".

>> +	  /* Apply selected filter, if any.  */
>> +	  switch (tec->flags & TUNCONF_FLAG_FILTER) {
>> +	  case TUNCONF_FILTER_PERPROC:
>> +	    /* Perform one-time calculations that aren't needed if we
>> +	       don't use this filter.  */
>> +	    if (prog_name_len == -1)
>> +	      {
>> +		ssize_t n = readlink ("/proc/self/exe",
>> +				      exebuf, sizeof (exebuf) - 1);
>
> Do we have to do this inside the for loop?

We only do it once, and only if the tunable has that filter.  I assume
most systems will not even have a tunables.conf, much less a per-program
filter, so I didn't want to do file I/O if not needed.
  
Yury Khrustalev March 26, 2026, 9:55 a.m. UTC | #3
On Wed, Mar 25, 2026 at 02:36:13PM -0400, DJ Delorie wrote:
> Yury Khrustalev <yury.khrustalev@arm.com> writes:
> >>   $glibc.only-for.unsecure-binaries=1
> >>   @glibc.only-for.secure-binaries=1
> >
> > We probably need a bit information about /etc/tunables.conf in the
> > manual.
> 
> Agreed.  Writing up all the documentation is the next task on my list,
> but I'm trying to avoid this patch rotting for a couple more years while
> waiting for every little feature to be haggled over, reviewed, and
> committed.  This patch set is step 1 ;-)

Agreed.

> 
> >> -  __tunables_init (__environ);
> >> +  __tunables_init (__environ, argv);
> >
> > Nit: For some deeply intrinsic reason I prefer argv to go first.
> 
> I won't put in argc even if you ask ;-)
> 
> >> +tst-tunconf1-ENV = GLIBC_TUNABLES=glibc.malloc.tcache_count=5
> >
> > If I add glibc.malloc.tcache_max=42 to this line, the test fails.
> > It looks like the environment variable overrides tunables.conf,
> > is this intentional?
> 
> Yes!  At this point, all the security implementation is missing (aside
> from AT_SECURE, because that was easy).

Noted.

> The "problem" with the security
> model I chose is that each tunable needs some inherent logic about
> what's "more secure".  For numeric values, is a bigger one more secure,
> or a smaller one?  What about hwcaps?  Is adding a hwcap more secure, or
> removing one?  Does it depend on which hwcap?

OK, so I prefer to think about this in a different way. Whatever mechanism
we implement here, we should not have to understand the details of each
tunable. This would be Sisyphean effort. Some tunables are not supposed
to have "monotonicity" of their values.

We're adding 3rd way to provide a value for a tunable. So far we have:

 0) Default values
 1) Values from the GLIBC_TUNABLES environment variable.

Now we will have a value that comes from /etc/tunables.conf.

So, regardless of what each value actually means, we should decide
which source of this value is to be used where there are more than
one value provided for this tunable.

For example, right now, the value from GLIBC_TUNABLES overrides
the default value. Now, which one has higher priority, env var or
system-wide config?

I have an opinion here but I don't want to impose it :)

> I put the syntax in because it affects the file format and I didn't want
> to have to change that later.  I might end up skipping the "more secure"
> implementation and just do the "override or not" options, but even the,
> what does "do not override" mean for hwcaps?  Is it per-cap or the whole
> string?
> 
> Either way, it (like the docs and man pages) was intentionally left as a
> "next step".

I think we should decide on precedence at this point. Since it's not
dependent on which value is "better" or "more secure", we should be able
to do this.

> 
> >> +	  /* Apply selected filter, if any.  */
> >> +	  switch (tec->flags & TUNCONF_FLAG_FILTER) {
> >> +	  case TUNCONF_FILTER_PERPROC:
> >> +	    /* Perform one-time calculations that aren't needed if we
> >> +	       don't use this filter.  */
> >> +	    if (prog_name_len == -1)
> >> +	      {
> >> +		ssize_t n = readlink ("/proc/self/exe",
> >> +				      exebuf, sizeof (exebuf) - 1);
> >
> > Do we have to do this inside the for loop?
> 
> We only do it once, and only if the tunable has that filter.  I assume
> most systems will not even have a tunables.conf, much less a per-program
> filter, so I didn't want to do file I/O if not needed.

Ah, thanks for clarification, I haven't followed the code properly. What
you do makes sense.

Cheers,
Yury
  
DJ Delorie March 26, 2026, 5:38 p.m. UTC | #4
Yury Khrustalev <yury.khrustalev@arm.com> writes:
> For example, right now, the value from GLIBC_TUNABLES overrides
> the default value. Now, which one has higher priority, env var or
> system-wide config?
>
> I have an opinion here but I don't want to impose it :)

I think the config needs to be able to choose from these (ignoring the
defaults, for now, because we *always* have defaults):

  1) system-wide
  2) env var

  1) system-wide only

  1) env var only (by not mentioning it in tunables.conf)

The only other non-Sisyphean option would be:

  1) system-wide
  2) env var if numerically larger (closer to +Inf)

  1) system-wide
  2) env var if numerically smaller (closer to -Inf)

but that obviously won't include non-numeric tunables.
  
Yury Khrustalev March 30, 2026, 9:05 a.m. UTC | #5
On Thu, Mar 26, 2026 at 01:38:50PM -0400, DJ Delorie wrote:
> 
> Yury Khrustalev <yury.khrustalev@arm.com> writes:
> > For example, right now, the value from GLIBC_TUNABLES overrides
> > the default value. Now, which one has higher priority, env var or
> > system-wide config?
> >
> > I have an opinion here but I don't want to impose it :)
> 
> I think the config needs to be able to choose from these (ignoring the
> defaults, for now, because we *always* have defaults):
> 
>   1) system-wide
>   2) env var
> 
>   1) system-wide only
> 
>   1) env var only (by not mentioning it in tunables.conf)
> 
> The only other non-Sisyphean option would be:
> 
>   1) system-wide
>   2) env var if numerically larger (closer to +Inf)

We should not take into account values of tunables. This would be a
mistake: like I said, some tunables don't have any monotonicity of
their values.

>   1) system-wide
>   2) env var if numerically smaller (closer to -Inf)

Ditto

> 
> but that obviously won't include non-numeric tunables.
> 

One more reason not to consider the values of tunables.

I think we should use the following approach:

1) Use default if not overridden (via env var or tunables.conf).
2) Env var overrides tunables.conf unless tunables.conf has special
   "final" marker for this tunable.

Plain and simple. Again, actual values of the tunables are irrelevant.
  
DJ Delorie March 30, 2026, 4:03 p.m. UTC | #6
Yury Khrustalev <yury.khrustalev@arm.com> writes:
> We should not take into account values of tunables. This would be a
> mistake: like I said, some tunables don't have any monotonicity of
> their values.

True, but some tunables *do* and why not let the admin have the option?

Consider:

* Admin limits the number of malloc heaps to 16 because glibc
  autodetects the number of CPUs wrong.

* User further limits it to 1 to avoid heap-related fragmentation.
  
Yury Khrustalev March 30, 2026, 4:59 p.m. UTC | #7
On Mon, Mar 30, 2026 at 12:03:36PM -0400, DJ Delorie wrote:
> Yury Khrustalev <yury.khrustalev@arm.com> writes:
> > We should not take into account values of tunables. This would be a
> > mistake: like I said, some tunables don't have any monotonicity of
> > their values.
> 
> True, but some tunables *do* and why not let the admin have the option?

Let's do it step by step. What I propose will cover all possible
scenarios: admin enforces a value (1), recommends a value (2), doesn't
care (3).

> 
> Consider:
> 
> * Admin limits the number of malloc heaps to 16 because glibc
>   autodetects the number of CPUs wrong.

This will complicate things and we would have to assess how useful this
use case is going to be before we try to implement it.

Plus, we probably should avoid making tunables.conf Turing-complete,
otherwise it will become a vulnerability of its own :) If you allow to
narrow down the accepted range, you'd have to support both upper and
lower boundary. This is doable, but I'd rather add this in another
patch, but only if there is some need for this.

> 
> * User further limits it to 1 to avoid heap-related fragmentation.

Also, as far as setting tunables is concerned, there is no notion of
"limiting" anything: we set a value if we want to use a non-default one.

In a nutshell, let's keep it simple. We could try adding scripting
support, GPU acceleration, and distributed compute a few decayed protons
later :)
  
DJ Delorie March 30, 2026, 5:11 p.m. UTC | #8
Yury Khrustalev <yury.khrustalev@arm.com> writes:
> Let's do it step by step. What I propose will cover all possible
> scenarios: admin enforces a value (1), recommends a value (2), doesn't
> care (3).

Sure, I'm ok with only implementing the "overridable or not" feature at
first.  I'd like to leave the two-bit field to allow for two other
options later, though.  I think we can spare a bit for unseen future
needs, and I still think allowing an admin to permit bigger/smaller
values, has some value.

>> Consider:
>> 
>> * Admin limits the number of malloc heaps to 16 because glibc
>>   autodetects the number of CPUs wrong.
>
> This will complicate things and we would have to assess how useful this
> use case is going to be before we try to implement it.

I wasn't trying to justify the use case, just trying to demonstrate why
a bigger/smaller restriction might be useful to an admin.

> Plus, we probably should avoid making tunables.conf Turing-complete,

I offered to put a full LR(1) language in there with a byte code
interpreter, but Florian quickly stopped me ;-)

(I've done it before.  It's not that difficult.  Let me whip up a
patch... no?  are you sure?  well, ok...  ;)

> otherwise it will become a vulnerability of its own :) If you allow to
> narrow down the accepted range, you'd have to support both upper and
> lower boundary. This is doable, but I'd rather add this in another
> patch, but only if there is some need for this.

Later is OK.  Simple is good, I don't want to do a range, just
increase/decrease.

>> * User further limits it to 1 to avoid heap-related fragmentation.
>
> Also, as far as setting tunables is concerned, there is no notion of
> "limiting" anything: we set a value if we want to use a non-default one.

I was referring to the effect of the tunable.  That tunable
(malloc.arena_max) is a maximum, so it's action is to limit.
  

Patch

diff --git a/csu/libc-start.c b/csu/libc-start.c
index 1c58561bce..ae36170045 100644
--- a/csu/libc-start.c
+++ b/csu/libc-start.c
@@ -264,7 +264,7 @@  LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
   _dl_aux_init (auxvec);
 # endif
 
-  __tunables_init (__environ);
+  __tunables_init (__environ, argv);
 
   ARCH_INIT_CPU_FEATURES ();
 
diff --git a/elf/Makefile b/elf/Makefile
index 5398fe0d2c..4fbd03cefc 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -323,6 +323,7 @@  tests-internal := \
   $(tests-static-internal) \
   tst-tls1 \
   tst-tls_tp_offset \
+  tst-tunconf1 \
   # tests-internal
 
 tests-static := $(tests-static-normal) $(tests-static-internal)
@@ -333,6 +334,8 @@  tests-static += \
   tst-tls9-static \
   # tests-static
 
+tst-tunconf1-ENV = GLIBC_TUNABLES=glibc.malloc.tcache_count=5
+
 static-dlopen-environment = \
   LD_LIBRARY_PATH=$(ld-library-path):$(common-objpfx)dlfcn
 tst-tls9-static-ENV = $(static-dlopen-environment)
@@ -563,6 +566,7 @@  tests-container += \
   tst-pldd \
   tst-preload-pthread-libc \
   tst-rootdir \
+  tst-tunconf1 \
   # tests-container
 
 test-srcs = \
diff --git a/elf/cache.c b/elf/cache.c
index e798f7e2d8..2809f255f5 100644
--- a/elf/cache.c
+++ b/elf/cache.c
@@ -300,9 +300,10 @@  print_extensions (struct cache_extension_all_loaded *ext,
 	     thc->signature, thc->version, thc->num_tunables);
       for (i = 0; i < count; ++ i)
 	{
-	  printf("  [%d] %s : %s [flags 0x%08x",
+	  printf("  [%d] %s (%d) : %s [flags 0x%08x",
 		 i,
 		 cache_data + tec[i].name_offset,
+		 tec[i].tunable_id,
 		 cache_data + tec[i].value_offset,
 		 tec[i].flags);
 	  if (tec[i].flag_offset != 0)
diff --git a/elf/dl-tunables.c b/elf/dl-tunables.c
index 29fcd4504b..acee6eb872 100644
--- a/elf/dl-tunables.c
+++ b/elf/dl-tunables.c
@@ -291,13 +291,22 @@  parse_tunables (const char *valstring)
    ENV_ALIAS to find values.  Later we will also use the tunable names to find
    values.  */
 void
-__tunables_init (char **envp)
+__tunables_init (char **envp, char **argv)
 {
   char *envname = NULL;
   char *envval = NULL;
   char **prev_envp = envp;
 
 #if defined(SHARED) && defined (USE_LDCONFIG)
+  const char *prog_name = (argv && argv[0]) ? argv[0] : "";
+  int prog_name_len = -1;
+  const char *base_name = NULL;
+  int base_name_len = -1;
+#ifdef PATH_MAX
+  char exebuf[PATH_MAX];
+#else
+  char exebuf[256];
+#endif
   const struct tunable_header_cached *thc;
   const char *td;
 
@@ -330,9 +339,70 @@  __tunables_init (char **envp)
 	      if (tid == -1)
 		continue;
 	    }
-	  /* At this point, TID is valid for the tunable we want.  See
-	     if the parsed type matches the desired type.  */
-
+	  /* At this point, TID is valid for the tunable we want.  */
+
+	  if (tec->flags & TUNCONF_EXCLUDE_SECURE && __libc_enable_secure)
+	    goto skip_due_to_filter;
+	  if (tec->flags & TUNCONF_EXCLUDE_UNSECURE && !__libc_enable_secure)
+	    goto skip_due_to_filter;
+
+	  /* Apply selected filter, if any.  */
+	  switch (tec->flags & TUNCONF_FLAG_FILTER) {
+	  case TUNCONF_FILTER_PERPROC:
+	    /* Perform one-time calculations that aren't needed if we
+	       don't use this filter.  */
+	    if (prog_name_len == -1)
+	      {
+		ssize_t n = readlink ("/proc/self/exe",
+				      exebuf, sizeof (exebuf) - 1);
+		if (n > 0 && n < sizeof(exebuf)-1)
+		  {
+		    /* If /proc/self/exe exists and we can read it,
+		       it's more reliable than argv[] so use it.  */
+		    exebuf[n] = '\0';
+		    prog_name = exebuf;
+		  }
+		else if (__libc_enable_secure)
+		  prog_name = NULL;
+		if (prog_name != NULL)
+		  {
+		    const char *slash = NULL, *cp;
+		    for (cp = prog_name; *cp; ++ cp)
+		      if (*cp == '/')
+			slash = cp;
+		    if (slash)
+		      base_name = slash + 1;
+		    else
+		      base_name = prog_name;
+		    prog_name_len = strlen (prog_name);
+		    base_name_len = strlen (base_name);
+		  }
+	      }
+	    /* prog_name and the cached string are both NUL terminated.  */
+	    if (prog_name)
+	      {
+		if (((const char *)(td + tec->flag_offset))[0] == '/')
+		  {
+		    if (memcmp (prog_name, td + tec->flag_offset, prog_name_len) != 0)
+		      goto skip_due_to_filter;
+		  }
+		else
+		  {
+		    if (memcmp (base_name, td + tec->flag_offset, base_name_len) != 0)
+		      goto skip_due_to_filter;
+		  }
+	      }
+	    else
+	      /* Program is AT_SECURE but the only source of program
+		 name is argv[0], which is not secure, so we do not
+		 match any name-based filter.  */
+	      goto skip_due_to_filter;
+	    break;
+	  default:
+	    break;
+	  }
+
+	  /* See if the parsed type matches the desired type.  */
 	  if (tunable_list[tid].type.type_code == TUNABLE_TYPE_STRING)
 	    {
 	      /* This is a memory leak but there's no easy way around
@@ -355,6 +425,8 @@  __tunables_init (char **envp)
 				      value, strlen (value));
 		}
 	    }
+
+	skip_due_to_filter:;
 	}
     }
 #endif /* defined(SHARED) && defined (USE_LDCONFIG) */
diff --git a/elf/dl-tunables.h b/elf/dl-tunables.h
index 45aeed47bc..3f34329614 100644
--- a/elf/dl-tunables.h
+++ b/elf/dl-tunables.h
@@ -47,7 +47,7 @@  typedef void (*tunable_callback_t) (tunable_val_t *);
 
 #include "dl-tunable-list.h"
 
-extern void __tunables_init (char **);
+extern void __tunables_init (char **, char **);
 extern void __tunables_print (void);
 extern bool __tunable_is_initialized (tunable_id_t);
 extern void __tunable_get_val (tunable_id_t, void *, tunable_callback_t);
diff --git a/elf/ldconfig-parse.c b/elf/ldconfig-parse.c
index b7bb664eb5..baddfdbac0 100644
--- a/elf/ldconfig-parse.c
+++ b/elf/ldconfig-parse.c
@@ -47,8 +47,10 @@  ldconfig_parse_config_1 (const char *filename, bool do_chroot,
 
      opt_chroot - If non-NULL, all paths are relative to this.
 
-     callback - for each non-blank line in the file, this function is called
-	with the line and it's location.
+     callback - for each non-blank line in the file, this function is
+	called with the line and it's location.  Will also be called
+	with a NULL line at the start and end of each file, for
+	file-scoped config items.
  */
 
 void
diff --git a/elf/ldconfig.c b/elf/ldconfig.c
index 11b063eb5c..1ea55400f3 100644
--- a/elf/ldconfig.c
+++ b/elf/ldconfig.c
@@ -435,6 +435,9 @@  add_dir_1 (const char *line, const char *from_file, int from_line)
 static void
 add_dir_callback (char *line, const char *from_file, int from_line)
 {
+  /* Denotes file boundaries.  Not needed here.  */
+  if (line == NULL)
+    return;
   if (!strncasecmp (line, "hwcap", 5) && isblank (line[5]))
     error (0, 0, _("%s:%u: hwcap directive ignored"), from_file, from_line);
   else
diff --git a/elf/tst-tunconf1.c b/elf/tst-tunconf1.c
new file mode 100644
index 0000000000..c95a7cb8ba
--- /dev/null
+++ b/elf/tst-tunconf1.c
@@ -0,0 +1,36 @@ 
+/* Test that the tunables cache can override env vars.
+   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/>.  */
+
+#include <stdio.h>
+#include <support/check.h>
+
+#include "dl-tunables.h"
+
+static int
+do_test (void)
+{
+  size_t tcache_count = TUNABLE_GET_FULL (glibc, malloc, tcache_count, size_t, NULL);
+  size_t tcache_max = TUNABLE_GET_FULL (glibc, malloc, tcache_max, size_t, NULL);
+  printf("tcache count is %ld (should be 5, from env)\n", (long)tcache_count);
+  TEST_COMPARE ((long)tcache_count, 5);
+  printf("tcache max is %ld (should be 4, from /etc)\n", (long)tcache_max);
+  TEST_COMPARE ((long)tcache_max, 4);
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/elf/tst-tunconf1.root/etc/tunables.conf b/elf/tst-tunconf1.root/etc/tunables.conf
new file mode 100644
index 0000000000..6cd6c8a949
--- /dev/null
+++ b/elf/tst-tunconf1.root/etc/tunables.conf
@@ -0,0 +1,15 @@ 
+# These test the parser for both the overridability characters as well as
+# tunables that either never exist, or only exist on some platforms.
+!glibc.foo=19
+-glibc.cpu.cached_memopt=1
++glibc.cpu.hwcaps=some,random,string
+@glibc.test_secure=1
+$glibc.test_unsecure=1
+
+# These are checked inside the test case
+glibc.malloc.tcache_max=6
+$glibc.malloc.tcache_count=3
+[proc:/bin/ls]
+glibc.malloc.tcache_max=7
+[proc:tst-tunconf1]
+glibc.malloc.tcache_max=4
diff --git a/elf/tst-tunconf1.root/ldconfig.run b/elf/tst-tunconf1.root/ldconfig.run
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/elf/tst-tunconf1.root/postclean.req b/elf/tst-tunconf1.root/postclean.req
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/elf/tunconf.c b/elf/tunconf.c
index d0fc1b7352..c912abd876 100644
--- a/elf/tunconf.c
+++ b/elf/tunconf.c
@@ -66,12 +66,16 @@  typedef enum {
 struct tunable_entry_int {
   struct stringtable_entry *name;
   struct stringtable_entry *value;
+  struct stringtable_entry *filter;
   TOP top;
+  bool exclude_secure:1;
+  bool exclude_unsecure:1;
   int tunable_id;
   int value_is_negative:1;
   int value_was_parsed:1;
   unsigned long long value_ull;
   signed long long value_sll;
+  long filter_flags;
 
   struct tunable_entry_int *next;
 };
@@ -79,8 +83,76 @@  struct tunable_entry_int {
 struct tunable_entry_int *entry_list;
 struct tunable_entry_int **entry_list_next = &entry_list;
 
+static int filter_flags = 0;
+static char *filter_string = NULL;
+
 /*----------------------------------------------------------------------*/
 
+static void
+clear_filter (void)
+{
+  free (filter_string);
+  filter_string = NULL;
+  filter_flags = 0;
+}
+
+/* Filters are lines the are bracketed, like
+   [prog:foo]
+*/
+static void
+parse_filter (char *line, const char *filename, int lineno)
+{
+  const char *colon = NULL;
+  const char *right_bracket = NULL;
+  const char *cp;
+
+  for (cp=line; *cp != 0; cp++)
+    {
+      if (*cp == ':')
+	colon = cp;
+      if (*cp == ']')
+	{
+	  right_bracket = cp;
+	  break;
+	}
+    }
+  /* Special case: [] means "no filter" */
+  if (right_bracket != NULL && right_bracket == line + 1)
+    {
+      clear_filter ();
+      return;
+    }
+  if (colon == NULL)
+    {
+      printf("%s:%d: syntax error, filter line ignored: `%s' (missing ':')\n",
+	     filename, lineno, line);
+      return;
+    }
+  if (right_bracket == NULL)
+    {
+      printf("%s:%d: syntax error, filter line ignored: `%s' (missing ']')\n",
+	     filename, lineno, line);
+      return;
+    }
+
+  if (filter_string != NULL)
+    {
+      clear_filter ();
+    }
+
+  if (memcmp ("proc", line + 1, colon - line - 1) == 0)
+    {
+      filter_string = (char *) malloc (right_bracket - colon);
+      memcpy (filter_string, colon + 1, right_bracket - colon - 1);
+      filter_string [right_bracket - colon] = 0;
+      filter_flags = TUNCONF_FILTER_PERPROC;
+    }
+
+  else
+    printf("%s:%d: unrecognized filter `%.*s', ignored\n", filename, lineno, (int)(colon - line - 1), line + 1);
+}
+
+
 static void
 add_tunable (char *line, const char *filename, int lineno)
 {
@@ -91,12 +163,20 @@  add_tunable (char *line, const char *filename, int lineno)
   char *orig_line;
   struct tunable_entry_int *entry;
   int i, id;
+  bool exclude_secure = 0, exclude_unsecure = 0;
+
+  /* Denotes file boundaries.  */
+  if (line == NULL)
+    {
+      clear_filter();
+      return;
+    }
 
   orig_line = line;
 
   /* Leading whitespace has already been stripped.  */
 
-  if (*line == '!' || *line == '+' || *line == '-')
+  while (*line)
     {
       switch (*line)
 	{
@@ -109,11 +189,25 @@  add_tunable (char *line, const char *filename, int lineno)
 	case '-':
 	  top = TOP_DENY;
 	  break;
+	case '@':
+	  exclude_unsecure = 1;
+	  break;
+	case '$':
+	  exclude_secure = 1;
+	  break;
+	case '[':
+	  parse_filter (line, filename, lineno);
+	  return;
+	case ' ':
+	case '\t':
+	  break;
+
+	default:
+	  goto done;
 	}
       line ++;
-      while (*line && isspace(*line))
-	line ++;
     }
+ done:
 
   /* NAME now points to the start of the tunable name.  */
  name = line;
@@ -174,6 +268,14 @@  add_tunable (char *line, const char *filename, int lineno)
   entry->value = cache_store_string (value);
   entry->tunable_id = id;
   entry->top = top;
+  entry->exclude_secure = exclude_secure;
+  entry->exclude_unsecure = exclude_unsecure;
+
+  if (filter_flags)
+    {
+      entry->filter_flags = filter_flags;
+      entry->filter = cache_store_string (filter_string);
+    }
 
   *entry_list_next = entry;
   entry_list_next = & (entry->next);
@@ -248,11 +350,23 @@  get_tunconf_ext (uint32_t string_table_offset)
 	  tec->flags |= TUNCONF_OVERRIDE_DENY;
 	  break;
 	}
+      if (tei->exclude_secure)
+	tec->flags |= TUNCONF_EXCLUDE_SECURE;
+      if (tei->exclude_unsecure)
+	tec->flags |= TUNCONF_EXCLUDE_UNSECURE;
 
       tec->tunable_id = tei->tunable_id;
       tec->name_offset = tei->name->offset + string_table_offset;
       tec->value_offset = tei->value->offset + string_table_offset;
-      tec->flag_offset = 0;
+
+      if (tei->filter_flags != 0)
+	{
+	  tec->flag_offset = tei->filter->offset + string_table_offset;
+	  tec->flags |= tei->filter_flags;
+	}
+      else
+	tec->flag_offset = 0;
+
       tec->unused_1 = 0;
       if (tei->value_is_negative)
 	tec->parsed_value = (uint64_t) tei->value_sll;
diff --git a/elf/tunconf.h b/elf/tunconf.h
index 9551119167..e880aa574a 100644
--- a/elf/tunconf.h
+++ b/elf/tunconf.h
@@ -10,6 +10,9 @@ 
 #define TUNCONF_OVERRIDE_STRICTER	0x00000008
 #define TUNCONF_OVERRIDE_DENY		0x0000000C
 
+#define TUNCONF_EXCLUDE_SECURE		0x00000010
+#define TUNCONF_EXCLUDE_UNSECURE	0x00000020
+
 #define TUNCONF_FLAG_FILTER		0x0000ff00
 #define TUNCONF_FILTER_PERPROC		0x00000100
 
diff --git a/sysdeps/mach/hurd/dl-sysdep.c b/sysdeps/mach/hurd/dl-sysdep.c
index 0e348d6440..fe6d453756 100644
--- a/sysdeps/mach/hurd/dl-sysdep.c
+++ b/sysdeps/mach/hurd/dl-sysdep.c
@@ -98,7 +98,7 @@  _dl_sysdep_start (void **start_argptr,
 
       __libc_enable_secure = _dl_hurd_data->flags & EXEC_SECURE;
 
-      __tunables_init (_environ);
+      __tunables_init (_environ, _dl_argv);
 
       /* Initialize DSO sorting algorithm after tunables.  */
       _dl_sort_maps_init ();
diff --git a/sysdeps/unix/sysv/linux/dl-sysdep.c b/sysdeps/unix/sysv/linux/dl-sysdep.c
index cb1f94ee23..c2701f274c 100644
--- a/sysdeps/unix/sysv/linux/dl-sysdep.c
+++ b/sysdeps/unix/sysv/linux/dl-sysdep.c
@@ -107,7 +107,7 @@  _dl_sysdep_start (void **start_argptr,
 
   dl_hwcap_check ();
 
-  __tunables_init (_environ);
+  __tunables_init (_environ, (char **) (start_argptr + 1));
 
   /* Initialize DSO sorting algorithm after tunables.  */
   _dl_sort_maps_init ();