1#![allow(unsafe_op_in_unsafe_fn)]
4
5#[cfg(any(target_os = "windows", test))]
6mod ssh_config_dependencies;
7
8use std::fmt;
9use std::sync::Arc;
10
11#[derive(Clone)]
13pub struct WindowsSandboxCancellationToken {
14 is_cancelled: Arc<dyn Fn() -> bool + Send + Sync>,
15}
16
17impl WindowsSandboxCancellationToken {
18 pub fn new(is_cancelled: impl Fn() -> bool + Send + Sync + 'static) -> Self {
20 Self {
21 is_cancelled: Arc::new(is_cancelled),
22 }
23 }
24
25 pub fn is_cancelled(&self) -> bool {
27 (self.is_cancelled)()
28 }
29}
30
31impl fmt::Debug for WindowsSandboxCancellationToken {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.debug_struct("WindowsSandboxCancellationToken")
34 .finish_non_exhaustive()
35 }
36}
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub enum WindowsSandboxProxySettingsMode {
42 #[default]
43 Reconcile,
44 Preserve,
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
49pub struct WindowsSandboxProvisioningSettings {
50 pub proxy_ports: Vec<u16>,
52 pub allow_local_binding: bool,
54}
55
56#[cfg(target_os = "windows")]
57mod acl;
58#[cfg(target_os = "windows")]
59mod allow;
60#[cfg(target_os = "windows")]
61mod audit;
62#[cfg(target_os = "windows")]
63mod cap;
64#[cfg(target_os = "windows")]
65mod deny_read_acl;
66#[cfg(target_os = "windows")]
67mod deny_read_state;
68#[cfg(target_os = "windows")]
69mod desktop;
70#[cfg(target_os = "windows")]
71mod dpapi;
72#[cfg(target_os = "windows")]
73mod env;
74#[cfg(target_os = "windows")]
75mod helper_materialization;
76#[cfg(target_os = "windows")]
77mod hide_users;
78#[cfg(target_os = "windows")]
79mod identity;
80#[cfg(target_os = "windows")]
81mod logging;
82#[cfg(target_os = "windows")]
83mod path_normalization;
84#[cfg(target_os = "windows")]
85mod process;
86#[cfg(target_os = "windows")]
87mod resolved_permissions;
88#[cfg(target_os = "windows")]
89mod token;
90#[cfg(target_os = "windows")]
91mod wfp;
92#[cfg(target_os = "windows")]
93mod wfp_setup;
94#[cfg(target_os = "windows")]
95mod winutil;
96#[cfg(target_os = "windows")]
97mod workspace_acl;
98
99mod deny_read_resolver;
100
101#[cfg(target_os = "windows")]
102mod conpty;
103
104#[cfg(target_os = "windows")]
105mod elevated;
106
107#[cfg(target_os = "windows")]
108mod elevated_impl;
109
110#[cfg(target_os = "windows")]
111mod proc_thread_attr;
112
113#[cfg(target_os = "windows")]
114mod sandbox_utils;
115
116#[cfg(target_os = "windows")]
117mod setup;
118
119#[cfg(target_os = "windows")]
120mod setup_error;
121
122#[cfg(target_os = "windows")]
123mod spawn_prep;
124
125#[cfg(target_os = "windows")]
126mod stdio_bridge;
127
128#[cfg(target_os = "windows")]
129mod unified_exec;
130#[cfg(target_os = "windows")]
131mod wrapper;
132
133#[cfg(target_os = "windows")]
134pub(crate) use elevated::ipc_framed;
135
136#[cfg(target_os = "windows")]
137pub(crate) use elevated::runner_client;
138
139#[cfg(target_os = "windows")]
140pub(crate) use elevated::runner_pipe;
141
142#[cfg(target_os = "windows")]
143pub use acl::add_deny_read_ace;
144#[cfg(target_os = "windows")]
145pub use acl::add_deny_write_ace;
146
147#[cfg(target_os = "windows")]
148pub use acl::allow_null_device;
149#[cfg(target_os = "windows")]
150pub use acl::ensure_allow_mask_aces;
151#[cfg(target_os = "windows")]
152pub use acl::ensure_allow_mask_aces_with_inheritance;
153#[cfg(target_os = "windows")]
154pub use acl::ensure_allow_write_aces;
155#[cfg(target_os = "windows")]
156pub use acl::fetch_dacl_handle;
157#[cfg(target_os = "windows")]
158pub use acl::path_mask_allows;
159#[cfg(target_os = "windows")]
160pub use acl::path_write_aces_need_refresh;
161#[cfg(target_os = "windows")]
162pub use audit::apply_world_writable_scan_and_denies_for_permissions;
163#[cfg(target_os = "windows")]
164pub use cap::load_or_create_cap_sids;
165#[cfg(target_os = "windows")]
166pub use cap::workspace_cap_sid_for_cwd;
167#[cfg(target_os = "windows")]
168pub use cap::workspace_write_cap_sid_for_root;
169#[cfg(target_os = "windows")]
170pub use cap::workspace_write_root_contains_path;
171#[cfg(target_os = "windows")]
172pub use cap::workspace_write_root_overlaps_path;
173#[cfg(target_os = "windows")]
174pub use conpty::ConptyInstance;
175#[cfg(target_os = "windows")]
176pub use conpty::spawn_conpty_process_as_user;
177#[cfg(target_os = "windows")]
178pub use deny_read_acl::apply_deny_read_acls;
179#[cfg(target_os = "windows")]
180pub use deny_read_acl::plan_deny_read_acl_paths;
181pub use deny_read_resolver::resolve_windows_deny_read_paths;
182#[cfg(target_os = "windows")]
183pub use deny_read_state::sync_persistent_deny_read_acls;
184#[cfg(target_os = "windows")]
185pub use desktop::LaunchDesktop;
186#[cfg(target_os = "windows")]
187pub use dpapi::protect as dpapi_protect;
188#[cfg(target_os = "windows")]
189pub use dpapi::unprotect as dpapi_unprotect;
190#[cfg(target_os = "windows")]
191pub use elevated_impl::ElevatedSandboxProfileCaptureRequest;
192#[cfg(target_os = "windows")]
193pub use elevated_impl::run_windows_sandbox_capture_for_permission_profile as run_windows_sandbox_capture_for_permission_profile_elevated;
194#[cfg(target_os = "windows")]
195pub use helper_materialization::resolve_current_exe_for_launch;
196#[cfg(target_os = "windows")]
197pub use helper_materialization::resolve_exe_for_launch;
198#[cfg(target_os = "windows")]
199pub use hide_users::hide_current_user_profile_dir;
200#[cfg(target_os = "windows")]
201pub use hide_users::hide_newly_created_users;
202#[cfg(target_os = "windows")]
203pub use identity::require_logon_sandbox_creds;
204#[cfg(target_os = "windows")]
205pub use identity::sandbox_setup_is_complete;
206#[cfg(target_os = "windows")]
207pub use ipc_framed::ErrorPayload;
208#[cfg(target_os = "windows")]
209pub use ipc_framed::ErrorStage;
210#[cfg(target_os = "windows")]
211pub use ipc_framed::ExitPayload;
212#[cfg(target_os = "windows")]
213pub use ipc_framed::FramedMessage;
214#[cfg(target_os = "windows")]
215pub use ipc_framed::IPC_PROTOCOL_VERSION;
216#[cfg(target_os = "windows")]
217pub use ipc_framed::Message;
218#[cfg(target_os = "windows")]
219pub use ipc_framed::OutputPayload;
220#[cfg(target_os = "windows")]
221pub use ipc_framed::OutputStream;
222#[cfg(target_os = "windows")]
223pub use ipc_framed::ResizePayload;
224#[cfg(target_os = "windows")]
225pub use ipc_framed::SpawnReady;
226#[cfg(target_os = "windows")]
227pub use ipc_framed::SpawnRequest;
228#[cfg(target_os = "windows")]
229pub use ipc_framed::decode_bytes;
230#[cfg(target_os = "windows")]
231pub use ipc_framed::encode_bytes;
232#[cfg(target_os = "windows")]
233pub use ipc_framed::read_frame;
234#[cfg(target_os = "windows")]
235pub use ipc_framed::write_frame;
236#[cfg(target_os = "windows")]
237pub use logging::current_log_file_path;
238#[cfg(target_os = "windows")]
239pub use logging::current_log_file_path_for_codex_home;
240#[cfg(target_os = "windows")]
241pub use logging::log_file_path_for_utc_date;
242#[cfg(target_os = "windows")]
243pub use logging::log_note;
244#[cfg(target_os = "windows")]
245pub use logging::log_writer;
246#[cfg(target_os = "windows")]
247pub use path_normalization::canonicalize_path;
248#[cfg(target_os = "windows")]
249pub use process::ConsoleMode;
250#[cfg(target_os = "windows")]
251pub use process::PipeSpawnHandles;
252#[cfg(target_os = "windows")]
253pub use process::StderrMode;
254#[cfg(target_os = "windows")]
255pub use process::StdinMode;
256#[cfg(target_os = "windows")]
257pub use process::create_process_as_user;
258#[cfg(target_os = "windows")]
259pub use process::read_handle_loop;
260#[cfg(target_os = "windows")]
261pub use process::spawn_process_with_pipes;
262#[cfg(target_os = "windows")]
263pub use resolved_permissions::ResolvedWindowsSandboxPermissions;
264#[cfg(target_os = "windows")]
265pub use resolved_permissions::WindowsSandboxTokenMode;
266#[cfg(target_os = "windows")]
267pub use resolved_permissions::token_mode_for_permission_profile;
268#[cfg(target_os = "windows")]
269pub use setup::SETUP_VERSION;
270#[cfg(target_os = "windows")]
271pub use setup::SandboxSetupRequest;
272#[cfg(target_os = "windows")]
273pub use setup::SetupRootOverrides;
274#[cfg(target_os = "windows")]
275pub use setup::run_elevated_provisioning_setup;
276#[cfg(target_os = "windows")]
277pub use setup::run_elevated_setup;
278#[cfg(target_os = "windows")]
279pub use setup::run_setup_refresh;
280#[cfg(target_os = "windows")]
281pub use setup::run_setup_refresh_with_extra_read_roots;
282#[cfg(target_os = "windows")]
283pub use setup::sandbox_bin_dir;
284#[cfg(target_os = "windows")]
285pub use setup::sandbox_dir;
286#[cfg(target_os = "windows")]
287pub use setup::sandbox_secrets_dir;
288#[cfg(target_os = "windows")]
289pub use setup_error::SetupErrorCode;
290#[cfg(target_os = "windows")]
291pub use setup_error::SetupErrorReport;
292#[cfg(target_os = "windows")]
293pub use setup_error::SetupFailure;
294#[cfg(target_os = "windows")]
295pub use setup_error::extract_failure as extract_setup_failure;
296#[cfg(target_os = "windows")]
297pub use setup_error::sanitize_setup_metric_tag_value;
298#[cfg(target_os = "windows")]
299pub use setup_error::setup_error_path;
300#[cfg(target_os = "windows")]
301pub use setup_error::write_setup_error_report;
302#[cfg(target_os = "windows")]
303pub use stdio_bridge::forward_sandbox_session_stdio;
304#[cfg(target_os = "windows")]
305#[doc(hidden)]
306pub use token::LocalSid;
307#[cfg(target_os = "windows")]
308pub use token::convert_string_sid_to_sid;
309#[cfg(target_os = "windows")]
310pub use token::create_readonly_token_with_cap_from;
311#[cfg(target_os = "windows")]
312pub use token::create_readonly_token_with_caps_and_user_from;
313#[cfg(target_os = "windows")]
314pub use token::create_readonly_token_with_caps_from;
315#[cfg(target_os = "windows")]
316pub use token::create_workspace_write_token_with_caps_and_user_from;
317#[cfg(target_os = "windows")]
318pub use token::create_workspace_write_token_with_caps_from;
319#[cfg(target_os = "windows")]
320pub use token::get_current_token_for_restriction;
321#[cfg(target_os = "windows")]
322pub use unified_exec::WindowsSandboxSessionRequest;
323#[cfg(target_os = "windows")]
324pub use unified_exec::spawn_windows_sandbox_session_elevated_for_permission_profile;
325#[cfg(target_os = "windows")]
326pub use unified_exec::spawn_windows_sandbox_session_for_level;
327#[cfg(target_os = "windows")]
328pub use unified_exec::spawn_windows_sandbox_session_legacy;
329#[cfg(target_os = "windows")]
330pub use wfp::install_wfp_filters_for_account;
331#[cfg(target_os = "windows")]
332pub use wfp_setup::install_wfp_filters;
333#[cfg(target_os = "windows")]
334pub use windows_impl::CaptureResult;
335#[cfg(target_os = "windows")]
336pub use windows_impl::run_windows_sandbox_capture;
337#[cfg(target_os = "windows")]
338pub use windows_impl::run_windows_sandbox_capture_with_filesystem_overrides;
339#[cfg(target_os = "windows")]
340pub use windows_impl::run_windows_sandbox_legacy_preflight;
341#[cfg(target_os = "windows")]
342pub use winutil::quote_windows_arg;
343#[cfg(target_os = "windows")]
344pub use winutil::string_from_sid_bytes;
345#[cfg(target_os = "windows")]
346pub use winutil::to_wide;
347#[cfg(target_os = "windows")]
348pub use workspace_acl::is_command_cwd_root;
349#[cfg(target_os = "windows")]
350pub use wrapper::CODEX_WINDOWS_SANDBOX_ARG1;
351#[cfg(target_os = "windows")]
352pub use wrapper::create_windows_sandbox_command_args_for_permission_profile;
353#[cfg(target_os = "windows")]
354pub use wrapper::run_windows_sandbox_wrapper_main;
355
356#[cfg(not(target_os = "windows"))]
357pub use stub::CaptureResult;
358#[cfg(not(target_os = "windows"))]
359pub use stub::run_windows_sandbox_capture;
360#[cfg(not(target_os = "windows"))]
361pub use stub::run_windows_sandbox_legacy_preflight;
362
363#[cfg(target_os = "windows")]
364mod windows_impl {
365 use super::WindowsSandboxCancellationToken;
366 use super::logging::log_failure;
367 use super::logging::log_note;
368 use super::logging::log_success;
369 use super::process::ConsoleMode;
370 use super::process::create_process_as_user;
371 use super::sandbox_utils::ensure_codex_home_exists;
372 use super::spawn_prep::LegacyAclSids;
373 use super::spawn_prep::SpawnPrepOptions;
374 use super::spawn_prep::allow_null_device_for_workspace_write;
375 use super::spawn_prep::apply_legacy_session_acl_rules;
376 use super::spawn_prep::legacy_session_capability_roots;
377 use super::spawn_prep::prepare_legacy_session_security;
378 use super::spawn_prep::prepare_legacy_spawn_context;
379 use super::spawn_prep::root_capability_sids;
380 use anyhow::Result;
381 use codex_protocol::models::PermissionProfile;
382 use codex_utils_absolute_path::AbsolutePathBuf;
383 use std::collections::HashMap;
384 use std::io;
385 use std::path::Path;
386 use std::ptr;
387 use std::sync::Arc;
388 use std::time::Duration;
389 use std::time::Instant;
390 use windows_sys::Win32::Foundation::CloseHandle;
391 use windows_sys::Win32::Foundation::GetLastError;
392 use windows_sys::Win32::Foundation::HANDLE;
393 use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
394 use windows_sys::Win32::Foundation::SetHandleInformation;
395 use windows_sys::Win32::System::Pipes::CreatePipe;
396 use windows_sys::Win32::System::Threading::GetExitCodeProcess;
397 use windows_sys::Win32::System::Threading::INFINITE;
398 use windows_sys::Win32::System::Threading::WaitForSingleObject;
399
400 type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE));
401
402 enum WaitOutcome {
403 Exited,
404 TimedOut,
405 Cancelled,
406 }
407
408 fn wait_for_process(
409 process: HANDLE,
410 timeout_ms: Option<u64>,
411 cancellation: Option<&WindowsSandboxCancellationToken>,
412 ) -> WaitOutcome {
413 let Some(cancellation) = cancellation else {
414 let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
415 let res = unsafe { WaitForSingleObject(process, timeout) };
416 return if res == 0x0000_0102 {
417 WaitOutcome::TimedOut
418 } else {
419 WaitOutcome::Exited
420 };
421 };
422
423 let deadline = timeout_ms.map(|ms| Instant::now() + Duration::from_millis(ms));
424 loop {
425 if cancellation.is_cancelled() {
426 return WaitOutcome::Cancelled;
427 }
428 let wait_ms = match deadline {
429 Some(deadline) => {
430 let remaining = deadline.saturating_duration_since(Instant::now());
431 if remaining.is_zero() {
432 return WaitOutcome::TimedOut;
433 }
434 remaining.min(Duration::from_millis(50)).as_millis() as u32
435 }
436 None => 50,
437 };
438 let res = unsafe { WaitForSingleObject(process, wait_ms) };
439 if res == 0x0000_0102 {
440 continue;
441 }
442 return WaitOutcome::Exited;
443 }
444 }
445
446 unsafe fn setup_stdio_pipes() -> io::Result<PipeHandles> {
447 let mut in_r: HANDLE = 0;
448 let mut in_w: HANDLE = 0;
449 let mut out_r: HANDLE = 0;
450 let mut out_w: HANDLE = 0;
451 let mut err_r: HANDLE = 0;
452 let mut err_w: HANDLE = 0;
453 if CreatePipe(&mut in_r, &mut in_w, ptr::null_mut(), 0) == 0 {
454 return Err(io::Error::from_raw_os_error(GetLastError() as i32));
455 }
456 if CreatePipe(&mut out_r, &mut out_w, ptr::null_mut(), 0) == 0 {
457 return Err(io::Error::from_raw_os_error(GetLastError() as i32));
458 }
459 if CreatePipe(&mut err_r, &mut err_w, ptr::null_mut(), 0) == 0 {
460 return Err(io::Error::from_raw_os_error(GetLastError() as i32));
461 }
462 if SetHandleInformation(in_r, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
463 return Err(io::Error::from_raw_os_error(GetLastError() as i32));
464 }
465 if SetHandleInformation(out_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
466 return Err(io::Error::from_raw_os_error(GetLastError() as i32));
467 }
468 if SetHandleInformation(err_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
469 return Err(io::Error::from_raw_os_error(GetLastError() as i32));
470 }
471 Ok(((in_r, in_w), (out_r, out_w), (err_r, err_w)))
472 }
473
474 pub struct CaptureResult {
475 pub exit_code: i32,
476 pub stdout: Vec<u8>,
477 pub stderr: Vec<u8>,
478 pub timed_out: bool,
479 }
480
481 #[allow(clippy::too_many_arguments)]
482 pub fn run_windows_sandbox_capture(
483 permission_profile: &PermissionProfile,
484 workspace_roots: &[AbsolutePathBuf],
485 codex_home: &Path,
486 command: Vec<String>,
487 cwd: &Path,
488 env_map: HashMap<String, String>,
489 timeout_ms: Option<u64>,
490 cancellation: Option<WindowsSandboxCancellationToken>,
491 use_private_desktop: bool,
492 ) -> Result<CaptureResult> {
493 run_windows_sandbox_capture_with_filesystem_overrides(
494 permission_profile,
495 workspace_roots,
496 codex_home,
497 command,
498 cwd,
499 env_map,
500 timeout_ms,
501 cancellation,
502 &[],
503 &[],
504 use_private_desktop,
505 )
506 }
507
508 #[allow(clippy::too_many_arguments)]
509 pub fn run_windows_sandbox_capture_with_filesystem_overrides(
510 permission_profile: &PermissionProfile,
511 workspace_roots: &[AbsolutePathBuf],
512 codex_home: &Path,
513 command: Vec<String>,
514 cwd: &Path,
515 mut env_map: HashMap<String, String>,
516 timeout_ms: Option<u64>,
517 cancellation: Option<WindowsSandboxCancellationToken>,
518 additional_deny_read_paths: &[AbsolutePathBuf],
519 additional_deny_write_paths: &[AbsolutePathBuf],
520 use_private_desktop: bool,
521 ) -> Result<CaptureResult> {
522 let additional_deny_read_paths = additional_deny_read_paths
523 .iter()
524 .map(AbsolutePathBuf::to_path_buf)
525 .collect::<Vec<_>>();
526 let additional_deny_write_paths = additional_deny_write_paths
527 .iter()
528 .map(AbsolutePathBuf::to_path_buf)
529 .collect::<Vec<_>>();
530 let common = prepare_legacy_spawn_context(
531 permission_profile,
532 workspace_roots,
533 codex_home,
534 cwd,
535 &mut env_map,
536 &command,
537 SpawnPrepOptions {
538 inherit_path: false,
539 add_git_safe_directory: false,
540 },
541 )?;
542 let permissions = common.permissions;
543 let current_dir = common.current_dir;
544 let logs_base_dir = common.logs_base_dir.as_deref();
545 let uses_write_capabilities = common.uses_write_capabilities;
546 if !permissions.has_full_disk_read_access() {
547 anyhow::bail!(
548 "Restricted read-only access requires the elevated Windows sandbox backend"
549 );
550 }
551 if !additional_deny_read_paths.is_empty() {
554 anyhow::bail!("deny-read overrides require the elevated Windows sandbox backend");
555 }
556 let capability_roots =
557 legacy_session_capability_roots(&permissions, ¤t_dir, &env_map, codex_home);
558 let security = prepare_legacy_session_security(
559 uses_write_capabilities,
560 codex_home,
561 cwd,
562 capability_roots,
563 )?;
564 allow_null_device_for_workspace_write(uses_write_capabilities);
565 apply_legacy_session_acl_rules(
566 &permissions,
567 codex_home,
568 ¤t_dir,
569 &env_map,
570 &additional_deny_read_paths,
571 &additional_deny_write_paths,
572 LegacyAclSids {
573 readonly_sid: security.readonly_sid.as_ref(),
574 readonly_sid_str: security.readonly_sid_str.as_deref(),
575 write_root_sids: &security.write_root_sids,
576 },
577 )?;
578 let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
579 let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
580 let spawn_res = unsafe {
581 create_process_as_user(
582 security.h_token,
583 &command,
584 cwd,
585 &env_map,
586 logs_base_dir,
587 Some((in_r, out_w, err_w)),
588 ConsoleMode::Inherit,
589 use_private_desktop,
590 )
591 };
592 let created = match spawn_res {
593 Ok(v) => v,
594 Err(err) => {
595 unsafe {
596 CloseHandle(in_r);
597 CloseHandle(in_w);
598 CloseHandle(out_r);
599 CloseHandle(out_w);
600 CloseHandle(err_r);
601 CloseHandle(err_w);
602 CloseHandle(security.h_token);
603 }
604 return Err(err);
605 }
606 };
607 let pi = created.process_info;
608 let job = Arc::clone(&created.job);
609 let _desktop = created;
610
611 unsafe {
612 CloseHandle(in_r);
613 CloseHandle(in_w);
615 CloseHandle(out_w);
616 CloseHandle(err_w);
617 }
618
619 let (tx_out, rx_out) = std::sync::mpsc::channel::<Vec<u8>>();
620 let (tx_err, rx_err) = std::sync::mpsc::channel::<Vec<u8>>();
621 let t_out = std::thread::spawn(move || {
622 let mut buf = Vec::new();
623 let mut tmp = [0u8; 8192];
624 loop {
625 let mut read_bytes: u32 = 0;
626 let ok = unsafe {
627 windows_sys::Win32::Storage::FileSystem::ReadFile(
628 out_r,
629 tmp.as_mut_ptr(),
630 tmp.len() as u32,
631 &mut read_bytes,
632 std::ptr::null_mut(),
633 )
634 };
635 if ok == 0 || read_bytes == 0 {
636 break;
637 }
638 buf.extend_from_slice(&tmp[..read_bytes as usize]);
639 }
640 let _ = tx_out.send(buf);
641 });
642 let t_err = std::thread::spawn(move || {
643 let mut buf = Vec::new();
644 let mut tmp = [0u8; 8192];
645 loop {
646 let mut read_bytes: u32 = 0;
647 let ok = unsafe {
648 windows_sys::Win32::Storage::FileSystem::ReadFile(
649 err_r,
650 tmp.as_mut_ptr(),
651 tmp.len() as u32,
652 &mut read_bytes,
653 std::ptr::null_mut(),
654 )
655 };
656 if ok == 0 || read_bytes == 0 {
657 break;
658 }
659 buf.extend_from_slice(&tmp[..read_bytes as usize]);
660 }
661 let _ = tx_err.send(buf);
662 });
663
664 let wait_outcome = wait_for_process(pi.hProcess, timeout_ms, cancellation.as_ref());
665 let timed_out = matches!(wait_outcome, WaitOutcome::TimedOut);
666 let cancelled = matches!(wait_outcome, WaitOutcome::Cancelled);
667 let mut exit_code_u32: u32 = 1;
668 if !timed_out && !cancelled {
669 unsafe {
670 GetExitCodeProcess(pi.hProcess, &mut exit_code_u32);
671 }
672 }
673 if timed_out || cancelled {
674 if let Err(job_err) = job.terminate() {
675 log_note(
676 &format!("capture failed to terminate process tree: {job_err}"),
677 logs_base_dir,
678 );
679 let root_result = unsafe {
680 windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1)
681 };
682 if root_result == 0 {
683 log_note(
684 &format!("capture failed to terminate root process: {}", unsafe {
685 GetLastError()
686 }),
687 logs_base_dir,
688 );
689 }
690 }
691 } else if let Err(err) = job.preserve_descendants() {
692 log_note(
693 &format!("capture failed to preserve descendants after root exit: {err}"),
694 logs_base_dir,
695 );
696 }
697
698 unsafe {
699 if pi.hThread != 0 {
700 CloseHandle(pi.hThread);
701 }
702 if pi.hProcess != 0 {
703 CloseHandle(pi.hProcess);
704 }
705 CloseHandle(security.h_token);
706 }
707 let _ = t_out.join();
708 let _ = t_err.join();
709 let stdout = rx_out.recv().unwrap_or_default();
710 let stderr = rx_err.recv().unwrap_or_default();
711 let exit_code = if timed_out {
712 128 + 64
713 } else {
714 exit_code_u32 as i32
715 };
716
717 if exit_code == 0 {
718 log_success(&command, logs_base_dir);
719 } else {
720 log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir);
721 }
722
723 Ok(CaptureResult {
724 exit_code,
725 stdout,
726 stderr,
727 timed_out,
728 })
729 }
730
731 pub fn run_windows_sandbox_legacy_preflight(
732 permission_profile: &PermissionProfile,
733 workspace_roots: &[AbsolutePathBuf],
734 codex_home: &Path,
735 cwd: &Path,
736 env_map: &HashMap<String, String>,
737 ) -> Result<()> {
738 let Ok(permissions) = super::resolved_permissions::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
739 permission_profile,
740 workspace_roots,
741 ) else {
742 return Ok(());
743 };
744 if !permissions.uses_write_capabilities_for_cwd(cwd, env_map) {
745 return Ok(());
746 }
747
748 ensure_codex_home_exists(codex_home)?;
749 let current_dir = cwd.to_path_buf();
750 let capability_roots =
751 legacy_session_capability_roots(&permissions, ¤t_dir, env_map, codex_home);
752 let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?;
753 apply_legacy_session_acl_rules(
754 &permissions,
755 codex_home,
756 ¤t_dir,
757 env_map,
758 &[],
759 &[],
760 LegacyAclSids {
761 readonly_sid: None,
762 readonly_sid_str: None,
763 write_root_sids: &write_root_sids,
764 },
765 )?;
766
767 Ok(())
768 }
769
770 #[cfg(test)]
771 mod tests {
772 use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
773 use codex_protocol::models::PermissionProfile;
774 use codex_protocol::permissions::NetworkSandboxPolicy;
775 use std::collections::HashMap;
776 use std::path::Path;
777
778 fn workspace_profile(network_policy: NetworkSandboxPolicy) -> PermissionProfile {
779 PermissionProfile::workspace_write_with(
780 &[],
781 network_policy,
782 false,
783 false,
784 )
785 }
786
787 fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool {
788 ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
789 permission_profile,
790 &[],
791 )
792 .expect("managed permissions")
793 .should_apply_network_block()
794 }
795
796 #[test]
797 fn applies_network_block_when_access_is_disabled() {
798 assert!(should_apply_network_block(&workspace_profile(
799 NetworkSandboxPolicy::Restricted
800 )));
801 }
802
803 #[test]
804 fn skips_network_block_when_access_is_allowed() {
805 assert!(!should_apply_network_block(&workspace_profile(
806 NetworkSandboxPolicy::Enabled
807 )));
808 }
809
810 #[test]
811 fn applies_network_block_for_read_only() {
812 assert!(should_apply_network_block(&PermissionProfile::read_only()));
813 }
814
815 #[test]
816 fn legacy_preflight_skips_profiles_without_managed_filesystem_permissions() {
817 for permission_profile in [
818 PermissionProfile::Disabled,
819 PermissionProfile::External {
820 network: NetworkSandboxPolicy::Restricted,
821 },
822 ] {
823 super::run_windows_sandbox_legacy_preflight(
824 &permission_profile,
825 &[],
826 Path::new("."),
827 Path::new("."),
828 &HashMap::new(),
829 )
830 .expect("unsupported profiles do not need ACL preflight");
831 }
832 }
833 }
834}
835
836#[cfg(not(target_os = "windows"))]
837mod stub {
838 use super::WindowsSandboxCancellationToken;
839 use anyhow::Result;
840 use anyhow::bail;
841 use codex_protocol::models::PermissionProfile;
842 use codex_utils_absolute_path::AbsolutePathBuf;
843 use std::collections::HashMap;
844 use std::path::Path;
845
846 #[derive(Debug, Default)]
847 pub struct CaptureResult {
848 pub exit_code: i32,
849 pub stdout: Vec<u8>,
850 pub stderr: Vec<u8>,
851 pub timed_out: bool,
852 }
853
854 #[allow(clippy::too_many_arguments)]
855 pub fn run_windows_sandbox_capture(
856 _permission_profile: &PermissionProfile,
857 _workspace_roots: &[AbsolutePathBuf],
858 _codex_home: &Path,
859 _command: Vec<String>,
860 _cwd: &Path,
861 _env_map: HashMap<String, String>,
862 _timeout_ms: Option<u64>,
863 _cancellation: Option<WindowsSandboxCancellationToken>,
864 _use_private_desktop: bool,
865 ) -> Result<CaptureResult> {
866 bail!("Windows sandbox is only available on Windows")
867 }
868
869 pub fn run_windows_sandbox_legacy_preflight(
870 _permission_profile: &PermissionProfile,
871 _workspace_roots: &[AbsolutePathBuf],
872 _codex_home: &Path,
873 _cwd: &Path,
874 _env_map: &HashMap<String, String>,
875 ) -> Result<()> {
876 bail!("Windows sandbox is only available on Windows")
877 }
878}