Skip to content

Cap non-editor unbounded async_fs reads (APP-4801 remainder) - #15450

Open
warp-agent-staging[bot] wants to merge 1 commit into
masterfrom
factory/app-4801-cap-non-editor-reads
Open

Cap non-editor unbounded async_fs reads (APP-4801 remainder)#15450
warp-agent-staging[bot] wants to merge 1 commit into
masterfrom
factory/app-4801-cap-non-editor-reads

Conversation

@warp-agent-staging

Copy link
Copy Markdown
Contributor

Description

APP-4801 documents a jemalloc-symbolized heap profile pattern (async_fs::read_to_string -> std::fs::read_to_string -> String::try_reserve_exact) where reading a pathologically large file reserves its entire on-disk size (tens of GiB) in one shot, tripping the "Excessive memory usage detected" Sentry alert (issue 7259255054).

History: #15052 originally implemented this ticket end-to-end, including the crates/warp_files/FileModel editor-buffer read path. #15052 was closed unmerged: another factory run consolidated the FileModel editor path into #15038 (fix: cap unbounded file reads in FileModel to prevent multi-GB memory spikes, APP-4519, branch oz/app-5343-cap-file-read-size), which is still an open draft. #15038's own closing comment on #15052 noted that #15052 "also caps several unrelated read paths ... that are outside APP-4519's scope and not covered by #15038 ... worth re-proposing separately (APP-4801) if still wanted." This PR is that re-proposal: it carries forward only the sites #15038 does not cover.

crates/warp_files/FileModel (the editor buffer load path) is intentionally NOT included here — that's #15038's territory. Do not re-add it in this PR.

Linked Issue

Linear: APP-4801
Closed predecessor: #15052

  • Filed by the Sentry memory-triage bot; no ready-to-spec/ready-to-implement label applicable.
  • No UI changes; no screenshots needed.

Changes

  • crates/warp_util/src/file.rs: shared read_to_string_capped/read_capped helpers. Each opens the file once and reads at most max_bytes + 1 bytes from that single handle via AsyncReadExt::take, rejecting if that limit is reached; metadata() is still queried but only to pre-size the buffer (itself clamped to the cap), never to decide accept/reject. This avoids the fail-open TOCTOU gap of stat-then-reread-by-path (a file can grow/be atomically replaced between the two calls, or be a FIFO/character device that reports an unrelated stat length).
  • app/src/drive/import/nodes.rs: caps the FileType::Notebook (read_to_string) and FileType::Workflow (read) reads in parse_file at 25 MiB. No server-side Drive upload size limit was found in this repo to align with; picked a cap generous enough for hand-authored markdown/YAML but well below the pathological range.
  • app/src/ai/mcp/file_mcp_watcher.rs: caps MCP config reads (.mcp.json, Claude/Codex/Agents configs) at 1 MiB. An oversized file now returns FileMCPConfigParseOutcome::Error(FileMCPConfigDiagnosticKind::Read), not Missing, so a temporarily-huge config file doesn't look like the user removed their MCP servers.
  • crates/repo_metadata/src/repositories.rs: caps .git gitfile reads (worktrees/submodules) at 1 MiB (sized for Windows long-path installations, whose gitdir: line can run well beyond a few KiB — a tighter cap would make find_git_repo treat a valid worktree/submodule as "not a gitfile" and silently walk past the real repository). An oversized/unreadable file is treated like any other unreadable gitfile (not a valid gitfile — keep walking up).
  • app/src/settings/import/alacritty_parser.rs: caps Alacritty config reads at 1 MiB. An oversized file maps to ConfigError::FileIOError, not FileNotFoundError.
  • app/src/ai/metadata_project_rules.rs: caps project rule file reads at 1 MiB (matching REMOTE_CONTEXT_MAX_FILE_BYTES, used for the same kind of content on the remote path). The local-read loop is extracted into read_local_rule_contents for direct unit testing.
  • app/src/ai/skills/file_watchers/skill_watcher.rs: read_local_project_skill_contents used a synchronous fs::read_to_string with the same unbounded-reservation bug. Made it async and capped it at REMOTE_CONTEXT_MAX_FILE_BYTES (1 MiB) to match the remote read path's existing cap.

