@@ -180,8 +180,8 @@ pub fn parse_input(input: ProfileInput) -> Result<(Sandbox, ProgramSpec), Sandlo
180180 for p in input. filesystem . deny . iter ( ) { b = b. fs_deny ( p) ; }
181181 if let Some ( c) = input. filesystem . chroot { b = b. chroot ( c) ; }
182182 for spec in input. filesystem . mount . iter ( ) {
183- let ( virt, host) = parse_mount_spec ( spec) ?;
184- b = b . fs_mount ( virt, host) ;
183+ let ( virt, host, read_only ) = parse_mount_spec ( spec) ?;
184+ b = if read_only { b . fs_mount_ro ( virt , host ) } else { b . fs_mount ( virt, host) } ;
185185 }
186186 if let Some ( s) = input. filesystem . on_exit . as_deref ( ) { b = b. on_exit ( parse_branch_action ( s) ?) ; }
187187 if let Some ( s) = input. filesystem . on_error . as_deref ( ) { b = b. on_error ( parse_branch_action ( s) ?) ; }
@@ -250,17 +250,27 @@ fn parse_branch_action(s: &str) -> Result<crate::sandbox::BranchAction, Sandlock
250250}
251251
252252/// Parses a `"VIRTUAL:HOST"` mount spec string into a `(virtual, host)` pair.
253- fn parse_mount_spec ( s : & str ) -> Result < ( PathBuf , PathBuf ) , SandlockError > {
253+ /// Parse a `VIRTUAL:HOST` mount spec, with an optional trailing `:ro` (or the
254+ /// default `:rw`) selecting a read-only mount. Returns
255+ /// `(virtual_path, host_path, read_only)`.
256+ pub fn parse_mount_spec ( s : & str ) -> Result < ( PathBuf , PathBuf , bool ) , SandlockError > {
254257 use crate :: error:: SandboxError ;
255- let ( virt, host) = s. split_once ( ':' ) . ok_or_else ( || SandlockError :: Sandbox ( SandboxError :: Invalid (
256- format ! ( "invalid mount spec {s:?}; expected \" VIRTUAL:HOST\" " ) ,
258+ let ( body, read_only) = if let Some ( b) = s. strip_suffix ( ":ro" ) {
259+ ( b, true )
260+ } else if let Some ( b) = s. strip_suffix ( ":rw" ) {
261+ ( b, false )
262+ } else {
263+ ( s, false )
264+ } ;
265+ let ( virt, host) = body. split_once ( ':' ) . ok_or_else ( || SandlockError :: Sandbox ( SandboxError :: Invalid (
266+ format ! ( "invalid mount spec {s:?}; expected \" VIRTUAL:HOST[:ro]\" " ) ,
257267 ) ) ) ?;
258268 if virt. is_empty ( ) || host. is_empty ( ) {
259269 return Err ( SandlockError :: Sandbox ( SandboxError :: Invalid (
260270 format ! ( "invalid mount spec {s:?}; both VIRTUAL and HOST must be non-empty" ) ,
261271 ) ) ) ;
262272 }
263- Ok ( ( PathBuf :: from ( virt) , PathBuf :: from ( host) ) )
273+ Ok ( ( PathBuf :: from ( virt) , PathBuf :: from ( host) , read_only ) )
264274}
265275
266276/// Parses an RFC3339 timestamp string into `SystemTime`.
@@ -400,6 +410,20 @@ mod tests {
400410 assert_eq ! ( policy. extra_deny_syscalls, vec![ "ptrace" . to_string( ) ] ) ;
401411 }
402412
413+ #[ test]
414+ fn parse_mount_spec_ro_suffix ( ) {
415+ let ( v, h, ro) = parse_mount_spec ( "/work:/host" ) . unwrap ( ) ;
416+ assert_eq ! ( ( v. to_str( ) . unwrap( ) , h. to_str( ) . unwrap( ) , ro) , ( "/work" , "/host" , false ) ) ;
417+ let ( _, _, ro) = parse_mount_spec ( "/work:/host:rw" ) . unwrap ( ) ;
418+ assert ! ( !ro) ;
419+ let ( v, h, ro) = parse_mount_spec ( "/work:/host:ro" ) . unwrap ( ) ;
420+ assert_eq ! ( ( v. to_str( ) . unwrap( ) , h. to_str( ) . unwrap( ) , ro) , ( "/work" , "/host" , true ) ) ;
421+ // a host path containing colons still parses; only a trailing :ro/:rw is an option
422+ let ( _, h, ro) = parse_mount_spec ( "/v:/a:b:ro" ) . unwrap ( ) ;
423+ assert_eq ! ( ( h. to_str( ) . unwrap( ) , ro) , ( "/a:b" , true ) ) ;
424+ assert ! ( parse_mount_spec( "nocolon" ) . is_err( ) ) ;
425+ }
426+
403427 #[ test]
404428 fn parse_mount_spec_rejects_missing_colon ( ) {
405429 let toml = r#"
0 commit comments