Add a bubblewrap backend for sandbox integrity checks (#51840)

## What changed

Add Linux dependency discovery and policy preparation to `exec-server`'s sandbox integrity module. Inventory bundled and system `bwrap` candidates and the deny-glob scanner using the launcher's location and the command's `PATH` and working directory.

Resolve `:tmpdir` and tilde-prefixed deny globs using the command's `TMPDIR` and `HOME`, with account home-directory fallback. Reuse Linux sandbox helpers to expand deny globs in the command's explicit environment.

## Testing

Add Linux tests covering symlinked launchers, relative and empty `PATH` entries, and empty, relative, and tilde-prefixed `TMPDIR` values alongside home-relative deny globs.

GitOrigin-RevId: fe404fa4be77ece677b22d52d77a7234dbf0cffe
This commit is contained in:
iceweasel-oai
2026-10-07 21:50:48 +00:00
committed by copyberry
parent 72c959528e
commit d9960e12bb
14 changed files with 393 additions and 40 deletions
+1
View File
@@ -3214,6 +3214,7 @@ dependencies = [
"codex-file-system",
"codex-file-watcher",
"codex-http-client",
"codex-linux-sandbox",
"codex-mxc-sandbox",
"codex-network-proxy",
"codex-otel",
+3
View File
@@ -73,6 +73,9 @@ tracing = { workspace = true }
url = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
[target.'cfg(target_os = "linux")'.dependencies]
codex-linux-sandbox = { workspace = true }
[target.'cfg(unix)'.dependencies]
codex-uds = { workspace = true }
libc = { workspace = true }
+1 -1
View File
@@ -41,7 +41,7 @@ mod rpc;
mod rpc_server_requests;
mod rpc_timing;
mod runtime_options;
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "linux", target_os = "windows"))]
#[expect(
dead_code,
reason = "The integrity runner is added later in this stack."
@@ -30,7 +30,7 @@ negatives. Backend glob/symlink nuances and writable hard-link aliases are not
fully modeled. The separate glob scan can differ from enforcement's snapshot.
Metrics describe observed checker outcomes, not accuracy or missed violations.
Windows scanner discovery currently uses `which`, while enforcement uses
MXC scanner discovery currently uses `which`, while enforcement uses
`Command`. Their search-order and environment differences can select different
executables or cause only one scan to use the internal walker. This is an accepted
telemetry limitation for this phase; changing production scanner selection is deferred.
@@ -7,6 +7,8 @@ use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
#[cfg(target_os = "linux")]
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::sandbox::SandboxOverride;
use codex_sandboxing::FileContentsChecker;
@@ -65,3 +67,106 @@ pub(super) fn sandbox_request(
arg0: None,
}
}
#[cfg(target_os = "linux")]
#[test]
fn dependency_inventory_uses_launcher_location_and_command_path() -> io::Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
let root = AbsolutePathBuf::from_absolute_path(temp.path())?.canonicalize()?;
let cwd = root.join("command");
let tools = root.join("tools");
let bundled = tools.join("codex-resources/bwrap");
fs::create_dir(&cwd)?;
fs::create_dir(&tools)?;
fs::create_dir(tools.join("codex-resources"))?;
for path in [
tools.join("bwrap"),
tools.join("rg"),
cwd.join("rg"),
tools.join("codex"),
bundled.clone(),
] {
fs::write(&path, "fixture")?;
fs::set_permissions(&path, fs::Permissions::from_mode(/*mode*/ 0o755))?;
}
let launcher = cwd.join("sandbox-helper");
std::os::unix::fs::symlink(tools.join("codex"), &launcher)?;
let policy = FileSystemSandboxPolicy::restricted(vec![deny_glob("*.key")]);
let mut request = sandbox_request(&cwd, &policy);
request.command[0] = launcher.to_string_lossy().into_owned();
for (path, scanner) in [
("../tools", tools.join("rg")),
(":../tools", tools.join("rg")),
] {
request.env.insert("PATH".to_owned(), path.to_owned());
let dependencies = backend::critical_dependencies(&request, &policy, &cwd)?
.into_iter()
.filter(|(name, _)| {
matches!(
*name,
"bundled_bwrap_candidate" | "system_bwrap_candidate" | "glob_scanner"
)
})
.map(|(name, path)| path.and_then(fs::canonicalize).map(|path| (name, path)))
.collect::<io::Result<Vec<_>>>()?;
assert_eq!(
dependencies,
vec![
("bundled_bwrap_candidate", bundled.to_path_buf()),
(
"system_bwrap_candidate",
tools.join("bwrap").into_path_buf()
),
("glob_scanner", scanner.into_path_buf()),
]
);
}
Ok(())
}
#[cfg(target_os = "linux")]
#[test]
fn policy_preparation_uses_command_tmpdir_and_home() -> io::Result<()> {
let temp = tempfile::tempdir()?;
let root = AbsolutePathBuf::from_absolute_path(temp.path())?.canonicalize()?;
let cwd = root.join("command");
let home = root.join("home");
fs::create_dir(&cwd)?;
fs::create_dir(&home)?;
let denied = home.join("secret.key");
fs::write(&denied, "fixture")?;
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
},
FileSystemAccessMode::Write,
),
deny_glob("~/secret.*"),
]);
let mut request = sandbox_request(&cwd, &policy);
request
.env
.insert("HOME".to_owned(), home.to_string_lossy().into_owned());
for (tmpdir, expected) in [
("", None),
("scratch", Some(cwd.join("scratch"))),
("~/scratch", Some(home.join("scratch"))),
] {
request.env.insert("TMPDIR".to_owned(), tmpdir.to_owned());
let mut entries = expected
.into_iter()
.map(|path| FileSystemSandboxEntry::new(path.into(), FileSystemAccessMode::Write))
.collect::<Vec<_>>();
entries.push(FileSystemSandboxEntry::new(
denied.clone().into(),
FileSystemAccessMode::Deny,
));
assert_eq!(
backend::prepare_policy(&request, policy.clone(), &cwd)?,
FileSystemSandboxPolicy::restricted(entries),
);
}
Ok(())
}
@@ -0,0 +1,134 @@
//! Discover bubblewrap dependencies separately from resolving configured policy rules.
use super::dependencies::Dependency;
use super::dependencies::codex_and_launcher_dependencies;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxType;
use codex_shell_command::shell_detect::get_user_home_path;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use std::ffi::OsString;
use std::io;
use std::path::PathBuf;
pub(super) const SANDBOX_TYPE: SandboxType = SandboxType::LinuxSeccomp;
pub(super) const NAME: &str = "bubblewrap";
pub(super) fn critical_dependencies(
request: &SandboxExecRequest,
policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> io::Result<Vec<Dependency>> {
let command_cwd = request.cwd.to_abs_path()?;
let path = request
.env
.get("PATH")
.map(|path| {
std::env::join_paths(
std::env::split_paths(path).map(|entry| command_cwd.as_path().join(entry)),
)
})
.transpose()
.map_err(io::Error::other)?;
let mut dependencies = codex_and_launcher_dependencies(request);
if let Some(exe) = request.command.first()
&& let Some(bwrap) = codex_linux_sandbox::find_bundled_bwrap_for_exe(
&command_cwd.as_path().join(exe),
command_cwd.as_path(),
|key| request.env.get(key).map(OsString::from),
)
{
dependencies.push(("bundled_bwrap_candidate", Ok(bwrap.into_path_buf())));
}
if let Some(path) = &path
&& let Some(bwrap) = codex_sandboxing::find_executable_in_search_paths(
"bwrap",
std::env::split_paths(path),
command_cwd.as_path(),
policy,
cwd.as_path(),
)
{
// The launcher probes this executable even if it later uses its bundled copy.
dependencies.push(("system_bwrap_candidate", Ok(bwrap)));
}
if !policy
.get_unreadable_globs_with_cwd(cwd.as_path())
.is_empty()
&& let Some(path) = &path
&& let Some(scanner) = codex_sandboxing::find_executable_in_search_paths(
codex_linux_sandbox::GLOB_SCAN_PROGRAM,
std::env::split_paths(path),
command_cwd.as_path(),
policy,
cwd.as_path(),
)
{
dependencies.push(("glob_scanner", Ok(scanner)));
}
Ok(dependencies)
}
pub(super) fn prepare_policy(
request: &SandboxExecRequest,
mut policy: FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> io::Result<FileSystemSandboxPolicy> {
let command_cwd = request.cwd.to_abs_path()?;
let needs_home = policy.entries.iter().any(|entry| match &entry.path {
FileSystemPath::GlobPattern { pattern } => pattern.starts_with('~'),
FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
} => request
.env
.get("TMPDIR")
.is_some_and(|path| path.starts_with('~')),
FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => false,
});
let prepare = || {
// The helper resolves :tmpdir from its filtered environment, not the executor's.
policy.entries.retain_mut(|entry| {
if matches!(
entry.path,
FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir
}
) {
let Some(tmpdir) = request.env.get("TMPDIR").filter(|path| !path.is_empty()) else {
return false;
};
entry.path =
AbsolutePathBuf::resolve_path_against_base(tmpdir, &command_cwd).into();
}
true
});
let patterns = policy.get_unreadable_globs_with_cwd(cwd.as_path());
if !patterns.is_empty() {
let paths = codex_linux_sandbox::expand_unreadable_globs_in_environment(
&patterns,
cwd.as_path(),
&policy,
policy.glob_scan_max_depth,
&request.env,
command_cwd.as_path(),
)
.map_err(io::Error::other)?;
policy = policy.with_expanded_deny_globs(paths);
}
Ok(policy)
};
if !needs_home {
return prepare();
}
let home = request
.env
.get("HOME")
.filter(|home| !home.is_empty())
.map(PathBuf::from)
.or_else(get_user_home_path)
.ok_or_else(|| io::Error::other("could not resolve the command's home directory"))?;
AbsolutePathBufGuard::with_home_directory(&home, prepare)
}
@@ -1,4 +1,5 @@
#[path = "mxc.rs"]
#[cfg_attr(target_os = "linux", path = "bubblewrap.rs")]
#[cfg_attr(target_os = "windows", path = "mxc.rs")]
mod backend;
mod dependencies;
+21 -15
View File
@@ -1,51 +1,57 @@
use std::ffi::OsString;
#[cfg(debug_assertions)]
use std::fs::File;
#[cfg(debug_assertions)]
use std::io::BufRead;
use std::path::Path;
use std::path::PathBuf;
#[cfg(debug_assertions)]
const BAZEL_BWRAP_ENV_VAR: &str = "CARGO_BIN_EXE_bwrap";
#[cfg(debug_assertions)]
pub(crate) fn candidate() -> Option<PathBuf> {
if option_env!("BAZEL_PACKAGE").is_none() || !runfiles_env_present() {
pub(crate) fn candidate(cwd: &Path, env: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
if option_env!("BAZEL_PACKAGE").is_none() || !runfiles_env_present(&env) {
return None;
}
let raw = PathBuf::from(std::env::var_os(BAZEL_BWRAP_ENV_VAR)?);
let raw = PathBuf::from(env(BAZEL_BWRAP_ENV_VAR)?);
if raw.is_absolute() {
return Some(raw);
}
resolve_runfile(raw.to_str()?)
resolve_runfile(raw.to_str()?, cwd, &env)
}
#[cfg(not(debug_assertions))]
pub(crate) fn candidate() -> Option<PathBuf> {
pub(crate) fn candidate(_cwd: &Path, _env: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
None
}
#[cfg(debug_assertions)]
fn runfiles_env_present() -> bool {
std::env::var_os("RUNFILES_DIR").is_some()
|| std::env::var_os("TEST_SRCDIR").is_some()
|| std::env::var_os("RUNFILES_MANIFEST_FILE").is_some()
fn runfiles_env_present(env: &impl Fn(&str) -> Option<OsString>) -> bool {
env("RUNFILES_DIR").is_some()
|| env("TEST_SRCDIR").is_some()
|| env("RUNFILES_MANIFEST_FILE").is_some()
}
#[cfg(debug_assertions)]
fn resolve_runfile(logical_path: &str) -> Option<PathBuf> {
fn resolve_runfile(
logical_path: &str,
cwd: &Path,
env: &impl Fn(&str) -> Option<OsString>,
) -> Option<PathBuf> {
let mut logical_paths = vec![logical_path.to_string()];
if let Ok(workspace) = std::env::var("TEST_WORKSPACE")
if let Some(workspace) = env("TEST_WORKSPACE").and_then(|value| value.into_string().ok())
&& !workspace.is_empty()
{
logical_paths.push(format!("{workspace}/{logical_path}"));
}
for root_env in ["RUNFILES_DIR", "TEST_SRCDIR"] {
let Some(root) = std::env::var_os(root_env) else {
let Some(root) = env(root_env) else {
continue;
};
let root = PathBuf::from(root);
let root = cwd.join(root);
for logical_path in &logical_paths {
let candidate = root.join(logical_path);
if candidate.exists() {
@@ -54,14 +60,14 @@ fn resolve_runfile(logical_path: &str) -> Option<PathBuf> {
}
}
let manifest = PathBuf::from(std::env::var_os("RUNFILES_MANIFEST_FILE")?);
let manifest = cwd.join(env("RUNFILES_MANIFEST_FILE")?);
let file = File::open(manifest).ok()?;
for line in std::io::BufReader::new(file).lines().map_while(Result::ok) {
let Some((key, value)) = line.split_once(' ') else {
continue;
};
if logical_paths.iter().any(|logical_path| logical_path == key) {
return Some(PathBuf::from(value));
return Some(cwd.join(value));
}
}
None
+40 -10
View File
@@ -1,5 +1,6 @@
use std::ffi::CStr;
use std::ffi::CString;
use std::ffi::OsString;
use std::fs::File;
use std::io::Read;
use std::os::fd::AsRawFd;
@@ -27,9 +28,38 @@ pub(crate) struct BundledBwrapLauncher {
pub(crate) fn launcher() -> Option<BundledBwrapLauncher> {
let current_exe = std::env::current_exe().ok()?;
find_for_install_context(InstallContext::current())
.or_else(|| find_legacy_for_exe(&current_exe))
.map(|program| BundledBwrapLauncher { program })
find_program(
InstallContext::current(),
&current_exe,
Path::new("."),
|key| std::env::var_os(key),
)
.map(|program| BundledBwrapLauncher { program })
}
/// Discover the bundled fallback for an explicit launcher and its environment.
pub fn find_bundled_bwrap_for_exe(
exe: &Path,
command_cwd: &Path,
env: impl Fn(&str) -> Option<OsString>,
) -> Option<AbsolutePathBuf> {
let exe = std::fs::canonicalize(exe).ok()?;
let context = InstallContext::from_exe(
/*is_macos*/ false,
Some(&exe),
/*method_override*/ None,
);
find_program(&context, &exe, command_cwd, env)
}
fn find_program(
context: &InstallContext,
exe: &Path,
cwd: &Path,
env: impl Fn(&str) -> Option<OsString>,
) -> Option<AbsolutePathBuf> {
find_for_install_context(context)
.or_else(|| find_legacy_for_exe(exe, bazel_bwrap::candidate(cwd, env)))
}
impl BundledBwrapLauncher {
@@ -77,8 +107,8 @@ fn find_for_install_context(context: &InstallContext) -> Option<AbsolutePathBuf>
.filter(|path| is_executable_file(path))
}
fn find_legacy_for_exe(exe: &Path) -> Option<AbsolutePathBuf> {
legacy_candidates_for_exe(exe)
fn find_legacy_for_exe(exe: &Path, bazel_candidate: Option<PathBuf>) -> Option<AbsolutePathBuf> {
legacy_candidates_for_exe(exe, bazel_candidate)
.into_iter()
.find(|candidate| is_executable_file(candidate))
.map(|path| {
@@ -91,7 +121,7 @@ fn find_legacy_for_exe(exe: &Path) -> Option<AbsolutePathBuf> {
})
}
fn legacy_candidates_for_exe(exe: &Path) -> Vec<PathBuf> {
fn legacy_candidates_for_exe(exe: &Path, bazel_candidate: Option<PathBuf>) -> Vec<PathBuf> {
let Some(exe_dir) = exe.parent() else {
return Vec::new();
};
@@ -102,7 +132,7 @@ fn legacy_candidates_for_exe(exe: &Path) -> Vec<PathBuf> {
candidates.push(package_target_dir.join("codex-resources").join("bwrap"));
}
candidates.push(exe_dir.join("bwrap"));
if let Some(path) = bazel_bwrap::candidate() {
if let Some(path) = bazel_candidate {
candidates.push(path);
}
candidates
@@ -236,7 +266,7 @@ mod tests {
write_executable(&expected_bwrap);
assert_eq!(
find_legacy_for_exe(&exe),
find_legacy_for_exe(&exe, /*bazel_candidate*/ None),
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
);
}
@@ -251,7 +281,7 @@ mod tests {
write_executable(&expected_bwrap);
assert_eq!(
find_legacy_for_exe(&exe),
find_legacy_for_exe(&exe, /*bazel_candidate*/ None),
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
);
}
@@ -265,7 +295,7 @@ mod tests {
write_executable(&expected_bwrap);
assert_eq!(
find_legacy_for_exe(&exe),
find_legacy_for_exe(&exe, /*bazel_candidate*/ None),
Some(AbsolutePathBuf::from_absolute_path(&expected_bwrap).expect("absolute"))
);
}
+56 -6
View File
@@ -15,6 +15,7 @@
//! - bubblewrap used to construct the filesystem view before exec.
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::ffi::OsString;
use std::fs;
@@ -37,6 +38,7 @@ use codex_protocol::protocol::FileSystemPath;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::WritableRoot;
use codex_sandboxing::find_executable_in_search_paths;
use codex_sandboxing::find_pre_sandbox_executable_in_path;
use codex_utils_absolute_path::AbsolutePathBuf;
use globset::GlobBuilder;
@@ -59,6 +61,9 @@ const LINUX_PLATFORM_DEFAULT_READ_ROOTS: &[&str] = &[
"/run/current-system/sw",
];
/// External scanner used by deny-glob expansion and its dependency inventory.
pub const GLOB_SCAN_PROGRAM: &str = "rg";
const MAX_UNREADABLE_GLOB_MATCHES: usize = 8192;
pub(crate) const WSL_INTEROP_DIR: &str = "/run/WSL";
pub(crate) const WSLG_DISTRO_ROOT: &str = "/mnt/wslg/distro";
@@ -499,10 +504,17 @@ fn create_filesystem_args(
expand_unreadable_globs_with_ripgrep(
&unreadable_globs,
cwd,
file_system_sandbox_policy,
|| {
find_pre_sandbox_executable_in_path(
GLOB_SCAN_PROGRAM,
file_system_sandbox_policy,
cwd,
)
},
options
.glob_scan_max_depth
.or(file_system_sandbox_policy.glob_scan_max_depth),
|_| {},
)?
.into_iter()
.map(AbsolutePathBuf::into_path_buf),
@@ -888,17 +900,47 @@ fn append_daemon_socket_masks(
Ok(())
}
fn expand_unreadable_globs_with_ripgrep(
/// Expand deny globs with the launcher's scanner in an explicit command environment.
/// The supplied environment replaces inheritance, including when it is empty.
pub fn expand_unreadable_globs_in_environment(
patterns: &[String],
cwd: &Path,
file_system_policy: &FileSystemSandboxPolicy,
max_depth: Option<usize>,
env: &HashMap<String, String>,
command_cwd: &Path,
) -> Result<Vec<AbsolutePathBuf>> {
expand_unreadable_globs_with_ripgrep(
patterns,
cwd,
|| {
find_executable_in_search_paths(
GLOB_SCAN_PROGRAM,
std::env::split_paths(env.get("PATH")?),
command_cwd,
file_system_policy,
cwd,
)
},
max_depth,
|command| {
command.env_clear().envs(env).current_dir(command_cwd);
},
)
}
fn expand_unreadable_globs_with_ripgrep(
patterns: &[String],
cwd: &Path,
resolve_scanner: impl FnOnce() -> Option<PathBuf>,
max_depth: Option<usize>,
configure_command: impl Fn(&mut Command),
) -> Result<Vec<AbsolutePathBuf>> {
if patterns.is_empty() || max_depth == Some(0) {
return Ok(Vec::new());
}
let rg_path = find_pre_sandbox_executable_in_path("rg", file_system_policy, cwd);
let rg_path = resolve_scanner();
// Group each pattern by the static path prefix before its first glob
// metacharacter. That keeps scans narrow, avoids searching from `/`, and
@@ -924,7 +966,13 @@ fn expand_unreadable_globs_with_ripgrep(
let mut expanded_paths = BTreeSet::new();
for (search_root, globs) in patterns_by_search_root {
let paths = match &rg_path {
Some(rg_path) => ripgrep_files(rg_path, search_root.as_path(), &globs, max_depth)?,
Some(rg_path) => ripgrep_files(
rg_path,
search_root.as_path(),
&globs,
max_depth,
&configure_command,
)?,
None => glob_files(search_root.as_path(), &globs, max_depth)?,
};
for path in paths {
@@ -1013,11 +1061,13 @@ fn ripgrep_files(
search_root: &Path,
globs: &[String],
max_depth: Option<usize>,
configure_command: &impl Fn(&mut Command),
) -> Result<Vec<AbsolutePathBuf>> {
let mut command = Command::new(rg_path);
configure_command(&mut command);
// Use `rg --files` rather than shell expansion so dotfiles and ignored files
// are still considered. A status 1 with no stderr is ripgrep's "no matches"
// case, not a sandbox construction error.
let mut command = Command::new(rg_path);
command
.arg("--no-config")
.arg("--files")
@@ -3028,7 +3078,7 @@ mod tests {
}
fn ripgrep_available() -> bool {
Command::new("rg")
Command::new(GLOB_SCAN_PROGRAM)
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
+7
View File
@@ -28,6 +28,13 @@ mod proxy_routing;
#[cfg(target_os = "linux")]
mod wslg;
#[cfg(target_os = "linux")]
pub use bundled_bwrap::find_bundled_bwrap_for_exe;
#[cfg(target_os = "linux")]
pub use bwrap::GLOB_SCAN_PROGRAM;
#[cfg(target_os = "linux")]
pub use bwrap::expand_unreadable_globs_in_environment;
/// Exit status returned when bundled bubblewrap fails digest verification.
#[cfg(target_os = "linux")]
pub const BUNDLED_BWRAP_DIGEST_VERIFICATION_FAILURE_EXIT_CODE: i32 = 8;
+2 -1
View File
@@ -206,7 +206,8 @@ pub fn find_pre_sandbox_executable_in_path(
)
}
fn find_executable_in_search_paths(
/// Apply the pre-sandbox executable selection rules to an explicit command context.
pub fn find_executable_in_search_paths(
program: &str,
search_paths: impl IntoIterator<Item = PathBuf>,
cwd: &Path,
+2
View File
@@ -15,6 +15,8 @@ mod windows;
#[cfg(windows)]
mod windows_mxc;
#[cfg(target_os = "linux")]
pub use bwrap::find_executable_in_search_paths;
#[cfg(target_os = "linux")]
pub use bwrap::find_pre_sandbox_executable_in_path;
#[cfg(target_os = "linux")]
+18 -5
View File
@@ -60,9 +60,22 @@ pub fn detect_shell_type(shell_path: impl AsRef<std::path::Path>) -> Option<Shel
#[cfg(unix)]
fn get_user_shell_path() -> Option<PathBuf> {
get_user_path(|passwd| passwd.pw_shell)
.map(|path| PathBuf::from(path.to_string_lossy().into_owned()))
}
/// Read the current account's home directory without consulting the environment.
#[cfg(unix)]
pub fn get_user_home_path() -> Option<PathBuf> {
get_user_path(|passwd| passwd.pw_dir).filter(|path| !path.as_os_str().is_empty())
}
#[cfg(unix)]
fn get_user_path(field: fn(&libc::passwd) -> *mut libc::c_char) -> Option<PathBuf> {
let uid = unsafe { libc::getuid() };
use std::ffi::CStr;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::ptr;
let mut passwd = MaybeUninit::<libc::passwd>::uninit();
@@ -96,14 +109,14 @@ fn get_user_shell_path() -> Option<PathBuf> {
}
let passwd = unsafe { passwd.assume_init_ref() };
if passwd.pw_shell.is_null() {
let path = field(passwd);
if path.is_null() {
return None;
}
let shell_path = unsafe { CStr::from_ptr(passwd.pw_shell) }
.to_string_lossy()
.into_owned();
return Some(PathBuf::from(shell_path));
// Both callers select a field stored in the still-live passwd buffer.
let path = unsafe { CStr::from_ptr(path) };
return Some(PathBuf::from(std::ffi::OsStr::from_bytes(path.to_bytes())));
}
if status != libc::ERANGE {