use crate::runtime::SandboxPolicy;
pub(crate) const fn windows_sandbox_unavailable() -> bool {
cfg!(windows)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SandboxDisableReason {
Policy,
AppImageUserNamespacesRestricted,
AppImageUserNamespacesUnavailable,
WindowsBrokerUnavailable,
}
impl SandboxDisableReason {
pub(crate) fn message(self) -> &'static str {
match self {
Self::Policy => "the application set SandboxPolicy::Disabled",
Self::AppImageUserNamespacesRestricted => {
"running from an AppImage, which cannot ship the setuid chrome-sandbox helper, \
and unprivileged user namespaces are restricted \
(/proc/sys/kernel/apparmor_restrict_unprivileged_userns is 1)"
}
Self::AppImageUserNamespacesUnavailable => {
"running from an AppImage, which cannot ship the setuid chrome-sandbox helper, \
and unprivileged user namespaces are unavailable \
(/proc/sys/user/max_user_namespaces is 0)"
}
Self::WindowsBrokerUnavailable => {
"the Windows sandbox needs a broker this runtime cannot supply: CEF requires the \
application to be hosted by its bootstrap executable as a DLL, and a Tauri \
application is built as an executable"
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SandboxDecision {
Keep,
Disable(SandboxDisableReason),
Refuse(SandboxDisableReason),
}
pub(crate) fn sandbox_decision(
policy: SandboxPolicy,
windows_broker_unavailable: bool,
running_from_appimage: bool,
sandbox_helper_available: bool,
apparmor_restrict_unprivileged_userns: Option<u64>,
max_user_namespaces: Option<u64>,
) -> SandboxDecision {
if let SandboxPolicy::Disabled = policy {
return SandboxDecision::Disable(SandboxDisableReason::Policy);
}
if windows_broker_unavailable {
return match policy {
SandboxPolicy::Required => {
SandboxDecision::Refuse(SandboxDisableReason::WindowsBrokerUnavailable)
}
_ => SandboxDecision::Disable(SandboxDisableReason::WindowsBrokerUnavailable),
};
}
match policy {
SandboxPolicy::Disabled => SandboxDecision::Disable(SandboxDisableReason::Policy),
SandboxPolicy::Required => SandboxDecision::Keep,
SandboxPolicy::Auto => {
if !running_from_appimage || sandbox_helper_available {
return SandboxDecision::Keep;
}
if apparmor_restrict_unprivileged_userns == Some(1) {
SandboxDecision::Disable(SandboxDisableReason::AppImageUserNamespacesRestricted)
} else if max_user_namespaces == Some(0) {
SandboxDecision::Disable(SandboxDisableReason::AppImageUserNamespacesUnavailable)
} else {
SandboxDecision::Keep
}
}
}
}
#[cfg(target_os = "macos")]
pub(crate) fn launched_without_sandbox() -> bool {
std::env::args().any(|arg| arg == "--no-sandbox")
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn helper_stat_is_usable(uid: u32, mode: u32) -> bool {
const SETUID: u32 = 0o4000;
const OTHER_EXECUTE: u32 = 0o0001;
uid == 0 && mode & SETUID != 0 && mode & OTHER_EXECUTE != 0
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn resolve_sandbox_decision(policy: SandboxPolicy) -> SandboxDecision {
let running_from_appimage = running_from_appimage();
sandbox_decision(
policy,
windows_sandbox_unavailable(),
running_from_appimage,
sandbox_helper_available(running_from_appimage),
read_sysctl("/proc/sys/kernel/apparmor_restrict_unprivileged_userns"),
read_sysctl("/proc/sys/user/max_user_namespaces"),
)
}
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
pub(crate) fn resolve_sandbox_decision(policy: SandboxPolicy) -> SandboxDecision {
sandbox_decision(
policy,
windows_sandbox_unavailable(),
false,
false,
None,
None,
)
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn running_from_appimage() -> bool {
std::env::var_os("APPIMAGE").is_some_and(|path| !path.is_empty())
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn sandbox_helper_available(running_from_appimage: bool) -> bool {
if let Some(path) = std::env::var_os("CHROME_DEVEL_SANDBOX").filter(|path| !path.is_empty()) {
return helper_path_is_usable(std::path::Path::new(&path));
}
if running_from_appimage {
return false;
}
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join("chrome-sandbox")))
.is_some_and(|helper| helper_path_is_usable(&helper))
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn helper_path_is_usable(path: &std::path::Path) -> bool {
use std::os::unix::fs::MetadataExt;
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
let usable = helper_stat_is_usable(metadata.uid(), metadata.mode());
if !usable {
log::debug!(
"ignoring the chrome-sandbox helper at {}: it is not a root-owned setuid binary executable by others",
path.display()
);
}
usable
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn read_sysctl(path: &str) -> Option<u64> {
std::fs::read_to_string(path).ok()?.trim().parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn auto(
running_from_appimage: bool,
sandbox_helper_available: bool,
apparmor: Option<u64>,
max_user_namespaces: Option<u64>,
) -> SandboxDecision {
sandbox_decision(
SandboxPolicy::Auto,
false,
running_from_appimage,
sandbox_helper_available,
apparmor,
max_user_namespaces,
)
}
#[test]
fn explicit_policies_ignore_the_system() {
for appimage in [false, true] {
for helper in [false, true] {
assert_eq!(
sandbox_decision(
SandboxPolicy::Disabled,
false,
appimage,
helper,
Some(1),
Some(0)
),
SandboxDecision::Disable(SandboxDisableReason::Policy)
);
assert_eq!(
sandbox_decision(
SandboxPolicy::Required,
false,
appimage,
helper,
Some(1),
Some(0)
),
SandboxDecision::Keep,
"Required must keep the sandbox even when Chromium will abort"
);
}
}
}
#[test]
fn a_platform_without_a_broker_never_reports_a_sandbox_it_does_not_have() {
assert_eq!(
sandbox_decision(SandboxPolicy::Auto, true, false, false, None, None),
SandboxDecision::Disable(SandboxDisableReason::WindowsBrokerUnavailable)
);
}
#[test]
fn required_refuses_to_start_where_the_sandbox_cannot_be_provided() {
assert_eq!(
sandbox_decision(SandboxPolicy::Required, true, false, false, None, None),
SandboxDecision::Refuse(SandboxDisableReason::WindowsBrokerUnavailable)
);
}
#[test]
fn disabled_is_answered_before_the_platform_is_consulted() {
assert_eq!(
sandbox_decision(SandboxPolicy::Disabled, true, false, false, None, None),
SandboxDecision::Disable(SandboxDisableReason::Policy)
);
}
#[test]
fn auto_keeps_the_sandbox_outside_an_appimage() {
assert_eq!(auto(false, false, Some(1), Some(0)), SandboxDecision::Keep);
assert_eq!(auto(false, true, None, None), SandboxDecision::Keep);
}
#[test]
fn auto_keeps_the_sandbox_when_the_helper_is_available() {
assert_eq!(auto(true, true, Some(1), Some(0)), SandboxDecision::Keep);
}
#[test]
fn auto_keeps_the_sandbox_when_user_namespaces_work() {
assert_eq!(
auto(true, false, Some(0), Some(31231)),
SandboxDecision::Keep
);
}
#[test]
fn auto_disables_for_an_appimage_restricted_by_apparmor() {
assert_eq!(
auto(true, false, Some(1), Some(31231)),
SandboxDecision::Disable(SandboxDisableReason::AppImageUserNamespacesRestricted)
);
}
#[test]
fn auto_disables_for_an_appimage_without_user_namespaces() {
assert_eq!(
auto(true, false, Some(0), Some(0)),
SandboxDecision::Disable(SandboxDisableReason::AppImageUserNamespacesUnavailable)
);
assert_eq!(
auto(true, false, None, Some(0)),
SandboxDecision::Disable(SandboxDisableReason::AppImageUserNamespacesUnavailable)
);
}
#[test]
fn unreadable_sysctls_are_not_evidence_of_a_restriction() {
assert_eq!(auto(true, false, None, None), SandboxDecision::Keep);
}
#[test]
fn apparmor_restriction_is_reported_over_a_missing_namespace_quota() {
assert_eq!(
auto(true, false, Some(1), Some(0)),
SandboxDecision::Disable(SandboxDisableReason::AppImageUserNamespacesRestricted)
);
}
#[test]
fn nothing_to_probe_means_the_policy_decides() {
assert_eq!(
sandbox_decision(SandboxPolicy::Auto, false, false, false, None, None),
SandboxDecision::Keep,
"Auto must keep the sandbox where there is no AppImage case to escape"
);
assert_eq!(
sandbox_decision(SandboxPolicy::Required, false, false, false, None, None),
SandboxDecision::Keep
);
assert_eq!(
sandbox_decision(SandboxPolicy::Disabled, false, false, false, None, None),
SandboxDecision::Disable(SandboxDisableReason::Policy),
"Disabled is the only way for macOS to lose the sandbox"
);
}
#[test]
fn the_windows_broker_is_reported_as_unavailable_only_on_windows() {
assert_eq!(windows_sandbox_unavailable(), cfg!(windows));
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[test]
fn a_correctly_installed_helper_is_usable() {
assert!(helper_stat_is_usable(0, 0o104755));
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[test]
fn a_helper_missing_any_of_chromiums_conditions_is_not_usable() {
assert!(
!helper_stat_is_usable(1000, 0o104755),
"a helper not owned by root cannot raise privileges"
);
assert!(
!helper_stat_is_usable(0, 0o100755),
"without the setuid bit the helper runs as the calling user"
);
assert!(
!helper_stat_is_usable(0, 0o104750),
"the helper has to be executable by others"
);
assert!(!helper_stat_is_usable(1000, 0o100644));
}
}