[PATCH v7 4/4] Add system-wide tunables: Filters

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Thu May 28 16:20:28 GMT 2026



On 11/02/26 01:33, 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
> ---
>  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                           | 130 +++++++++++++++++++++++-
>  elf/tunconf.h                           |   3 +
>  sysdeps/mach/hurd/dl-sysdep.c           |   2 +-
>  sysdeps/unix/sysv/linux/dl-sysdep.c     |   2 +-
>  15 files changed, 273 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
> 
> 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 fe8f66366f..4655cf961f 100644
> --- a/elf/cache.c
> +++ b/elf/cache.c
> @@ -304,9 +304,10 @@ print_extensions (struct cache_extension_all_loaded *ext,
>  	      >= (void *) & tec[count]);
>        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 20e4056d74..c9dc6b8b62 100644
> --- a/elf/dl-tunables.c
> +++ b/elf/dl-tunables.c
> @@ -292,7 +292,7 @@ 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;
> @@ -304,6 +304,15 @@ __tunables_init (char **envp)
>      TUNABLE_SET (glibc, malloc, hugetlb, 1);
>  
>  #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;
>  
> @@ -336,9 +345,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)

I think it should use the filter prefix here, instead fo program one: /bin/l matches
filter [proc:/bin/ls]:

  memcmp ("/bin/l", "/bin/ls", strlen ("/bin/l")

Also, if the program name is longer than the filter it might resulting in OOB read:
/bin/lsa filter [proc:/bin/ls]:

  memcmp ("/bin/lsa", "/bin/ls", strlen ("/bin/lsa")


Smae for the other check below.


> +		      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

I think this would apply system-wide tunables AT_SECURE binaries by default, whereas
GLIBC_TUNABLES is dropped for same case. There is the '$' filter, but the whole
reason env tunables are stripped under AT_SECURE is defense-in-depth (as for other
environment variable that might alter program execution).

Due the possible wide attack surface of AT_SECURE binaries, I think it would be
better to the inverse: system tunables should be ignored for AT_SECURE unless a 
line explicitly opts in.

> @@ -361,6 +431,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 1b7bf0ac2b..8fae4bb652 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,84 @@ 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)
> +    {
> +      error_at_line (0, 0, filename, lineno,
> +		     "syntax error, filter line ignored: `%s' (missing ':')\n",
> +		     line);
> +      return;
> +    }
> +  if (right_bracket == NULL)
> +    {
> +      error_at_line (0, 0, filename, lineno,
> +		     "syntax error, filter line ignored: `%s' (missing ']')\n",
> +		     line);
> +      return;
> +    }
> +
> +  if (filter_string != NULL)
> +    {
> +      clear_filter ();
> +    }
> +
> +  if (memcmp ("proc", line + 1, colon - line - 1) == 0)

I don't think we can use memcmp here:

  * if the line has '[procXXXX:foo]' then 'colon - line - 1' will be 8 and
    it will trigger an OOB read.

  * if the linehas [pr:foo], [p:foo], even [:foo], the length will be 2/1/0
    and any prefix of "proc" (or empty) is accepted.

> +    {
> +      /* Consider this example: [proc:foo] ..."  */
> +      /* We allocate 4 bytes, [0] through [3].  */
> +      filter_string = (char *) malloc (right_bracket - colon);
> +      /* We copy "foo" for 3 bytes, [0] through [2].  */
> +      memcpy (filter_string, colon + 1, right_bracket - colon - 1);
> +      /*  [3] = 0 so now "foo\0".  */
> +      filter_string [right_bracket - colon - 1] = 0;
> +      filter_flags = TUNCONF_FILTER_PERPROC;
> +    }
> +
> +  else
> +    error_at_line (0, 0, filename, lineno,
> +		   "unrecognized filter `%.*s', ignored\n",
> +		   (int)(colon - line - 1), line + 1);
> +}
> +
> +
>  static void
>  add_tunable (char *line, const char *filename, int lineno)
>  {
> @@ -91,12 +171,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 +197,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 +276,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 +358,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 ();



More information about the Libc-alpha mailing list