elf: Ignore GLIBC_TUNABLES for setuid/setgid binaries

The tunable privilege levels were a retrofit to try and keep the malloc
tunable environment variables' behavior unchanged across security
boundaries.  However, CVE-2023-4911 shows how tricky can be
tunable parsing in a security-sensitive environment.

Not only parsing, but the malloc tunable essentially changes some
semantics on setuid/setgid processes.  Although it is not a direct
security issue, allowing users to change setuid/setgid semantics is not
a good security practice, and requires extra code and analysis to check
if each tunable is safe to use on all security boundaries.

It also means that security opt-in features, like aarch64 MTE, would
need to be explicit enabled by an administrator with a wrapper script
or with a possible future system-wide tunable setting.

Co-authored-by: Siddhesh Poyarekar  <siddhesh@sourceware.org>
Reviewed-by: DJ Delorie <dj@redhat.com>
This commit is contained in:
Adhemerval Zanella 2023-11-06 17:25:36 -03:00
parent a72a4eb10b
commit 9c96c87d60
9 changed files with 299 additions and 161 deletions

View File

@ -263,7 +263,6 @@ tests-static-normal := \
tst-dl-iter-static \
tst-dst-static \
tst-env-setuid \
tst-env-setuid-tunables \
tst-getauxval-static \
tst-linkall-static \
tst-single_threaded-pthread-static \
@ -276,10 +275,12 @@ tests-static-normal := \
tests-static-internal := \
tst-dl-printf-static \
tst-dl_find_object-static \
tst-env-setuid-tunables \
tst-ptrguard1-static \
tst-stackguard1-static \
tst-tls1-static \
tst-tls1-static-non-pie \
tst-tunables \
# tests-static-internal
CRT-tst-tls1-static-non-pie := $(csu-objpfx)crt1.o
@ -2662,6 +2663,8 @@ $(objpfx)tst-glibc-hwcaps-mask.out: \
# tst-glibc-hwcaps-cache.
$(objpfx)tst-glibc-hwcaps-cache.out: $(objpfx)tst-glibc-hwcaps
tst-tunables-ARGS = -- $(host-test-program-cmd)
$(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

View File

@ -64,16 +64,6 @@ struct _tunable
tunable_val_t val; /* The value. */
bool initialized; /* Flag to indicate that the tunable is
initialized. */
tunable_seclevel_t security_level; /* Specify the security level for the
tunable with respect to AT_SECURE
programs. See description of
tunable_seclevel_t to see a
description of the values.
Note that even if the tunable is
read, it may not get used by the
target module if the value is
considered unsafe. */
/* Compatibility elements. */
const char env_alias[TUNABLE_ALIAS_MAX]; /* The compatibility environment
variable name. */

View File

@ -154,50 +154,51 @@ __tunable_set_val (tunable_id_t id, tunable_val_t *valp, tunable_num_t *minp,
do_tunable_update_val (cur, valp, minp, maxp);
}
/* Parse the tunable string TUNESTR and adjust it to drop any tunables that may
be unsafe for AT_SECURE processes so that it can be used as the new
environment variable value for GLIBC_TUNABLES. VALSTRING is the original
environment variable string which we use to make NULL terminated values so
that we don't have to allocate memory again for it. */
/* Parse the tunable string VALSTRING. VALSTRING is a duplicated value,
where delimiters ':' are replaced with '\0', so string tunables are null
terminated. */
static void
parse_tunables (char *tunestr, char *valstring)
parse_tunables (char *valstring)
{
if (tunestr == NULL || *tunestr == '\0')
if (valstring == NULL || *valstring == '\0')
return;
char *p = tunestr;
size_t off = 0;
char *p = valstring;
bool done = false;
while (true)
while (!done)
{
char *name = p;
size_t len = 0;
/* First, find where the name ends. */
while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;
while (*p != '=' && *p != ':' && *p != '\0')
p++;
/* If we reach the end of the string before getting a valid name-value
pair, bail out. */
if (p[len] == '\0')
if (*p == '\0')
break;
/* We did not find a valid name-value pair before encountering the
colon. */
if (p[len]== ':')
if (*p == ':')
{
p += len + 1;
p++;
continue;
}
p += len + 1;
/* Skip the '='. */
p++;
/* Take the value from the valstring since we need to NULL terminate it. */
char *value = &valstring[p - tunestr];
len = 0;
const char *value = p;
while (p[len] != ':' && p[len] != '\0')
len++;
while (*p != ':' && *p != '\0')
p++;
if (*p == '\0')
done = true;
else
*p++ = '\0';
/* Add the tunable if it exists. */
for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
@ -206,50 +207,11 @@ parse_tunables (char *tunestr, char *valstring)
if (tunable_is_name (cur->name, name))
{
/* If we are in a secure context (AT_SECURE) then ignore the
tunable unless it is explicitly marked as secure. Tunable
values take precedence over their envvar aliases. We write
the tunables that are not SXID_ERASE back to TUNESTR, thus
dropping all SXID_ERASE tunables and any invalid or
unrecognized tunables. */
if (__libc_enable_secure)
{
if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
{
if (off > 0)
tunestr[off++] = ':';
const char *n = cur->name;
while (*n != '\0')
tunestr[off++] = *n++;
tunestr[off++] = '=';
for (size_t j = 0; j < len; j++)
tunestr[off++] = value[j];
}
if (cur->security_level != TUNABLE_SECLEVEL_NONE)
break;
}
value[len] = '\0';
tunable_initialize (cur, value);
break;
}
}
/* We reached the end while processing the tunable string. */
if (p[len] == '\0')
break;
p += len + 1;
}
/* Terminate tunestr before we leave. */
if (__libc_enable_secure)
tunestr[off] = '\0';
}
/* Initialize the tunables list from the environment. For now we only use the
@ -263,16 +225,16 @@ __tunables_init (char **envp)
size_t len = 0;
char **prev_envp = envp;
/* Ignore tunables for AT_SECURE programs. */
if (__libc_enable_secure)
return;
while ((envp = get_next_env (envp, &envname, &len, &envval,
&prev_envp)) != NULL)
{
if (tunable_is_name ("GLIBC_TUNABLES", envname))
{
char *new_env = tunables_strdup (envname);
if (new_env != NULL)
parse_tunables (new_env + len + 1, envval);
/* Put in the updated envval. */
*prev_envp = new_env;
parse_tunables (tunables_strdup (envval));
continue;
}
@ -290,39 +252,6 @@ __tunables_init (char **envp)
/* We have a match. Initialize and move on to the next line. */
if (tunable_is_name (name, envname))
{
/* For AT_SECURE binaries, we need to check the security settings of
the tunable and decide whether we read the value and also whether
we erase the value so that child processes don't inherit them in
the environment. */
if (__libc_enable_secure)
{
if (cur->security_level == TUNABLE_SECLEVEL_SXID_ERASE)
{
/* Erase the environment variable. */
char **ep = prev_envp;
while (*ep != NULL)
{
if (tunable_is_name (name, *ep))
{
char **dp = ep;
do
dp[0] = dp[1];
while (*dp++);
}
else
++ep;
}
/* Reset the iterator so that we read the environment again
from the point we erased. */
envp = prev_envp;
}
if (cur->security_level != TUNABLE_SECLEVEL_NONE)
continue;
}
tunable_initialize (cur, envval);
break;
}

View File

@ -21,14 +21,6 @@
# minval: Optional minimum acceptable value
# maxval: Optional maximum acceptable value
# env_alias: An alias environment variable
# security_level: Specify security level of the tunable for AT_SECURE binaries.
# Valid values are:
#
# SXID_ERASE: (default) Do not read and do not pass on to
# child processes.
# SXID_IGNORE: Do not read, but retain for non-AT_SECURE
# subprocesses.
# NONE: Read all the time.
glibc {
malloc {
@ -41,7 +33,6 @@ glibc {
top_pad {
type: SIZE_T
env_alias: MALLOC_TOP_PAD_
security_level: SXID_IGNORE
default: 131072
}
perturb {
@ -49,35 +40,29 @@ glibc {
minval: 0
maxval: 0xff
env_alias: MALLOC_PERTURB_
security_level: SXID_IGNORE
}
mmap_threshold {
type: SIZE_T
env_alias: MALLOC_MMAP_THRESHOLD_
security_level: SXID_IGNORE
}
trim_threshold {
type: SIZE_T
env_alias: MALLOC_TRIM_THRESHOLD_
security_level: SXID_IGNORE
}
mmap_max {
type: INT_32
env_alias: MALLOC_MMAP_MAX_
security_level: SXID_IGNORE
minval: 0
}
arena_max {
type: SIZE_T
env_alias: MALLOC_ARENA_MAX
minval: 1
security_level: SXID_IGNORE
}
arena_test {
type: SIZE_T
env_alias: MALLOC_ARENA_TEST
minval: 1
security_level: SXID_IGNORE
}
tcache_max {
type: SIZE_T
@ -91,7 +76,6 @@ glibc {
mxfast {
type: SIZE_T
minval: 0
security_level: SXID_IGNORE
}
hugetlb {
type: SIZE_T
@ -158,7 +142,6 @@ glibc {
type: INT_32
minval: 0
maxval: 255
security_level: SXID_IGNORE
}
decorate_maps {
type: INT_32

View File

@ -15,14 +15,10 @@
License along with the GNU C Library; if not, see
<https://www.gnu.org/licenses/>. */
/* Verify that tunables correctly filter out unsafe tunables like
glibc.malloc.check and glibc.malloc.mmap_threshold but also retain
glibc.malloc.mmap_threshold in an unprivileged child. */
#define _LIBC 1
#include "config.h"
#undef _LIBC
/* Verify that GLIBC_TUNABLES is kept unchanged but no tunable is actually
enabled for AT_SECURE processes. */
#include <dl-tunables.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
@ -40,7 +36,7 @@
#include <support/test-driver.h>
#include <support/capture_subprocess.h>
const char *teststrings[] =
static const char *teststrings[] =
{
"glibc.malloc.check=2:glibc.malloc.mmap_threshold=4096",
"glibc.malloc.check=2:glibc.malloc.check=2:glibc.malloc.mmap_threshold=4096",
@ -74,6 +70,23 @@ test_child (int off)
ret = 0;
fflush (stdout);
/* Also check if the set tunables are effectively unchanged. */
int32_t check = TUNABLE_GET_FULL (glibc, malloc, check, int32_t, NULL);
size_t mmap_threshold = TUNABLE_GET_FULL (glibc, malloc, mmap_threshold,
size_t, NULL);
int32_t perturb = TUNABLE_GET_FULL (glibc, malloc, perturb, int32_t, NULL);
printf (" [%d] glibc.malloc.check=%d\n", off, check);
fflush (stdout);
printf (" [%d] glibc.malloc.mmap_threshold=%zu\n", off, mmap_threshold);
fflush (stdout);
printf (" [%d] glibc.malloc.perturb=%d\n", off, perturb);
fflush (stdout);
ret |= check != 0;
ret |= mmap_threshold != 0;
ret |= perturb != 0;
return ret;
}

244
elf/tst-tunables.c Normal file
View File

@ -0,0 +1,244 @@
/* Check GLIBC_TUNABLES parsing.
Copyright (C) 2023 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 <array_length.h>
#include <dl-tunables.h>
#include <getopt.h>
#include <intprops.h>
#include <stdint.h>
#include <stdlib.h>
#include <support/capture_subprocess.h>
#include <support/check.h>
static int restart;
#define CMDLINE_OPTIONS \
{ "restart", no_argument, &restart, 1 },
static const struct test_t
{
const char *env;
int32_t expected_malloc_check;
size_t expected_mmap_threshold;
int32_t expected_perturb;
} tests[] =
{
/* Expected tunable format. */
{
"glibc.malloc.check=2",
2,
0,
0,
},
{
"glibc.malloc.check=2:glibc.malloc.mmap_threshold=4096",
2,
4096,
0,
},
/* Empty tunable are ignored. */
{
"glibc.malloc.check=2::glibc.malloc.mmap_threshold=4096",
2,
4096,
0,
},
/* As well empty values. */
{
"glibc.malloc.check=:glibc.malloc.mmap_threshold=4096",
0,
4096,
0,
},
/* Tunable are processed from left to right, so last one is the one set. */
{
"glibc.malloc.check=1:glibc.malloc.check=2",
2,
0,
0,
},
{
"glibc.malloc.check=1:glibc.malloc.check=2:glibc.malloc.mmap_threshold=4096",
2,
4096,
0,
},
{
"glibc.malloc.check=2:glibc.malloc.mmap_threshold=4096:glibc.malloc.check=1",
1,
4096,
0,
},
/* 0x800 is larger than tunable maxval (0xff), so the tunable is unchanged. */
{
"glibc.malloc.perturb=0x800",
0,
0,
0,
},
{
"glibc.malloc.perturb=0x55",
0,
0,
0x55,
},
/* Out of range values are just ignored. */
{
"glibc.malloc.perturb=0x800:glibc.malloc.mmap_threshold=4096",
0,
4096,
0,
},
/* Invalid keys are ignored. */
{
":glibc.malloc.garbage=2:glibc.malloc.check=1",
1,
0,
0,
},
{
"glibc.malloc.perturb=0x800:not_valid.malloc.check=2:glibc.malloc.mmap_threshold=4096",
0,
4096,
0,
},
{
"glibc.not_valid.check=2:glibc.malloc.mmap_threshold=4096",
0,
4096,
0,
},
{
"not_valid.malloc.check=2:glibc.malloc.mmap_threshold=4096",
0,
4096,
0,
},
/* Invalid subkeys are ignored. */
{
"glibc.malloc.garbage=2:glibc.maoc.mmap_threshold=4096:glibc.malloc.check=2",
2,
0,
0,
},
{
"glibc.malloc.check=4:glibc.malloc.garbage=2:glibc.maoc.mmap_threshold=4096",
0,
0,
0,
},
{
"not_valid.malloc.check=2",
0,
0,
0,
},
{
"glibc.not_valid.check=2",
0,
0,
0,
},
/* An ill-formatted tunable in the for key=key=value will considere the
value as 'key=value' (which can not be parsed as an integer). */
{
"glibc.malloc.mmap_threshold=glibc.malloc.mmap_threshold=4096",
0,
0,
0,
},
/* The ill-formatted tunable is also skipped. */
{
"glibc.malloc.mmap_threshold=glibc.malloc.mmap_threshold=4096:glibc.malloc.check=2",
2,
0,
0,
},
/* For an integer tunable, parse will stop on non number character. */
{
"glibc.malloc.check=2=2",
2,
0,
0,
},
{
"glibc.malloc.check=2=2:glibc.malloc.mmap_threshold=4096",
2,
4096,
0,
}
};
static int
handle_restart (int i)
{
TEST_COMPARE (tests[i].expected_malloc_check,
TUNABLE_GET_FULL (glibc, malloc, check, int32_t, NULL));
TEST_COMPARE (tests[i].expected_mmap_threshold,
TUNABLE_GET_FULL (glibc, malloc, mmap_threshold, size_t, NULL));
TEST_COMPARE (tests[i].expected_perturb,
TUNABLE_GET_FULL (glibc, malloc, perturb, int32_t, NULL));
return 0;
}
static int
do_test (int argc, char *argv[])
{
/* We must have either:
- One our fource parameters left if called initially:
+ path to ld.so optional
+ "--library-path" optional
+ the library path optional
+ the application name
+ the test to check */
TEST_VERIFY_EXIT (argc == 2 || argc == 5);
if (restart)
return handle_restart (atoi (argv[1]));
char nteststr[INT_BUFSIZE_BOUND (int)];
char *spargv[10];
{
int i = 0;
for (; i < argc - 1; i++)
spargv[i] = argv[i + 1];
spargv[i++] = (char *) "--direct";
spargv[i++] = (char *) "--restart";
spargv[i++] = nteststr;
spargv[i] = NULL;
}
for (int i = 0; i < array_length (tests); i++)
{
snprintf (nteststr, sizeof nteststr, "%d", i);
printf ("[%d] Spawned test for %s\n", i, tests[i].env);
setenv ("GLIBC_TUNABLES", tests[i].env, 1);
struct support_capture_subprocess result
= support_capture_subprogram (spargv[0], spargv);
support_capture_subprocess_check (&result, "tst-tunables", 0,
sc_allow_stderr);
support_capture_subprocess_free (&result);
}
return 0;
}
#define TEST_FUNCTION_ARGV do_test
#include <support/test-driver.c>

View File

@ -59,15 +59,6 @@ The list of allowed attributes are:
- env_alias: An alias environment variable
- security_level: Specify security level of the tunable for AT_SECURE
binaries. Valid values are:
SXID_ERASE: (default) Do not read and do not pass on to
child processes.
SXID_IGNORE: Do not read, but retain for non-AT_SECURE
child processes.
NONE: Read all the time.
2. Use TUNABLE_GET/TUNABLE_SET/TUNABLE_SET_WITH_BOUNDS to get and set tunables.
3. OPTIONAL: If tunables in a namespace are being used multiple times within a

View File

@ -61,9 +61,6 @@ $1 == "}" {
if (!env_alias[top_ns,ns,tunable]) {
env_alias[top_ns,ns,tunable] = "{0}"
}
if (!security_level[top_ns,ns,tunable]) {
security_level[top_ns,ns,tunable] = "SXID_ERASE"
}
len = length(top_ns"."ns"."tunable)
if (len > max_name_len)
max_name_len = len
@ -118,17 +115,6 @@ $1 == "}" {
if (len > max_alias_len)
max_alias_len = len
}
else if (attr == "security_level") {
if (val == "SXID_ERASE" || val == "SXID_IGNORE" || val == "NONE") {
security_level[top_ns,ns,tunable] = val
}
else {
printf("Line %d: Invalid value (%s) for security_level: %s, ", NR, val,
$0)
print("Allowed values are 'SXID_ERASE', 'SXID_IGNORE', or 'NONE'")
exit 1
}
}
else if (attr == "default") {
if (types[top_ns,ns,tunable] == "STRING") {
default_val[top_ns,ns,tunable] = sprintf(".strval = \"%s\"", val);
@ -177,9 +163,9 @@ END {
n = indices[2];
m = indices[3];
printf (" {TUNABLE_NAME_S(%s, %s, %s)", t, n, m)
printf (", {TUNABLE_TYPE_%s, %s, %s}, {%s}, false, TUNABLE_SECLEVEL_%s, %s},\n",
printf (", {TUNABLE_TYPE_%s, %s, %s}, {%s}, false, %s},\n",
types[t,n,m], minvals[t,n,m], maxvals[t,n,m],
default_val[t,n,m], security_level[t,n,m], env_alias[t,n,m]);
default_val[t,n,m], env_alias[t,n,m]);
}
print "};"
print "#endif"

View File

@ -23,7 +23,6 @@ glibc {
minval: 0
maxval: 1
env_alias: LD_PREFER_MAP_32BIT_EXEC
security_level: SXID_IGNORE
}
}
}