11use std:: fs:: OpenOptions ;
22use std:: future:: Future ;
33use std:: io:: { Read , Write } ;
4+ use std:: path:: { Path , PathBuf } ;
45use std:: pin:: Pin ;
56use std:: process:: { Child , Command , Stdio } ;
67use std:: sync:: atomic:: { AtomicU32 , Ordering } ;
@@ -200,7 +201,16 @@ pub(super) fn poll_builtin_io_op(
200201/// Opens a file handle for runtime I/O.
201202#[ pd_host_function( name = "io::open" ) ]
202203pub ( super ) fn builtin_io_open ( vm : & mut Vm , path : & str , mode : & str ) -> VmResult < HostCallResult < i64 > > {
203- let path = path. to_string ( ) ;
204+ let writes = match mode {
205+ "r" => false ,
206+ "w" | "a" | "r+" | "w+" | "a+" => true ,
207+ other => {
208+ return Err ( VmError :: HostError ( format ! (
209+ "unsupported io_open mode '{other}', expected r/w/a/r+/w+/a+"
210+ ) ) ) ;
211+ }
212+ } ;
213+ let path = authorize_io_path ( vm, path, writes) ?;
204214 let mode = mode. to_string ( ) ;
205215 let op_id = schedule_io_task ( vm, None , move || {
206216 let mut options = OpenOptions :: new ( ) ;
@@ -256,6 +266,16 @@ pub(super) fn builtin_io_popen(
256266 "unsupported io_popen mode '{mode}', expected r or w"
257267 ) ) ) ;
258268 }
269+ if vm
270+ . host
271+ . io_policy
272+ . as_ref ( )
273+ . is_some_and ( |policy| !policy. allow_process )
274+ {
275+ return Err ( VmError :: HostError (
276+ "io_popen requires the process capability" . to_string ( ) ,
277+ ) ) ;
278+ }
259279 let command = command. to_string ( ) ;
260280 let mode = mode. to_string ( ) ;
261281 let op_id = schedule_io_task ( vm, None , move || {
@@ -294,23 +314,31 @@ pub(super) fn builtin_io_popen(
294314/// Reads all remaining text from an I/O handle.
295315#[ pd_host_function( name = "io::read_all" ) ]
296316pub ( super ) fn builtin_io_read_all ( vm : & mut Vm , handle_id : i64 ) -> VmResult < HostCallResult < String > > {
317+ let max_read_bytes = vm
318+ . host
319+ . io_policy
320+ . as_ref ( )
321+ . map ( |policy| policy. max_read_bytes ) ;
297322 let handle = resource_handle ( handle_id) ?;
298323 let resource = io_resource_for_handle ( vm, handle) ?;
299324 let op_id = schedule_io_task ( vm, Some ( handle) , move || {
300325 let result = resource. with_handle_mut ( |handle| {
301326 let mut out = String :: new ( ) ;
302327 match handle {
303- IoHandle :: File ( file) => file
304- . read_to_string ( & mut out)
305- . map_err ( |err| VmError :: HostError ( format ! ( "io_read_all failed: {err}" ) ) ) ?,
306- IoHandle :: PopenRead { child } => child
307- . stdout
308- . as_mut ( )
309- . ok_or_else ( || {
310- VmError :: HostError ( "io_read_all popen handle missing stdout" . to_string ( ) )
311- } ) ?
312- . read_to_string ( & mut out)
313- . map_err ( |err| VmError :: HostError ( format ! ( "io_read_all failed: {err}" ) ) ) ?,
328+ IoHandle :: File ( file) => {
329+ read_to_string_with_limit ( file, max_read_bytes, & mut out) ?;
330+ }
331+ IoHandle :: PopenRead { child } => {
332+ read_to_string_with_limit (
333+ child. stdout . as_mut ( ) . ok_or_else ( || {
334+ VmError :: HostError (
335+ "io_read_all popen handle missing stdout" . to_string ( ) ,
336+ )
337+ } ) ?,
338+ max_read_bytes,
339+ & mut out,
340+ ) ?;
341+ }
314342 IoHandle :: PopenWrite { .. } => {
315343 return Err ( VmError :: HostError (
316344 "io_read_all requires a readable handle" . to_string ( ) ,
@@ -327,17 +355,23 @@ pub(super) fn builtin_io_read_all(vm: &mut Vm, handle_id: i64) -> VmResult<HostC
327355/// Reads a single line of text from an I/O handle.
328356#[ pd_host_function( name = "io::read_line" ) ]
329357pub ( super ) fn builtin_io_read_line ( vm : & mut Vm , handle_id : i64 ) -> VmResult < HostCallResult < String > > {
358+ let max_read_bytes = vm
359+ . host
360+ . io_policy
361+ . as_ref ( )
362+ . map ( |policy| policy. max_read_bytes ) ;
330363 let handle = resource_handle ( handle_id) ?;
331364 let resource = io_resource_for_handle ( vm, handle) ?;
332365 let op_id = schedule_io_task ( vm, Some ( handle) , move || {
333366 let result = resource. with_handle_mut ( |handle| {
334367 let line = match handle {
335- IoHandle :: File ( file) => read_line_from_reader ( file) ?,
336- IoHandle :: PopenRead { child } => {
337- read_line_from_reader ( child. stdout . as_mut ( ) . ok_or_else ( || {
368+ IoHandle :: File ( file) => read_line_from_reader ( file, max_read_bytes ) ?,
369+ IoHandle :: PopenRead { child } => read_line_from_reader (
370+ child. stdout . as_mut ( ) . ok_or_else ( || {
338371 VmError :: HostError ( "io_read_line popen handle missing stdout" . to_string ( ) )
339- } ) ?) ?
340- }
372+ } ) ?,
373+ max_read_bytes,
374+ ) ?,
341375 IoHandle :: PopenWrite { .. } => {
342376 return Err ( VmError :: HostError (
343377 "io_read_line requires a readable handle" . to_string ( ) ,
@@ -358,6 +392,14 @@ pub(super) fn builtin_io_write(
358392 handle_id : i64 ,
359393 text : & str ,
360394) -> VmResult < HostCallResult < i64 > > {
395+ if let Some ( policy) = vm. host . io_policy . as_ref ( )
396+ && text. len ( ) > policy. max_write_bytes
397+ {
398+ return Err ( VmError :: HostError ( format ! (
399+ "io_write exceeds the configured write limit of {} bytes" ,
400+ policy. max_write_bytes
401+ ) ) ) ;
402+ }
361403 let bytes = text. as_bytes ( ) . to_vec ( ) ;
362404 let handle = resource_handle ( handle_id) ?;
363405 let resource = io_resource_for_handle ( vm, handle) ?;
@@ -438,15 +480,65 @@ pub(super) fn builtin_io_close(vm: &mut Vm, handle_id: i64) -> VmResult<HostCall
438480/// Returns whether a file system path exists.
439481#[ pd_host_function( name = "io::exists" ) ]
440482pub ( super ) fn builtin_io_exists ( vm : & mut Vm , path : & str ) -> VmResult < HostCallResult < bool > > {
441- let path = path . to_string ( ) ;
483+ let path = authorize_io_path ( vm , path , false ) ? ;
442484 let op_id = schedule_io_task ( vm, None , move || {
443- IoAsyncCompletion :: result ( Ok ( CallReturn :: one ( Value :: Bool (
444- std:: path:: Path :: new ( path. as_str ( ) ) . exists ( ) ,
445- ) ) ) )
485+ IoAsyncCompletion :: result ( Ok ( CallReturn :: one ( Value :: Bool ( path. exists ( ) ) ) ) )
446486 } ) ?;
447487 Ok ( HostCallResult :: Pending ( op_id) )
448488}
449489
490+ fn authorize_io_path ( vm : & Vm , path : & str , writes : bool ) -> VmResult < PathBuf > {
491+ let requested = PathBuf :: from ( path) ;
492+ let Some ( policy) = vm. host . io_policy . as_ref ( ) else {
493+ return Ok ( requested) ;
494+ } ;
495+ if writes && !policy. allow_write {
496+ return Err ( VmError :: HostError (
497+ "io path write requires the write capability" . to_string ( ) ,
498+ ) ) ;
499+ }
500+ let absolute = if requested. is_absolute ( ) {
501+ requested
502+ } else {
503+ std:: env:: current_dir ( )
504+ . map_err ( |error| VmError :: HostError ( format ! ( "io path resolution failed: {error}" ) ) ) ?
505+ . join ( requested)
506+ } ;
507+ let canonical = canonicalize_io_target ( & absolute) ?;
508+ for root in & policy. allowed_roots {
509+ let root = Path :: new ( root) . canonicalize ( ) . map_err ( |error| {
510+ VmError :: HostError ( format ! (
511+ "io allowed root '{root}' cannot be resolved: {error}"
512+ ) )
513+ } ) ?;
514+ if canonical. starts_with ( root) {
515+ return Ok ( canonical) ;
516+ }
517+ }
518+ Err ( VmError :: HostError ( format ! (
519+ "io path '{}' is outside the allowed roots" ,
520+ canonical. display( )
521+ ) ) )
522+ }
523+
524+ fn canonicalize_io_target ( path : & Path ) -> VmResult < PathBuf > {
525+ if path. exists ( ) {
526+ return path
527+ . canonicalize ( )
528+ . map_err ( |error| VmError :: HostError ( format ! ( "io path resolution failed: {error}" ) ) ) ;
529+ }
530+ let parent = path
531+ . parent ( )
532+ . ok_or_else ( || VmError :: HostError ( format ! ( "io path '{}' has no parent" , path. display( ) ) ) ) ?;
533+ let file_name = path. file_name ( ) . ok_or_else ( || {
534+ VmError :: HostError ( format ! ( "io path '{}' has no file name" , path. display( ) ) )
535+ } ) ?;
536+ parent
537+ . canonicalize ( )
538+ . map ( |parent| parent. join ( file_name) )
539+ . map_err ( |error| VmError :: HostError ( format ! ( "io path resolution failed: {error}" ) ) )
540+ }
541+
450542fn spawn_shell_command ( command : & str , mode : & str ) -> VmResult < Child > {
451543 let mut process = if cfg ! ( windows) {
452544 let mut cmd = Command :: new ( "cmd" ) ;
@@ -875,7 +967,37 @@ fn terminate_process_tree(process_id: u32) -> VmResult<()> {
875967 ) ) )
876968}
877969
878- fn read_line_from_reader ( reader : & mut impl Read ) -> VmResult < String > {
970+ fn read_to_string_with_limit (
971+ reader : & mut impl Read ,
972+ max_read_bytes : Option < usize > ,
973+ out : & mut String ,
974+ ) -> VmResult < ( ) > {
975+ match max_read_bytes {
976+ None => {
977+ reader
978+ . read_to_string ( out)
979+ . map_err ( |err| VmError :: HostError ( format ! ( "io_read_all failed: {err}" ) ) ) ?;
980+ }
981+ Some ( limit) => {
982+ let take_limit = u64:: try_from ( limit) . unwrap_or ( u64:: MAX ) . saturating_add ( 1 ) ;
983+ reader
984+ . take ( take_limit)
985+ . read_to_string ( out)
986+ . map_err ( |err| VmError :: HostError ( format ! ( "io_read_all failed: {err}" ) ) ) ?;
987+ if out. len ( ) > limit {
988+ return Err ( VmError :: HostError ( format ! (
989+ "io_read_all exceeds the configured read limit of {limit} bytes"
990+ ) ) ) ;
991+ }
992+ }
993+ }
994+ Ok ( ( ) )
995+ }
996+
997+ fn read_line_from_reader (
998+ reader : & mut impl Read ,
999+ max_read_bytes : Option < usize > ,
1000+ ) -> VmResult < String > {
8791001 let mut bytes = Vec :: new ( ) ;
8801002 let mut one = [ 0u8 ; 1 ] ;
8811003 loop {
@@ -886,6 +1008,12 @@ fn read_line_from_reader(reader: &mut impl Read) -> VmResult<String> {
8861008 break ;
8871009 }
8881010 bytes. push ( one[ 0 ] ) ;
1011+ if max_read_bytes. is_some_and ( |limit| bytes. len ( ) > limit) {
1012+ return Err ( VmError :: HostError ( format ! (
1013+ "io_read_line exceeds the configured read limit of {} bytes" ,
1014+ max_read_bytes. expect( "read limit should be present" )
1015+ ) ) ) ;
1016+ }
8891017 if one[ 0 ] == b'\n' {
8901018 break ;
8911019 }
0 commit comments