cli: add macOS seatbelt debug flags for Mach services and Apple events

This commit is contained in:
Conrad Kramer
2026-05-05 12:06:34 -07:00
committed by Greg
parent 4de7a2b9d8
commit 186ed88fa1
5 changed files with 348 additions and 21 deletions

View File

@@ -49,6 +49,9 @@ pub async fn run_command_under_seatbelt(
config_profile: _,
cwd,
include_managed_config,
allow_mach_services,
allow_appleevent_bundle_ids,
allow_lsopen,
allow_unix_sockets,
log_denials,
config_overrides,
@@ -58,8 +61,8 @@ pub async fn run_command_under_seatbelt(
&permissions_profile,
include_managed_config,
);
run_command_under_sandbox(
DebugSandboxConfigOptions {
run_command_under_sandbox(RunCommandUnderSandboxParams {
config_options: DebugSandboxConfigOptions {
permissions_profile,
cwd,
managed_requirements_mode,
@@ -68,10 +71,13 @@ pub async fn run_command_under_seatbelt(
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Seatbelt,
sandbox_type: SandboxType::Seatbelt,
log_denials,
&allow_unix_sockets,
)
allow_mach_services: &allow_mach_services,
allow_appleevent_bundle_ids: &allow_appleevent_bundle_ids,
allow_lsopen,
allow_unix_sockets: &allow_unix_sockets,
})
.await
}
@@ -101,8 +107,8 @@ pub async fn run_command_under_landlock(
&permissions_profile,
include_managed_config,
);
run_command_under_sandbox(
DebugSandboxConfigOptions {
run_command_under_sandbox(RunCommandUnderSandboxParams {
config_options: DebugSandboxConfigOptions {
permissions_profile,
cwd,
managed_requirements_mode,
@@ -111,10 +117,13 @@ pub async fn run_command_under_landlock(
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Landlock,
/*log_denials*/ false,
&[],
)
sandbox_type: SandboxType::Landlock,
log_denials: false,
allow_mach_services: &[],
allow_appleevent_bundle_ids: &[],
allow_lsopen: false,
allow_unix_sockets: &[],
})
.await
}
@@ -135,8 +144,8 @@ pub async fn run_command_under_windows_sandbox(
&permissions_profile,
include_managed_config,
);
run_command_under_sandbox(
DebugSandboxConfigOptions {
run_command_under_sandbox(RunCommandUnderSandboxParams {
config_options: DebugSandboxConfigOptions {
permissions_profile,
cwd,
managed_requirements_mode,
@@ -145,10 +154,13 @@ pub async fn run_command_under_windows_sandbox(
command,
config_overrides,
codex_linux_sandbox_exe,
SandboxType::Windows,
/*log_denials*/ false,
&[],
)
sandbox_type: SandboxType::Windows,
log_denials: false,
allow_mach_services: &[],
allow_appleevent_bundle_ids: &[],
allow_lsopen: false,
allow_unix_sockets: &[],
})
.await
}
@@ -186,16 +198,43 @@ impl ManagedRequirementsMode {
}
}
async fn run_command_under_sandbox(
struct RunCommandUnderSandboxParams<'a> {
config_options: DebugSandboxConfigOptions,
command: Vec<String>,
config_overrides: CliConfigOverrides,
codex_linux_sandbox_exe: Option<PathBuf>,
sandbox_type: SandboxType,
log_denials: bool,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
allow_unix_sockets: &[AbsolutePathBuf],
) -> anyhow::Result<()> {
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
allow_mach_services: &'a [String],
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
allow_appleevent_bundle_ids: &'a [String],
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
allow_lsopen: bool,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
allow_unix_sockets: &'a [AbsolutePathBuf],
}
async fn run_command_under_sandbox(params: RunCommandUnderSandboxParams<'_>) -> anyhow::Result<()> {
let RunCommandUnderSandboxParams {
config_options,
command,
config_overrides,
codex_linux_sandbox_exe,
sandbox_type,
log_denials,
allow_mach_services,
allow_appleevent_bundle_ids,
allow_lsopen,
allow_unix_sockets,
} = params;
#[cfg(not(target_os = "macos"))]
let _ = (
allow_mach_services,
allow_appleevent_bundle_ids,
allow_lsopen,
allow_unix_sockets,
);
let config = load_debug_sandbox_config(
config_overrides
.parse_overrides()
@@ -280,6 +319,9 @@ async fn run_command_under_sandbox(
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
enforce_managed_network: false,
network: network.as_ref(),
extra_mach_services: allow_mach_services,
extra_appleevent_bundle_ids: allow_appleevent_bundle_ids,
allow_lsopen,
extra_allow_unix_sockets: allow_unix_sockets,
});
spawn_debug_sandbox_child(

View File

@@ -50,6 +50,18 @@ pub struct SeatbeltCommand {
)]
pub include_managed_config: bool,
/// Allow the sandboxed command to look up this Mach service name. Repeat to allow multiple services.
#[arg(long = "allow-mach-service", value_name = "SERVICE", value_parser = parse_non_empty_string)]
pub allow_mach_services: Vec<String>,
/// Allow the sandboxed command to send AppleEvents to this destination bundle ID. Repeat to allow multiple destinations.
#[arg(long = "allow-appleevent-destination", value_name = "BUNDLE_ID", value_parser = parse_non_empty_string)]
pub allow_appleevent_bundle_ids: Vec<String>,
/// Allow the sandboxed command to use LaunchServices open APIs.
#[arg(long = "allow-lsopen", default_value_t = false)]
pub allow_lsopen: bool,
/// Allow the sandboxed command to bind/connect AF_UNIX sockets rooted at this path. Relative paths are resolved against the current directory. Repeat to allow multiple paths.
#[arg(long = "allow-unix-socket", value_parser = parse_allow_unix_socket_path)]
pub allow_unix_sockets: Vec<AbsolutePathBuf>,
@@ -71,6 +83,15 @@ fn parse_allow_unix_socket_path(raw: &str) -> Result<AbsolutePathBuf, String> {
.map_err(|err| format!("invalid path {raw}: {err}"))
}
fn parse_non_empty_string(raw: &str) -> Result<String, String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
Err("value must not be empty".to_string())
} else {
Ok(trimmed.to_string())
}
}
#[derive(Debug, Parser)]
pub struct LandlockCommand {
/// Named permissions profile to apply from the active configuration stack.
@@ -140,3 +161,45 @@ pub struct WindowsCommand {
#[arg(trailing_var_arg = true)]
pub command: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::SeatbeltCommand;
use clap::Parser;
use pretty_assertions::assert_eq;
#[test]
fn seatbelt_command_parses_additional_allowlist_flags() {
let command = SeatbeltCommand::try_parse_from([
"seatbelt",
"--allow-mach-service",
"com.apple.foo",
"--allow-mach-service",
"com.apple.bar",
"--allow-appleevent-destination",
"com.apple.finder",
"--allow-lsopen",
"--allow-unix-socket",
"/tmp/codex.sock",
"--",
"/bin/echo",
"hi",
])
.expect("parse");
assert_eq!(
command.allow_mach_services,
vec!["com.apple.foo".to_string(), "com.apple.bar".to_string()]
);
assert_eq!(
command.allow_appleevent_bundle_ids,
vec!["com.apple.finder".to_string()]
);
assert!(command.allow_lsopen);
assert_eq!(command.allow_unix_sockets.len(), 1);
assert_eq!(
command.command,
vec!["/bin/echo".to_string(), "hi".to_string()]
);
}
}

View File

@@ -233,6 +233,9 @@ impl SandboxManager {
sandbox_policy_cwd,
enforce_managed_network,
network,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
let mut full_command = Vec::with_capacity(1 + args.len());

View File

@@ -241,6 +241,61 @@ fn unix_socket_policy(proxy: &ProxyPolicyInputs) -> String {
policy
}
fn seatbelt_string_literal(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
fn extra_mach_lookup_policy(extra_mach_services: &[String]) -> String {
let services = extra_mach_services
.iter()
.map(|service| service.trim())
.filter(|service| !service.is_empty())
.collect::<BTreeSet<_>>();
if services.is_empty() {
return String::new();
}
let services = services
.into_iter()
.map(|service| format!(" (global-name \"{}\")", seatbelt_string_literal(service)))
.collect::<Vec<_>>()
.join("\n");
format!("(allow mach-lookup\n{services}\n)")
}
fn extra_appleevent_policy(extra_appleevent_bundle_ids: &[String]) -> String {
let bundle_ids = extra_appleevent_bundle_ids
.iter()
.map(|bundle_id| bundle_id.trim())
.filter(|bundle_id| !bundle_id.is_empty())
.collect::<BTreeSet<_>>();
if bundle_ids.is_empty() {
return String::new();
}
let destinations = bundle_ids
.into_iter()
.map(|bundle_id| {
format!(
" (appleevent-destination \"{}\")",
seatbelt_string_literal(bundle_id)
)
})
.collect::<Vec<_>>()
.join("\n");
format!(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))\n(allow appleevent-send\n{destinations}\n)"
)
}
fn lsopen_policy(allow_lsopen: bool) -> String {
if allow_lsopen {
"(allow lsopen)".to_string()
} else {
String::new()
}
}
#[cfg_attr(not(test), allow(dead_code))]
fn dynamic_network_policy(
sandbox_policy: &SandboxPolicy,
@@ -584,6 +639,9 @@ fn create_seatbelt_command_args_for_legacy_policy(
sandbox_policy_cwd,
enforce_managed_network,
network,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
})
}
@@ -596,6 +654,9 @@ pub struct CreateSeatbeltCommandArgsParams<'a> {
pub sandbox_policy_cwd: &'a Path,
pub enforce_managed_network: bool,
pub network: Option<&'a NetworkProxy>,
pub extra_mach_services: &'a [String],
pub extra_appleevent_bundle_ids: &'a [String],
pub allow_lsopen: bool,
pub extra_allow_unix_sockets: &'a [AbsolutePathBuf],
}
@@ -607,6 +668,9 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
sandbox_policy_cwd,
enforce_managed_network,
network,
extra_mach_services,
extra_appleevent_bundle_ids,
allow_lsopen,
extra_allow_unix_sockets,
} = args;
@@ -704,6 +768,9 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
let proxy = proxy_policy_inputs(network, extra_allow_unix_sockets);
let network_policy =
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
let mach_lookup_policy = extra_mach_lookup_policy(extra_mach_services);
let appleevent_policy = extra_appleevent_policy(extra_appleevent_bundle_ids);
let lsopen_policy = lsopen_policy(allow_lsopen);
let include_platform_defaults = file_system_sandbox_policy.include_platform_defaults();
let deny_read_policy =
@@ -715,6 +782,15 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
deny_read_policy,
network_policy,
];
if !mach_lookup_policy.is_empty() {
policy_sections.push(mach_lookup_policy);
}
if !appleevent_policy.is_empty() {
policy_sections.push(appleevent_policy);
}
if !lsopen_policy.is_empty() {
policy_sections.push(lsopen_policy);
}
if include_platform_defaults {
policy_sections.push(MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS.to_string());
}

View File

@@ -206,6 +206,9 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access()
sandbox_policy_cwd: Path::new("/"),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
@@ -278,6 +281,9 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
sandbox_policy_cwd: Path::new("/"),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
@@ -581,6 +587,9 @@ fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() {
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &extra_allow_unix_sockets,
});
let policy = seatbelt_policy_arg(&args);
@@ -639,6 +648,9 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: Some(&network_proxy),
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &extra_allow_unix_sockets,
});
@@ -680,6 +692,9 @@ fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths()
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &extra_allow_unix_sockets,
});
let policy = seatbelt_policy_arg(&args);
@@ -723,6 +738,134 @@ fn unix_socket_policy_non_empty_output_is_newline_terminated() {
);
}
#[test]
fn create_seatbelt_args_allowlists_extra_mach_services() {
let cwd = TempDir::new().expect("temp cwd");
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&SandboxPolicy::new_read_only_policy(),
cwd.path(),
);
let extra_mach_services = vec![
"com.apple.beta".to_string(),
"com.apple.alpha".to_string(),
"com.apple.beta".to_string(),
];
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: vec!["/usr/bin/true".to_string()],
file_system_sandbox_policy: &file_system_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &extra_mach_services,
extra_appleevent_bundle_ids: &[],
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
let policy = seatbelt_policy_arg(&args);
assert!(
policy.contains(
"(allow mach-lookup\n (global-name \"com.apple.alpha\")\n (global-name \"com.apple.beta\")\n)"
),
"policy should allow the requested Mach services in stable order:\n{policy}"
);
assert_eq!(
policy.matches("(global-name \"com.apple.alpha\")").count(),
1
);
assert_eq!(
policy.matches("(global-name \"com.apple.beta\")").count(),
1
);
}
#[test]
fn create_seatbelt_args_allowlists_appleevent_destinations() {
let cwd = TempDir::new().expect("temp cwd");
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&SandboxPolicy::new_read_only_policy(),
cwd.path(),
);
let extra_appleevent_bundle_ids = vec![
"com.apple.mail".to_string(),
"com.apple.finder".to_string(),
"com.apple.mail".to_string(),
];
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: vec!["/usr/bin/true".to_string()],
file_system_sandbox_policy: &file_system_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &extra_appleevent_bundle_ids,
allow_lsopen: false,
extra_allow_unix_sockets: &[],
});
let policy = seatbelt_policy_arg(&args);
assert!(
policy.contains(
"(allow mach-lookup\n (global-name \"com.apple.coreservices.appleevents\"))"
),
"policy should allow lookup of the AppleEvents service:\n{policy}"
);
assert!(
policy.contains(
"(allow appleevent-send\n (appleevent-destination \"com.apple.finder\")\n (appleevent-destination \"com.apple.mail\")\n)"
),
"policy should allow the requested AppleEvent destinations in stable order:\n{policy}"
);
assert_eq!(
policy
.matches("(global-name \"com.apple.coreservices.appleevents\")")
.count(),
1
);
assert_eq!(
policy
.matches("(appleevent-destination \"com.apple.finder\")")
.count(),
1
);
assert_eq!(
policy
.matches("(appleevent-destination \"com.apple.mail\")")
.count(),
1
);
}
#[test]
fn create_seatbelt_args_allows_lsopen_when_requested() {
let cwd = TempDir::new().expect("temp cwd");
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&SandboxPolicy::new_read_only_policy(),
cwd.path(),
);
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: vec!["/usr/bin/true".to_string()],
file_system_sandbox_policy: &file_system_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
network: None,
extra_mach_services: &[],
extra_appleevent_bundle_ids: &[],
allow_lsopen: true,
extra_allow_unix_sockets: &[],
});
let policy = seatbelt_policy_arg(&args);
assert!(
policy.contains("(allow lsopen)"),
"policy should allow lsopen when requested:\n{policy}"
);
assert_eq!(policy.matches("(allow lsopen)").count(), 1);
}
#[test]
fn unix_socket_dir_params_use_stable_param_names() {
let params = unix_socket_dir_params(&ProxyPolicyInputs {