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 } ;
@@ -204,7 +205,16 @@ pub(super) fn builtin_io_open(
204205 path : & str ,
205206 mode : & str ,
206207) -> VmResult < HostCallResult < i64 > > {
207- let path = path. to_string ( ) ;
208+ let writes = match mode {
209+ "r" => false ,
210+ "w" | "a" | "r+" | "w+" | "a+" => true ,
211+ other => {
212+ return Err ( VmError :: HostError ( format ! (
213+ "unsupported io_open mode '{other}', expected r/w/a/r+/w+/a+"
214+ ) ) ) ;
215+ }
216+ } ;
217+ let path = authorize_io_path ( vm, path, writes) ?;
208218 let mode = mode. to_string ( ) ;
209219 let op_id = schedule_io_task ( vm, None , move || {
210220 let mut options = OpenOptions :: new ( ) ;
@@ -260,6 +270,16 @@ pub(super) fn builtin_io_popen(
260270 "unsupported io_popen mode '{mode}', expected r or w"
261271 ) ) ) ;
262272 }
273+ if vm
274+ . host
275+ . io_policy
276+ . as_ref ( )
277+ . is_some_and ( |policy| !policy. allow_process )
278+ {
279+ return Err ( VmError :: HostError (
280+ "io_popen requires the process capability" . to_string ( ) ,
281+ ) ) ;
282+ }
263283 let command = command. to_string ( ) ;
264284 let mode = mode. to_string ( ) ;
265285 let op_id = schedule_io_task ( vm, None , move || {
@@ -298,23 +318,31 @@ pub(super) fn builtin_io_popen(
298318/// Reads all remaining text from an I/O handle.
299319#[ pd_host_function( name = "io::read_all" ) ]
300320pub ( super ) fn builtin_io_read_all ( vm : & mut Vm , handle_id : i64 ) -> VmResult < HostCallResult < String > > {
321+ let max_read_bytes = vm
322+ . host
323+ . io_policy
324+ . as_ref ( )
325+ . map ( |policy| policy. max_read_bytes ) ;
301326 let handle = resource_handle ( handle_id) ?;
302327 let resource = io_resource_for_handle ( vm, handle) ?;
303328 let op_id = schedule_io_task ( vm, Some ( handle) , move || {
304329 let result = resource. with_handle_mut ( |handle| {
305330 let mut out = String :: new ( ) ;
306331 match handle {
307- IoHandle :: File ( file) => file
308- . read_to_string ( & mut out)
309- . map_err ( |err| VmError :: HostError ( format ! ( "io_read_all failed: {err}" ) ) ) ?,
310- IoHandle :: PopenRead { child } => child
311- . stdout
312- . as_mut ( )
313- . ok_or_else ( || {
314- VmError :: HostError ( "io_read_all popen handle missing stdout" . to_string ( ) )
315- } ) ?
316- . read_to_string ( & mut out)
317- . map_err ( |err| VmError :: HostError ( format ! ( "io_read_all failed: {err}" ) ) ) ?,
332+ IoHandle :: File ( file) => {
333+ read_to_string_with_limit ( file, max_read_bytes, & mut out) ?;
334+ }
335+ IoHandle :: PopenRead { child } => {
336+ read_to_string_with_limit (
337+ child. stdout . as_mut ( ) . ok_or_else ( || {
338+ VmError :: HostError (
339+ "io_read_all popen handle missing stdout" . to_string ( ) ,
340+ )
341+ } ) ?,
342+ max_read_bytes,
343+ & mut out,
344+ ) ?;
345+ }
318346 IoHandle :: PopenWrite { .. } => {
319347 return Err ( VmError :: HostError (
320348 "io_read_all requires a readable handle" . to_string ( ) ,
@@ -334,17 +362,23 @@ pub(super) fn builtin_io_read_line(
334362 vm : & mut Vm ,
335363 handle_id : i64 ,
336364) -> VmResult < HostCallResult < String > > {
365+ let max_read_bytes = vm
366+ . host
367+ . io_policy
368+ . as_ref ( )
369+ . map ( |policy| policy. max_read_bytes ) ;
337370 let handle = resource_handle ( handle_id) ?;
338371 let resource = io_resource_for_handle ( vm, handle) ?;
339372 let op_id = schedule_io_task ( vm, Some ( handle) , move || {
340373 let result = resource. with_handle_mut ( |handle| {
341374 let line = match handle {
342- IoHandle :: File ( file) => read_line_from_reader ( file) ?,
343- IoHandle :: PopenRead { child } => {
344- read_line_from_reader ( child. stdout . as_mut ( ) . ok_or_else ( || {
375+ IoHandle :: File ( file) => read_line_from_reader ( file, max_read_bytes ) ?,
376+ IoHandle :: PopenRead { child } => read_line_from_reader (
377+ child. stdout . as_mut ( ) . ok_or_else ( || {
345378 VmError :: HostError ( "io_read_line popen handle missing stdout" . to_string ( ) )
346- } ) ?) ?
347- }
379+ } ) ?,
380+ max_read_bytes,
381+ ) ?,
348382 IoHandle :: PopenWrite { .. } => {
349383 return Err ( VmError :: HostError (
350384 "io_read_line requires a readable handle" . to_string ( ) ,
@@ -365,6 +399,14 @@ pub(super) fn builtin_io_write(
365399 handle_id : i64 ,
366400 text : & str ,
367401) -> VmResult < HostCallResult < i64 > > {
402+ if let Some ( policy) = vm. host . io_policy . as_ref ( )
403+ && text. len ( ) > policy. max_write_bytes
404+ {
405+ return Err ( VmError :: HostError ( format ! (
406+ "io_write exceeds the configured write limit of {} bytes" ,
407+ policy. max_write_bytes
408+ ) ) ) ;
409+ }
368410 let bytes = text. as_bytes ( ) . to_vec ( ) ;
369411 let handle = resource_handle ( handle_id) ?;
370412 let resource = io_resource_for_handle ( vm, handle) ?;
@@ -445,15 +487,65 @@ pub(super) fn builtin_io_close(vm: &mut Vm, handle_id: i64) -> VmResult<HostCall
445487/// Returns whether a file system path exists.
446488#[ pd_host_function( name = "io::exists" ) ]
447489pub ( super ) fn builtin_io_exists ( vm : & mut Vm , path : & str ) -> VmResult < HostCallResult < bool > > {
448- let path = path . to_string ( ) ;
490+ let path = authorize_io_path ( vm , path , false ) ? ;
449491 let op_id = schedule_io_task ( vm, None , move || {
450- IoAsyncCompletion :: result ( Ok ( CallReturn :: one ( Value :: Bool (
451- std:: path:: Path :: new ( path. as_str ( ) ) . exists ( ) ,
452- ) ) ) )
492+ IoAsyncCompletion :: result ( Ok ( CallReturn :: one ( Value :: Bool ( path. exists ( ) ) ) ) )
453493 } ) ?;
454494 Ok ( HostCallResult :: Pending ( op_id) )
455495}
456496
497+ fn authorize_io_path ( vm : & Vm , path : & str , writes : bool ) -> VmResult < PathBuf > {
498+ let requested = PathBuf :: from ( path) ;
499+ let Some ( policy) = vm. host . io_policy . as_ref ( ) else {
500+ return Ok ( requested) ;
501+ } ;
502+ if writes && !policy. allow_write {
503+ return Err ( VmError :: HostError (
504+ "io path write requires the write capability" . to_string ( ) ,
505+ ) ) ;
506+ }
507+ let absolute = if requested. is_absolute ( ) {
508+ requested
509+ } else {
510+ std:: env:: current_dir ( )
511+ . map_err ( |error| VmError :: HostError ( format ! ( "io path resolution failed: {error}" ) ) ) ?
512+ . join ( requested)
513+ } ;
514+ let canonical = canonicalize_io_target ( & absolute) ?;
515+ for root in & policy. allowed_roots {
516+ let root = Path :: new ( root) . canonicalize ( ) . map_err ( |error| {
517+ VmError :: HostError ( format ! (
518+ "io allowed root '{root}' cannot be resolved: {error}"
519+ ) )
520+ } ) ?;
521+ if canonical. starts_with ( root) {
522+ return Ok ( canonical) ;
523+ }
524+ }
525+ Err ( VmError :: HostError ( format ! (
526+ "io path '{}' is outside the allowed roots" ,
527+ canonical. display( )
528+ ) ) )
529+ }
530+
531+ fn canonicalize_io_target ( path : & Path ) -> VmResult < PathBuf > {
532+ if path. exists ( ) {
533+ return path
534+ . canonicalize ( )
535+ . map_err ( |error| VmError :: HostError ( format ! ( "io path resolution failed: {error}" ) ) ) ;
536+ }
537+ let parent = path
538+ . parent ( )
539+ . ok_or_else ( || VmError :: HostError ( format ! ( "io path '{}' has no parent" , path. display( ) ) ) ) ?;
540+ let file_name = path. file_name ( ) . ok_or_else ( || {
541+ VmError :: HostError ( format ! ( "io path '{}' has no file name" , path. display( ) ) )
542+ } ) ?;
543+ parent
544+ . canonicalize ( )
545+ . map ( |parent| parent. join ( file_name) )
546+ . map_err ( |error| VmError :: HostError ( format ! ( "io path resolution failed: {error}" ) ) )
547+ }
548+
457549fn spawn_shell_command ( command : & str , mode : & str ) -> VmResult < Child > {
458550 let mut process = if cfg ! ( windows) {
459551 let mut cmd = Command :: new ( "cmd" ) ;
@@ -882,7 +974,37 @@ fn terminate_process_tree(process_id: u32) -> VmResult<()> {
882974 ) ) )
883975}
884976
885- fn read_line_from_reader ( reader : & mut impl Read ) -> VmResult < String > {
977+ fn read_to_string_with_limit (
978+ reader : & mut impl Read ,
979+ max_read_bytes : Option < usize > ,
980+ out : & mut String ,
981+ ) -> VmResult < ( ) > {
982+ match max_read_bytes {
983+ None => {
984+ reader
985+ . read_to_string ( out)
986+ . map_err ( |err| VmError :: HostError ( format ! ( "io_read_all failed: {err}" ) ) ) ?;
987+ }
988+ Some ( limit) => {
989+ let take_limit = u64:: try_from ( limit) . unwrap_or ( u64:: MAX ) . saturating_add ( 1 ) ;
990+ reader
991+ . take ( take_limit)
992+ . read_to_string ( out)
993+ . map_err ( |err| VmError :: HostError ( format ! ( "io_read_all failed: {err}" ) ) ) ?;
994+ if out. len ( ) > limit {
995+ return Err ( VmError :: HostError ( format ! (
996+ "io_read_all exceeds the configured read limit of {limit} bytes"
997+ ) ) ) ;
998+ }
999+ }
1000+ }
1001+ Ok ( ( ) )
1002+ }
1003+
1004+ fn read_line_from_reader (
1005+ reader : & mut impl Read ,
1006+ max_read_bytes : Option < usize > ,
1007+ ) -> VmResult < String > {
8861008 let mut bytes = Vec :: new ( ) ;
8871009 let mut one = [ 0u8 ; 1 ] ;
8881010 loop {
@@ -893,6 +1015,12 @@ fn read_line_from_reader(reader: &mut impl Read) -> VmResult<String> {
8931015 break ;
8941016 }
8951017 bytes. push ( one[ 0 ] ) ;
1018+ if max_read_bytes. is_some_and ( |limit| bytes. len ( ) > limit) {
1019+ return Err ( VmError :: HostError ( format ! (
1020+ "io_read_line exceeds the configured read limit of {} bytes" ,
1021+ max_read_bytes. expect( "read limit should be present" )
1022+ ) ) ) ;
1023+ }
8961024 if one[ 0 ] == b'\n' {
8971025 break ;
8981026 }
0 commit comments