Testing

  • Added/carried-over regression tests at every changed call site (warp_util::file, Drive import parse_file, MCP config parsing, find_git_repo, Alacritty config parsing, project rule reading, skill_watcher.rs's local skill read) asserting an oversized file is rejected/skipped rather than read wholesale, and that pre-existing error semantics (e.g. NotFound vs. other IO errors) are preserved.
  • warp_util::file's own tests include a /dev/zero case (character device reporting a stat length of 0 while yielding unbounded data) and a FIFO case (also stat length 0, content that grows past the cap while the read is in progress) — both correctly reject rather than hanging/growing unbounded.
  • ./script/format — clean.
  • cargo clippy --workspace --exclude warp_completer --all-targets --tests -- -D warnings — clean.
  • cargo clippy -p warp --all-targets --tests -- -D warnings — clean.
  • cargo clippy -p warp_completer --all-targets --tests -- -D warnings — clean.
  • cargo nextest run --no-fail-fast -p warp_util -p repo_metadata --features local_fs,test-util — 258/258 pass (2 skipped); targeted regression tests in -p warp — 27/27 pass.
  • cargo nextest run --no-fail-fast -p warp --features local_fs (full crate) — 6566/6569 pass; the 3 failures are pre-existing/environment-only and unrelated to this diff (e.g. an unauthorized sandbox nsc namespace CLI call).
  • Not a UI change, so no visual proof is needed.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-BUG-FIX: Fixed several file-import and config-reading code paths that could reserve gigabytes of memory up front when reading a pathologically large file.

APP-4801 documents a jemalloc-symbolized heap profile pattern
(async_fs::read_to_string -> std::fs::read_to_string ->
String::try_reserve_exact) where reading a pathologically large file
reserves its entire on-disk size (tens of GiB) in one shot, tripping the
"Excessive memory usage detected" Sentry alert (issue 7259255054).

This is the remainder of #15052 (closed unmerged), which was consolidated
into #15038 (fix: cap unbounded file reads in FileModel, APP-4519) for the
editor/FileModel path only. #15038 does not cover the sites below, which
are outside APP-4519's scope; re-proposing them here per #15052's closing
comment.

Added the shared `warp_util::file::read_to_string_capped`/`read_capped`
helpers: open the file once and enforce the byte ceiling during the read
itself (via `AsyncReadExt::take`), never via a separate stat-based
accept/reject decision. This avoids the fail-open TOCTOU gap of a
stat-then-reread-by-path pattern (a file can grow/be atomically replaced
between the two, or be a FIFO/device that reports an unrelated `stat`
length).

Capped call sites, each with a doc comment justifying its cap:
- app/src/drive/import/nodes.rs::parse_file - Drive import Notebook
  (read_to_string) and Workflow (read) reads, 25 MiB.
- app/src/ai/mcp/file_mcp_watcher.rs::parse_mcp_config_file - MCP config
  reads, 1 MiB. Oversized -> FileMCPConfigDiagnosticKind::Read, not Missing.
- crates/repo_metadata/src/repositories.rs::find_git_repo - .git gitfile
  reads, 1 MiB (sized for Windows long-path gitdir lines). Oversized is
  treated like any other unreadable gitfile.
- app/src/settings/import/alacritty_parser.rs::from_file_bounded_depth -
  Alacritty config reads, 1 MiB. Oversized -> ConfigError::FileIOError, not
  FileNotFoundError.
- app/src/ai/metadata_project_rules.rs::read_project_rule_contents - project
  rule file reads, 1 MiB (matches REMOTE_CONTEXT_MAX_FILE_BYTES).
- app/src/ai/skills/file_watchers/skill_watcher.rs::read_local_project_skill_contents -
  local skill file reads (previously synchronous fs::read_to_string with the
  same bug), 1 MiB, matching the remote path's existing cap.

Regression tests carried over for every site above, plus the warp_util
helper's own tests (including a /dev/zero and a FIFO test proving the cap
is enforced independent of what `stat` reports).

Note: `crates/warp_util/src/file.rs` overlaps with #15038, which adds its
own `MAX_LOADABLE_FILE_SIZE_BYTES` / `FileLoadError::TooLarge` there. This
change is additive and independent (new free functions, no changes to
existing types), but the file will conflict textually with #15038 -
flagging so whoever lands second can reconcile easily.

Co-Authored-By: Warp Agent <agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Aug 22, 2026
@warp-agent-staging warp-agent-staging Bot added factory:wilson area:performance:memory Memory usage, allocation, leaks, and memory-bound performance. labels Aug 22, 2026
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

This PR was generated with Warp.

View run View conversation

@warp-agent-staging
warp-agent-staging Bot marked this pull request as ready for review August 22, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:performance:memory Memory usage, allocation, leaks, and memory-bound performance. cla-signed factory:wilson

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants