@@ -202,6 +202,19 @@ fn detect_fstype(path: &std::path::Path) -> &'static str {
202202 }
203203}
204204
205+ /// Whether the chroot rootfs should be reported read-only in the synthesized
206+ /// mount tables. Only meaningful under a chroot, where the rootfs is presented
207+ /// as its own mount: it is read-only unless `/` was granted write (a read-write
208+ /// OCI rootfs or `--fs-write /`). Without a chroot, the root is the host's real
209+ /// (read-write) `/`, restricted by Landlock rather than a read-only mount.
210+ fn root_is_read_only ( policy : & NotifPolicy ) -> bool {
211+ policy. chroot_root . is_some ( )
212+ && !policy
213+ . chroot_writable
214+ . iter ( )
215+ . any ( |p| p. as_path ( ) == std:: path:: Path :: new ( "/" ) )
216+ }
217+
205218/// Generate a virtual /proc/mounts showing only the sandbox's own mounts.
206219///
207220/// Produces standard `/proc/mounts` format: `device mountpoint type options dump pass`
@@ -210,20 +223,28 @@ fn detect_fstype(path: &std::path::Path) -> &'static str {
210223pub ( crate ) fn generate_proc_mounts (
211224 chroot_root : Option < & std:: path:: Path > ,
212225 chroot_mounts : & [ ( std:: path:: PathBuf , std:: path:: PathBuf ) ] ,
226+ chroot_mount_ro : & [ std:: path:: PathBuf ] ,
227+ root_ro : bool ,
213228) -> Vec < u8 > {
214229 let mut buf = String :: new ( ) ;
215230
216231 if let Some ( root) = chroot_root {
217232 let fstype = detect_fstype ( root) ;
218- buf. push_str ( & format ! ( "sandlock / {} rw,relatime 0 0\n " , fstype) ) ;
233+ let opts = if root_ro { "ro,relatime" } else { "rw,relatime" } ;
234+ buf. push_str ( & format ! ( "sandlock / {} {} 0 0\n " , fstype, opts) ) ;
219235 } else {
220- buf. push_str ( "rootfs / rootfs rw 0 0\n " ) ;
236+ buf. push_str ( & format ! ( "rootfs / rootfs {} 0 0\n " , if root_ro { "ro" } else { "rw" } ) ) ;
221237 }
222238
223239 for ( virtual_path, host_path) in chroot_mounts {
224240 let vp = virtual_path. to_string_lossy ( ) ;
225241 let fstype = detect_fstype ( host_path) ;
226- buf. push_str ( & format ! ( "sandlock {} {} rw,relatime 0 0\n " , vp, fstype) ) ;
242+ let opts = if chroot_mount_ro. iter ( ) . any ( |d| d == virtual_path) {
243+ "ro,relatime"
244+ } else {
245+ "rw,relatime"
246+ } ;
247+ buf. push_str ( & format ! ( "sandlock {} {} {} 0 0\n " , vp, fstype, opts) ) ;
227248 }
228249
229250 buf. into_bytes ( )
@@ -236,27 +257,35 @@ pub(crate) fn generate_proc_mounts(
236257pub ( crate ) fn generate_proc_mountinfo (
237258 chroot_root : Option < & std:: path:: Path > ,
238259 chroot_mounts : & [ ( std:: path:: PathBuf , std:: path:: PathBuf ) ] ,
260+ chroot_mount_ro : & [ std:: path:: PathBuf ] ,
261+ root_ro : bool ,
239262) -> Vec < u8 > {
240263 let mut buf = String :: new ( ) ;
241264 let mut mount_id: u32 = 20 ;
265+ let ( root_opts, root_super) = if root_ro { ( "ro,relatime" , "ro" ) } else { ( "rw,relatime" , "rw" ) } ;
242266
243267 if let Some ( root) = chroot_root {
244268 let fstype = detect_fstype ( root) ;
245269 buf. push_str ( & format ! (
246- "{} 1 8:1 / / rw,relatime - {} sandlock rw \n " , mount_id, fstype
270+ "{} 1 8:1 / / {} - {} sandlock {} \n " , mount_id, root_opts , fstype, root_super
247271 ) ) ;
248272 } else {
249273 buf. push_str ( & format ! (
250- "{} 1 0:1 / / rw - rootfs rootfs rw \n " , mount_id
274+ "{} 1 0:1 / / {} - rootfs rootfs {} \n " , mount_id, root_super , root_super
251275 ) ) ;
252276 }
253277 mount_id += 1 ;
254278
255279 for ( virtual_path, host_path) in chroot_mounts {
256280 let vp = virtual_path. to_string_lossy ( ) ;
257281 let fstype = detect_fstype ( host_path) ;
282+ let ( opts, sup) = if chroot_mount_ro. iter ( ) . any ( |d| d == virtual_path) {
283+ ( "ro,relatime" , "ro" )
284+ } else {
285+ ( "rw,relatime" , "rw" )
286+ } ;
258287 buf. push_str ( & format ! (
259- "{} 20 8:1 / {} rw,relatime - {} sandlock rw \n " , mount_id, vp, fstype
288+ "{} 20 8:1 / {} {} - {} sandlock {} \n " , mount_id, vp, opts , fstype, sup
260289 ) ) ;
261290 mount_id += 1 ;
262291 }
@@ -461,6 +490,8 @@ pub(crate) async fn handle_proc_open(
461490 let content = generate_proc_mounts (
462491 policy. chroot_root . as_deref ( ) ,
463492 & policy. chroot_mounts ,
493+ & policy. chroot_mount_ro ,
494+ root_is_read_only ( policy) ,
464495 ) ;
465496 return inject_memfd ( & content) ;
466497 }
@@ -470,6 +501,8 @@ pub(crate) async fn handle_proc_open(
470501 let content = generate_proc_mountinfo (
471502 policy. chroot_root . as_deref ( ) ,
472503 & policy. chroot_mounts ,
504+ & policy. chroot_mount_ro ,
505+ root_is_read_only ( policy) ,
473506 ) ;
474507 return inject_memfd ( & content) ;
475508 }
@@ -1156,7 +1189,8 @@ mod tests {
11561189 ( std:: path:: PathBuf :: from( "/work" ) , tmp. clone( ) ) ,
11571190 ( std:: path:: PathBuf :: from( "/data" ) , tmp. clone( ) ) ,
11581191 ] ;
1159- let content = generate_proc_mounts ( Some ( tmp. as_path ( ) ) , & mounts) ;
1192+ let ro = vec ! [ std:: path:: PathBuf :: from( "/data" ) ] ;
1193+ let content = generate_proc_mounts ( Some ( tmp. as_path ( ) ) , & mounts, & ro, false ) ;
11601194 let text = String :: from_utf8 ( content) . unwrap ( ) ;
11611195 // Root entry with detected fstype (not hardcoded ext4)
11621196 assert ! ( text. starts_with( "sandlock / " ) , "Should start with root entry, got: {}" , text) ;
@@ -1167,12 +1201,24 @@ mod tests {
11671201 // Fstype should be detected, not "unknown" (tmp is on a real fs)
11681202 let root_line = text. lines ( ) . next ( ) . unwrap ( ) ;
11691203 assert ! ( !root_line. contains( "unknown" ) , "root fstype should be detected, got: {}" , root_line) ;
1204+ // Options reflect read-only: /data is ro, /work and root are rw.
1205+ assert ! ( text. lines( ) . any( |l| l. starts_with( "sandlock / " ) && l. contains( " rw,relatime " ) ) ) ;
1206+ assert ! ( text. lines( ) . any( |l| l. starts_with( "sandlock /work " ) && l. contains( " rw,relatime " ) ) ) ;
1207+ assert ! ( text. lines( ) . any( |l| l. starts_with( "sandlock /data " ) && l. contains( " ro,relatime " ) ) ) ;
1208+ }
1209+
1210+ #[ test]
1211+ fn test_generate_proc_mounts_read_only_root ( ) {
1212+ let tmp = std:: env:: temp_dir ( ) ;
1213+ let content = generate_proc_mounts ( Some ( tmp. as_path ( ) ) , & [ ] , & [ ] , true ) ;
1214+ let text = String :: from_utf8 ( content) . unwrap ( ) ;
1215+ assert ! ( text. lines( ) . next( ) . unwrap( ) . contains( " ro,relatime " ) , "got: {}" , text) ;
11701216 }
11711217
11721218 #[ test]
11731219 fn test_generate_proc_mounts_no_chroot ( ) {
11741220 let mounts: Vec < ( std:: path:: PathBuf , std:: path:: PathBuf ) > = vec ! [ ] ;
1175- let content = generate_proc_mounts ( None , & mounts) ;
1221+ let content = generate_proc_mounts ( None , & mounts, & [ ] , false ) ;
11761222 let text = String :: from_utf8 ( content) . unwrap ( ) ;
11771223 assert ! ( text. contains( "rootfs / rootfs rw 0 0" ) ) ;
11781224 assert_eq ! ( text. lines( ) . count( ) , 1 ) ;
@@ -1184,7 +1230,7 @@ mod tests {
11841230 let mounts = vec ! [
11851231 ( std:: path:: PathBuf :: from( "/work" ) , tmp. clone( ) ) ,
11861232 ] ;
1187- let content = generate_proc_mountinfo ( Some ( tmp. as_path ( ) ) , & mounts) ;
1233+ let content = generate_proc_mountinfo ( Some ( tmp. as_path ( ) ) , & mounts, & [ ] , false ) ;
11881234 let text = String :: from_utf8 ( content) . unwrap ( ) ;
11891235 assert ! ( text. contains( "/ / rw,relatime -" ) ) ;
11901236 assert ! ( text. contains( "/ /work rw,relatime -" ) ) ;
@@ -1195,7 +1241,7 @@ mod tests {
11951241 #[ test]
11961242 fn test_generate_proc_mountinfo_no_chroot ( ) {
11971243 let mounts: Vec < ( std:: path:: PathBuf , std:: path:: PathBuf ) > = vec ! [ ] ;
1198- let content = generate_proc_mountinfo ( None , & mounts) ;
1244+ let content = generate_proc_mountinfo ( None , & mounts, & [ ] , false ) ;
11991245 let text = String :: from_utf8 ( content) . unwrap ( ) ;
12001246 assert ! ( text. contains( "/ / rw - rootfs rootfs rw" ) ) ;
12011247 assert_eq ! ( text. lines( ) . count( ) , 1 ) ;
0 commit comments