Skip to content

Commit 848cbad

Browse files
johnl-oaicopyberry
authored andcommitted
Enforce managed deny-read rules in the Windows sandbox (#38660)
## Why Windows sandbox requests must preserve managed filesystem deny rules across every execution path and setup refresh. Unsupported policies should fail closed instead of allowing a command to run without the requested protection. ## What changed - Resolve Windows filesystem overrides while constructing each sandbox execution request so both `shell_command` and `exec_command` enforce exact-path and glob deny-read entries. - Carry resolved deny-read paths into Windows sandbox setup refreshes, including workspace-relative entries. - Reject unelevated restricted-token requests that cannot enforce deny-read rules. - Reject recursive globs rooted at a filesystem root unless `glob_scan_max_depth` bounds their expansion. ## Testing Added coverage for deny-read enforcement through both command runtimes, setup refresh resolution, restricted-token failure, and bounded root-level glob scans. GitOrigin-RevId: 33dfa4d1a45b14850cbf58f9173717e3e707d9bf
1 parent 5186e2c commit 848cbad

8 files changed

Lines changed: 486 additions & 47 deletions

File tree

‎codex-rs/core/src/exec.rs‎

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ pub(crate) use codex_sandboxing::is_likely_sandbox_denied;
4545
#[cfg(test)]
4646
use codex_sandboxing::permission_profile_supports_windows_restricted_token_sandbox;
4747
use codex_sandboxing::record_filesystem_sandbox_violation;
48+
#[cfg(test)]
4849
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
50+
#[cfg(test)]
4951
use codex_sandboxing::resolve_windows_restricted_token_filesystem_overrides;
5052
#[cfg(test)]
5153
use codex_sandboxing::unsupported_windows_restricted_token_sandbox_reason;
54+
#[cfg(any(test, target_os = "windows"))]
5255
use codex_sandboxing::windows_sandbox_uses_elevated_backend;
5356
use codex_utils_absolute_path::AbsolutePathBuf;
5457
use codex_utils_path_uri::PathUri;
@@ -379,7 +382,7 @@ pub fn build_exec_request(
379382
expiration,
380383
capture_policy,
381384
};
382-
let mut exec_req = manager
385+
let request = manager
383386
.transform(SandboxTransformRequest {
384387
command,
385388
permissions: permission_profile,
@@ -393,38 +396,13 @@ pub fn build_exec_request(
393396
windows_sandbox_level,
394397
windows_sandbox_private_desktop,
395398
})
396-
.map(|request| {
397-
let windows_sandbox_workspace_roots = if windows_sandbox_workspace_roots.is_empty() {
398-
vec![sandbox_cwd.clone()]
399-
} else {
400-
windows_sandbox_workspace_roots.to_vec()
401-
};
402-
ExecRequest::from_sandbox_exec_request(
403-
request,
404-
options,
405-
windows_sandbox_workspace_roots,
406-
)
407-
})
408399
.map_err(CodexErr::from)?;
409-
let use_windows_elevated_backend =
410-
windows_sandbox_uses_elevated_backend(exec_req.windows_sandbox_level);
411-
exec_req.windows_sandbox_filesystem_overrides = if use_windows_elevated_backend {
412-
resolve_windows_elevated_filesystem_overrides(
413-
exec_req.sandbox,
414-
&exec_req.permission_profile,
415-
sandbox_cwd,
416-
use_windows_elevated_backend,
417-
)
400+
let windows_sandbox_workspace_roots = if windows_sandbox_workspace_roots.is_empty() {
401+
vec![sandbox_cwd.clone()]
418402
} else {
419-
resolve_windows_restricted_token_filesystem_overrides(
420-
exec_req.sandbox,
421-
&exec_req.permission_profile,
422-
sandbox_cwd,
423-
exec_req.windows_sandbox_level,
424-
)
425-
}
426-
.map_err(CodexErr::UnsupportedOperation)?;
427-
Ok(exec_req)
403+
windows_sandbox_workspace_roots.to_vec()
404+
};
405+
ExecRequest::from_sandbox_exec_request(request, options, windows_sandbox_workspace_roots)
428406
}
429407

430408
pub(crate) async fn execute_exec_request(

‎codex-rs/core/src/sandboxing/mod.rs‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ use codex_network_proxy::ManagedNetworkSandboxContext;
1919
use codex_network_proxy::NetworkProxy;
2020
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
2121
use codex_protocol::config_types::WindowsSandboxLevel;
22+
use codex_protocol::error::CodexErr;
2223
use codex_protocol::exec_output::ExecToolCallOutput;
2324
use codex_protocol::models::PermissionProfile;
2425
pub use codex_protocol::models::SandboxPermissions;
2526
use codex_sandboxing::SandboxExecRequest;
2627
use codex_sandboxing::SandboxType;
2728
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
29+
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
30+
use codex_sandboxing::resolve_windows_restricted_token_filesystem_overrides;
31+
use codex_sandboxing::windows_sandbox_uses_elevated_backend;
2832
use codex_utils_absolute_path::AbsolutePathBuf;
2933
use codex_utils_path_uri::PathUri;
34+
use codex_utils_string::truncate_middle_with_token_budget;
3035
use std::collections::HashMap;
3136

3237
#[derive(Debug)]
@@ -112,7 +117,7 @@ impl ExecRequest {
112117
request: SandboxExecRequest,
113118
options: ExecOptions,
114119
windows_sandbox_workspace_roots: Vec<AbsolutePathBuf>,
115-
) -> Self {
120+
) -> Result<Self, CodexErr> {
116121
let SandboxExecRequest {
117122
command,
118123
cwd,
@@ -131,6 +136,36 @@ impl ExecRequest {
131136
expiration,
132137
capture_policy,
133138
} = options;
139+
let windows_sandbox_filesystem_overrides = if sandbox == SandboxType::WindowsRestrictedToken
140+
{
141+
let sandbox_policy_cwd = windows_sandbox_policy_cwd
142+
.to_abs_path()
143+
.map_err(|err| CodexErr::InvalidRequest(format!("invalid sandbox cwd: {err}")))?;
144+
let use_windows_elevated_backend =
145+
windows_sandbox_uses_elevated_backend(windows_sandbox_level);
146+
if use_windows_elevated_backend {
147+
resolve_windows_elevated_filesystem_overrides(
148+
sandbox,
149+
&permission_profile,
150+
&sandbox_policy_cwd,
151+
use_windows_elevated_backend,
152+
)
153+
} else {
154+
resolve_windows_restricted_token_filesystem_overrides(
155+
sandbox,
156+
&permission_profile,
157+
&sandbox_policy_cwd,
158+
windows_sandbox_level,
159+
)
160+
}
161+
.map_err(|error| {
162+
CodexErr::UnsupportedOperation(
163+
truncate_middle_with_token_budget(&error, /*max_tokens*/ 900).0,
164+
)
165+
})?
166+
} else {
167+
None
168+
};
134169
let network_sandbox_policy = permission_profile.network_sandbox_policy();
135170
if !network_sandbox_policy.is_enabled() {
136171
env.insert(
@@ -142,7 +177,7 @@ impl ExecRequest {
142177
if sandbox == SandboxType::MacosSeatbelt {
143178
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
144179
}
145-
Self {
180+
Ok(Self {
146181
command,
147182
cwd,
148183
env,
@@ -157,13 +192,13 @@ impl ExecRequest {
157192
windows_sandbox_level,
158193
windows_sandbox_private_desktop,
159194
permission_profile,
160-
windows_sandbox_filesystem_overrides: None,
195+
windows_sandbox_filesystem_overrides,
161196
arg0,
162197
exec_server_sandbox: None,
163198
exec_server_enforce_managed_network: false,
164199
exec_server_managed_network: None,
165200
exec_server_network_proxy: None,
166-
}
201+
})
167202
}
168203
}
169204

‎codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ impl CoreShellCommandExecutor {
981981
exec_request,
982982
options,
983983
self.windows_sandbox_workspace_roots.clone(),
984-
);
984+
)?;
985985
if let Some(network) = exec_request.network.as_ref() {
986986
network
987987
.apply_to_env_for_optional_environment(

‎codex-rs/core/src/tools/sandboxing.rs‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -453,11 +453,7 @@ impl<'a> SandboxAttempt<'a> {
453453
.iter()
454454
.map(PathUri::to_abs_path)
455455
.collect::<std::io::Result<Vec<_>>>()?;
456-
Ok(crate::sandboxing::ExecRequest::from_sandbox_exec_request(
457-
request,
458-
options,
459-
workspace_roots,
460-
))
456+
crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options, workspace_roots)
461457
}
462458

463459
pub fn env_for_exec_server(
@@ -487,8 +483,11 @@ impl<'a> SandboxAttempt<'a> {
487483
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
488484
})
489485
.map_err(CodexErr::from)?;
490-
let mut exec_request =
491-
crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options, Vec::new());
486+
let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request(
487+
request,
488+
options,
489+
Vec::new(),
490+
)?;
492491
exec_request.exec_server_managed_network = managed_network;
493492
if self.sandbox_requested {
494493
exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {

‎codex-rs/core/src/tools/sandboxing_tests.rs‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use codex_network_proxy::ManagedNetworkSandboxContext;
55
use codex_protocol::permissions::FileSystemAccessMode;
66
use codex_protocol::permissions::FileSystemPath;
77
use codex_protocol::permissions::FileSystemSandboxEntry;
8+
use codex_protocol::permissions::FileSystemSpecialPath;
9+
use codex_protocol::permissions::NetworkSandboxPolicy;
810
use codex_protocol::protocol::GranularApprovalConfig;
911
use codex_sandboxing::SandboxCommand;
1012
use codex_sandboxing::SandboxManager;
@@ -202,6 +204,107 @@ fn deny_read_blocks_explicit_escalation_and_policy_bypass() {
202204
);
203205
}
204206

207+
#[test]
208+
fn windows_sandbox_env_preserves_denied_reads_or_rejects_unsupported_backend() {
209+
let temp_dir = tempfile::TempDir::new().expect("create sandbox workspace");
210+
let cwd = AbsolutePathBuf::from_absolute_path(
211+
dunce::canonicalize(temp_dir.path()).expect("canonicalize sandbox workspace"),
212+
)
213+
.expect("absolute sandbox workspace");
214+
let denied_path = cwd.join("blocked");
215+
std::fs::create_dir_all(denied_path.as_path()).expect("create denied directory");
216+
let denied_path = AbsolutePathBuf::from_absolute_path(
217+
dunce::canonicalize(denied_path.as_path()).expect("canonicalize denied directory"),
218+
)
219+
.expect("absolute denied directory");
220+
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
221+
FileSystemSandboxEntry {
222+
path: FileSystemPath::Special {
223+
value: FileSystemSpecialPath::Root,
224+
},
225+
access: FileSystemAccessMode::Read,
226+
missing_path_behavior: None,
227+
},
228+
FileSystemSandboxEntry {
229+
path: FileSystemPath::Special {
230+
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
231+
},
232+
access: FileSystemAccessMode::Write,
233+
missing_path_behavior: None,
234+
},
235+
FileSystemSandboxEntry {
236+
path: FileSystemPath::Path {
237+
path: denied_path.clone(),
238+
},
239+
access: FileSystemAccessMode::Deny,
240+
missing_path_behavior: None,
241+
},
242+
]);
243+
let permissions = codex_protocol::models::PermissionProfile::from_runtime_permissions(
244+
&file_system_policy,
245+
NetworkSandboxPolicy::Restricted,
246+
);
247+
let cwd_uri = PathUri::from_abs_path(&cwd);
248+
let manager = SandboxManager::new();
249+
let mut attempt = SandboxAttempt {
250+
sandbox: SandboxType::WindowsRestrictedToken,
251+
sandbox_requested: true,
252+
permissions: &permissions,
253+
exec_server_permissions: &permissions,
254+
enforce_managed_network: false,
255+
manager: &manager,
256+
sandbox_cwd: &cwd_uri,
257+
workspace_roots: std::slice::from_ref(&cwd_uri),
258+
codex_linux_sandbox_exe: None,
259+
use_legacy_landlock: false,
260+
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Elevated,
261+
windows_sandbox_private_desktop: false,
262+
network_denial_cancellation_token: None,
263+
network_proxy: None,
264+
};
265+
let command = || SandboxCommand {
266+
program: "cmd.exe".into(),
267+
args: vec!["/C".to_string(), "echo sandboxed".to_string()],
268+
cwd: cwd_uri.clone(),
269+
env: HashMap::new(),
270+
managed_network: None,
271+
additional_permissions: None,
272+
};
273+
let options = || crate::sandboxing::ExecOptions {
274+
expiration: crate::exec::ExecExpiration::DefaultTimeout,
275+
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
276+
};
277+
278+
let request = attempt
279+
.env_for(
280+
command(),
281+
options(),
282+
/*network*/ None,
283+
/*environment_id*/ None,
284+
)
285+
.expect("prepare elevated Windows sandbox request");
286+
let overrides = request
287+
.windows_sandbox_filesystem_overrides
288+
.expect("elevated Windows sandbox should preserve deny-read overrides");
289+
assert_eq!(overrides.additional_deny_read_paths, vec![denied_path]);
290+
assert_eq!(request.windows_sandbox_workspace_roots, vec![cwd]);
291+
292+
attempt.windows_sandbox_level =
293+
codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken;
294+
let error = attempt
295+
.env_for(
296+
command(),
297+
options(),
298+
/*network*/ None,
299+
/*environment_id*/ None,
300+
)
301+
.expect_err("restricted-token Windows sandbox cannot enforce deny-read restrictions");
302+
assert_eq!(
303+
error.to_string(),
304+
"unsupported operation: windows unelevated restricted-token sandbox cannot enforce deny-read restrictions directly; refusing to run unsandboxed"
305+
);
306+
}
307+
205308
#[test]
206309
fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
207310
let cwd: AbsolutePathBuf = std::env::current_dir()

0 commit comments

Comments
 (0)