Drop --clearenv; control the sandbox's environment at exec time

Moves responsibility for the sandboxed command's environment out of
bwrap's own --clearenv/--setenv flags and into the process-launching
code itself: build_sandbox_env() (bwrap.cpp) constructs the exact
environment (PATH, HOME, PWD, TERM if present), and run_bwrap() passes
it to run_process_foreground()'s new optional env override, which
replaces the forked child's entire environment via clearenv()/setenv()
(plain POSIX, not the GNU-only execvpe(), since the target platform
includes musl) right before exec. bwrap, nsenter (when interposed),
and slocker-lite-priv-drop all just forward whatever environment
they're launched with, so controlling it once at the outermost exec
is sufficient.

Also adds PWD=/ to the constructed environment: per bwrap's own man
page, --clearenv never actually unset PWD in the first place (bwrap
manages it separately, alongside --chdir), so the old --clearenv/
--setenv sequence was leaving it unset by omission rather than by
choice. Hardcoded to "/" to match --chdir's own value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-23 14:56:11 +00:00
parent 7a333788c1
commit bec5456c33
5 changed files with 69 additions and 19 deletions
+25 -10
View File
@@ -64,6 +64,29 @@ std::optional<std::filesystem::path> find_priv_drop_helper() {
return candidate;
}
// Builds the exact environment the sandboxed command should see. Passed directly
// to run_process_foreground() as the environment to exec bwrap with, rather than
// relying on bwrap's own --clearenv/--setenv (which run_bwrap() no longer uses) --
// bwrap, and any nsenter/priv-drop-helper interposed ahead of it, all just forward
// whatever environment they were themselves launched with, so controlling it once
// here is sufficient. PWD is set to match --chdir's own value in
// build_bwrap_args() ("/") -- per bwrap's own man page, --clearenv never actually
// unset PWD in the first place (bwrap manages it separately, alongside --chdir),
// so this isn't a straight port of a prior --setenv, just keeping the explicitly
// constructed environment a complete, accurate match for what the sandbox should
// see.
std::vector<std::pair<std::string, std::string>> build_sandbox_env(std::optional<ResolvedUser> user) {
std::vector<std::pair<std::string, std::string>> env = {
{"PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
{"HOME", user ? user->home : "/root"},
{"PWD", "/"},
};
if (const char* term = std::getenv("TERM")) {
env.emplace_back("TERM", term);
}
return env;
}
struct NamespaceProbe {
int clone_flag;
const char* bwrap_arg;
@@ -234,22 +257,13 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
"/tmp",
"--chdir",
"/",
"--clearenv",
"--setenv",
"PATH",
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
};
args.insert(args.end(), filesystem_args.begin(), filesystem_args.end());
args.insert(args.end(), {"--setenv", "HOME", user ? user->home : "/root"});
for (const auto& volume : volumes) {
args.insert(args.end(), {"--bind", volume.host_directory, volume.container_path});
}
if (const char* term = std::getenv("TERM")) {
args.insert(args.end(), {"--setenv", "TERM", term});
}
bool bound_priv_drop_helper = false;
if (user) {
auto helper_path = find_priv_drop_helper();
@@ -315,7 +329,8 @@ int run_bwrap(const std::string& root, const std::vector<std::string>& command,
std::optional<SessionLock> session_lock;
int exit_code = run_process_foreground(
*argv, [&](pid_t pid) { session_lock = create_session_lock(container_name, pid); });
*argv, [&](pid_t pid) { session_lock = create_session_lock(container_name, pid); },
build_sandbox_env(user));
if (session_lock) {
release_session_lock(*session_lock);
+3
View File
@@ -78,6 +78,9 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
// see there for when it does/doesn't take effect. While bwrap is running,
// `container_name` (paired with its actual pid) is recorded as a locked session
// pid file under $XDG_STATE_HOME (see pid_file.h) -- removed again once it exits.
// The sandboxed command's environment is built directly here (build_sandbox_env())
// and passed to run_process_foreground()'s own env override, rather than relying
// on bwrap's own --clearenv/--setenv (which build_bwrap_args() no longer uses).
// Returns bwrap's exit code, or -1 on failure to launch.
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
+8 -2
View File
@@ -108,8 +108,8 @@ ProcessResult run_process(const std::vector<std::string>& argv) {
return {exit_code, output};
}
int run_process_foreground(const std::vector<std::string>& argv,
const std::function<void(pid_t)>& on_start) {
int run_process_foreground(const std::vector<std::string>& argv, const std::function<void(pid_t)>& on_start,
const std::optional<std::vector<std::pair<std::string, std::string>>>& env) {
spdlog::debug("running external command: {}", fmt::join(argv, " "));
pid_t pid = fork();
@@ -118,6 +118,12 @@ int run_process_foreground(const std::vector<std::string>& argv,
}
if (pid == 0) {
if (env) {
clearenv();
for (const auto& [key, value] : *env) {
setenv(key.c_str(), value.c_str(), 1);
}
}
auto c_argv = to_c_argv(argv);
execvp(c_argv[0], c_argv.data());
const char* msg = "run_process_foreground: execvp failed\n";
+10 -3
View File
@@ -21,6 +21,7 @@
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <sys/types.h>
@@ -40,10 +41,16 @@ ProcessResult run_process(const std::vector<std::string>& argv);
// succeeds and before this blocks in waitpid() -- e.g. so a caller can record the
// real pid of what it just launched. This stays accurate even when argv itself
// execs into something else before the real target (e.g. nsenter handing off to
// the final command), since exec() never changes the pid. Returns the exit code,
// or -1 if fork or exec failed (on_start is not called in that case).
// the final command), since exec() never changes the pid. If `env` is set, the
// child replaces its entire environment with exactly these key/value pairs (via
// clearenv()/setenv(), before exec) instead of inheriting this process's own --
// e.g. run_bwrap() uses this to give the sandboxed command a minimal, controlled
// environment without relying on bwrap's own --clearenv/--setenv. Returns the
// exit code, or -1 if fork or exec failed (on_start is not called in that case).
int run_process_foreground(const std::vector<std::string>& argv,
const std::function<void(pid_t)>& on_start = nullptr);
const std::function<void(pid_t)>& on_start = nullptr,
const std::optional<std::vector<std::pair<std::string, std::string>>>& env =
std::nullopt);
// Searches $PATH for an executable regular file named `name`, in PATH order.
// Returns its full path, or nullopt if not found.