Codex Handbook
windows-sandbox-rs/src/unified_exec/backends/legacy.rs 443 lines
use super::windows_common::finish_driver_spawn;use super::windows_common::normalize_windows_tty_input;use crate::conpty::ConptyInstance;use crate::conpty::spawn_conpty_process_as_user;use crate::desktop::LaunchDesktop;use crate::logging::log_failure;use crate::logging::log_success;use crate::process::StderrMode;use crate::process::StdinMode;use crate::process::read_handle_loop;use crate::process::spawn_process_with_pipes;use crate::spawn_prep::LegacyAclSids;use crate::spawn_prep::SpawnPrepOptions;use crate::spawn_prep::allow_null_device_for_workspace_write;use crate::spawn_prep::apply_legacy_session_acl_rules;use crate::spawn_prep::legacy_session_capability_roots;use crate::spawn_prep::prepare_legacy_session_security;use crate::spawn_prep::prepare_legacy_spawn_context;use anyhow::Result;use codex_protocol::models::PermissionProfile;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_pty::ProcessDriver;use codex_utils_pty::SpawnedProcess;use codex_utils_pty::TerminalSize;use std::collections::HashMap;use std::path::Path;use std::ptr;use std::sync::Arc;use std::sync::Mutex as StdMutex;use tokio::sync::broadcast;use tokio::sync::mpsc;use tokio::sync::oneshot;use windows_sys::Win32::Foundation::CloseHandle;use windows_sys::Win32::Foundation::GetLastError;use windows_sys::Win32::Foundation::HANDLE;use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;use windows_sys::Win32::Storage::FileSystem::WriteFile;use windows_sys::Win32::System::Console::COORD;use windows_sys::Win32::System::Console::ResizePseudoConsole;use windows_sys::Win32::System::Threading::GetExitCodeProcess;use windows_sys::Win32::System::Threading::INFINITE;use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;use windows_sys::Win32::System::Threading::TerminateProcess;use windows_sys::Win32::System::Threading::WaitForSingleObject;const WAIT_TIMEOUT: u32 = 0x0000_0102;struct LegacyProcessHandles {    process: PROCESS_INFORMATION,    output_join: std::thread::JoinHandle<()>,    writer_handle: tokio::task::JoinHandle<()>,    hpc: Option<HANDLE>,    conpty_owner: Option<ConptyInstance>,    token_handle: HANDLE,    desktop: Option<LaunchDesktop>,}#[allow(clippy::too_many_arguments)]fn spawn_legacy_process(    h_token: HANDLE,    command: &[String],    cwd: &Path,    env_map: &HashMap<String, String>,    use_private_desktop: bool,    tty: bool,    stdin_open: bool,    stdout_tx: broadcast::Sender<Vec<u8>>,    stderr_tx: Option<broadcast::Sender<Vec<u8>>>,    writer_rx: mpsc::Receiver<Vec<u8>>,    logs_base_dir: Option<&Path>,) -> Result<LegacyProcessHandles> {    let (pi, output_join, writer_handle, hpc, conpty_owner, desktop) = if tty {        let (pi, mut conpty) = spawn_conpty_process_as_user(            h_token,            command,            cwd,            env_map,            use_private_desktop,            logs_base_dir,        )?;        let hpc = conpty.raw_handle();        let output_join = spawn_output_reader(conpty.take_output_read(), stdout_tx);        let writer_handle = spawn_input_writer(            Some(conpty.take_input_write()),            writer_rx,            /*normalize_newlines*/ true,        );        (pi, output_join, writer_handle, hpc, Some(conpty), None)    } else {        let pipe_handles = spawn_process_with_pipes(            h_token,            command,            cwd,            env_map,            if stdin_open {                StdinMode::Open            } else {                StdinMode::Closed            },            StderrMode::Separate,            use_private_desktop,            logs_base_dir,        )?;        let stdout_join = spawn_output_reader(pipe_handles.stdout_read, stdout_tx);        let Some(stderr_read) = pipe_handles.stderr_read else {            anyhow::bail!("separate stderr handle should be present");        };        let Some(stderr_tx) = stderr_tx else {            anyhow::bail!("separate stderr channel should be present");        };        let stderr_join = spawn_output_reader(stderr_read, stderr_tx);        let output_join = std::thread::spawn(move || {            let _ = stdout_join.join();            let _ = stderr_join.join();        });        let writer_handle = spawn_input_writer(            pipe_handles.stdin_write,            writer_rx,            /*normalize_newlines*/ false,        );        (            pipe_handles.process,            output_join,            writer_handle,            None,            None,            Some(pipe_handles.desktop),        )    };    Ok(LegacyProcessHandles {        process: pi,        output_join,        writer_handle,        hpc,        conpty_owner,        token_handle: h_token,        desktop,    })}fn spawn_output_reader(    output_read: HANDLE,    output_tx: broadcast::Sender<Vec<u8>>,) -> std::thread::JoinHandle<()> {    read_handle_loop(output_read, move |chunk| {        let _ = output_tx.send(chunk.to_vec());    })}fn spawn_input_writer(    input_write: Option<HANDLE>,    mut writer_rx: mpsc::Receiver<Vec<u8>>,    normalize_newlines: bool,) -> tokio::task::JoinHandle<()> {    tokio::task::spawn_blocking(move || {        let mut previous_was_cr = false;        while let Some(bytes) = writer_rx.blocking_recv() {            let Some(handle) = input_write else {                continue;            };            let bytes = if normalize_newlines {                normalize_windows_tty_input(&bytes, &mut previous_was_cr)            } else {                bytes            };            if write_all_handle(handle, &bytes).is_err() {                break;            }        }        if let Some(handle) = input_write {            unsafe {                CloseHandle(handle);            }        }    })}fn write_all_handle(handle: HANDLE, mut bytes: &[u8]) -> Result<()> {    while !bytes.is_empty() {        let mut written = 0u32;        let ok = unsafe {            WriteFile(                handle,                bytes.as_ptr() as *const _,                bytes.len() as u32,                &mut written,                ptr::null_mut(),            )        };        if ok == 0 {            let err = unsafe { GetLastError() } as i32;            return Err(anyhow::anyhow!("WriteFile failed: {err}"));        }        if written == 0 {            anyhow::bail!("WriteFile returned success but wrote 0 bytes");        }        bytes = &bytes[written as usize..];    }    Ok(())}#[allow(clippy::too_many_arguments)]fn finalize_exit(    exit_tx: oneshot::Sender<i32>,    process_handle: Arc<StdMutex<Option<HANDLE>>>,    thread_handle: HANDLE,    output_join: std::thread::JoinHandle<()>,    logs_base_dir: Option<&Path>,    command: Vec<String>,) {    let exit_code = {        let mut raw_exit = 1u32;        if let Ok(guard) = process_handle.lock()            && let Some(handle) = guard.as_ref()        {            unsafe {                WaitForSingleObject(*handle, INFINITE);                GetExitCodeProcess(*handle, &mut raw_exit);            }        }        raw_exit as i32    };    let _ = output_join.join();    let _ = exit_tx.send(exit_code);    unsafe {        if thread_handle != 0 && thread_handle != INVALID_HANDLE_VALUE {            CloseHandle(thread_handle);        }        if let Ok(mut guard) = process_handle.lock()            && let Some(handle) = guard.take()        {            CloseHandle(handle);        }    }    if exit_code == 0 {        log_success(&command, logs_base_dir);    } else {        log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir);    }}fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize) -> Result<()> {    let guard = hpc        .lock()        .map_err(|_| anyhow::anyhow!("failed to lock ConPTY handle"))?;    let hpc = guard        .as_ref()        .copied()        .ok_or_else(|| anyhow::anyhow!("process is not attached to a PTY"))?;    let result = unsafe {        ResizePseudoConsole(            hpc,            COORD {                X: size.cols as i16,                Y: size.rows as i16,            },        )    };    if result == 0 {        Ok(())    } else {        Err(anyhow::anyhow!(            "failed to resize console: HRESULT {result}"        ))    }}#[allow(clippy::too_many_arguments)]pub(crate) async fn spawn_windows_sandbox_session_legacy(    permission_profile: &PermissionProfile,    workspace_roots: &[AbsolutePathBuf],    codex_home: &Path,    command: Vec<String>,    cwd: &Path,    mut env_map: HashMap<String, String>,    timeout_ms: Option<u64>,    additional_deny_read_paths: &[AbsolutePathBuf],    additional_deny_write_paths: &[AbsolutePathBuf],    tty: bool,    stdin_open: bool,    use_private_desktop: bool,) -> Result<SpawnedProcess> {    let common = prepare_legacy_spawn_context(        permission_profile,        workspace_roots,        codex_home,        cwd,        &mut env_map,        &command,        SpawnPrepOptions {            inherit_path: false,            add_git_safe_directory: false,        },    )?;    if !common.permissions.has_full_disk_read_access() {        anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");    }    // WRITE_RESTRICTED tokens consult restricting SIDs only for writes, so this    // backend cannot make capability-SID deny-read ACLs authoritative.    if !additional_deny_read_paths.is_empty() {        anyhow::bail!("deny-read overrides require the elevated Windows sandbox backend");    }    let additional_deny_write_paths = additional_deny_write_paths        .iter()        .map(AbsolutePathBuf::to_path_buf)        .collect::<Vec<_>>();    let capability_roots = legacy_session_capability_roots(        &common.permissions,        &common.current_dir,        &env_map,        codex_home,    );    let security = prepare_legacy_session_security(        common.uses_write_capabilities,        codex_home,        cwd,        capability_roots,    )?;    allow_null_device_for_workspace_write(common.uses_write_capabilities);    apply_legacy_session_acl_rules(        &common.permissions,        codex_home,        &common.current_dir,        &env_map,        &[],        &additional_deny_write_paths,        LegacyAclSids {            readonly_sid: security.readonly_sid.as_ref(),            readonly_sid_str: security.readonly_sid_str.as_deref(),            write_root_sids: &security.write_root_sids,        },    )?;    let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);    let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);    let stderr_rx = if tty {        None    } else {        Some(broadcast::channel::<Vec<u8>>(256))    };    let (exit_tx, exit_rx) = oneshot::channel::<i32>();    let LegacyProcessHandles {        process: pi,        output_join,        writer_handle,        hpc,        mut conpty_owner,        token_handle,        desktop,    } = match spawn_legacy_process(        security.h_token,        &command,        cwd,        &env_map,        use_private_desktop,        tty,        stdin_open,        stdout_tx,        stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()),        writer_rx,        common.logs_base_dir.as_deref(),    ) {        Ok(handles) => handles,        Err(err) => {            unsafe {                CloseHandle(security.h_token);            }            return Err(err);        }    };    let hpc_handle = hpc.map(|hpc| Arc::new(StdMutex::new(Some(hpc))));    let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));    let wait_handle = Arc::clone(&process_handle);    let command_for_wait = command.clone();    let hpc_for_wait = hpc_handle.clone();    std::thread::spawn(move || {        let _desktop = desktop;        let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);        let wait_res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };        if wait_res == WAIT_TIMEOUT {            unsafe {                if let Ok(guard) = wait_handle.lock()                    && let Some(handle) = guard.as_ref()                {                    let _ = TerminateProcess(*handle, 1);                }            }        }        if let Some(hpc) = hpc_for_wait            && let Ok(mut guard) = hpc.lock()        {            let _ = guard.take();        }        drop(conpty_owner.take());        unsafe {            if token_handle != 0 && token_handle != INVALID_HANDLE_VALUE {                CloseHandle(token_handle);            }        }        finalize_exit(            exit_tx,            wait_handle,            pi.hThread,            output_join,            common.logs_base_dir.as_deref(),            command_for_wait,        );    });    let terminator = {        let process_handle = Arc::clone(&process_handle);        Some(Box::new(move || {            if let Ok(guard) = process_handle.lock()                && let Some(handle) = guard.as_ref()            {                unsafe {                    let _ = TerminateProcess(*handle, 1);                }            }        }) as Box<dyn FnMut() + Send + Sync>)    };    let driver = ProcessDriver {        writer_tx,        stdout_rx,        stderr_rx: stderr_rx.map(|(_tx, rx)| rx),        exit_rx,        terminator,        writer_handle: Some(writer_handle),        resizer: hpc_handle.map(|hpc| {            Box::new(move |size| resize_conpty_handle(&hpc, size))                as Box<dyn FnMut(TerminalSize) -> Result<()> + Send>        }),    };    Ok(finish_driver_spawn(driver, stdin_open))}