Skip to content

fix(scan): cap what a repository archive unpacks to before it reaches tmpfs - #469

Open
deanrie wants to merge 1 commit into
cursor:mainfrom
deanrie:scan-cap-unpacked-archive
Open

deanrie wants to merge 1 commit into
cursor:mainfrom
deanrie:scan-cap-unpacked-archive

Conversation

@deanrie

@deanrie deanrie commented Oct 24, 2026

Copy link
Copy Markdown

What

cloneRepo in lib/plugins/scan.ts caps the GitHub tarball download at REPO_ARCHIVE_MAX_BYTES (100 MB). The comment above the constant says why: "Cap how much of the repo archive we materialize on the function's tmpfs", since /tmp is shared and has a ~500 MB limit. Only the compressed bytes are counted, though. The .tar.gz written to disk and the tree tar.x extracts from it are unbounded.

gzip compresses low-entropy data at up to ~1000:1, so a public repo with a few 100 MB files of zeros (GitHub's per-file limit) produces a tarball of a few hundred KB that passes the download cap and the content-length check, then writes hundreds of MB to tmpfs. The ENOSPC that follows is a plain Error, which runSecurityAgent treats as transient and rethrows, so the drain route lets pgmq redeliver the scan up to MAX_ATTEMPTS (5), each attempt filling /tmp again and failing whatever other scan shares the instance. Anyone can submit a plugin that points at such a repo.

Measured with a standalone replica of the pipeline (same stages in the same order) against a local HTTP server:

archive download unpacks to tar 7.5.15 (bun.lock) tar 7.5.22
300 MB of zeros 306 KB 300 MB extracted, 300 MB on disk refused, ratio exactly 1000
300 MB, 3 random bytes per KiB (144:1) 2.1 MB 300 MB extracted, 302 MB on disk extracted, 302 MB on disk

The maxDecompressionRatio guard in later 7.5.x releases is relative, so a 95 MB download at 500:1 would still unpack to ~47 GB. An absolute cap is needed either way.

Fix

The download is decompressed inside the pipeline (createGunzip) under a second byte budget, REPO_UNPACKED_MAX_BYTES (100 MB), and the file written to disk is the plain repo.tar, so extraction cannot grow past it. Peak tmpfs use is at most the two caps together. Exceeding either budget raises UnscannableRepoError, which already maps to the suspicious / repository_clone_failed verdict for manual review, so the scan is not retried. archiveSizeGuard became byteBudget, used for both stages, with the same log line per stage.

With the change, both archives above stop at exactly 100 MB of decompressed bytes with Repository archive unpacks to more than the 104857600-byte scan limit. and the scratch dir is cleaned up. Real tarballs of cursor/plugin-template and xai-org/grok-build-plugin-cc extract with the same file list as before.

The repo has no test runner, so the replica script is not committed, as with #467. tsc --noEmit -p apps/cursor/tsconfig.json shows the same single pre-existing error on this file (platform in AgentOptions, already noted in the source) and nothing new; biome check on the file is clean.

The scan capped the compressed download at 100 MB to bound what it
materializes on the function's /tmp, but the tar it wrote and the tree it
extracted were unbounded. gzip turns a few hundred KB of zeros into
hundreds of MB, so a repository with large low-entropy files passed the
download cap and filled the shared 500 MB tmpfs, and the ENOSPC counted as
retryable, so the queue redelivered the same scan up to five times.

The download is now decompressed in the pipeline under a second budget of
100 MB, and the file written to disk is the plain tar, so extraction cannot
grow past it. Exceeding either budget is an UnscannableRepoError, which
already maps to a suspicious verdict for manual review instead of a retry.
tar 7.5.15, the version in bun.lock, has no decompression guard of its own;
the ratio guard in later 7.5.x releases is relative and lets a 2 MB
download unpack to 300 MB at 144:1, so an absolute cap is still needed.
@vercel

vercel Bot commented Oct 24, 2026

Copy link
Copy Markdown

@deanrie is attempting to deploy a commit to the Anysphere Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant