From 5e6c023a796a21594064d06421c3bad3cef0a470 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Fri, 9 Oct 2026 12:58:35 -0300 Subject: [PATCH] fix(fs): cap the buffer allocated by the read command (#3626) In plugins/fs/src/commands.rs read, `vec![0; len]` used the length sent by the webview as is, so a huge `len` aborted the process on allocation failure, and the whole buffer (not only the bytes read) was sent back. The length is now capped to the rest of the file (at least 64 KiB) for regular files and to 64 MiB otherwise, the allocation uses try_reserve so a failure is an error, and the buffer is truncated to the bytes read. The JS side only ever used the first `nread` bytes. Unit test for the cap. --- .changes/fix-fs-read-allocation.md | 6 +++ plugins/fs/src/commands.rs | 66 ++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 .changes/fix-fs-read-allocation.md diff --git a/.changes/fix-fs-read-allocation.md b/.changes/fix-fs-read-allocation.md new file mode 100644 index 000000000..d1005752f --- /dev/null +++ b/.changes/fix-fs-read-allocation.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +`FileHandle.read` no longer allocates a buffer of the requested size up front: the length is capped to the rest of the file (64 MiB for files of unknown size), an allocation failure is reported as an error instead of aborting the app, and only the bytes that were read are sent back to the webview. diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index dff4e6a9f..083f07254 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -513,10 +513,21 @@ pub async fn read( rid: ResourceId, len: usize, ) -> CommandResult { - let mut data = vec![0; len]; let file: std::sync::Arc> = webview.resources_table().get(rid)?; - let nread = StdFileResource::with_lock(&file, |file| file.read(&mut data)) - .map_err(|e| format!("faied to read bytes from file with error: {e}"))?; + let (nread, mut data) = StdFileResource::with_lock(&file, |file| { + // `len` comes from the webview: do not blindly allocate it + let len = capped_read_len(file, len); + let mut data = Vec::new(); + // room for the bytes read and the trailing `nread` below + data.try_reserve_exact(len.saturating_add(8)) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::OutOfMemory, e))?; + data.resize(len, 0); + let nread = file.read(&mut data)?; + // only send the bytes that were read + data.truncate(nread); + std::io::Result::Ok((nread, data)) + }) + .map_err(|e| format!("failed to read bytes from file with error: {e}"))?; // This is an optimization to include the number of read bytes (as bigendian bytes) // at the end of returned vector so we can use `tauri::ipc::Response` @@ -543,6 +554,34 @@ pub async fn read( Ok(tauri::ipc::Response::new(data)) } +/// Size of the buffer allocated by the `read` command when the size of the file is unknown. +const MAX_READ_LEN_UNKNOWN_SIZE: usize = 64 * 1024 * 1024; +/// Minimum size of the buffer allocated by the `read` command for regular files, +/// so a file that grows while it is read is not reported as ended. +const MIN_READ_LEN: usize = 64 * 1024; + +/// Caps the length requested by the `read` command to what can be read from `file`: +/// the rest of the file for regular files, [`MAX_READ_LEN_UNKNOWN_SIZE`] otherwise. +fn capped_read_len(file: &mut File, len: usize) -> usize { + use std::io::Seek; + + let remaining = file + .metadata() + .ok() + .filter(|metadata| metadata.is_file()) + .and_then(|metadata| { + let position = file.stream_position().ok()?; + Some(metadata.len().saturating_sub(position)) + }); + let max = match remaining { + Some(remaining) => usize::try_from(remaining) + .unwrap_or(usize::MAX) + .max(MIN_READ_LEN), + None => MAX_READ_LEN_UNKNOWN_SIZE, + }; + len.min(max) +} + async fn read_file_inner( permission: &str, webview: Webview, @@ -1863,6 +1902,27 @@ mod test { assert!(parse_write_file_options(br#"{"append":"yes"}"#).is_err()); } + #[test] + fn read_len_is_capped_to_the_file() { + use super::{capped_read_len, MIN_READ_LEN}; + use std::io::{Seek, SeekFrom}; + + let path = std::env::temp_dir().join(format!( + "tauri-plugin-fs-test-read-len-{}", + std::process::id() + )); + std::fs::write(&path, vec![1u8; MIN_READ_LEN * 2]).unwrap(); + let mut file = std::fs::File::open(&path).unwrap(); + + assert_eq!(capped_read_len(&mut file, 10), 10); + assert_eq!(capped_read_len(&mut file, usize::MAX), MIN_READ_LEN * 2); + file.seek(SeekFrom::End(0)).unwrap(); + assert_eq!(capped_read_len(&mut file, usize::MAX), MIN_READ_LEN); + + drop(file); + let _ = std::fs::remove_file(path); + } + #[test] fn safe_file_path_parse() { use super::SafeFilePath;