@@ -40,18 +40,21 @@ pub(crate) fn reconcile_private_directory_subtree(
4040 } )
4141 . collect :: < io:: Result < Vec < _ > > > ( ) ?;
4242
43+ let boundary_device = rustix:: fs:: fstat ( boundary_directory) ?. st_dev ;
4344 let mut directory = rustix:: io:: dup ( boundary_directory) ?;
4445 for name in components. into_iter ( ) . flatten ( ) {
4546 match rustix:: fs:: mkdirat ( & directory, name, private_directory_mode ( ) ) {
4647 Ok ( ( ) ) | Err ( rustix:: io:: Errno :: EXIST ) => { }
4748 Err ( error) => return Err ( error. into ( ) ) ,
4849 }
49- directory = rustix:: fs:: openat (
50- & directory,
51- name,
52- directory_open_flags ( ) ,
53- rustix:: fs:: Mode :: empty ( ) ,
54- ) ?;
50+ let next = open_reconciliation_descendant ( & directory, name) ?;
51+ if rustix:: fs:: fstat ( & next) ?. st_dev != boundary_device {
52+ return Err ( io:: Error :: new (
53+ io:: ErrorKind :: PermissionDenied ,
54+ "ACME managed directory target crosses a filesystem boundary" ,
55+ ) ) ;
56+ }
57+ directory = next;
5558 rustix:: fs:: fchown (
5659 & directory,
5760 Some ( rustix:: fs:: Uid :: from_raw ( owner. 0 ) ) ,
@@ -62,6 +65,38 @@ pub(crate) fn reconcile_private_directory_subtree(
6265 Ok ( directory)
6366}
6467
68+ #[ cfg( target_os = "linux" ) ]
69+ fn open_reconciliation_descendant (
70+ directory : & rustix:: fd:: OwnedFd ,
71+ name : & std:: ffi:: OsStr ,
72+ ) -> io:: Result < rustix:: fd:: OwnedFd > {
73+ rustix:: fs:: openat2 (
74+ directory,
75+ name,
76+ directory_open_flags ( ) ,
77+ rustix:: fs:: Mode :: empty ( ) ,
78+ rustix:: fs:: ResolveFlags :: BENEATH
79+ | rustix:: fs:: ResolveFlags :: NO_SYMLINKS
80+ | rustix:: fs:: ResolveFlags :: NO_MAGICLINKS
81+ | rustix:: fs:: ResolveFlags :: NO_XDEV ,
82+ )
83+ . map_err ( Into :: into)
84+ }
85+
86+ #[ cfg( all( unix, not( target_os = "linux" ) ) ) ]
87+ fn open_reconciliation_descendant (
88+ directory : & rustix:: fd:: OwnedFd ,
89+ name : & std:: ffi:: OsStr ,
90+ ) -> io:: Result < rustix:: fd:: OwnedFd > {
91+ rustix:: fs:: openat (
92+ directory,
93+ name,
94+ directory_open_flags ( ) ,
95+ rustix:: fs:: Mode :: empty ( ) ,
96+ )
97+ . map_err ( Into :: into)
98+ }
99+
65100#[ cfg( unix) ]
66101fn walk_directory ( path : & Path , create : bool ) -> io:: Result < rustix:: fd:: OwnedFd > {
67102 let mut directory = rustix:: fs:: openat (
@@ -254,4 +289,57 @@ mod tests {
254289 assert_eq ! ( error. kind( ) , io:: ErrorKind :: InvalidInput ) ;
255290 assert ! ( !outside. exists( ) ) ;
256291 }
292+
293+ #[ cfg( target_os = "linux" ) ]
294+ #[ test]
295+ fn owned_directory_reconciliation_rejects_bind_mount ( ) {
296+ // Run only inside an isolated privileged mount namespace.
297+ const ENABLED : & str = "FLUXHEIM_ACME_MOUNT_BOUNDARY_TEST" ;
298+ if std:: env:: var_os ( ENABLED ) . as_deref ( ) != Some ( std:: ffi:: OsStr :: new ( "1" ) ) {
299+ eprintln ! ( "privileged ACME mount-boundary test skipped" ) ;
300+ return ;
301+ }
302+ if !rustix:: process:: geteuid ( ) . is_root ( ) {
303+ panic ! ( "privileged ACME mount-boundary test requires root" ) ;
304+ }
305+
306+ use std:: os:: unix:: fs:: { MetadataExt as _, PermissionsExt as _} ;
307+
308+ struct BindMount ( std:: path:: PathBuf ) ;
309+ impl Drop for BindMount {
310+ fn drop ( & mut self ) {
311+ let _ = rustix:: mount:: unmount ( & self . 0 , rustix:: mount:: UnmountFlags :: DETACH ) ;
312+ }
313+ }
314+
315+ let root = fluxheim_common:: test_support:: unique_temp_path ( "acme-directory-mount" ) ;
316+ let boundary = root. join ( "storage" ) ;
317+ let mount_point = boundary. join ( "mounted" ) ;
318+ let outside = root. join ( "outside" ) ;
319+ std:: fs:: create_dir_all ( & mount_point) . unwrap ( ) ;
320+ std:: fs:: create_dir_all ( & outside) . unwrap ( ) ;
321+ std:: fs:: set_permissions ( & outside, std:: fs:: Permissions :: from_mode ( 0o755 ) ) . unwrap ( ) ;
322+ let original = std:: fs:: symlink_metadata ( & outside) . unwrap ( ) ;
323+ rustix:: mount:: mount_bind ( & outside, & mount_point) . unwrap ( ) ;
324+ let _mount = BindMount ( mount_point. clone ( ) ) ;
325+
326+ let boundary_directory = open_directory_no_symlinks ( & boundary) . unwrap ( ) ;
327+ assert ! (
328+ reconcile_private_directory_subtree(
329+ & boundary,
330+ & boundary_directory,
331+ & mount_point. join( "child" ) ,
332+ ( 65_534 , 65_534 ) ,
333+ )
334+ . is_err( )
335+ ) ;
336+
337+ let after = std:: fs:: symlink_metadata ( & outside) . unwrap ( ) ;
338+ assert_eq ! ( ( after. uid( ) , after. gid( ) ) , ( original. uid( ) , original. gid( ) ) ) ;
339+ assert_eq ! (
340+ after. permissions( ) . mode( ) & 0o777 ,
341+ original. permissions( ) . mode( ) & 0o777
342+ ) ;
343+ assert ! ( !outside. join( "child" ) . exists( ) ) ;
344+ }
257345}
0 commit comments