@@ -160,6 +160,7 @@ const SOCKET_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
160160pub struct Preopen {
161161 pub host_dir : std:: path:: PathBuf ,
162162 pub guest_path : String ,
163+ pub read_only : bool ,
163164}
164165
165166impl Preopen {
@@ -192,9 +193,16 @@ impl Preopen {
192193 Ok ( Self {
193194 host_dir,
194195 guest_path,
196+ read_only : false ,
195197 } )
196198 }
197199
200+ /// Mark this preopen as read-only.
201+ pub fn read_only ( mut self ) -> Self {
202+ self . read_only = true ;
203+ self
204+ }
205+
198206 /// Parse a `HOST[:GUEST]` CLI argument. When `GUEST` is omitted the
199207 /// default guest mount point is `/host`.
200208 pub fn parse_cli ( s : & str ) -> Result < Self > {
@@ -1677,30 +1685,34 @@ fn register_net_tools(
16771685/// matching the guest-supplied path against each preopen's guest path.
16781686#[ derive( Clone ) ]
16791687struct FsRouter {
1680- entries : Vec < ( String , FsSandbox ) > ,
1688+ entries : Vec < ( String , FsSandbox , bool ) > ,
16811689}
16821690
16831691impl FsRouter {
16841692 fn new ( preopens : & [ Preopen ] ) -> Result < Self > {
16851693 let mut entries = Vec :: with_capacity ( preopens. len ( ) ) ;
16861694 for p in preopens {
1687- entries. push ( ( p. guest_path . clone ( ) , FsSandbox :: new ( & p. host_dir ) ?) ) ;
1695+ entries. push ( (
1696+ p. guest_path . clone ( ) ,
1697+ FsSandbox :: new ( & p. host_dir ) ?,
1698+ p. read_only ,
1699+ ) ) ;
16881700 }
16891701 // Sort by descending prefix length so longer matches win (e.g.
16901702 // /data/public should match before /data).
16911703 entries. sort_by ( |a, b| b. 0 . len ( ) . cmp ( & a. 0 . len ( ) ) ) ;
16921704 Ok ( Self { entries } )
16931705 }
16941706
1695- /// Pick the preopen matching `path` and return its sandbox plus
1696- /// the path-relative-to-that-sandbox.
1697- fn route < ' a > ( & ' a self , path : & ' a str ) -> Result < ( & ' a FsSandbox , & ' a str ) > {
1698- for ( prefix, fs) in & self . entries {
1707+ /// Pick the preopen matching `path` and return its sandbox,
1708+ /// the path-relative-to-that-sandbox, and whether it is read-only .
1709+ fn route < ' a > ( & ' a self , path : & ' a str ) -> Result < ( & ' a FsSandbox , & ' a str , bool ) > {
1710+ for ( prefix, fs, ro ) in & self . entries {
16991711 if path == prefix {
1700- return Ok ( ( fs, "" ) ) ;
1712+ return Ok ( ( fs, "" , * ro ) ) ;
17011713 }
17021714 if let Some ( tail) = path. strip_prefix ( prefix) . and_then ( |t| t. strip_prefix ( '/' ) ) {
1703- return Ok ( ( fs, tail) ) ;
1715+ return Ok ( ( fs, tail, * ro ) ) ;
17041716 }
17051717 }
17061718 Err ( anyhow ! (
@@ -1709,6 +1721,14 @@ impl FsRouter {
17091721 ) )
17101722 }
17111723
1724+ fn require_writable < ' a > ( & ' a self , path : & ' a str ) -> Result < ( & ' a FsSandbox , & ' a str ) > {
1725+ let ( fs, rel, ro) = self . route ( path) ?;
1726+ if ro {
1727+ return Err ( anyhow ! ( "read-only mount: write to {} denied" , path) ) ;
1728+ }
1729+ Ok ( ( fs, rel) )
1730+ }
1731+
17121732 fn register ( self , registry : & mut ToolRegistry ) {
17131733 use serde_json:: json;
17141734
@@ -1717,7 +1737,7 @@ impl FsRouter {
17171737 let path = args[ "path" ]
17181738 . as_str ( )
17191739 . ok_or_else ( || anyhow ! ( "fs_read: missing 'path'" ) ) ?;
1720- let ( fs, rel) = r. route ( path) ?;
1740+ let ( fs, rel, _ro ) = r. route ( path) ?;
17211741 let target = fs. resolve ( rel) ?;
17221742 let size = std:: fs:: metadata ( & target)
17231743 . map_err ( |e| anyhow ! ( "fs_read {:?}: {}" , path, e) ) ?
@@ -1751,7 +1771,7 @@ impl FsRouter {
17511771 ) ) ;
17521772 }
17531773 let append = args[ "append" ] . as_bool ( ) . unwrap_or ( false ) ;
1754- let ( fs, rel) = r. route ( path) ?;
1774+ let ( fs, rel) = r. require_writable ( path) ?;
17551775 let target = fs. resolve ( rel) ?;
17561776 use std:: io:: Write ;
17571777 let mut f = std:: fs:: OpenOptions :: new ( )
@@ -1769,7 +1789,7 @@ impl FsRouter {
17691789 let r = self . clone ( ) ;
17701790 registry. register ( "fs_list" , move |args| {
17711791 let path = args[ "path" ] . as_str ( ) . unwrap_or ( "" ) ;
1772- let ( fs, rel) = r. route ( path) ?;
1792+ let ( fs, rel, _ro ) = r. route ( path) ?;
17731793 let target = fs. resolve ( rel) ?;
17741794 let mut entries = Vec :: new ( ) ;
17751795 for entry in
@@ -1799,7 +1819,7 @@ impl FsRouter {
17991819 let path = args[ "path" ]
18001820 . as_str ( )
18011821 . ok_or_else ( || anyhow ! ( "fs_stat: missing 'path'" ) ) ?;
1802- let ( fs, rel) = r. route ( path) ?;
1822+ let ( fs, rel, _ro ) = r. route ( path) ?;
18031823 let target = fs. resolve ( rel) ?;
18041824 let md =
18051825 std:: fs:: metadata ( & target) . map_err ( |e| anyhow ! ( "fs_stat {:?}: {}" , path, e) ) ?;
@@ -1833,7 +1853,7 @@ impl FsRouter {
18331853 . ok_or_else ( || anyhow ! ( "fs_read_bytes: missing 'path'" ) ) ?;
18341854 let offset = args[ "offset" ] . as_u64 ( ) . unwrap_or ( 0 ) ;
18351855 let want = args[ "len" ] . as_u64 ( ) . unwrap_or ( 65536 ) . min ( MAX_FS_READ ) ;
1836- let ( fs, rel) = r. route ( path) ?;
1856+ let ( fs, rel, _ro ) = r. route ( path) ?;
18371857 let target = fs. resolve ( rel) ?;
18381858 let mut f = std:: fs:: File :: open ( & target)
18391859 . map_err ( |e| anyhow ! ( "fs_read_bytes {:?}: {}" , path, e) ) ?;
@@ -1888,7 +1908,7 @@ impl FsRouter {
18881908 }
18891909 }
18901910 let append = args[ "append" ] . as_bool ( ) . unwrap_or ( false ) ;
1891- let ( fs, rel) = r. route ( path) ?;
1911+ let ( fs, rel) = r. require_writable ( path) ?;
18921912 let target = fs. resolve ( rel) ?;
18931913 let mut f = std:: fs:: OpenOptions :: new ( )
18941914 . write ( true )
@@ -1923,7 +1943,7 @@ impl FsRouter {
19231943 MAX_TRUNCATE_LEN
19241944 ) ) ;
19251945 }
1926- let ( fs, rel) = r. route ( path) ?;
1946+ let ( fs, rel) = r. require_writable ( path) ?;
19271947 let target = fs. resolve ( rel) ?;
19281948 let f = std:: fs:: OpenOptions :: new ( )
19291949 . write ( true )
@@ -1940,7 +1960,7 @@ impl FsRouter {
19401960 . as_str ( )
19411961 . ok_or_else ( || anyhow ! ( "fs_mkdir: missing 'path'" ) ) ?;
19421962 let parents = args[ "parents" ] . as_bool ( ) . unwrap_or ( false ) ;
1943- let ( fs, rel) = r. route ( path) ?;
1963+ let ( fs, rel) = r. require_writable ( path) ?;
19441964 let target = fs. resolve ( rel) ?;
19451965 if parents {
19461966 std:: fs:: create_dir_all ( & target)
@@ -1956,7 +1976,7 @@ impl FsRouter {
19561976 let path = args[ "path" ]
19571977 . as_str ( )
19581978 . ok_or_else ( || anyhow ! ( "fs_unlink: missing 'path'" ) ) ?;
1959- let ( fs, rel) = r. route ( path) ?;
1979+ let ( fs, rel) = r. require_writable ( path) ?;
19601980 let target = fs. resolve ( rel) ?;
19611981 if target == * fs. root ( ) {
19621982 return Err ( anyhow ! ( "fs_unlink: cannot remove mount root" ) ) ;
@@ -3038,6 +3058,130 @@ mod tests {
30383058 assert ! ( s. contains( "\" text\" :\" hi\" " ) , "{s}" ) ;
30393059 }
30403060
3061+ // -- Read-only mount tests ------------------------------------------------
3062+
3063+ #[ test]
3064+ fn readonly_mount_allows_reads ( ) {
3065+ let root = tmpdir ( "ro-read" ) ;
3066+ fs:: write ( root. join ( "file.txt" ) , b"hello" ) . unwrap ( ) ;
3067+ let preopens = vec ! [ Preopen :: new( & root, "/data" ) . unwrap( ) . read_only( ) ] ;
3068+ let mut reg = ToolRegistry :: new ( ) ;
3069+ FsRouter :: new ( & preopens) . unwrap ( ) . register ( & mut reg) ;
3070+
3071+ let req = br#"{"name":"fs_read","args":{"path":"/data/file.txt"}}"# ;
3072+ let resp = reg. dispatch ( req) ;
3073+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3074+ assert ! ( s. contains( "\" text\" :\" hello\" " ) , "{s}" ) ;
3075+ }
3076+
3077+ #[ test]
3078+ fn readonly_mount_blocks_fs_write ( ) {
3079+ let root = tmpdir ( "ro-write" ) ;
3080+ let preopens = vec ! [ Preopen :: new( & root, "/data" ) . unwrap( ) . read_only( ) ] ;
3081+ let mut reg = ToolRegistry :: new ( ) ;
3082+ FsRouter :: new ( & preopens) . unwrap ( ) . register ( & mut reg) ;
3083+
3084+ let req = br#"{"name":"fs_write","args":{"path":"/data/new.txt","text":"nope"}}"# ;
3085+ let resp = reg. dispatch ( req) ;
3086+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3087+ assert ! ( s. contains( "\" error\" " ) , "{s}" ) ;
3088+ assert ! ( s. contains( "read-only mount" ) , "{s}" ) ;
3089+ }
3090+
3091+ #[ test]
3092+ fn readonly_mount_blocks_fs_mkdir ( ) {
3093+ let root = tmpdir ( "ro-mkdir" ) ;
3094+ let preopens = vec ! [ Preopen :: new( & root, "/data" ) . unwrap( ) . read_only( ) ] ;
3095+ let mut reg = ToolRegistry :: new ( ) ;
3096+ FsRouter :: new ( & preopens) . unwrap ( ) . register ( & mut reg) ;
3097+
3098+ let req = br#"{"name":"fs_mkdir","args":{"path":"/data/subdir"}}"# ;
3099+ let resp = reg. dispatch ( req) ;
3100+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3101+ assert ! ( s. contains( "\" error\" " ) , "{s}" ) ;
3102+ assert ! ( s. contains( "read-only mount" ) , "{s}" ) ;
3103+ }
3104+
3105+ #[ test]
3106+ fn readonly_mount_blocks_fs_unlink ( ) {
3107+ let root = tmpdir ( "ro-unlink" ) ;
3108+ fs:: write ( root. join ( "victim.txt" ) , b"data" ) . unwrap ( ) ;
3109+ let preopens = vec ! [ Preopen :: new( & root, "/data" ) . unwrap( ) . read_only( ) ] ;
3110+ let mut reg = ToolRegistry :: new ( ) ;
3111+ FsRouter :: new ( & preopens) . unwrap ( ) . register ( & mut reg) ;
3112+
3113+ let req = br#"{"name":"fs_unlink","args":{"path":"/data/victim.txt"}}"# ;
3114+ let resp = reg. dispatch ( req) ;
3115+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3116+ assert ! ( s. contains( "\" error\" " ) , "{s}" ) ;
3117+ assert ! ( s. contains( "read-only mount" ) , "{s}" ) ;
3118+ assert ! (
3119+ root. join( "victim.txt" ) . exists( ) ,
3120+ "file should not be deleted"
3121+ ) ;
3122+ }
3123+
3124+ #[ test]
3125+ fn readonly_mount_blocks_fs_truncate ( ) {
3126+ let root = tmpdir ( "ro-trunc" ) ;
3127+ fs:: write ( root. join ( "file.txt" ) , b"hello world" ) . unwrap ( ) ;
3128+ let preopens = vec ! [ Preopen :: new( & root, "/data" ) . unwrap( ) . read_only( ) ] ;
3129+ let mut reg = ToolRegistry :: new ( ) ;
3130+ FsRouter :: new ( & preopens) . unwrap ( ) . register ( & mut reg) ;
3131+
3132+ let req = br#"{"name":"fs_truncate","args":{"path":"/data/file.txt","length":0}}"# ;
3133+ let resp = reg. dispatch ( req) ;
3134+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3135+ assert ! ( s. contains( "\" error\" " ) , "{s}" ) ;
3136+ assert ! ( s. contains( "read-only mount" ) , "{s}" ) ;
3137+ }
3138+
3139+ #[ test]
3140+ fn readonly_mount_blocks_fs_write_bytes ( ) {
3141+ let root = tmpdir ( "ro-wbytes" ) ;
3142+ let preopens = vec ! [ Preopen :: new( & root, "/data" ) . unwrap( ) . read_only( ) ] ;
3143+ let mut reg = ToolRegistry :: new ( ) ;
3144+ FsRouter :: new ( & preopens) . unwrap ( ) . register ( & mut reg) ;
3145+
3146+ let req = br#"{"name":"fs_write_bytes","args":{"path":"/data/bin.dat","data":"AAAA"}}"# ;
3147+ let resp = reg. dispatch ( req) ;
3148+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3149+ assert ! ( s. contains( "\" error\" " ) , "{s}" ) ;
3150+ assert ! ( s. contains( "read-only mount" ) , "{s}" ) ;
3151+ }
3152+
3153+ #[ test]
3154+ fn mixed_rw_and_ro_mounts ( ) {
3155+ let rw_root = tmpdir ( "mixed-rw" ) ;
3156+ let ro_root = tmpdir ( "mixed-ro" ) ;
3157+ fs:: write ( ro_root. join ( "existing.txt" ) , b"read me" ) . unwrap ( ) ;
3158+ let preopens = vec ! [
3159+ Preopen :: new( & rw_root, "/rw" ) . unwrap( ) ,
3160+ Preopen :: new( & ro_root, "/ro" ) . unwrap( ) . read_only( ) ,
3161+ ] ;
3162+ let mut reg = ToolRegistry :: new ( ) ;
3163+ FsRouter :: new ( & preopens) . unwrap ( ) . register ( & mut reg) ;
3164+
3165+ // Write to rw mount succeeds
3166+ let req = br#"{"name":"fs_write","args":{"path":"/rw/ok.txt","text":"yes"}}"# ;
3167+ let resp = reg. dispatch ( req) ;
3168+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3169+ assert ! ( s. contains( "\" bytes_written\" " ) , "{s}" ) ;
3170+
3171+ // Read from ro mount succeeds
3172+ let req = br#"{"name":"fs_read","args":{"path":"/ro/existing.txt"}}"# ;
3173+ let resp = reg. dispatch ( req) ;
3174+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3175+ assert ! ( s. contains( "\" text\" :\" read me\" " ) , "{s}" ) ;
3176+
3177+ // Write to ro mount fails
3178+ let req = br#"{"name":"fs_write","args":{"path":"/ro/nope.txt","text":"no"}}"# ;
3179+ let resp = reg. dispatch ( req) ;
3180+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3181+ assert ! ( s. contains( "\" error\" " ) , "{s}" ) ;
3182+ assert ! ( s. contains( "read-only mount" ) , "{s}" ) ;
3183+ }
3184+
30413185 // -- NetworkPolicy tests --------------------------------------------------
30423186
30433187 #[ test]
0 commit comments