mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
4 Commits
main
...
dev/icewea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f21fad19e | ||
|
|
7da1d3830d | ||
|
|
ba6600a93c | ||
|
|
a4356dee7b |
@@ -233,7 +233,9 @@ pub unsafe fn dacl_has_write_deny_for_sid(p_dacl: *mut ACL, psid: *mut c_void) -
|
||||
| FILE_APPEND_DATA
|
||||
| FILE_WRITE_EA
|
||||
| FILE_WRITE_ATTRIBUTES
|
||||
| GENERIC_WRITE_MASK;
|
||||
| GENERIC_WRITE_MASK
|
||||
| DELETE
|
||||
| FILE_DELETE_CHILD;
|
||||
for i in 0..info.AceCount {
|
||||
let mut p_ace: *mut c_void = std::ptr::null_mut();
|
||||
if GetAce(p_dacl as *const ACL, i, &mut p_ace) == 0 {
|
||||
@@ -477,7 +479,9 @@ pub unsafe fn add_deny_write_ace(path: &Path, psid: *mut c_void) -> Result<bool>
|
||||
| FILE_APPEND_DATA
|
||||
| FILE_WRITE_EA
|
||||
| FILE_WRITE_ATTRIBUTES
|
||||
| GENERIC_WRITE_MASK;
|
||||
| GENERIC_WRITE_MASK
|
||||
| DELETE
|
||||
| FILE_DELETE_CHILD;
|
||||
explicit.grfAccessMode = DENY_ACCESS;
|
||||
explicit.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
|
||||
explicit.Trustee = trustee;
|
||||
|
||||
@@ -5,6 +5,7 @@ use rand::RngCore;
|
||||
use rand::SeedableRng;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -13,6 +14,13 @@ use std::path::PathBuf;
|
||||
pub struct CapSids {
|
||||
pub workspace: String,
|
||||
pub readonly: String,
|
||||
/// Per-workspace capability SIDs keyed by canonicalized CWD string.
|
||||
///
|
||||
/// This is used to isolate workspaces from other workspace sandbox writes and to
|
||||
/// apply per-workspace denies (e.g. protect `CWD/.codex`)
|
||||
/// without permanently affecting other workspaces.
|
||||
#[serde(default)]
|
||||
pub workspace_by_cwd: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn cap_sid_file(codex_home: &Path) -> PathBuf {
|
||||
@@ -30,8 +38,7 @@ fn make_random_cap_sid_string() -> String {
|
||||
|
||||
fn persist_caps(path: &Path, caps: &CapSids) -> Result<()> {
|
||||
if let Some(dir) = path.parent() {
|
||||
fs::create_dir_all(dir)
|
||||
.with_context(|| format!("create cap sid dir {}", dir.display()))?;
|
||||
fs::create_dir_all(dir).with_context(|| format!("create cap sid dir {}", dir.display()))?;
|
||||
}
|
||||
let json = serde_json::to_string(caps)?;
|
||||
fs::write(path, json).with_context(|| format!("write cap sid file {}", path.display()))?;
|
||||
@@ -52,6 +59,7 @@ pub fn load_or_create_cap_sids(codex_home: &Path) -> Result<CapSids> {
|
||||
let caps = CapSids {
|
||||
workspace: t.to_string(),
|
||||
readonly: make_random_cap_sid_string(),
|
||||
workspace_by_cwd: HashMap::new(),
|
||||
};
|
||||
persist_caps(&path, &caps)?;
|
||||
return Ok(caps);
|
||||
@@ -60,7 +68,30 @@ pub fn load_or_create_cap_sids(codex_home: &Path) -> Result<CapSids> {
|
||||
let caps = CapSids {
|
||||
workspace: make_random_cap_sid_string(),
|
||||
readonly: make_random_cap_sid_string(),
|
||||
workspace_by_cwd: HashMap::new(),
|
||||
};
|
||||
persist_caps(&path, &caps)?;
|
||||
Ok(caps)
|
||||
}
|
||||
|
||||
fn canonical_cwd_key(cwd: &Path) -> String {
|
||||
let canonical = dunce::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
|
||||
canonical
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
/// Returns the workspace-specific capability SID for `cwd`, creating and persisting it if missing.
|
||||
pub fn workspace_cap_sid_for_cwd(codex_home: &Path, cwd: &Path) -> Result<String> {
|
||||
let path = cap_sid_file(codex_home);
|
||||
let mut caps = load_or_create_cap_sids(codex_home)?;
|
||||
let key = canonical_cwd_key(cwd);
|
||||
if let Some(sid) = caps.workspace_by_cwd.get(&key) {
|
||||
return Ok(sid.clone());
|
||||
}
|
||||
let sid = make_random_cap_sid_string();
|
||||
caps.workspace_by_cwd.insert(key, sid.clone());
|
||||
persist_caps(&path, &caps)?;
|
||||
Ok(sid)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use anyhow::Result;
|
||||
use codex_windows_sandbox::allow_null_device;
|
||||
use codex_windows_sandbox::convert_string_sid_to_sid;
|
||||
use codex_windows_sandbox::create_process_as_user;
|
||||
use codex_windows_sandbox::create_readonly_token_with_cap_from;
|
||||
use codex_windows_sandbox::create_workspace_write_token_with_cap_from;
|
||||
use codex_windows_sandbox::create_readonly_token_with_caps_from;
|
||||
use codex_windows_sandbox::create_workspace_write_token_with_caps_from;
|
||||
use codex_windows_sandbox::get_current_token_for_restriction;
|
||||
use codex_windows_sandbox::hide_current_user_profile_dir;
|
||||
use codex_windows_sandbox::log_note;
|
||||
@@ -20,7 +20,9 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
|
||||
@@ -48,7 +50,7 @@ struct RunnerRequest {
|
||||
codex_home: PathBuf,
|
||||
// Real user's CODEX_HOME for shared data (caps, config).
|
||||
real_codex_home: PathBuf,
|
||||
cap_sid: String,
|
||||
cap_sids: Vec<String>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
env_map: HashMap<String, String>,
|
||||
@@ -112,27 +114,43 @@ pub fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?;
|
||||
let psid_cap: *mut c_void = unsafe { convert_string_sid_to_sid(&req.cap_sid).unwrap() };
|
||||
let mut cap_psids: Vec<*mut c_void> = Vec::new();
|
||||
for sid in &req.cap_sids {
|
||||
let Some(psid) = (unsafe { convert_string_sid_to_sid(sid) }) else {
|
||||
anyhow::bail!("ConvertStringSidToSidW failed for capability SID");
|
||||
};
|
||||
cap_psids.push(psid);
|
||||
}
|
||||
if cap_psids.is_empty() {
|
||||
anyhow::bail!("runner: empty capability SID list");
|
||||
}
|
||||
|
||||
// Create restricted token from current process token.
|
||||
let base = unsafe { get_current_token_for_restriction()? };
|
||||
let token_res: Result<(HANDLE, *mut c_void)> = unsafe {
|
||||
let token_res: Result<HANDLE> = unsafe {
|
||||
match &policy {
|
||||
SandboxPolicy::ReadOnly => create_readonly_token_with_cap_from(base, psid_cap),
|
||||
SandboxPolicy::ReadOnly => create_readonly_token_with_caps_from(base, &cap_psids),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
create_workspace_write_token_with_cap_from(base, psid_cap)
|
||||
create_workspace_write_token_with_caps_from(base, &cap_psids)
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
};
|
||||
let (h_token, psid_to_use) = token_res?;
|
||||
let h_token = token_res?;
|
||||
unsafe {
|
||||
CloseHandle(base);
|
||||
}
|
||||
unsafe {
|
||||
allow_null_device(psid_to_use);
|
||||
for psid in &cap_psids {
|
||||
allow_null_device(*psid);
|
||||
}
|
||||
for psid in cap_psids {
|
||||
if !psid.is_null() {
|
||||
LocalFree(psid as HLOCAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open named pipes for stdio.
|
||||
|
||||
@@ -196,7 +196,7 @@ mod windows_impl {
|
||||
codex_home: PathBuf,
|
||||
// Real user's CODEX_HOME for shared data (caps, config).
|
||||
real_codex_home: PathBuf,
|
||||
cap_sid: String,
|
||||
cap_sids: Vec<String>,
|
||||
request_file: Option<PathBuf>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
@@ -239,14 +239,17 @@ mod windows_impl {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (psid_to_use, cap_sid_str) = match &policy {
|
||||
let (psid_to_use, cap_sids) = match &policy {
|
||||
SandboxPolicy::ReadOnly => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() },
|
||||
caps.readonly.clone(),
|
||||
vec![caps.readonly.clone()],
|
||||
),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() },
|
||||
caps.workspace.clone(),
|
||||
vec![
|
||||
caps.workspace.clone(),
|
||||
crate::cap::workspace_cap_sid_for_cwd(codex_home, cwd)?,
|
||||
],
|
||||
),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
@@ -294,7 +297,7 @@ mod windows_impl {
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
codex_home: sandbox_base.clone(),
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sid: cap_sid_str.clone(),
|
||||
cap_sids: cap_sids.clone(),
|
||||
request_file: Some(req_file.clone()),
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
|
||||
@@ -5,8 +5,20 @@ macro_rules! windows_modules {
|
||||
}
|
||||
|
||||
windows_modules!(
|
||||
acl, allow, audit, cap, dpapi, env, hide_users, identity, logging, policy, process, token,
|
||||
winutil
|
||||
acl,
|
||||
allow,
|
||||
audit,
|
||||
cap,
|
||||
dpapi,
|
||||
env,
|
||||
hide_users,
|
||||
identity,
|
||||
logging,
|
||||
policy,
|
||||
process,
|
||||
token,
|
||||
winutil,
|
||||
workspace_acl
|
||||
);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -19,6 +31,8 @@ mod elevated_impl;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod setup_error;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::add_deny_write_ace;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::allow_null_device;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -36,6 +50,8 @@ pub use audit::apply_world_writable_scan_and_denies;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use cap::load_or_create_cap_sids;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use cap::workspace_cap_sid_for_cwd;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use dpapi::protect as dpapi_protect;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use dpapi::unprotect as dpapi_unprotect;
|
||||
@@ -88,7 +104,9 @@ pub use token::convert_string_sid_to_sid;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::create_readonly_token_with_cap_from;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::create_workspace_write_token_with_cap_from;
|
||||
pub use token::create_readonly_token_with_caps_from;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::create_workspace_write_token_with_caps_from;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::get_current_token_for_restriction;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -99,6 +117,12 @@ pub use windows_impl::CaptureResult;
|
||||
pub use winutil::string_from_sid_bytes;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use winutil::to_wide;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use workspace_acl::canonicalize_path;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use workspace_acl::is_command_cwd_root;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use workspace_acl::protect_workspace_codex_dir;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::apply_world_writable_scan_and_denies;
|
||||
@@ -116,6 +140,7 @@ mod windows_impl {
|
||||
use super::allow::compute_allow_paths;
|
||||
use super::allow::AllowDenyPaths;
|
||||
use super::cap::load_or_create_cap_sids;
|
||||
use super::cap::workspace_cap_sid_for_cwd;
|
||||
use super::env::apply_no_network_to_env;
|
||||
use super::env::ensure_non_interactive_pager;
|
||||
use super::env::normalize_null_device_env;
|
||||
@@ -127,9 +152,13 @@ mod windows_impl {
|
||||
use super::policy::SandboxPolicy;
|
||||
use super::process::make_env_block;
|
||||
use super::token::convert_string_sid_to_sid;
|
||||
use super::token::create_workspace_write_token_with_caps_from;
|
||||
use super::winutil::format_last_error;
|
||||
use super::winutil::quote_windows_arg;
|
||||
use super::winutil::to_wide;
|
||||
use super::workspace_acl::canonicalize_path;
|
||||
use super::workspace_acl::is_command_cwd_root;
|
||||
use super::workspace_acl::protect_workspace_codex_dir;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
@@ -229,15 +258,25 @@ mod windows_impl {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
|
||||
let (h_token, psid_generic, psid_workspace): (HANDLE, *mut c_void, Option<*mut c_void>) = unsafe {
|
||||
match &policy {
|
||||
SandboxPolicy::ReadOnly => {
|
||||
let psid = convert_string_sid_to_sid(&caps.readonly).unwrap();
|
||||
super::token::create_readonly_token_with_cap(psid)?
|
||||
let (h, _) = super::token::create_readonly_token_with_cap(psid)?;
|
||||
(h, psid, None)
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||
super::token::create_workspace_write_token_with_cap(psid)?
|
||||
let psid_generic = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||
let ws_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
|
||||
let psid_workspace = convert_string_sid_to_sid(&ws_sid).unwrap();
|
||||
let base = super::token::get_current_token_for_restriction()?;
|
||||
let h_res = create_workspace_write_token_with_caps_from(
|
||||
base,
|
||||
&[psid_generic, psid_workspace],
|
||||
);
|
||||
windows_sys::Win32::Foundation::CloseHandle(base);
|
||||
let h = h_res?;
|
||||
(h, psid_generic, Some(psid_workspace))
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
@@ -261,29 +300,39 @@ mod windows_impl {
|
||||
let persist_aces = is_workspace_write;
|
||||
let AllowDenyPaths { allow, deny } =
|
||||
compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||
let canonical_cwd = canonicalize_path(¤t_dir);
|
||||
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
|
||||
unsafe {
|
||||
for p in &allow {
|
||||
if let Ok(added) = add_allow_ace(p, psid_to_use) {
|
||||
let psid = if is_workspace_write && is_command_cwd_root(p, &canonical_cwd) {
|
||||
psid_workspace.unwrap_or(psid_generic)
|
||||
} else {
|
||||
psid_generic
|
||||
};
|
||||
if let Ok(added) = add_allow_ace(p, psid) {
|
||||
if added {
|
||||
if persist_aces {
|
||||
if p.is_dir() {
|
||||
// best-effort seeding omitted intentionally
|
||||
}
|
||||
} else {
|
||||
guards.push((p.clone(), psid_to_use));
|
||||
guards.push((p.clone(), psid));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for p in &deny {
|
||||
if let Ok(added) = add_deny_write_ace(p, psid_to_use) {
|
||||
if let Ok(added) = add_deny_write_ace(p, psid_generic) {
|
||||
if added && !persist_aces {
|
||||
guards.push((p.clone(), psid_to_use));
|
||||
guards.push((p.clone(), psid_generic));
|
||||
}
|
||||
}
|
||||
}
|
||||
allow_null_device(psid_to_use);
|
||||
allow_null_device(psid_generic);
|
||||
if let Some(psid) = psid_workspace {
|
||||
allow_null_device(psid);
|
||||
let _ = protect_workspace_codex_dir(¤t_dir, psid);
|
||||
}
|
||||
}
|
||||
|
||||
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
||||
|
||||
@@ -6,18 +6,22 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use base64::Engine;
|
||||
use codex_windows_sandbox::canonicalize_path;
|
||||
use codex_windows_sandbox::convert_string_sid_to_sid;
|
||||
use codex_windows_sandbox::ensure_allow_mask_aces_with_inheritance;
|
||||
use codex_windows_sandbox::ensure_allow_write_aces;
|
||||
use codex_windows_sandbox::extract_setup_failure;
|
||||
use codex_windows_sandbox::hide_newly_created_users;
|
||||
use codex_windows_sandbox::is_command_cwd_root;
|
||||
use codex_windows_sandbox::load_or_create_cap_sids;
|
||||
use codex_windows_sandbox::log_note;
|
||||
use codex_windows_sandbox::path_mask_allows;
|
||||
use codex_windows_sandbox::protect_workspace_codex_dir;
|
||||
use codex_windows_sandbox::sandbox_dir;
|
||||
use codex_windows_sandbox::sandbox_secrets_dir;
|
||||
use codex_windows_sandbox::string_from_sid_bytes;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::workspace_cap_sid_for_cwd;
|
||||
use codex_windows_sandbox::write_setup_error_report;
|
||||
use codex_windows_sandbox::SetupErrorCode;
|
||||
use codex_windows_sandbox::SetupErrorReport;
|
||||
@@ -75,6 +79,7 @@ struct Payload {
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
codex_home: PathBuf,
|
||||
command_cwd: PathBuf,
|
||||
read_roots: Vec<PathBuf>,
|
||||
write_roots: Vec<PathBuf>,
|
||||
real_user: String,
|
||||
@@ -560,6 +565,11 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
))
|
||||
})?
|
||||
};
|
||||
let workspace_sid_str = workspace_cap_sid_for_cwd(&payload.codex_home, &payload.command_cwd)?;
|
||||
let workspace_psid = unsafe {
|
||||
convert_string_sid_to_sid(&workspace_sid_str)
|
||||
.ok_or_else(|| anyhow::anyhow!("convert workspace capability SID failed"))?
|
||||
};
|
||||
let mut refresh_errors: Vec<String> = Vec::new();
|
||||
if !refresh_only {
|
||||
let firewall_result = firewall::ensure_offline_outbound_block(&offline_sid_str, log);
|
||||
@@ -609,12 +619,12 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
let cap_sid_str = caps.workspace.clone();
|
||||
let sandbox_group_sid_str =
|
||||
string_from_sid_bytes(&sandbox_group_sid).map_err(anyhow::Error::msg)?;
|
||||
let sid_strings = vec![sandbox_group_sid_str, cap_sid_str];
|
||||
let write_mask =
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE | FILE_DELETE_CHILD;
|
||||
let mut grant_tasks: Vec<PathBuf> = Vec::new();
|
||||
|
||||
let mut seen_write_roots: HashSet<PathBuf> = HashSet::new();
|
||||
let canonical_command_cwd = canonicalize_path(&payload.command_cwd);
|
||||
|
||||
for root in &payload.write_roots {
|
||||
if !seen_write_roots.insert(root.clone()) {
|
||||
@@ -628,7 +638,21 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
continue;
|
||||
}
|
||||
let mut need_grant = false;
|
||||
for (label, psid) in [("sandbox_group", sandbox_group_psid), ("cap", cap_psid)] {
|
||||
let is_command_cwd = is_command_cwd_root(root, &canonical_command_cwd);
|
||||
let cap_label = if is_command_cwd {
|
||||
"workspace_cap"
|
||||
} else {
|
||||
"cap"
|
||||
};
|
||||
let cap_psid_for_root = if is_command_cwd {
|
||||
workspace_psid
|
||||
} else {
|
||||
cap_psid
|
||||
};
|
||||
for (label, psid) in [
|
||||
("sandbox_group", sandbox_group_psid),
|
||||
(cap_label, cap_psid_for_root),
|
||||
] {
|
||||
let has = match path_mask_allows(root, &[psid], write_mask, true) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
@@ -667,7 +691,12 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
let (tx, rx) = mpsc::channel::<(PathBuf, Result<bool>)>();
|
||||
std::thread::scope(|scope| {
|
||||
for root in grant_tasks {
|
||||
let sid_strings = sid_strings.clone();
|
||||
let is_command_cwd = is_command_cwd_root(&root, &canonical_command_cwd);
|
||||
let sid_strings = if is_command_cwd {
|
||||
vec![sandbox_group_sid_str.clone(), workspace_sid_str.clone()]
|
||||
} else {
|
||||
vec![sandbox_group_sid_str.clone(), cap_sid_str.clone()]
|
||||
};
|
||||
let tx = tx.clone();
|
||||
scope.spawn(move || {
|
||||
// Convert SID strings to psids locally in this thread.
|
||||
@@ -758,6 +787,31 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
let _ = std::fs::remove_file(&legacy_users);
|
||||
}
|
||||
}
|
||||
|
||||
// Protect the current workspace's `.codex` directory from tampering (write/delete) by using a
|
||||
// workspace-specific capability SID. If `.codex` doesn't exist yet, skip it (it will be picked
|
||||
// up on the next refresh).
|
||||
match unsafe { protect_workspace_codex_dir(&payload.command_cwd, workspace_psid) } {
|
||||
Ok(true) => {
|
||||
let cwd_codex = payload.command_cwd.join(".codex");
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"applied deny ACE to protect workspace .codex {}",
|
||||
cwd_codex.display()
|
||||
),
|
||||
)?;
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
let cwd_codex = payload.command_cwd.join(".codex");
|
||||
refresh_errors.push(format!("deny ACE failed on {}: {err}", cwd_codex.display()));
|
||||
log_line(
|
||||
log,
|
||||
&format!("deny ACE failed on {}: {err}", cwd_codex.display()),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
if !sandbox_group_psid.is_null() {
|
||||
LocalFree(sandbox_group_psid as HLOCAL);
|
||||
@@ -765,6 +819,9 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
if !cap_psid.is_null() {
|
||||
LocalFree(cap_psid as HLOCAL);
|
||||
}
|
||||
if !workspace_psid.is_null() {
|
||||
LocalFree(workspace_psid as HLOCAL);
|
||||
}
|
||||
}
|
||||
if refresh_only && !refresh_errors.is_empty() {
|
||||
log_line(
|
||||
|
||||
@@ -81,6 +81,7 @@ pub fn run_setup_refresh(
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
command_cwd: command_cwd.to_path_buf(),
|
||||
read_roots,
|
||||
write_roots,
|
||||
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
|
||||
@@ -258,6 +259,7 @@ struct ElevationPayload {
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
codex_home: PathBuf,
|
||||
command_cwd: PathBuf,
|
||||
read_roots: Vec<PathBuf>,
|
||||
write_roots: Vec<PathBuf>,
|
||||
real_user: String,
|
||||
@@ -469,6 +471,7 @@ pub fn run_elevated_setup(
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
command_cwd: command_cwd.to_path_buf(),
|
||||
read_roots,
|
||||
write_roots,
|
||||
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
|
||||
|
||||
@@ -282,17 +282,6 @@ unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle.
|
||||
pub unsafe fn create_workspace_write_token_with_cap(
|
||||
psid_capability: *mut c_void,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let base = get_current_token_for_restriction()?;
|
||||
let res = create_workspace_write_token_with_cap_from(base, psid_capability);
|
||||
CloseHandle(base);
|
||||
res
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle.
|
||||
pub unsafe fn create_readonly_token_with_cap(
|
||||
@@ -306,61 +295,63 @@ pub unsafe fn create_readonly_token_with_cap(
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
||||
pub unsafe fn create_workspace_write_token_with_cap_from(
|
||||
base_token: HANDLE,
|
||||
psid_capability: *mut c_void,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?;
|
||||
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
let mut everyone = world_sid()?;
|
||||
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||
// Exact set and order: Capability, Logon, Everyone
|
||||
entries[0].Sid = psid_capability;
|
||||
entries[0].Attributes = 0;
|
||||
entries[1].Sid = psid_logon;
|
||||
entries[1].Attributes = 0;
|
||||
entries[2].Sid = psid_everyone;
|
||||
entries[2].Attributes = 0;
|
||||
let mut new_token: HANDLE = 0;
|
||||
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
||||
let ok = CreateRestrictedToken(
|
||||
base_token,
|
||||
flags,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
3,
|
||||
entries.as_mut_ptr(),
|
||||
&mut new_token,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||
}
|
||||
set_default_dacl(new_token, &[psid_logon, psid_everyone, psid_capability])?;
|
||||
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||
Ok((new_token, psid_capability))
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
||||
pub unsafe fn create_readonly_token_with_cap_from(
|
||||
base_token: HANDLE,
|
||||
psid_capability: *mut c_void,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let new_token = create_token_with_caps_from(base_token, &[psid_capability])?;
|
||||
Ok((new_token, psid_capability))
|
||||
}
|
||||
|
||||
/// Create a restricted token that includes all provided capability SIDs.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
||||
pub unsafe fn create_workspace_write_token_with_caps_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
create_token_with_caps_from(base_token, psid_capabilities)
|
||||
}
|
||||
|
||||
/// Create a restricted token that includes all provided capability SIDs.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
||||
pub unsafe fn create_readonly_token_with_caps_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
create_token_with_caps_from(base_token, psid_capabilities)
|
||||
}
|
||||
|
||||
unsafe fn create_token_with_caps_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
if psid_capabilities.is_empty() {
|
||||
return Err(anyhow!("no capability SIDs provided"));
|
||||
}
|
||||
let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?;
|
||||
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
let mut everyone = world_sid()?;
|
||||
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||
// Exact set and order: Capability, Logon, Everyone
|
||||
entries[0].Sid = psid_capability;
|
||||
entries[0].Attributes = 0;
|
||||
entries[1].Sid = psid_logon;
|
||||
entries[1].Attributes = 0;
|
||||
entries[2].Sid = psid_everyone;
|
||||
entries[2].Attributes = 0;
|
||||
|
||||
// Exact order: Capabilities..., Logon, Everyone
|
||||
let mut entries: Vec<SID_AND_ATTRIBUTES> =
|
||||
vec![std::mem::zeroed(); psid_capabilities.len() + 2];
|
||||
for (i, psid) in psid_capabilities.iter().enumerate() {
|
||||
entries[i].Sid = *psid;
|
||||
entries[i].Attributes = 0;
|
||||
}
|
||||
let logon_idx = psid_capabilities.len();
|
||||
entries[logon_idx].Sid = psid_logon;
|
||||
entries[logon_idx].Attributes = 0;
|
||||
entries[logon_idx + 1].Sid = psid_everyone;
|
||||
entries[logon_idx + 1].Attributes = 0;
|
||||
|
||||
let mut new_token: HANDLE = 0;
|
||||
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
||||
let ok = CreateRestrictedToken(
|
||||
@@ -370,14 +361,20 @@ pub unsafe fn create_readonly_token_with_cap_from(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
3,
|
||||
entries.len() as u32,
|
||||
entries.as_mut_ptr(),
|
||||
&mut new_token,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||
}
|
||||
set_default_dacl(new_token, &[psid_logon, psid_everyone, psid_capability])?;
|
||||
|
||||
let mut dacl_sids: Vec<*mut c_void> = Vec::with_capacity(psid_capabilities.len() + 2);
|
||||
dacl_sids.push(psid_logon);
|
||||
dacl_sids.push(psid_everyone);
|
||||
dacl_sids.extend_from_slice(psid_capabilities);
|
||||
set_default_dacl(new_token, &dacl_sids)?;
|
||||
|
||||
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||
Ok((new_token, psid_capability))
|
||||
Ok(new_token)
|
||||
}
|
||||
|
||||
24
codex-rs/windows-sandbox-rs/src/workspace_acl.rs
Normal file
24
codex-rs/windows-sandbox-rs/src/workspace_acl.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use crate::acl::add_deny_write_ace;
|
||||
use anyhow::Result;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn canonicalize_path(path: &Path) -> PathBuf {
|
||||
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
||||
}
|
||||
|
||||
pub fn is_command_cwd_root(root: &Path, canonical_command_cwd: &Path) -> bool {
|
||||
canonicalize_path(root) == canonical_command_cwd
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must ensure `psid` is a valid SID pointer.
|
||||
pub unsafe fn protect_workspace_codex_dir(cwd: &Path, psid: *mut c_void) -> Result<bool> {
|
||||
let cwd_codex = cwd.join(".codex");
|
||||
if cwd_codex.is_dir() {
|
||||
add_deny_write_ace(&cwd_codex, psid)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user