From 76dfe5c4b6f2088ae74cef2695b4988ce2c93d04 Mon Sep 17 00:00:00 2001 From: Abhinav Madake Date: Thu, 17 Sep 2026 23:36:33 +0530 Subject: [PATCH] Overwrite removed env strings so they do not leak via /proc/PID/environ Fixes #725 Signed-off-by: Abhinav Madake --- meson.build | 4 ++ tests/test-helper.py | 3 +- tests/test-sandbox.py | 29 +++++++++++++++ utils.c | 87 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 08d28c97..c87b73a4 100644 --- a/meson.build +++ b/meson.build @@ -87,6 +87,10 @@ if selinux_dep.found() endif endif +if cc.has_function('explicit_bzero', prefix : '#include ') + cdata.set('HAVE_EXPLICIT_BZERO', 1) +endif + if get_option('debug_logging') cdata.set('BWRAP_DEBUG', 1) endif diff --git a/tests/test-helper.py b/tests/test-helper.py index c1fc0314..3ffbcf5b 100644 --- a/tests/test-helper.py +++ b/tests/test-helper.py @@ -177,7 +177,7 @@ def _test_module(self, test_case): '--proc', '/proc', '--tmpfs', '/tmp'] -def run_bwrap(*extra_args, pass_fds=()): +def run_bwrap(*extra_args, pass_fds=(), env=None): """Run bwrap with base sandbox args plus extra_args, return CompletedProcess. Args may include DataFd instances, which are resolved to pipe fds. @@ -201,6 +201,7 @@ def run_bwrap(*extra_args, pass_fds=()): stdout=subprocess.PIPE, stderr=subprocess.PIPE, pass_fds=(*pass_fds, *opened_fds), + env=env, ) finally: for fd in opened_fds: diff --git a/tests/test-sandbox.py b/tests/test-sandbox.py index d33f1ad3..18cbe826 100644 --- a/tests/test-sandbox.py +++ b/tests/test-sandbox.py @@ -716,6 +716,35 @@ def test_proc_symlink_escape_blocked(self): def test_proc_symlink_escape_blocked_fallback(self): self._test_proc_symlink_escape(['--debug-opt=force-openat-fallback']) + # ------ Removed env vars must not leak via /proc/1/environ ------ + + _READ_PID1_ENVIRON = ['--unshare-pid', 'cat', '/proc/1/environ'] + + def _pid1_environ(self, *bwrap_args, env): + result = run_bwrap(*bwrap_args, *self._READ_PID1_ENVIRON, env=env) + self.assertEqual(result.returncode, 0, result.stderr) + return result.stdout.split(b'\0') + + def test_clearenv_not_in_pid1_environ(self): + env = dict(os.environ, BWRAP_TEST_SECRET='hunter2') + environ = self._pid1_environ('--clearenv', env=env) + self.assertNotIn(b'BWRAP_TEST_SECRET=hunter2', environ) + self.assertFalse(any(e.startswith(b'PATH=') for e in environ)) + + def test_unsetenv_not_in_pid1_environ(self): + env = dict(os.environ, BWRAP_TEST_SECRET='hunter2', + BWRAP_TEST_KEEP='visible') + environ = self._pid1_environ('--unsetenv', 'BWRAP_TEST_SECRET', + env=env) + self.assertNotIn(b'BWRAP_TEST_SECRET=hunter2', environ) + self.assertIn(b'BWRAP_TEST_KEEP=visible', environ) + + def test_setenv_overwrite_not_in_pid1_environ(self): + env = dict(os.environ, BWRAP_TEST_SECRET='hunter2') + environ = self._pid1_environ('--setenv', 'BWRAP_TEST_SECRET', 'new', + env=env) + self.assertNotIn(b'BWRAP_TEST_SECRET=hunter2', environ) + if __name__ == '__main__': run_tap_tests(sys.modules[__name__]) diff --git a/utils.c b/utils.c index 5865cdb5..3a656d4b 100644 --- a/utils.c +++ b/utils.c @@ -311,25 +311,112 @@ has_prefix (const char *str, return strncmp (str, prefix, strlen (prefix)) == 0; } +/* clearenv()/unsetenv() leave the initial strings readable via + * /proc/PID/environ, so overwrite them once unreferenced. */ +static char **initial_environ = NULL; + +static void +save_initial_environ (void) +{ + size_t n = 0; + + if (initial_environ != NULL) + return; + + if (environ != NULL) + while (environ[n] != NULL) + n++; + + initial_environ = xcalloc (n + 1, sizeof (char *)); + if (n > 0) + memcpy (initial_environ, environ, n * sizeof (char *)); +} + +static void +scrub_string (char *s) +{ +#ifdef HAVE_EXPLICIT_BZERO + explicit_bzero (s, strlen (s)); +#else + volatile char *p = s; + + while (*p != 0) + *p++ = 0; +#endif +} + +static bool +still_in_environ (const char *s) +{ + size_t i; + + if (environ == NULL) + return false; + + for (i = 0; environ[i] != NULL; i++) + if (environ[i] == s) + return true; + + return false; +} + +static void +scrub_initial_env (const char *name) +{ + size_t name_len = name != NULL ? strlen (name) : 0; + size_t i; + + save_initial_environ (); + + for (i = 0; initial_environ[i] != NULL; i++) + { + char *s = initial_environ[i]; + + if (*s == 0) + continue; + + if (name != NULL && + (strncmp (s, name, name_len) != 0 || s[name_len] != '=')) + continue; + + if (still_in_environ (s)) + continue; + + scrub_string (s); + } +} + void xclearenv (void) { + save_initial_environ (); + if (clearenv () != 0) die_with_error ("clearenv failed"); + + scrub_initial_env (NULL); } void xsetenv (const char *name, const char *value, int overwrite) { + save_initial_environ (); + if (setenv (name, value, overwrite)) die ("setenv failed"); + + scrub_initial_env (name); } void xunsetenv (const char *name) { + save_initial_environ (); + if (unsetenv (name)) die ("unsetenv failed"); + + scrub_initial_env (name); } char *