windows-sandbox: send permission profiles to elevated runner

This commit is contained in:
Michael Bolin
2026-05-15 15:42:49 -07:00
parent e0c614b2da
commit 7fbefbb0e1
6 changed files with 252 additions and 15 deletions

View File

@@ -22,11 +22,11 @@ use codex_windows_sandbox::OutputPayload;
use codex_windows_sandbox::OutputStream;
use codex_windows_sandbox::PipeSpawnHandles;
use codex_windows_sandbox::ResizePayload;
use codex_windows_sandbox::SandboxPolicy;
use codex_windows_sandbox::SpawnReady;
use codex_windows_sandbox::SpawnRequest;
use codex_windows_sandbox::StderrMode;
use codex_windows_sandbox::StdinMode;
use codex_windows_sandbox::WindowsSandboxTokenMode;
use codex_windows_sandbox::allow_null_device;
use codex_windows_sandbox::create_readonly_token_with_caps_and_user_from;
use codex_windows_sandbox::create_workspace_write_token_with_caps_and_user_from;
@@ -35,11 +35,11 @@ use codex_windows_sandbox::encode_bytes;
use codex_windows_sandbox::get_current_token_for_restriction;
use codex_windows_sandbox::hide_current_user_profile_dir;
use codex_windows_sandbox::log_note;
use codex_windows_sandbox::parse_policy;
use codex_windows_sandbox::read_frame;
use codex_windows_sandbox::read_handle_loop;
use codex_windows_sandbox::spawn_process_with_pipes;
use codex_windows_sandbox::to_wide;
use codex_windows_sandbox::token_mode_for_permission_profile;
use codex_windows_sandbox::write_frame;
use std::ffi::OsStr;
use std::fs::File;
@@ -235,7 +235,12 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
let log_dir = req.codex_home.clone();
hide_current_user_profile_dir(req.codex_home.as_path());
let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?;
let token_mode = token_mode_for_permission_profile(
&req.permission_profile,
&req.permission_profile_cwd,
&req.env,
)
.context("resolve permission profile token mode")?;
let mut cap_psids: Vec<LocalSid> = Vec::new();
for sid in &req.cap_sids {
cap_psids.push(
@@ -253,16 +258,13 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
let cap_psid_ptrs: Vec<*mut _> = cap_psids.iter().map(LocalSid::as_ptr).collect();
let base = OwnedWinHandle::new(unsafe { get_current_token_for_restriction()? });
let h_token = OwnedWinHandle::new(unsafe {
match &policy {
SandboxPolicy::ReadOnly { .. } => {
match token_mode {
WindowsSandboxTokenMode::ReadOnlyCapability => {
create_readonly_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
}
SandboxPolicy::WorkspaceWrite { .. } => {
WindowsSandboxTokenMode::WriteCapabilityRoots => {
create_workspace_write_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
unreachable!()
}
}
}?);
unsafe {

View File

@@ -10,6 +10,7 @@
use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_protocol::models::PermissionProfile;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
@@ -55,8 +56,8 @@ pub struct SpawnRequest {
pub command: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub policy_json_or_preset: String,
pub sandbox_policy_cwd: PathBuf,
pub permission_profile: PermissionProfile,
pub permission_profile_cwd: PathBuf,
pub codex_home: PathBuf,
pub real_codex_home: PathBuf,
pub cap_sids: Vec<String>,
@@ -164,6 +165,7 @@ pub fn read_frame<R: Read>(mut reader: R) -> Result<Option<FramedMessage>> {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn framed_round_trip() {
@@ -189,4 +191,43 @@ mod tests {
other => panic!("unexpected message: {other:?}"),
}
}
#[test]
fn spawn_request_serializes_permission_profile() {
let msg = FramedMessage {
version: 1,
message: Message::SpawnRequest {
payload: Box::new(SpawnRequest {
command: vec!["cmd.exe".to_string(), "/c".to_string(), "ver".to_string()],
cwd: PathBuf::from(r"C:\workspace"),
env: HashMap::new(),
permission_profile: PermissionProfile::read_only(),
permission_profile_cwd: PathBuf::from(r"C:\workspace"),
codex_home: PathBuf::from(r"C:\codex"),
real_codex_home: PathBuf::from(r"C:\Users\codex"),
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
timeout_ms: Some(1000),
tty: false,
stdin_open: false,
use_private_desktop: false,
}),
},
};
let encoded = serde_json::to_value(&msg).expect("serialize");
assert_eq!("spawn_request", encoded["type"]);
assert_eq!("managed", encoded["payload"]["permission_profile"]["type"]);
assert_eq!(None, encoded["payload"].get("policy_json_or_preset"));
assert_eq!(None, encoded["payload"].get("sandbox_policy_cwd"));
let decoded: FramedMessage = serde_json::from_value(encoded).expect("deserialize");
let Message::SpawnRequest { payload } = decoded.message else {
panic!("unexpected message");
};
assert_eq!(PermissionProfile::read_only(), payload.permission_profile);
assert_eq!(
PathBuf::from(r"C:\workspace"),
payload.permission_profile_cwd
);
}
}

View File

@@ -45,6 +45,7 @@ mod windows_impl {
use crate::setup::effective_write_roots_for_setup;
use crate::token::LocalSid;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
@@ -144,12 +145,14 @@ mod windows_impl {
}
(|| -> Result<CaptureResult> {
let permission_profile =
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd);
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
env: env_map.clone(),
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
permission_profile,
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
codex_home: sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids,

View File

@@ -197,6 +197,10 @@ pub use process::read_handle_loop;
#[cfg(target_os = "windows")]
pub use process::spawn_process_with_pipes;
#[cfg(target_os = "windows")]
pub use resolved_permissions::WindowsSandboxTokenMode;
#[cfg(target_os = "windows")]
pub use resolved_permissions::token_mode_for_permission_profile;
#[cfg(target_os = "windows")]
pub use setup::SETUP_VERSION;
#[cfg(target_os = "windows")]
pub use setup::SandboxSetupRequest;

View File

@@ -1,5 +1,8 @@
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
@@ -25,6 +28,33 @@ pub(crate) struct WindowsWritableRoot {
pub(crate) read_only_subpaths: Vec<PathBuf>,
}
/// Restricted-token family needed to enforce a Windows permission profile.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowsSandboxTokenMode {
ReadOnlyCapability,
WriteCapabilityRoots,
}
/// Chooses the restricted-token family needed for a managed permission profile.
pub fn token_mode_for_permission_profile(
permission_profile: &PermissionProfile,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Result<WindowsSandboxTokenMode> {
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile(permission_profile)?;
if permissions.file_system.has_full_disk_write_access() {
anyhow::bail!(
"permission profile requests full-disk filesystem writes, which cannot be enforced by the Windows sandbox"
);
}
if permissions.writable_roots_for_cwd(cwd, env_map).is_empty() {
Ok(WindowsSandboxTokenMode::ReadOnlyCapability)
} else {
Ok(WindowsSandboxTokenMode::WriteCapabilityRoots)
}
}
impl ResolvedWindowsSandboxPermissions {
pub(crate) fn from_legacy_policy(policy: &SandboxPolicy) -> Self {
Self {
@@ -40,6 +70,26 @@ impl ResolvedWindowsSandboxPermissions {
}
}
pub(crate) fn try_from_permission_profile(
permission_profile: &PermissionProfile,
) -> Result<Self> {
if !matches!(permission_profile, PermissionProfile::Managed { .. }) {
anyhow::bail!(
"only managed permission profiles can be enforced by the Windows sandbox"
);
}
let (file_system, network) = permission_profile.to_runtime_permissions();
if !matches!(file_system.kind, FileSystemSandboxKind::Restricted) {
anyhow::bail!(
"only restricted managed filesystem permissions can be enforced by the Windows sandbox"
);
}
Ok(Self {
file_system,
network,
})
}
pub(crate) fn should_apply_network_block(&self) -> bool {
!self.network.is_enabled()
}
@@ -102,3 +152,135 @@ fn windows_temp_env_roots(env_map: &HashMap<String, String>) -> Vec<PathBuf> {
.filter(|path| path.is_absolute())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
fn permission_profile_workspace_write_uses_windows_temp_env_vars() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
let temp_dir = tmp.path().join("temp");
std::fs::create_dir_all(&cwd).expect("create cwd");
std::fs::create_dir_all(&temp_dir).expect("create temp dir");
let mut env_map = HashMap::new();
env_map.insert("TEMP".to_string(), temp_dir.to_string_lossy().to_string());
env_map.insert("TMP".to_string(), temp_dir.to_string_lossy().to_string());
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile(
&PermissionProfile::workspace_write(),
)
.expect("managed permission profile");
let roots = permissions
.writable_roots_for_cwd(&cwd, &env_map)
.into_iter()
.map(|root| root.root)
.collect::<std::collections::HashSet<_>>();
let expected_roots = [
temp_dir,
dunce::canonicalize(&cwd).expect("canonicalize cwd"),
]
.into_iter()
.collect::<std::collections::HashSet<_>>();
assert_eq!(expected_roots, roots);
}
#[test]
fn token_mode_for_profile_without_writable_roots_uses_readonly_capability() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let token_mode = token_mode_for_permission_profile(
&PermissionProfile::read_only(),
&cwd,
&HashMap::new(),
)
.expect("token mode");
assert_eq!(WindowsSandboxTokenMode::ReadOnlyCapability, token_mode);
}
#[test]
fn token_mode_for_profile_with_writable_roots_uses_write_capabilities() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let token_mode = token_mode_for_permission_profile(
&PermissionProfile::workspace_write(),
&cwd,
&HashMap::new(),
)
.expect("token mode");
assert_eq!(WindowsSandboxTokenMode::WriteCapabilityRoots, token_mode);
}
#[test]
fn permission_profile_rejects_disabled_profiles() {
let err = ResolvedWindowsSandboxPermissions::try_from_permission_profile(
&PermissionProfile::Disabled,
)
.expect_err("disabled profile should not resolve for sandbox enforcement");
assert!(
err.to_string()
.contains("only managed permission profiles can be enforced")
);
}
#[test]
fn permission_profile_rejects_unrestricted_managed_filesystem() {
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Restricted,
};
let err =
ResolvedWindowsSandboxPermissions::try_from_permission_profile(&permission_profile)
.expect_err("unrestricted profile should not resolve for sandbox enforcement");
assert!(
err.to_string()
.contains("only restricted managed filesystem permissions can be enforced")
);
}
#[test]
fn token_mode_rejects_full_disk_write_entries() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
}],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let err = token_mode_for_permission_profile(&permission_profile, &cwd, &HashMap::new())
.expect_err("full disk writes should not resolve to a token mode");
assert!(
err.to_string()
.contains("full-disk filesystem writes, which cannot be enforced")
);
}
}

View File

@@ -10,6 +10,7 @@ use crate::ipc_framed::SpawnRequest;
use crate::runner_client::spawn_runner_transport;
use crate::spawn_prep::prepare_elevated_spawn_context;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
@@ -60,12 +61,16 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
&deny_write_paths_override,
)?;
let permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&elevated.common.policy,
sandbox_policy_cwd,
);
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
env: env_map.clone(),
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
permission_profile,
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
codex_home: elevated.common.sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids: elevated.cap_sids.clone(),