Skip to main content

codex_windows_sandbox/
lib.rs

1// Rust 2024 surfaces this lint across the crate; keep the edition bump separate
2// from the eventual unsafe cleanup.
3#![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/// Cancellation hook used by Windows sandbox capture backends.
12#[derive(Clone)]
13pub struct WindowsSandboxCancellationToken {
14    is_cancelled: Arc<dyn Fn() -> bool + Send + Sync>,
15}
16
17impl WindowsSandboxCancellationToken {
18    /// Creates a token backed by a cancellation predicate.
19    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    /// Returns whether the caller has requested cancellation.
26    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/// Controls whether a Windows sandbox launch reconciles persistent proxy
39/// firewall settings or preserves the settings established by another launch.
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub enum WindowsSandboxProxySettingsMode {
42    #[default]
43    Reconcile,
44    Preserve,
45}
46
47/// Network settings installed by an administrator during managed Windows sandbox setup.
48#[derive(Clone, Debug, Default, PartialEq, Eq)]
49pub struct WindowsSandboxProvisioningSettings {
50    /// Loopback proxy ports permitted for the offline sandbox identity.
51    pub proxy_ports: Vec<u16>,
52    /// Whether the offline sandbox identity may exchange arbitrary loopback traffic.
53    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        // WRITE_RESTRICTED tokens consult restricting SIDs only for writes, so this
552        // backend cannot make capability-SID deny-read ACLs authoritative.
553        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, &current_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            &current_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            // Close the parent's stdin write end so the child sees EOF immediately.
614            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, &current_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            &current_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                /*exclude_tmpdir_env_var*/ false,
783                /*exclude_slash_tmp*/ 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}