Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/ai/blocklist/action_model/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,7 @@ async fn read_binary_file_context(
let content = match read_file_as_binary(path).await {
Ok(content) => content,
Err(FileLoadError::DoesNotExist) => return Ok(BinaryFileReadResult::Missing),
Err(FileLoadError::FileTooLarge { .. }) => return Ok(BinaryFileReadResult::Missing),
Err(FileLoadError::IOError(e)) => return Err(anyhow::anyhow!(e)),
};

Expand Down
42 changes: 42 additions & 0 deletions app/src/code/global_buffer_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use warp_editor::content::buffer::{Buffer, ToBufferCharOffset};
use warp_editor::content::diff::{text_diff, TextDiff};
use warp_editor::content::edit::PreciseDelta;
use warp_editor::content::version::BufferVersion;
use warp_files::MAX_EDITOR_FILE_SIZE;
use warp_util::content_version::ContentVersion;
use warp_util::file::{FileId, FileLoadError, FileSaveError};
use warp_util::host_id::HostId;
Expand Down Expand Up @@ -491,6 +492,30 @@ impl GlobalBufferModel {
is_initial_load: bool,
ctx: &mut ModelContext<Self>,
) {
// Safety-net: reject content that exceeds the editor file size limit.
// The primary guard lives in FileModel (which checks file size before
// reading), but this catches content that arrives via other paths
// (e.g. remote buffers, auto-reload race) (APP-4519).
if content.len() as u64 > MAX_EDITOR_FILE_SIZE {
safe_error!(
safe: ("Refusing to populate buffer: content exceeds size limit"),
full: ("Refusing to populate buffer: content is {} bytes, limit is {} bytes",
content.len(), MAX_EDITOR_FILE_SIZE)
);
ctx.emit(GlobalBufferModelEvent::FailedToLoad {
file_id,
error: Rc::new(FileLoadError::FileTooLarge {
path: self
.file_path(file_id)
.map(Path::to_path_buf)
.unwrap_or_default(),
size: content.len() as u64,
limit: MAX_EDITOR_FILE_SIZE,
}),
});
return;
}

let Some(state) = self.buffers.get_mut(&file_id) else {
return;
};
Expand Down Expand Up @@ -1829,6 +1854,23 @@ impl GlobalBufferModel {
content.len(),
server_version,
);
// Reject oversized remote buffer content (APP-4519).
if content.len() as u64 > MAX_EDITOR_FILE_SIZE {
safe_error!(
safe: ("[remote-buffer] Content exceeds size limit"),
full: ("[remote-buffer] Content is {} bytes, limit is {} bytes",
content.len(), MAX_EDITOR_FILE_SIZE)
);
ctx.emit(GlobalBufferModelEvent::FailedToLoad {
file_id,
error: Rc::new(FileLoadError::FileTooLarge {
path: std::path::PathBuf::new(),
size: content.len() as u64,
limit: MAX_EDITOR_FILE_SIZE,
}),
});
return;
}
let Some(state) = self.buffers.get_mut(&file_id) else {
safe_error!(
safe: ("[remote-buffer] Buffer state missing after OpenBuffer response"),
Expand Down
44 changes: 42 additions & 2 deletions crates/warp_files/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ use watcher::{BulkFilesystemWatcher, BulkFilesystemWatcherEvent};
pub mod text_file_reader;
pub use text_file_reader::{TextFileReadResult, TextFileSegment};

/// Maximum file size (in bytes) that the editor will load into a buffer.
/// Files larger than this are rejected with `FileLoadError::FileTooLarge`
/// to prevent multi-gigabyte memory spikes from tree-sitter parsing and
/// SumTree allocation. 50 MiB is generous for source code and catches
/// accidental opens of binaries, logs, and data dumps.
pub const MAX_EDITOR_FILE_SIZE: u64 = 50 * 1024 * 1024;

#[derive(Debug)]
pub enum FileModelEvent {
FileLoaded {
Expand Down Expand Up @@ -419,6 +426,20 @@ impl FileModel {
let use_individual_watcher = watcher_type == WatcherType::Individual;
let future = ctx.spawn(
async move {
// Check file size before reading to avoid multi-gigabyte allocations
// for oversized files (APP-4519).
if let Ok(metadata) = async_fs::metadata(&file_path_buf).await {
if metadata.len() > MAX_EDITOR_FILE_SIZE {
return (
file_id,
Err(FileLoadError::FileTooLarge {
path: file_path_buf,
size: metadata.len(),
limit: MAX_EDITOR_FILE_SIZE,
}),
);
}
}
let contents = async_fs::read_to_string(&file_path_buf)
.await
.map_err(FileLoadError::from);
Expand Down Expand Up @@ -472,8 +493,15 @@ impl FileModel {
}

pub async fn read_content_for_file(file_path: &Path) -> Result<String, FileLoadError> {
if !Self::file_exists(file_path).await {
return Err(FileLoadError::DoesNotExist);
let metadata = async_fs::metadata(file_path)
.await
.map_err(|_| FileLoadError::DoesNotExist)?;
if metadata.len() > MAX_EDITOR_FILE_SIZE {
return Err(FileLoadError::FileTooLarge {
path: file_path.to_path_buf(),
size: metadata.len(),
limit: MAX_EDITOR_FILE_SIZE,
});
}
async_fs::read_to_string(file_path)
.await
Expand Down Expand Up @@ -1109,6 +1137,18 @@ impl FileModel {
async move {
let mut res = Vec::new();
for file_path in matching_files {
// Skip files exceeding the editor size limit to avoid
// memory spikes during auto-reload (APP-4519).
let too_large = async_fs::metadata(&file_path)
.await
.is_ok_and(|m| m.len() > MAX_EDITOR_FILE_SIZE);
if too_large {
log::warn!(
"Skipping auto-reload for oversized file: {}",
file_path.display()
);
continue;
}
if let Ok(content) = async_fs::read_to_string(&file_path).await {
res.push((file_path, content));
}
Expand Down
6 changes: 6 additions & 0 deletions crates/warp_util/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ pub enum FileSaveError {
pub enum FileLoadError {
#[error("File does not exist")]
DoesNotExist,
#[error("File is too large to open in the editor ({size} bytes, limit is {limit} bytes)")]
FileTooLarge {
path: PathBuf,
size: u64,
limit: u64,
},
#[error("IO error when loading file.")]
IOError(#[from] io::Error),
}
Expand Down