@@ -1598,9 +1598,11 @@ pub(crate) fn service_javascript_fs_sync_rpc(
15981598 ) ?;
15991599 record_fs_sync_subphase ( request. method . as_str ( ) , "parse" , phase_start) ;
16001600 let phase_start = Instant :: now ( ) ;
1601+ // Read the byte budget before taking the mutable borrow on `process`.
1602+ let max_filesystem_bytes = kernel. resource_limits ( ) . max_filesystem_bytes ;
16011603 if let Some ( mapped) = process. mapped_host_fd_mut ( fd) {
16021604 record_fs_sync_subphase ( request. method . as_str ( ) , "mapped_fd_match" , phase_start) ;
1603- return write_mapped_host_fd ( mapped, fd, & contents, position) ;
1605+ return write_mapped_host_fd ( mapped, fd, & contents, position, max_filesystem_bytes ) ;
16041606 }
16051607 record_fs_sync_subphase ( request. method . as_str ( ) , "mapped_fd_none" , phase_start) ;
16061608 let phase_start = Instant :: now ( ) ;
@@ -1650,11 +1652,14 @@ pub(crate) fn service_javascript_fs_sync_rpc(
16501652 record_fs_sync_subphase ( request. method . as_str ( ) , "parse" , phase_start) ;
16511653
16521654 let mut total_written = 0usize ;
1655+ // Read the byte budget before taking the mutable borrow on `process`.
1656+ let max_filesystem_bytes = kernel. resource_limits ( ) . max_filesystem_bytes ;
16531657 if let Some ( mapped) = process. mapped_host_fd_mut ( fd) {
16541658 record_fs_sync_subphase ( request. method . as_str ( ) , "mapped_fd_match" , phase_start) ;
16551659 let mut next_position = position;
16561660 for buffer in buffers {
1657- let written = write_all_mapped_host_fd ( mapped, fd, buffer, next_position) ?;
1661+ let written =
1662+ write_all_mapped_host_fd ( mapped, fd, buffer, next_position, max_filesystem_bytes) ?;
16581663 total_written = total_written. saturating_add ( written) ;
16591664 if let Some ( position) = & mut next_position {
16601665 * position = position. saturating_add ( written as u64 ) ;
@@ -4360,12 +4365,45 @@ fn read_mapped_host_fd(
43604365 Ok ( javascript_sync_rpc_bytes_value ( & bytes) )
43614366}
43624367
4368+ /// Enforce the VM's filesystem byte budget on a host-mapped write.
4369+ ///
4370+ /// Writes through a mapped host fd go straight to the host file and never reach
4371+ /// the kernel VFS, so `check_filesystem_usage` never sees them. Without this a
4372+ /// guest writing to a host-backed path (`/tmp`, `/workspace`) could exceed
4373+ /// `limits.resources.maxFilesystemBytes` without bound and fill the host disk
4374+ /// (LT-011 / LT-017). Only runs when a limit is configured, so the common
4375+ /// unlimited case keeps the original fast path.
4376+ fn check_mapped_host_fd_write_budget (
4377+ mapped : & crate :: state:: ActiveMappedHostFd ,
4378+ fd : u32 ,
4379+ len : usize ,
4380+ position : Option < u64 > ,
4381+ max_filesystem_bytes : Option < u64 > ,
4382+ ) -> Result < ( ) , SidecarError > {
4383+ let Some ( limit) = max_filesystem_bytes else {
4384+ return Ok ( ( ) ) ;
4385+ } ;
4386+ let current = mapped. file . metadata ( ) . map ( |meta| meta. len ( ) ) . unwrap_or ( 0 ) ;
4387+ let start = position. unwrap_or ( current) ;
4388+ let resulting = start. saturating_add ( len as u64 ) . max ( current) ;
4389+ if resulting > limit {
4390+ return Err ( SidecarError :: Execution ( format ! (
4391+ "ENOSPC: writing {len} byte(s) to mapped guest fd {fd} would reach {resulting} bytes, \
4392+ exceeding the VM filesystem limit of {limit} (limits.resources.maxFilesystemBytes); \
4393+ raise the limit to store more data"
4394+ ) ) ) ;
4395+ }
4396+ Ok ( ( ) )
4397+ }
4398+
43634399fn write_mapped_host_fd (
43644400 mapped : & mut crate :: state:: ActiveMappedHostFd ,
43654401 fd : u32 ,
43664402 contents : & [ u8 ] ,
43674403 position : Option < u64 > ,
4404+ max_filesystem_bytes : Option < u64 > ,
43684405) -> Result < Value , SidecarError > {
4406+ check_mapped_host_fd_write_budget ( mapped, fd, contents. len ( ) , position, max_filesystem_bytes) ?;
43694407 let written = match position {
43704408 Some ( offset) => mapped. file . write_at ( contents, offset) ,
43714409 None => mapped. file . write ( contents) ,
@@ -4384,7 +4422,9 @@ fn write_all_mapped_host_fd(
43844422 fd : u32 ,
43854423 contents : & [ u8 ] ,
43864424 position : Option < u64 > ,
4425+ max_filesystem_bytes : Option < u64 > ,
43874426) -> Result < usize , SidecarError > {
4427+ check_mapped_host_fd_write_budget ( mapped, fd, contents. len ( ) , position, max_filesystem_bytes) ?;
43884428 let mut total = 0usize ;
43894429 while total < contents. len ( ) {
43904430 let write_position = position. map ( |offset| offset. saturating_add ( total as u64 ) ) ;
0 commit comments