@@ -13,16 +13,18 @@ mod cwd_junction;
1313
1414use anyhow:: Context ;
1515use anyhow:: Result ;
16+ use codex_utils_pty:: JobProcess ;
17+ use codex_utils_pty:: KillOnCloseJob ;
1618use codex_windows_sandbox:: ErrorPayload ;
1719use codex_windows_sandbox:: ErrorStage ;
1820use codex_windows_sandbox:: ExitPayload ;
1921use codex_windows_sandbox:: FramedMessage ;
2022use codex_windows_sandbox:: IPC_PROTOCOL_VERSION ;
23+ use codex_windows_sandbox:: JobPipeSpawnHandles ;
2124use codex_windows_sandbox:: LocalSid ;
2225use codex_windows_sandbox:: Message ;
2326use codex_windows_sandbox:: OutputPayload ;
2427use codex_windows_sandbox:: OutputStream ;
25- use codex_windows_sandbox:: PipeSpawnHandles ;
2628use codex_windows_sandbox:: ResizePayload ;
2729use codex_windows_sandbox:: SpawnReady ;
2830use codex_windows_sandbox:: SpawnRequest ;
@@ -39,7 +41,7 @@ use codex_windows_sandbox::hide_current_user_profile_dir;
3941use codex_windows_sandbox:: log_note;
4042use codex_windows_sandbox:: read_frame;
4143use codex_windows_sandbox:: read_handle_loop;
42- use codex_windows_sandbox:: spawn_process_with_pipes ;
44+ use codex_windows_sandbox:: spawn_job_process_with_pipes ;
4345use codex_windows_sandbox:: to_wide;
4446use codex_windows_sandbox:: token_mode_for_permission_profile;
4547use codex_windows_sandbox:: write_frame;
@@ -62,38 +64,43 @@ use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
6264use windows_sys:: Win32 :: Storage :: FileSystem :: OPEN_EXISTING ;
6365use windows_sys:: Win32 :: System :: Console :: COORD ;
6466use windows_sys:: Win32 :: System :: Console :: ResizePseudoConsole ;
65- use windows_sys:: Win32 :: System :: JobObjects :: AssignProcessToJobObject ;
66- use windows_sys:: Win32 :: System :: JobObjects :: CreateJobObjectW ;
67- use windows_sys:: Win32 :: System :: JobObjects :: JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ;
68- use windows_sys:: Win32 :: System :: JobObjects :: JOBOBJECT_EXTENDED_LIMIT_INFORMATION ;
69- use windows_sys:: Win32 :: System :: JobObjects :: JobObjectExtendedLimitInformation ;
70- use windows_sys:: Win32 :: System :: JobObjects :: SetInformationJobObject ;
7167use windows_sys:: Win32 :: System :: Threading :: GetExitCodeProcess ;
72- use windows_sys:: Win32 :: System :: Threading :: GetProcessId ;
7368use windows_sys:: Win32 :: System :: Threading :: INFINITE ;
7469use windows_sys:: Win32 :: System :: Threading :: MUTEX_ALL_ACCESS ;
7570use windows_sys:: Win32 :: System :: Threading :: OpenMutexW ;
76- use windows_sys:: Win32 :: System :: Threading :: PROCESS_INFORMATION ;
77- use windows_sys:: Win32 :: System :: Threading :: TerminateProcess ;
7871use windows_sys:: Win32 :: System :: Threading :: WaitForSingleObject ;
7972
8073const READ_ACL_MUTEX_NAME : & str = "Local\\ CodexSandboxReadAcl" ;
8174const WAIT_TIMEOUT : u32 = 0x0000_0102 ;
8275
76+ /// Terminates the runner's child job when the parent control transport closes or requests it.
77+ ///
78+ /// Production uses the shared kill-on-close job controller. Tests implement this boundary to
79+ /// verify that transport failure is fail-closed without launching a real sandbox account.
80+ trait RunnerJobController : Send + ' static {
81+ fn terminate_job ( & self ) ;
82+ }
83+
84+ impl RunnerJobController for KillOnCloseJob {
85+ fn terminate_job ( & self ) {
86+ let _ = self . terminate_and_close ( 1 ) ;
87+ }
88+ }
89+
8390struct IpcSpawnedProcess {
8491 log_dir : PathBuf ,
85- pi : PROCESS_INFORMATION ,
92+ process : JobProcess ,
8693 stdout_handle : HANDLE ,
8794 stderr_handle : HANDLE ,
8895 stdin_handle : Option < HANDLE > ,
8996 conpty_owner : Option < codex_windows_sandbox:: ConptyInstance > ,
9097 hpc_handle : Option < HANDLE > ,
91- _pipe_handles : Option < PipeSpawnHandles > ,
98+ _pipe_desktop : Option < codex_windows_sandbox :: LaunchDesktop > ,
9299}
93100
94101/// Small RAII wrapper for raw Win32 handles.
95102///
96- /// The elevated runner has a few early-return paths where we acquire a token, job, or pipe
103+ /// The elevated runner has a few early-return paths where we acquire a token or pipe
97104/// handle and then may fail while preparing the child. Keeping those handles in a guard makes
98105/// the error paths read more directly and closes the gaps that were previously leaking them.
99106struct OwnedWinHandle ( HANDLE ) ;
@@ -126,25 +133,6 @@ impl Drop for OwnedWinHandle {
126133 }
127134}
128135
129- unsafe fn create_job_kill_on_close ( ) -> Result < HANDLE > {
130- let h_job = OwnedWinHandle :: new ( CreateJobObjectW ( std:: ptr:: null_mut ( ) , std:: ptr:: null ( ) ) ) ;
131- if h_job. raw ( ) == 0 {
132- return Err ( anyhow:: anyhow!( "CreateJobObjectW failed" ) ) ;
133- }
134- let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std:: mem:: zeroed ( ) ;
135- limits. BasicLimitInformation . LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ;
136- let ok = SetInformationJobObject (
137- h_job. raw ( ) ,
138- JobObjectExtendedLimitInformation ,
139- & mut limits as * mut _ as * mut _ ,
140- std:: mem:: size_of :: < JOBOBJECT_EXTENDED_LIMIT_INFORMATION > ( ) as u32 ,
141- ) ;
142- if ok == 0 {
143- return Err ( anyhow:: anyhow!( "SetInformationJobObject failed" ) ) ;
144- }
145- Ok ( h_job. into_raw ( ) )
146- }
147-
148136/// Open a named pipe created by the parent process.
149137fn open_pipe ( name : & str , access : u32 ) -> Result < HANDLE > {
150138 let path = to_wide ( name) ;
@@ -298,9 +286,9 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
298286
299287 let mut conpty_owner = None ;
300288 let mut hpc_handle: Option < HANDLE > = None ;
301- let mut pipe_handles = None ;
302- let ( pi , stdout_handle, stderr_handle, stdin_handle) = if req. tty {
303- let ( pi , mut conpty) = codex_windows_sandbox:: spawn_conpty_process_as_user (
289+ let mut pipe_desktop = None ;
290+ let ( process , stdout_handle, stderr_handle, stdin_handle) = if req. tty {
291+ let ( process , mut conpty) = codex_windows_sandbox:: spawn_job_conpty_process_as_user (
304292 h_token. raw ( ) ,
305293 & req. command ,
306294 & effective_cwd,
@@ -321,7 +309,7 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
321309 None
322310 } ;
323311 (
324- pi ,
312+ process ,
325313 output_read,
326314 windows_sys:: Win32 :: Foundation :: INVALID_HANDLE_VALUE ,
327315 stdin_handle,
@@ -332,7 +320,7 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
332320 } else {
333321 StdinMode :: Closed
334322 } ;
335- let spawned_pipes: PipeSpawnHandles = spawn_process_with_pipes (
323+ let spawned_pipes: JobPipeSpawnHandles = spawn_job_process_with_pipes (
336324 h_token. raw ( ) ,
337325 & req. command ,
338326 & effective_cwd,
@@ -342,24 +330,27 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
342330 req. use_private_desktop ,
343331 Some ( log_dir. as_path ( ) ) ,
344332 ) ?;
345- let pi = spawned_pipes. process ;
346- let stdout_handle = spawned_pipes. stdout_read ;
347- let stderr_handle = spawned_pipes
348- . stderr_read
349- . unwrap_or ( windows_sys:: Win32 :: Foundation :: INVALID_HANDLE_VALUE ) ;
350- let stdin_handle = spawned_pipes. stdin_write ;
351- pipe_handles = Some ( spawned_pipes) ;
352- ( pi, stdout_handle, stderr_handle, stdin_handle)
333+ let JobPipeSpawnHandles {
334+ process,
335+ stdin_write : stdin_handle,
336+ stdout_read : stdout_handle,
337+ stderr_read : stderr_handle,
338+ desktop,
339+ } = spawned_pipes;
340+ let stderr_handle =
341+ stderr_handle. unwrap_or ( windows_sys:: Win32 :: Foundation :: INVALID_HANDLE_VALUE ) ;
342+ pipe_desktop = Some ( desktop) ;
343+ ( process, stdout_handle, stderr_handle, stdin_handle)
353344 } ;
354345 Ok ( IpcSpawnedProcess {
355346 log_dir,
356- pi ,
347+ process ,
357348 stdout_handle,
358349 stderr_handle,
359350 stdin_handle,
360351 conpty_owner,
361352 hpc_handle,
362- _pipe_handles : pipe_handles ,
353+ _pipe_desktop : pipe_desktop ,
363354 } )
364355}
365356
@@ -396,16 +387,30 @@ fn spawn_input_loop(
396387 mut reader : File ,
397388 stdin_handle : Option < HANDLE > ,
398389 hpc_handle : Arc < StdMutex < Option < HANDLE > > > ,
399- process_handle : Arc < StdMutex < Option < HANDLE > > > ,
390+ job : impl RunnerJobController ,
400391 log_dir : Option < PathBuf > ,
401392) -> std:: thread:: JoinHandle < ( ) > {
402393 std:: thread:: spawn ( move || {
403394 let mut stdin_handle = stdin_handle;
404395 loop {
405396 let msg = match read_frame ( & mut reader) {
406397 Ok ( Some ( v) ) => v,
407- Ok ( None ) => break ,
408- Err ( _) => break ,
398+ Ok ( None ) => {
399+ log_note (
400+ "runner control pipe closed; terminating child job" ,
401+ log_dir. as_deref ( ) ,
402+ ) ;
403+ job. terminate_job ( ) ;
404+ break ;
405+ }
406+ Err ( err) => {
407+ log_note (
408+ & format ! ( "runner control pipe read failed: {err}; terminating child job" ) ,
409+ log_dir. as_deref ( ) ,
410+ ) ;
411+ job. terminate_job ( ) ;
412+ break ;
413+ }
409414 } ;
410415 match msg. message {
411416 Message :: Stdin { payload } => {
@@ -489,13 +494,7 @@ fn spawn_input_loop(
489494 }
490495 }
491496 Message :: Terminate { .. } => {
492- if let Ok ( guard) = process_handle. lock ( )
493- && let Some ( handle) = guard. as_ref ( )
494- {
495- unsafe {
496- let _ = TerminateProcess ( * handle, 1 ) ;
497- }
498- }
497+ job. terminate_job ( ) ;
499498 }
500499 Message :: SpawnRequest { .. } => { }
501500 Message :: SpawnReady { .. } => { }
@@ -567,27 +566,21 @@ pub fn main() -> Result<()> {
567566 }
568567 } ;
569568 let log_dir = Some ( ipc_spawn. log_dir . as_path ( ) ) ;
570- let pi = ipc_spawn. pi ;
569+ let process = ipc_spawn. process ;
570+ let process_handle = process. as_raw_handle ( ) as HANDLE ;
571+ let job = process. controller ( ) ;
571572 let stdout_handle = ipc_spawn. stdout_handle ;
572573 let stderr_handle = ipc_spawn. stderr_handle ;
573574 let mut conpty_owner = ipc_spawn. conpty_owner ;
575+ let pipe_desktop = ipc_spawn. _pipe_desktop ;
574576 let stdin_handle = ipc_spawn. stdin_handle ;
575577 let hpc_handle = Arc :: new ( StdMutex :: new ( ipc_spawn. hpc_handle ) ) ;
576578
577- let h_job = unsafe { create_job_kill_on_close ( ) . ok ( ) } ;
578- if let Some ( job) = h_job {
579- unsafe {
580- let _ = AssignProcessToJobObject ( job, pi. hProcess ) ;
581- }
582- }
583-
584- let process_handle = Arc :: new ( StdMutex :: new ( Some ( pi. hProcess ) ) ) ;
585-
586579 let msg = FramedMessage {
587580 version : IPC_PROTOCOL_VERSION ,
588581 message : Message :: SpawnReady {
589582 payload : SpawnReady {
590- process_id : unsafe { GetProcessId ( pi . hProcess ) } ,
583+ process_id : process . process_id ( ) ,
591584 } ,
592585 } ,
593586 } ;
@@ -626,39 +619,34 @@ pub fn main() -> Result<()> {
626619 pipe_read,
627620 stdin_handle,
628621 Arc :: clone ( & hpc_handle) ,
629- Arc :: clone ( & process_handle ) ,
622+ job . clone ( ) ,
630623 log_dir_owned,
631624 ) ;
632625
633626 let timeout = req. timeout_ms . map ( |ms| ms as u32 ) . unwrap_or ( INFINITE ) ;
634- let wait_res = unsafe { WaitForSingleObject ( pi . hProcess , timeout) } ;
627+ let wait_res = unsafe { WaitForSingleObject ( process_handle , timeout) } ;
635628 let timed_out = wait_res == WAIT_TIMEOUT ;
636629
637630 let exit_code: i32 ;
638631 unsafe {
639632 if timed_out {
640- let _ = TerminateProcess ( pi . hProcess , 1 ) ;
633+ let _ = job . terminate_and_close ( 1 ) ;
641634 exit_code = 128 + 64 ;
642635 } else {
643636 let mut raw_exit: u32 = 1 ;
644- GetExitCodeProcess ( pi . hProcess , & mut raw_exit) ;
637+ GetExitCodeProcess ( process_handle , & mut raw_exit) ;
645638 exit_code = raw_exit as i32 ;
646- }
647- if pi. hThread != 0 {
648- CloseHandle ( pi. hThread ) ;
649- }
650- if pi. hProcess != 0 {
651- CloseHandle ( pi. hProcess ) ;
652- }
653- if let Some ( job) = h_job {
654- CloseHandle ( job) ;
639+ // Root exit ends the session. Closing the job before joining readers ensures any
640+ // surviving descendants are stopped and release inherited output handles.
641+ let _ = job. close ( ) ;
655642 }
656643 }
657644
658645 if let Ok ( mut guard) = hpc_handle. lock ( ) {
659646 let _ = guard. take ( ) ;
660647 }
661648 drop ( conpty_owner. take ( ) ) ;
649+ drop ( pipe_desktop) ;
662650
663651 let _ = out_thread. join ( ) ;
664652 if let Some ( thread) = err_thread {
@@ -682,3 +670,7 @@ pub fn main() -> Result<()> {
682670
683671 std:: process:: exit ( exit_code) ;
684672}
673+
674+ #[ cfg( test) ]
675+ #[ path = "win_tests.rs" ]
676+ mod tests;
0 commit comments