windows-sandbox: drive write roots from resolved permissions

This commit is contained in:
Michael Bolin
2026-05-15 16:13:43 -07:00
parent 56238d2fa2
commit a4be788173
6 changed files with 136 additions and 94 deletions

View File

@@ -8,7 +8,8 @@ use crate::logging::debug_log;
use crate::logging::log_note;
use crate::path_normalization::canonical_path_key;
use crate::policy::SandboxPolicy;
use crate::setup::effective_write_roots_for_setup;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup::effective_write_roots_for_permissions;
use crate::token::LocalSid;
use crate::token::world_sid;
use anyhow::Result;
@@ -259,11 +260,18 @@ pub fn apply_capability_denies_for_world_writable(
let cap_path = cap_sid_file(codex_home);
let caps = load_or_create_cap_sids(codex_home)?;
std::fs::write(&cap_path, serde_json::to_string(&caps)?)?;
let (active_sids, workspace_roots): (Vec<LocalSid>, Vec<PathBuf>) = match sandbox_policy {
SandboxPolicy::WorkspaceWrite { .. } => {
let roots = effective_write_roots_for_setup(
sandbox_policy,
cwd,
if matches!(
sandbox_policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
return Ok(());
}
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(sandbox_policy, cwd);
let (active_sids, workspace_roots): (Vec<LocalSid>, Vec<PathBuf>) =
if permissions.uses_write_capabilities_for_cwd(cwd, env_map) {
let roots = effective_write_roots_for_permissions(
&permissions,
cwd,
env_map,
codex_home,
@@ -277,14 +285,9 @@ pub fn apply_capability_denies_for_world_writable(
})
.collect::<Result<Vec<_>>>()?;
(active_sids, roots)
}
SandboxPolicy::ReadOnly { .. } => {
} else {
(vec![LocalSid::from_string(&caps.readonly)?], Vec::new())
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
return Ok(());
}
};
};
for path in flagged {
if workspace_roots
.iter()

View File

@@ -39,10 +39,11 @@ mod windows_impl {
use crate::logging::log_success;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::spawn_runner_transport;
use crate::sandbox_utils::ensure_codex_home_exists;
use crate::sandbox_utils::inject_git_safe_directory;
use crate::setup::effective_write_roots_for_setup;
use crate::setup::effective_write_roots_for_permissions;
use crate::token::LocalSid;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
@@ -81,6 +82,8 @@ mod windows_impl {
.map(AbsolutePathBuf::to_path_buf)
.collect::<Vec<_>>();
let policy = parse_policy(policy_json_or_preset)?;
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, cwd);
normalize_null_device_env(&mut env_map);
ensure_non_interactive_pager(&mut env_map);
inherit_path_env(&mut env_map);
@@ -112,32 +115,26 @@ mod windows_impl {
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
}
let caps = load_or_create_cap_sids(codex_home)?;
let (sid_for_null, cap_sids) = match &policy {
SandboxPolicy::ReadOnly { .. } => {
let sid = LocalSid::from_string(&caps.readonly)?;
(sid, vec![caps.readonly])
}
SandboxPolicy::WorkspaceWrite { .. } => {
let write_roots = effective_write_roots_for_setup(
&policy,
sandbox_policy_cwd,
cwd,
&env_map,
codex_home,
write_roots_override,
);
let cap_sids = write_roots
.iter()
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
.collect::<Result<Vec<_>>>()?;
if cap_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
unreachable!("DangerFullAccess handled above")
let (sid_for_null, cap_sids) = if permissions.uses_write_capabilities_for_cwd(cwd, &env_map)
{
let write_roots = effective_write_roots_for_permissions(
&permissions,
cwd,
&env_map,
codex_home,
write_roots_override,
);
let cap_sids = write_roots
.iter()
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
.collect::<Result<Vec<_>>>()?;
if cap_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
} else {
let sid = LocalSid::from_string(&caps.readonly)?;
(sid, vec![caps.readonly])
};
unsafe {

View File

@@ -1,12 +1,13 @@
use crate::dpapi;
use crate::logging::debug_log;
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup::SandboxNetworkIdentity;
use crate::setup::SandboxUserRecord;
use crate::setup::SandboxUsersFile;
use crate::setup::SetupMarker;
use crate::setup::gather_read_roots;
use crate::setup::gather_write_roots;
use crate::setup::gather_write_roots_for_permissions;
use crate::setup::offline_proxy_settings_from_env;
use crate::setup::run_elevated_setup;
use crate::setup::run_setup_refresh_with_overrides;
@@ -143,14 +144,16 @@ pub fn require_logon_sandbox_creds(
deny_write_paths_override: &[PathBuf],
proxy_enforced: bool,
) -> Result<SandboxCreds> {
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, command_cwd);
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
let needed_read = read_roots_override
.map(<[PathBuf]>::to_vec)
.unwrap_or_else(|| gather_read_roots(command_cwd, policy, codex_home));
let needed_write = write_roots_override
.map(<[PathBuf]>::to_vec)
.unwrap_or_else(|| gather_write_roots(policy, policy_cwd, command_cwd, env_map));
let network_identity = SandboxNetworkIdentity::from_policy(policy, proxy_enforced);
.unwrap_or_else(|| gather_write_roots_for_permissions(&permissions, command_cwd, env_map));
let network_identity = SandboxNetworkIdentity::from_permissions(&permissions, proxy_enforced);
let desired_offline_proxy_settings = offline_proxy_settings_from_env(env_map, network_identity);
// NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the
// restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for

View File

@@ -94,6 +94,18 @@ impl ResolvedWindowsSandboxPermissions {
!self.network.is_enabled()
}
pub(crate) fn network_policy(&self) -> NetworkSandboxPolicy {
self.network
}
pub(crate) fn uses_write_capabilities_for_cwd(
&self,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> bool {
!self.writable_roots_for_cwd(cwd, env_map).is_empty()
}
pub(crate) fn writable_roots_for_cwd(
&self,
cwd: &Path,

View File

@@ -17,6 +17,7 @@ use crate::logging::log_note;
use crate::path_normalization::canonical_path_key;
use crate::path_normalization::canonicalize_path;
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup_error::SetupErrorCode;
use crate::setup_error::SetupFailure;
use crate::setup_error::clear_setup_error_report;
@@ -173,8 +174,12 @@ fn run_setup_refresh_inner(
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
request.policy,
request.command_cwd,
);
let network_identity =
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
SandboxNetworkIdentity::from_permissions(&permissions, request.proxy_enforced);
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
let payload = ElevationPayload {
version: SETUP_VERSION,
@@ -389,15 +394,16 @@ pub(crate) fn gather_read_roots(
gather_legacy_full_read_roots(command_cwd, policy, codex_home)
}
pub(crate) fn gather_write_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
pub(crate) fn gather_write_roots_for_permissions(
permissions: &ResolvedWindowsSandboxPermissions,
command_cwd: &Path,
env_map: &HashMap<String, String>,
) -> Vec<PathBuf> {
let AllowDenyPaths { allow, .. } =
compute_allow_paths(policy, policy_cwd, command_cwd, env_map);
let roots: Vec<PathBuf> = allow.into_iter().collect();
let roots = permissions
.writable_roots_for_cwd(command_cwd, env_map)
.into_iter()
.map(|root| root.root)
.collect::<Vec<_>>();
let mut dedup: HashSet<PathBuf> = HashSet::new();
let mut out: Vec<PathBuf> = Vec::new();
for r in canonical_existing(&roots) {
@@ -410,7 +416,25 @@ pub(crate) fn gather_write_roots(
pub(crate) fn effective_write_roots_for_setup(
policy: &SandboxPolicy,
policy_cwd: &Path,
_policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
write_roots_override: Option<&[PathBuf]>,
) -> Vec<PathBuf> {
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, command_cwd);
effective_write_roots_for_permissions(
&permissions,
command_cwd,
env_map,
codex_home,
write_roots_override,
)
}
pub(crate) fn effective_write_roots_for_permissions(
permissions: &ResolvedWindowsSandboxPermissions,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
@@ -419,7 +443,7 @@ pub(crate) fn effective_write_roots_for_setup(
let write_roots = if let Some(roots) = write_roots_override {
canonical_existing(roots)
} else {
gather_write_roots(policy, policy_cwd, command_cwd, env_map)
gather_write_roots_for_permissions(permissions, command_cwd, env_map)
};
let write_roots = expand_user_profile_root(write_roots);
let write_roots = filter_user_profile_root(write_roots);
@@ -463,8 +487,11 @@ pub(crate) enum SandboxNetworkIdentity {
}
impl SandboxNetworkIdentity {
pub(crate) fn from_policy(policy: &SandboxPolicy, proxy_enforced: bool) -> Self {
if proxy_enforced || !policy.has_full_network_access() {
pub(crate) fn from_permissions(
permissions: &ResolvedWindowsSandboxPermissions,
proxy_enforced: bool,
) -> Self {
if proxy_enforced || !permissions.network_policy().is_enabled() {
Self::Offline
} else {
Self::Online
@@ -744,8 +771,12 @@ pub fn run_elevated_setup(
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
request.policy,
request.command_cwd,
);
let network_identity =
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
SandboxNetworkIdentity::from_permissions(&permissions, request.proxy_enforced);
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
let payload = ElevationPayload {
version: SETUP_VERSION,

View File

@@ -3,6 +3,7 @@ use crate::acl::add_deny_write_ace;
use crate::acl::allow_null_device;
use crate::allow::AllowDenyPaths;
use crate::allow::compute_allow_paths;
use crate::allow::compute_allow_paths_for_permissions;
use crate::cap::load_or_create_cap_sids;
use crate::cap::workspace_write_cap_sid_for_root;
use crate::cap::workspace_write_root_contains_path;
@@ -23,7 +24,7 @@ use crate::policy::parse_policy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::sandbox_utils::ensure_codex_home_exists;
use crate::sandbox_utils::inject_git_safe_directory;
use crate::setup::effective_write_roots_for_setup;
use crate::setup::effective_write_roots_for_permissions;
use crate::token::LocalSid;
use crate::token::create_readonly_token_with_cap;
use crate::token::create_workspace_write_token_with_caps_from;
@@ -43,10 +44,11 @@ use windows_sys::Win32::Foundation::HANDLE;
pub(crate) struct SpawnContext {
pub(crate) policy: SandboxPolicy,
pub(crate) permissions: ResolvedWindowsSandboxPermissions,
pub(crate) current_dir: PathBuf,
pub(crate) sandbox_base: PathBuf,
pub(crate) logs_base_dir: Option<PathBuf>,
pub(crate) is_workspace_write: bool,
pub(crate) uses_write_capabilities: bool,
}
pub(crate) struct ElevatedSpawnContext {
@@ -110,14 +112,16 @@ fn prepare_spawn_context_common(
let logs_base_dir = Some(sandbox_base.clone());
log_start(command, logs_base_dir.as_deref());
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, cwd);
let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);
Ok(SpawnContext {
policy,
permissions,
current_dir: cwd.to_path_buf(),
sandbox_base,
logs_base_dir,
is_workspace_write,
uses_write_capabilities,
})
}
@@ -139,7 +143,7 @@ pub(crate) fn prepare_legacy_spawn_context(
inherit_path,
add_git_safe_directory,
)?;
if should_apply_network_block(&common.policy) {
if common.permissions.should_apply_network_block() {
apply_no_network_to_env(env_map)?;
}
Ok(common)
@@ -191,19 +195,20 @@ pub(crate) fn prepare_legacy_session_security(
pub(crate) fn legacy_session_capability_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
_policy_cwd: &Path,
current_dir: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> Vec<PathBuf> {
let allow_paths = compute_allow_paths(policy, policy_cwd, current_dir, env_map)
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, current_dir);
let allow_paths = compute_allow_paths_for_permissions(&permissions, current_dir, env_map)
.allow
.into_iter()
.collect::<Vec<_>>();
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
effective_write_roots_for_setup(
policy,
policy_cwd,
if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) {
effective_write_roots_for_permissions(
&permissions,
current_dir,
env_map,
codex_home,
@@ -379,7 +384,7 @@ pub(crate) fn apply_legacy_session_acl_rules(
allow_null_device(readonly_sid.as_ptr());
}
if persist_aces
&& matches!(policy, SandboxPolicy::WorkspaceWrite { .. })
&& !acl_sids.write_root_sids.is_empty()
&& let Some(workspace_sid) =
matching_root_capability(current_dir, acl_sids.write_root_sids)
{
@@ -417,24 +422,19 @@ pub(crate) fn prepare_elevated_spawn_context(
/*add_git_safe_directory*/ true,
)?;
let AllowDenyPaths { allow, deny } = compute_allow_paths(
&common.policy,
sandbox_policy_cwd,
&common.current_dir,
env_map,
);
let AllowDenyPaths { allow, deny } =
compute_allow_paths_for_permissions(&common.permissions, &common.current_dir, env_map);
let write_roots: Vec<PathBuf> = allow.into_iter().collect();
let deny_write_paths: Vec<PathBuf> = deny.into_iter().collect();
let computed_write_roots_override = if common.is_workspace_write {
let computed_write_roots_override = if common.uses_write_capabilities {
Some(write_roots.as_slice())
} else {
None
};
let write_roots_for_setup = write_roots_override.or(computed_write_roots_override);
let effective_write_roots = if common.is_workspace_write {
effective_write_roots_for_setup(
&common.policy,
sandbox_policy_cwd,
let effective_write_roots = if common.uses_write_capabilities {
effective_write_roots_for_permissions(
&common.permissions,
&common.current_dir,
env_map,
codex_home,
@@ -443,7 +443,7 @@ pub(crate) fn prepare_elevated_spawn_context(
} else {
Vec::new()
};
let setup_write_roots_override = if common.is_workspace_write {
let setup_write_roots_override = if common.uses_write_capabilities {
Some(effective_write_roots.as_slice())
} else {
write_roots_override
@@ -466,24 +466,20 @@ pub(crate) fn prepare_elevated_spawn_context(
/*proxy_enforced*/ false,
)?;
let caps = load_or_create_cap_sids(codex_home)?;
let (psid_to_use, cap_sids) = match &common.policy {
SandboxPolicy::ReadOnly { .. } => (
let (psid_to_use, cap_sids) = if common.uses_write_capabilities {
let cap_sids = root_capability_sids(codex_home, cwd, effective_write_roots)?
.into_iter()
.map(|root_sid| root_sid.sid_str)
.collect::<Vec<_>>();
if cap_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
} else {
(
LocalSid::from_string(&caps.readonly)?,
vec![caps.readonly.clone()],
),
SandboxPolicy::WorkspaceWrite { .. } => {
let cap_sids = root_capability_sids(codex_home, cwd, effective_write_roots)?
.into_iter()
.map(|root_sid| root_sid.sid_str)
.collect::<Vec<_>>();
if cap_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
unreachable!("dangerous policies rejected before elevated session prep")
}
)
};
unsafe {