diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml
index 1b936f05cab..ac290101cc5 100644
--- a/codex-rs/windows-sandbox-rs/Cargo.toml
+++ b/codex-rs/windows-sandbox-rs/Cargo.toml
@@ -3,17 +3,29 @@ edition = "2021"
license.workspace = true
name = "codex-windows-sandbox"
version.workspace = true
+build = "build.rs"
[lib]
name = "codex_windows_sandbox"
path = "src/lib.rs"
+[[bin]]
+name = "codex-windows-sandbox-setup"
+path = "src/bin/setup_main.rs"
+
[dependencies]
anyhow = "1.0"
-chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
+base64 = { workspace = true }
dunce = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
+chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
+windows = { version = "0.58", features = [
+ "Win32_Foundation",
+ "Win32_NetworkManagement_WindowsFirewall",
+ "Win32_System_Com",
+ "Win32_System_Variant",
+] }
[dependencies.codex-protocol]
package = "codex-protocol"
path = "../protocol"
@@ -41,11 +53,18 @@ features = [
"Win32_System_Console",
"Win32_Storage_FileSystem",
"Win32_System_Diagnostics_ToolHelp",
+ "Win32_NetworkManagement_NetManagement",
"Win32_Networking_WinSock",
"Win32_System_LibraryLoader",
"Win32_System_Com",
+ "Win32_Security_Cryptography",
"Win32_Security_Authentication_Identity",
+ "Win32_UI_Shell",
+ "Win32_System_Registry",
]
version = "0.52"
[dev-dependencies]
tempfile = "3"
+
+[build-dependencies]
+winres = "0.1"
diff --git a/codex-rs/windows-sandbox-rs/build.rs b/codex-rs/windows-sandbox-rs/build.rs
new file mode 100644
index 00000000000..768f6e82808
--- /dev/null
+++ b/codex-rs/windows-sandbox-rs/build.rs
@@ -0,0 +1,5 @@
+fn main() {
+ let mut res = winres::WindowsResource::new();
+ res.set_manifest_file("codex-windows-sandbox-setup.manifest");
+ let _ = res.compile();
+}
diff --git a/codex-rs/windows-sandbox-rs/codex-windows-sandbox-setup.manifest b/codex-rs/windows-sandbox-rs/codex-windows-sandbox-setup.manifest
new file mode 100644
index 00000000000..14807962f83
--- /dev/null
+++ b/codex-rs/windows-sandbox-rs/codex-windows-sandbox-setup.manifest
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/codex-rs/windows-sandbox-rs/src/acl.rs b/codex-rs/windows-sandbox-rs/src/acl.rs
index 34d523d1f53..f84c5368930 100644
--- a/codex-rs/windows-sandbox-rs/src/acl.rs
+++ b/codex-rs/windows-sandbox-rs/src/acl.rs
@@ -34,6 +34,7 @@ use windows_sys::Win32::Storage::FileSystem::FILE_ALL_ACCESS;
use windows_sys::Win32::Storage::FileSystem::FILE_APPEND_DATA;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
+use windows_sys::Win32::Storage::FileSystem::FILE_DELETE_CHILD;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
@@ -45,12 +46,16 @@ use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_DATA;
use windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA;
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
use windows_sys::Win32::Storage::FileSystem::READ_CONTROL;
+use windows_sys::Win32::Storage::FileSystem::DELETE;
const SE_KERNEL_OBJECT: u32 = 6;
const INHERIT_ONLY_ACE: u8 = 0x08;
const GENERIC_WRITE_MASK: u32 = 0x4000_0000;
const DENY_ACCESS: i32 = 3;
/// Fetch DACL via handle-based query; caller must LocalFree the returned SD.
+///
+/// # Safety
+/// Caller must free the returned security descriptor with `LocalFree` and pass an existing path.
pub unsafe fn fetch_dacl_handle(path: &Path) -> Result<(*mut ACL, *mut c_void)> {
let wpath = to_wide(path);
let h = CreateFileW(
@@ -88,11 +93,13 @@ pub unsafe fn fetch_dacl_handle(path: &Path) -> Result<(*mut ACL, *mut c_void)>
Ok((p_dacl, p_sd))
}
-/// Fast mask-based check: does any ACE for provided SIDs grant at least one desired bit? Skips inherit-only.
-pub unsafe fn dacl_quick_mask_allows(
+/// Fast mask-based check: does an ACE for provided SIDs grant the desired mask? Skips inherit-only.
+/// When `require_all_bits` is true, all bits in `desired_mask` must be present; otherwise any bit suffices.
+pub unsafe fn dacl_mask_allows(
p_dacl: *mut ACL,
psids: &[*mut c_void],
desired_mask: u32,
+ require_all_bits: bool,
) -> bool {
if p_dacl.is_null() {
return false;
@@ -141,22 +148,25 @@ pub unsafe fn dacl_quick_mask_allows(
let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE);
let mut mask = ace.Mask;
MapGenericMask(&mut mask, &mapping);
- if (mask & desired_mask) != 0 {
+ if (require_all_bits && (mask & desired_mask) == desired_mask)
+ || (!require_all_bits && (mask & desired_mask) != 0)
+ {
return true;
}
}
false
}
-/// Path-based wrapper around the quick mask check (single DACL fetch).
-pub fn path_quick_mask_allows(
+/// Path-based wrapper around the mask check (single DACL fetch).
+pub fn path_mask_allows(
path: &Path,
psids: &[*mut c_void],
desired_mask: u32,
+ require_all_bits: bool,
) -> Result {
unsafe {
let (p_dacl, sd) = fetch_dacl_handle(path)?;
- let has = dacl_quick_mask_allows(p_dacl, psids, desired_mask);
+ let has = dacl_mask_allows(p_dacl, psids, desired_mask, require_all_bits);
if !sd.is_null() {
LocalFree(sd as HLOCAL);
}
@@ -326,16 +336,23 @@ pub unsafe fn dacl_effective_allows_mask(
}
#[allow(dead_code)]
-const WRITE_ALLOW_MASK: u32 = FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE;
+const WRITE_ALLOW_MASK: u32 = FILE_GENERIC_READ
+ | FILE_GENERIC_WRITE
+ | FILE_GENERIC_EXECUTE
+ | DELETE
+ | FILE_DELETE_CHILD;
/// Ensure all provided SIDs have a write-capable allow ACE on the path.
/// Returns true if any ACE was added.
+///
+/// # Safety
+/// Caller must pass valid SID pointers and an existing path; free the returned security descriptor with `LocalFree`.
#[allow(dead_code)]
pub unsafe fn ensure_allow_write_aces(path: &Path, sids: &[*mut c_void]) -> Result {
let (p_dacl, p_sd) = fetch_dacl_handle(path)?;
let mut entries: Vec = Vec::new();
for sid in sids {
- if dacl_quick_mask_allows(p_dacl, &[*sid], WRITE_ALLOW_MASK) {
+ if dacl_mask_allows(p_dacl, &[*sid], WRITE_ALLOW_MASK, true) {
continue;
}
entries.push(EXPLICIT_ACCESS_W {
diff --git a/codex-rs/windows-sandbox-rs/src/audit.rs b/codex-rs/windows-sandbox-rs/src/audit.rs
index 7e35bf7517b..e0a8970fe9b 100644
--- a/codex-rs/windows-sandbox-rs/src/audit.rs
+++ b/codex-rs/windows-sandbox-rs/src/audit.rs
@@ -1,5 +1,5 @@
use crate::acl::add_deny_write_ace;
-use crate::acl::path_quick_mask_allows;
+use crate::acl::path_mask_allows;
use crate::cap::cap_sid_file;
use crate::cap::load_or_create_cap_sids;
use crate::logging::{debug_log, log_note};
@@ -84,7 +84,7 @@ unsafe fn path_has_world_write_allow(path: &Path) -> Result {
let mut world = world_sid()?;
let psid_world = world.as_mut_ptr() as *mut c_void;
let write_mask = FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES;
- path_quick_mask_allows(path, &[psid_world], write_mask)
+ path_mask_allows(path, &[psid_world], write_mask, false)
}
pub fn audit_everyone_writable(
diff --git a/codex-rs/windows-sandbox-rs/src/bin/setup_main.rs b/codex-rs/windows-sandbox-rs/src/bin/setup_main.rs
new file mode 100644
index 00000000000..c3bc5724f3b
--- /dev/null
+++ b/codex-rs/windows-sandbox-rs/src/bin/setup_main.rs
@@ -0,0 +1,12 @@
+#[path = "../setup_main_win.rs"]
+mod win;
+
+#[cfg(target_os = "windows")]
+fn main() -> anyhow::Result<()> {
+ win::main()
+}
+
+#[cfg(not(target_os = "windows"))]
+fn main() {
+ panic!("codex-windows-sandbox-setup is Windows-only");
+}
diff --git a/codex-rs/windows-sandbox-rs/src/dpapi.rs b/codex-rs/windows-sandbox-rs/src/dpapi.rs
new file mode 100644
index 00000000000..d254fa11693
--- /dev/null
+++ b/codex-rs/windows-sandbox-rs/src/dpapi.rs
@@ -0,0 +1,81 @@
+use anyhow::anyhow;
+use anyhow::Result;
+use windows_sys::Win32::Foundation::GetLastError;
+use windows_sys::Win32::Foundation::HLOCAL;
+use windows_sys::Win32::Foundation::LocalFree;
+use windows_sys::Win32::Security::Cryptography::CryptProtectData;
+use windows_sys::Win32::Security::Cryptography::CryptUnprotectData;
+use windows_sys::Win32::Security::Cryptography::CRYPT_INTEGER_BLOB;
+use windows_sys::Win32::Security::Cryptography::CRYPTPROTECT_UI_FORBIDDEN;
+
+fn make_blob(data: &[u8]) -> CRYPT_INTEGER_BLOB {
+ CRYPT_INTEGER_BLOB {
+ cbData: data.len() as u32,
+ pbData: data.as_ptr() as *mut u8,
+ }
+}
+
+#[allow(clippy::unnecessary_mut_passed)]
+pub fn protect(data: &[u8]) -> Result> {
+ let mut in_blob = make_blob(data);
+ let mut out_blob = CRYPT_INTEGER_BLOB {
+ cbData: 0,
+ pbData: std::ptr::null_mut(),
+ };
+ let ok = unsafe {
+ CryptProtectData(
+ &mut in_blob,
+ std::ptr::null(),
+ std::ptr::null(),
+ std::ptr::null_mut(),
+ std::ptr::null_mut(),
+ CRYPTPROTECT_UI_FORBIDDEN,
+ &mut out_blob,
+ )
+ };
+ if ok == 0 {
+ return Err(anyhow!("CryptProtectData failed: {}", unsafe { GetLastError() }));
+ }
+ let slice =
+ unsafe { std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize) }.to_vec();
+ unsafe {
+ if !out_blob.pbData.is_null() {
+ LocalFree(out_blob.pbData as HLOCAL);
+ }
+ }
+ Ok(slice)
+}
+
+#[allow(clippy::unnecessary_mut_passed)]
+pub fn unprotect(blob: &[u8]) -> Result> {
+ let mut in_blob = make_blob(blob);
+ let mut out_blob = CRYPT_INTEGER_BLOB {
+ cbData: 0,
+ pbData: std::ptr::null_mut(),
+ };
+ let ok = unsafe {
+ CryptUnprotectData(
+ &mut in_blob,
+ std::ptr::null_mut(),
+ std::ptr::null(),
+ std::ptr::null_mut(),
+ std::ptr::null_mut(),
+ CRYPTPROTECT_UI_FORBIDDEN,
+ &mut out_blob,
+ )
+ };
+ if ok == 0 {
+ return Err(anyhow!(
+ "CryptUnprotectData failed: {}",
+ unsafe { GetLastError() }
+ ));
+ }
+ let slice =
+ unsafe { std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize) }.to_vec();
+ unsafe {
+ if !out_blob.pbData.is_null() {
+ LocalFree(out_blob.pbData as HLOCAL);
+ }
+ }
+ Ok(slice)
+}
diff --git a/codex-rs/windows-sandbox-rs/src/identity.rs b/codex-rs/windows-sandbox-rs/src/identity.rs
new file mode 100644
index 00000000000..1c984dd9279
--- /dev/null
+++ b/codex-rs/windows-sandbox-rs/src/identity.rs
@@ -0,0 +1,171 @@
+use crate::dpapi;
+use crate::logging::debug_log;
+use crate::policy::SandboxPolicy;
+use crate::setup::run_elevated_setup;
+use crate::setup::sandbox_users_path;
+use crate::setup::setup_marker_path;
+use crate::setup::SandboxUserRecord;
+use crate::setup::SandboxUsersFile;
+use crate::setup::SetupMarker;
+use crate::setup::{gather_read_roots, gather_write_roots};
+use anyhow::anyhow;
+use anyhow::Context;
+use anyhow::Result;
+use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
+use base64::Engine;
+use std::collections::HashMap;
+use std::fs;
+use std::path::Path;
+
+#[derive(Debug, Clone)]
+struct SandboxIdentity {
+ username: String,
+ password: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct SandboxCreds {
+ pub username: String,
+ pub password: String,
+}
+
+fn load_marker(codex_home: &Path) -> Result