@@ -17,6 +17,8 @@ use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};
1717use std:: thread:: { self , JoinHandle } ;
1818use std:: time:: { Duration , Instant } ;
1919
20+ use crate :: confined_fs:: ConfinedDirectory ;
21+
2022#[ cfg( windows) ]
2123use super :: windows_process_tree:: ProcessJob ;
2224#[ cfg( unix) ]
@@ -97,6 +99,10 @@ pub struct BoundedProcessRequest {
9799 pub argv : Vec < String > ,
98100 /// Optional explicit working directory. Must be absolute when present.
99101 pub cwd : Option < PathBuf > ,
102+ /// Optional retained confined directory used as the child cwd.
103+ ///
104+ /// Mutually exclusive with [`Self::cwd`] and [`Self::workspace_root`].
105+ pub confined_cwd : Option < ConfinedDirectory > ,
100106 /// Workspace root used as the child cwd when `cwd` is omitted.
101107 pub workspace_root : Option < PathBuf > ,
102108 /// Explicit environment entries. They are allowlisted; inheritance is
@@ -129,6 +135,7 @@ impl fmt::Debug for BoundedProcessRequest {
129135 . debug_struct ( "BoundedProcessRequest" )
130136 . field ( "argv_count" , & self . argv . len ( ) )
131137 . field ( "cwd_present" , & self . cwd . is_some ( ) )
138+ . field ( "confined_cwd_present" , & self . confined_cwd . is_some ( ) )
132139 . field ( "workspace_root_present" , & self . workspace_root . is_some ( ) )
133140 . field ( "env_count" , & self . env . len ( ) )
134141 . field ( "inherit_env" , & self . inherit_env )
@@ -183,6 +190,7 @@ impl BoundedProcessRequest {
183190 Self {
184191 argv,
185192 cwd : None ,
193+ confined_cwd : None ,
186194 workspace_root : None ,
187195 env : BTreeMap :: new ( ) ,
188196 inherit_env : false ,
@@ -201,6 +209,11 @@ impl BoundedProcessRequest {
201209 self
202210 }
203211
212+ pub fn with_confined_cwd ( mut self , cwd : ConfinedDirectory ) -> Self {
213+ self . confined_cwd = Some ( cwd) ;
214+ self
215+ }
216+
204217 pub fn with_workspace_root ( mut self , root : impl Into < PathBuf > ) -> Self {
205218 self . workspace_root = Some ( root. into ( ) ) ;
206219 self
@@ -262,22 +275,32 @@ impl BoundedProcessRequest {
262275 pub fn validate ( & self ) -> Result < ( ) , ValidationError > {
263276 validate_argv ( & self . argv ) ?;
264277
265- if let Some ( cwd) = self . resolved_cwd ( ) {
266- let cwd_len = os_string_len ( cwd. as_os_str ( ) ) ;
267- if cwd_len == 0 {
268- return Err ( ValidationError :: EmptyCwd ) ;
269- }
270- if cwd_len > MAX_ARG_TOTAL_BYTES {
271- return Err ( ValidationError :: CwdTooLong ) ;
272- }
273- if os_string_has_nul ( cwd. as_os_str ( ) ) {
274- return Err ( ValidationError :: CwdContainsNul ) ;
275- }
276- if !cwd. is_absolute ( ) {
277- return Err ( ValidationError :: CwdNotAbsolute ) ;
278+ let has_path_cwd = self . cwd . is_some ( ) || self . workspace_root . is_some ( ) ;
279+ if self . confined_cwd . is_some ( ) && has_path_cwd {
280+ return Err ( ValidationError :: ConflictingCwd ) ;
281+ }
282+ if self . confined_cwd . is_none ( ) {
283+ if let Some ( cwd) = self . resolved_cwd ( ) {
284+ let cwd_len = os_string_len ( cwd. as_os_str ( ) ) ;
285+ if cwd_len == 0 {
286+ return Err ( ValidationError :: EmptyCwd ) ;
287+ }
288+ if cwd_len > MAX_ARG_TOTAL_BYTES {
289+ return Err ( ValidationError :: CwdTooLong ) ;
290+ }
291+ if os_string_has_nul ( cwd. as_os_str ( ) ) {
292+ return Err ( ValidationError :: CwdContainsNul ) ;
293+ }
294+ if !cwd. is_absolute ( ) {
295+ return Err ( ValidationError :: CwdNotAbsolute ) ;
296+ }
297+ } else {
298+ return Err ( ValidationError :: CwdRequired ) ;
278299 }
279- } else {
280- return Err ( ValidationError :: CwdRequired ) ;
300+ }
301+ #[ cfg( not( unix) ) ]
302+ if self . confined_cwd . is_some ( ) {
303+ return Err ( ValidationError :: ConfinedCwdUnsupported ) ;
281304 }
282305
283306 if self . inherit_env {
@@ -427,6 +450,8 @@ pub enum ValidationError {
427450 CwdNotAbsolute ,
428451 CwdTooLong ,
429452 CwdContainsNul ,
453+ ConflictingCwd ,
454+ ConfinedCwdUnsupported ,
430455 EnvCountExceeded ,
431456 InvalidEnvKey ,
432457 EnvKeyTooLong ,
@@ -460,6 +485,8 @@ impl fmt::Display for ValidationError {
460485 Self :: CwdNotAbsolute => "cwd must be an absolute path" ,
461486 Self :: CwdTooLong => "cwd exceeds the configured bound" ,
462487 Self :: CwdContainsNul => "cwd contains a NUL byte" ,
488+ Self :: ConflictingCwd => "cwd path and confined cwd cannot both be specified" ,
489+ Self :: ConfinedCwdUnsupported => "confined cwd is unavailable on this target" ,
463490 Self :: EnvCountExceeded => "environment entry count exceeds the configured bound" ,
464491 Self :: InvalidEnvKey => "environment key has invalid grammar" ,
465492 Self :: EnvKeyTooLong => "environment key exceeds the configured bound" ,
@@ -1981,15 +2008,44 @@ impl BoundedProcess {
19812008 return Err ( BoundedProcessError :: Cancelled ) ;
19822009 }
19832010
1984- let cwd = request
1985- . resolved_cwd ( )
1986- . cloned ( )
1987- . ok_or ( BoundedProcessError :: InvalidRequest (
1988- ValidationError :: CwdRequired ,
1989- ) ) ?;
2011+ let confined_cwd = request. confined_cwd . clone ( ) ;
19902012 let mut command = std:: process:: Command :: new ( & request. argv [ 0 ] ) ;
19912013 command. args ( & request. argv [ 1 ..] ) ;
1992- command. current_dir ( cwd) ;
2014+ if let Some ( directory) = & confined_cwd {
2015+ #[ cfg( unix) ]
2016+ {
2017+ let fd = directory. as_raw_fd ( ) ;
2018+ // SAFETY: `fchdir` is async-signal-safe. The retained directory
2019+ // descriptor stays alive in `confined_cwd` until `spawn` returns,
2020+ // and close-on-exec remains set so the child does not inherit it
2021+ // across `exec`.
2022+ unsafe {
2023+ command. pre_exec ( move || {
2024+ if libc:: fchdir ( fd) != 0 {
2025+ Err ( std:: io:: Error :: last_os_error ( ) )
2026+ } else {
2027+ Ok ( ( ) )
2028+ }
2029+ } ) ;
2030+ }
2031+ }
2032+ #[ cfg( not( unix) ) ]
2033+ {
2034+ let _ = directory;
2035+ return Err ( BoundedProcessError :: InvalidRequest (
2036+ ValidationError :: ConfinedCwdUnsupported ,
2037+ ) ) ;
2038+ }
2039+ } else {
2040+ let cwd =
2041+ request
2042+ . resolved_cwd ( )
2043+ . cloned ( )
2044+ . ok_or ( BoundedProcessError :: InvalidRequest (
2045+ ValidationError :: CwdRequired ,
2046+ ) ) ?;
2047+ command. current_dir ( cwd) ;
2048+ }
19932049 command. env_clear ( ) ;
19942050 command. envs ( & request. env ) ;
19952051 command
@@ -2004,6 +2060,7 @@ impl BoundedProcess {
20042060 let mut child = command
20052061 . spawn ( )
20062062 . map_err ( |error| BoundedProcessError :: Spawn ( spawn_error ( & error) ) ) ?;
2063+ drop ( confined_cwd) ;
20072064 let pid = child. id ( ) ;
20082065 #[ cfg( target_os = "linux" ) ]
20092066 let pidfd = match linux_pidfd:: PidFd :: open ( pid) {
0 commit comments