@@ -16,6 +16,7 @@ use codex_windows_sandbox::ErrorPayload;
1616use codex_windows_sandbox:: ExitPayload ;
1717use codex_windows_sandbox:: FramedMessage ;
1818use codex_windows_sandbox:: LaunchDesktop ;
19+ use codex_windows_sandbox:: LocalSid ;
1920use codex_windows_sandbox:: Message ;
2021use codex_windows_sandbox:: OutputPayload ;
2122use codex_windows_sandbox:: OutputStream ;
@@ -27,7 +28,6 @@ use codex_windows_sandbox::SpawnRequest;
2728use codex_windows_sandbox:: StderrMode ;
2829use codex_windows_sandbox:: StdinMode ;
2930use codex_windows_sandbox:: allow_null_device;
30- use codex_windows_sandbox:: convert_string_sid_to_sid;
3131use codex_windows_sandbox:: create_readonly_token_with_caps_from;
3232use codex_windows_sandbox:: create_workspace_write_token_with_caps_from;
3333use codex_windows_sandbox:: decode_bytes;
@@ -41,7 +41,6 @@ use codex_windows_sandbox::read_handle_loop;
4141use codex_windows_sandbox:: spawn_process_with_pipes;
4242use codex_windows_sandbox:: to_wide;
4343use codex_windows_sandbox:: write_frame;
44- use std:: ffi:: c_void;
4544use std:: fs:: File ;
4645use std:: os:: windows:: io:: FromRawHandle ;
4746use std:: path:: Path ;
@@ -52,8 +51,7 @@ use std::sync::Mutex as StdMutex;
5251use windows_sys:: Win32 :: Foundation :: CloseHandle ;
5352use windows_sys:: Win32 :: Foundation :: GetLastError ;
5453use windows_sys:: Win32 :: Foundation :: HANDLE ;
55- use windows_sys:: Win32 :: Foundation :: HLOCAL ;
56- use windows_sys:: Win32 :: Foundation :: LocalFree ;
54+ use windows_sys:: Win32 :: Foundation :: INVALID_HANDLE_VALUE ;
5755use windows_sys:: Win32 :: Storage :: FileSystem :: CreateFileW ;
5856use windows_sys:: Win32 :: Storage :: FileSystem :: FILE_GENERIC_READ ;
5957use windows_sys:: Win32 :: Storage :: FileSystem :: FILE_GENERIC_WRITE ;
@@ -94,23 +92,58 @@ struct IpcSpawnedProcess {
9492 _pipe_handles : Option < PipeSpawnHandles > ,
9593}
9694
95+ /// Small RAII wrapper for raw Win32 handles.
96+ ///
97+ /// The elevated runner has a few early-return paths where we acquire a token, job, or pipe
98+ /// handle and then may fail while preparing the child. Keeping those handles in a guard makes
99+ /// the error paths read more directly and closes the gaps that were previously leaking them.
100+ struct OwnedWinHandle ( HANDLE ) ;
101+
102+ impl OwnedWinHandle {
103+ fn new ( handle : HANDLE ) -> Self {
104+ Self ( handle)
105+ }
106+
107+ fn raw ( & self ) -> HANDLE {
108+ self . 0
109+ }
110+
111+ fn into_raw ( mut self ) -> HANDLE {
112+ // Transfer ownership to the caller. After this point the caller is responsible for
113+ // eventually closing the returned HANDLE.
114+ let handle = self . 0 ;
115+ self . 0 = 0 ;
116+ handle
117+ }
118+ }
119+
120+ impl Drop for OwnedWinHandle {
121+ fn drop ( & mut self ) {
122+ if self . 0 != 0 && self . 0 != INVALID_HANDLE_VALUE {
123+ unsafe {
124+ CloseHandle ( self . 0 ) ;
125+ }
126+ }
127+ }
128+ }
129+
97130unsafe fn create_job_kill_on_close ( ) -> Result < HANDLE > {
98- let h = CreateJobObjectW ( std:: ptr:: null_mut ( ) , std:: ptr:: null ( ) ) ;
99- if h == 0 {
131+ let h_job = OwnedWinHandle :: new ( CreateJobObjectW ( std:: ptr:: null_mut ( ) , std:: ptr:: null ( ) ) ) ;
132+ if h_job . raw ( ) == 0 {
100133 return Err ( anyhow:: anyhow!( "CreateJobObjectW failed" ) ) ;
101134 }
102135 let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std:: mem:: zeroed ( ) ;
103136 limits. BasicLimitInformation . LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ;
104137 let ok = SetInformationJobObject (
105- h ,
138+ h_job . raw ( ) ,
106139 JobObjectExtendedLimitInformation ,
107140 & mut limits as * mut _ as * mut _ ,
108141 std:: mem:: size_of :: < JOBOBJECT_EXTENDED_LIMIT_INFORMATION > ( ) as u32 ,
109142 ) ;
110143 if ok == 0 {
111144 return Err ( anyhow:: anyhow!( "SetInformationJobObject failed" ) ) ;
112145 }
113- Ok ( h )
146+ Ok ( h_job . into_raw ( ) )
114147}
115148
116149/// Open a named pipe created by the parent process.
@@ -190,45 +223,42 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
190223 let log_dir = req. codex_home . clone ( ) ;
191224 hide_current_user_profile_dir ( req. codex_home . as_path ( ) ) ;
192225 let policy = parse_policy ( & req. policy_json_or_preset ) . context ( "parse policy_json_or_preset" ) ?;
193- let mut cap_psids: Vec < * mut c_void > = Vec :: new ( ) ;
226+ let mut cap_psids: Vec < LocalSid > = Vec :: new ( ) ;
194227 for sid in & req. cap_sids {
195- let Some ( psid ) = ( unsafe { convert_string_sid_to_sid ( sid ) } ) else {
196- anyhow :: bail! ( "ConvertStringSidToSidW failed for capability SID" ) ;
197- } ;
198- cap_psids . push ( psid ) ;
228+ cap_psids . push (
229+ LocalSid :: from_string ( sid )
230+ . context ( "ConvertStringSidToSidW failed for capability SID" ) ? ,
231+ ) ;
199232 }
200233 if cap_psids. is_empty ( ) {
201234 anyhow:: bail!( "runner: empty capability SID list" ) ;
202235 }
203236
204- let base = unsafe { get_current_token_for_restriction ( ) ? } ;
205- let token_res: Result < ( HANDLE , * mut c_void ) > = unsafe {
237+ // The token helpers still take raw SID pointers, but we keep ownership in `LocalSid`
238+ // wrappers for as long as possible. That way any failure after SID parsing but before the
239+ // child is fully spawned still releases the backing LocalAlloc memory automatically.
240+ let cap_psid_ptrs: Vec < * mut _ > = cap_psids. iter ( ) . map ( LocalSid :: as_ptr) . collect ( ) ;
241+ let base = OwnedWinHandle :: new ( unsafe { get_current_token_for_restriction ( ) ? } ) ;
242+ let h_token = OwnedWinHandle :: new ( unsafe {
206243 match & policy {
207244 SandboxPolicy :: ReadOnly { .. } => {
208- create_readonly_token_with_caps_from ( base, & cap_psids)
209- . map ( |h_token| ( h_token, cap_psids[ 0 ] ) )
245+ create_readonly_token_with_caps_from ( base. raw ( ) , & cap_psid_ptrs)
210246 }
211247 SandboxPolicy :: WorkspaceWrite { .. } => {
212- create_workspace_write_token_with_caps_from ( base, & cap_psids)
213- . map ( |h_token| ( h_token, cap_psids[ 0 ] ) )
248+ create_workspace_write_token_with_caps_from ( base. raw ( ) , & cap_psid_ptrs)
214249 }
215250 SandboxPolicy :: DangerFullAccess | SandboxPolicy :: ExternalSandbox { .. } => {
216251 unreachable ! ( )
217252 }
218253 }
219- } ;
220- let ( h_token, psid_to_use) = token_res?;
254+ } ?) ;
221255 unsafe {
222- CloseHandle ( base) ;
223- allow_null_device ( psid_to_use) ;
224- for psid in & cap_psids {
256+ // These ACL adjustments need the raw SID values, but ownership stays with `cap_psids`.
257+ // We do not manually `LocalFree` anything here; the wrappers handle every return path.
258+ allow_null_device ( cap_psid_ptrs[ 0 ] ) ;
259+ for psid in & cap_psid_ptrs {
225260 allow_null_device ( * psid) ;
226261 }
227- for psid in cap_psids {
228- if !psid. is_null ( ) {
229- LocalFree ( psid as HLOCAL ) ;
230- }
231- }
232262 }
233263
234264 let effective_cwd = effective_cwd ( & req. cwd , Some ( log_dir. as_path ( ) ) ) ;
@@ -238,7 +268,7 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
238268 let mut pipe_handles = None ;
239269 let ( pi, stdout_handle, stderr_handle, stdin_handle) = if req. tty {
240270 let ( pi, conpty) = codex_windows_sandbox:: spawn_conpty_process_as_user (
241- h_token,
271+ h_token. raw ( ) ,
242272 & req. command ,
243273 & effective_cwd,
244274 & req. env ,
@@ -269,7 +299,7 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
269299 StdinMode :: Closed
270300 } ;
271301 let spawned_pipes: PipeSpawnHandles = spawn_process_with_pipes (
272- h_token,
302+ h_token. raw ( ) ,
273303 & req. command ,
274304 & effective_cwd,
275305 & req. env ,
@@ -287,10 +317,6 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
287317 pipe_handles = Some ( spawned_pipes) ;
288318 ( pi, stdout_handle, stderr_handle, stdin_handle)
289319 } ;
290-
291- unsafe {
292- CloseHandle ( h_token) ;
293- }
294320 Ok ( IpcSpawnedProcess {
295321 log_dir,
296322 pi,
@@ -337,7 +363,7 @@ fn spawn_input_loop(
337363 stdin_handle : Option < HANDLE > ,
338364 hpc_handle : Arc < StdMutex < Option < HANDLE > > > ,
339365 process_handle : Arc < StdMutex < Option < HANDLE > > > ,
340- _log_dir : Option < PathBuf > ,
366+ log_dir : Option < PathBuf > ,
341367) -> std:: thread:: JoinHandle < ( ) > {
342368 std:: thread:: spawn ( move || {
343369 let mut stdin_handle = stdin_handle;
@@ -353,15 +379,54 @@ fn spawn_input_loop(
353379 continue ;
354380 } ;
355381 if let Some ( handle) = stdin_handle {
356- let mut written: u32 = 0 ;
357- unsafe {
358- let _ = windows_sys:: Win32 :: Storage :: FileSystem :: WriteFile (
359- handle,
360- bytes. as_ptr ( ) ,
361- bytes. len ( ) as u32 ,
362- & mut written,
363- ptr:: null_mut ( ) ,
364- ) ;
382+ let mut offset = 0usize ;
383+ // `WriteFile` can report success after consuming only part of the buffer
384+ // when the target is a pipe. Treat this like a normal partial write and
385+ // keep advancing until every decoded stdin byte has been forwarded.
386+ //
387+ // If the child closes stdin or the pipe enters an error state, we log
388+ // that fact, close our local HANDLE, and stop trying to forward later
389+ // `Stdin` frames. That prevents silent truncation while also avoiding an
390+ // endless stream of failing writes after the child is already gone.
391+ while offset < bytes. len ( ) {
392+ let chunk = & bytes[ offset..] ;
393+ let chunk_len = chunk. len ( ) . min ( u32:: MAX as usize ) ;
394+ let mut written = 0u32 ;
395+ let ok = unsafe {
396+ windows_sys:: Win32 :: Storage :: FileSystem :: WriteFile (
397+ handle,
398+ chunk. as_ptr ( ) ,
399+ chunk_len as u32 ,
400+ & mut written,
401+ ptr:: null_mut ( ) ,
402+ )
403+ } ;
404+ if ok == 0 {
405+ log_note (
406+ & format ! (
407+ "runner stdin write failed after {offset} bytes: {}" ,
408+ unsafe { GetLastError ( ) }
409+ ) ,
410+ log_dir. as_deref ( ) ,
411+ ) ;
412+ unsafe {
413+ CloseHandle ( handle) ;
414+ }
415+ stdin_handle = None ;
416+ break ;
417+ }
418+ if written == 0 {
419+ log_note (
420+ "runner stdin write made no progress; closing child stdin" ,
421+ log_dir. as_deref ( ) ,
422+ ) ;
423+ unsafe {
424+ CloseHandle ( handle) ;
425+ }
426+ stdin_handle = None ;
427+ break ;
428+ }
429+ offset += written as usize ;
365430 }
366431 }
367432 }
@@ -432,11 +497,14 @@ pub fn main() -> Result<()> {
432497 anyhow:: bail!( "runner: no pipe-out provided" ) ;
433498 } ;
434499
435- let h_pipe_in = open_pipe ( & pipe_in, FILE_GENERIC_READ ) ?;
436- let h_pipe_out = open_pipe ( & pipe_out, FILE_GENERIC_WRITE ) ?;
437- let mut pipe_read = unsafe { File :: from_raw_handle ( h_pipe_in as _ ) } ;
500+ // Open both pipe ends under guards first so a failure on the second open cannot leak the
501+ // first HANDLE. Only after both opens succeed do we transfer ownership into `File`, which
502+ // then becomes responsible for closing them.
503+ let h_pipe_in = OwnedWinHandle :: new ( open_pipe ( & pipe_in, FILE_GENERIC_READ ) ?) ;
504+ let h_pipe_out = OwnedWinHandle :: new ( open_pipe ( & pipe_out, FILE_GENERIC_WRITE ) ?) ;
505+ let mut pipe_read = unsafe { File :: from_raw_handle ( h_pipe_in. into_raw ( ) as _ ) } ;
438506 let pipe_write = Arc :: new ( StdMutex :: new ( unsafe {
439- File :: from_raw_handle ( h_pipe_out as _ )
507+ File :: from_raw_handle ( h_pipe_out. into_raw ( ) as _ )
440508 } ) ) ;
441509
442510 let req = match read_spawn_request ( & mut pipe_read) {
0 commit comments