Skip to content

fix: limit decompressed size when serving vscode/unpkg web resources - #2060

Merged
netomi merged 5 commits into
mainfrom
fix/web-resource-decompression-limit
Aug 24, 2026
Merged

netomi merged 5 commits into
mainfrom
fix/web-resource-decompression-limit

Conversation

@netomi

@netomi netomi commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

WebResourceService.getWebResource (used by /vscode/unpkg/{namespace}/{extension}/{version}/{path}) extracts a single file from a published VSIX on first request and caches it under java.io.tmpdir. The extraction had no bound on the decompressed size — only the compressed VSIX upload is size-limited (ovsx.publishing.max-content-size, 512 MB by default). A VSIX containing one or more highly compressible entries can therefore expand to a much larger file on disk than the uploaded package size when that entry is requested, and repeating this with different files/versions can exhaust the filesystem backing the temp cache.

Additionally:

  • The web resource cache (ovsx.caching.files-webresource) evicted by entry count, not total size, so it did nothing to bound disk usage from oversized cached files.
  • If extraction failed partway through (e.g. disk full), the partial file was left behind, and FileUtil.writeSync would then treat that leftover file as "already written," permanently blocking retries at that cache path.

Fix

  • WebResourceService.writeBinaryFile can reject an entry whose declared size exceeds ovsx.caching.files-webresource.max-file-size before extracting anything, returning 413 Content Too Large. The actual copy is also bounded by SizeLimitInputStream against the declared size, so an entry with an inconsistent zip header can't produce more bytes than it claimed. The limit is opt-in, disabled by default (-1): /vscode/unpkg serves arbitrary files bundled in a VSIX (e.g. a WASM binary), which can legitimately be much larger than the handful of known-small metadata files ArchiveUtil.MAX_ENTRY_SIZE was designed for, so there's no single default that doesn't risk rejecting something that published successfully. Set a positive byte value to opt into the decompression-bomb protection.
  • FileUtil.writeSync now deletes a partially written file if the writer throws, instead of leaving a truncated file that blocks future attempts at that path. This also benefits the Azure/AWS/GCS storage services, which share this helper.
  • The webResourceCache bean switched from maximumSize (entry count) to maximumWeight, weighing each cached file by its actual byte size via FileSizeWeigher, bounded by ovsx.caching.files-webresource.max-total-size (default 2 GiB). It also still respects ovsx.caching.files-webresource.max-size (entry count, default 150, restored): FileSizeWeigher floors every entry's weight to ceil(max-total-size / max-size), so an entry above that floor is still charged its real size (byte budget holds) while every entry claims at least a 1/max-size share of the budget (entry count can never exceed max-size) — a single maximumWeight cache enforcing both limits at once, including for zero-byte files.

Testing

  • WebResourceServiceTest, FileUtilTest, FileSizeWeigherTest cover: oversized declared entry rejected before extraction, a lying zip header stopped mid-copy with no partial file left behind, normal files served correctly, the size limit disabled by default (a large file no longer rejected), partial-write cleanup and retry in FileUtil.writeSync, and the weigher's floor behavior (small/empty files charged their floor share, large files still charged their real size).
  • Existing VSCodeAPITest, ArchiveUtilTest, and the Azure/AWS/GCS storage test suites still pass. Full adapter/cache/util suites: 88/88.

WebResourceService.writeBinaryFile extracted a VSIX zip entry to disk
with no bound on the decompressed size. A small, highly compressed
VSIX could therefore expand into an oversized file under
java.io.tmpdir when a single entry was requested via /vscode/unpkg/,
and the upload-time size check only limits the compressed package.

- Reject entries whose declared size exceeds a new
  ovsx.caching.files-webresource.max-file-size limit (default 32 MB,
  matching ArchiveUtil.MAX_ENTRY_SIZE) before extracting, and bound
  the actual copy with SizeLimitInputStream so a zip entry with an
  inconsistent header can't produce more bytes than declared.
- FileUtil.writeSync now deletes a partially written file if the
  writer fails, instead of leaving it behind. Previously a failed
  write (e.g. disk full) left a truncated file that permanently
  blocked retries at that cache path, since writeSync only
  (re-)invokes the writer when the file doesn't already exist.
- Switch the web resource cache from counting entries (maximumSize)
  to weighing them by file size (maximumWeight + new FileSizeWeigher),
  configurable via ovsx.caching.files-webresource.max-total-size
  (default 2 GiB), so the cache is bounded by total disk usage rather
  than file count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR mitigates potential disk-exhaustion risks when serving VS Code “unpkg” web resources by bounding per-file extraction size, cleaning up partial writes, and making the on-disk cache size-aware.

Changes:

  • Add a per-entry decompressed size limit when extracting and caching a requested VSIX entry, with a 413 Content Too Large response when the declared size exceeds the configured maximum.
  • Ensure FileUtil.writeSync deletes partially written files when the writer fails, allowing retries and preventing corrupted cache artifacts.
  • Switch the web resource file cache from count-based eviction to byte-size-based eviction using a FileSizeWeigher, with new configuration for maximum total cache size.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
server/src/main/java/org/eclipse/openvsx/adapter/WebResourceService.java Enforces declared-size checks and bounds extraction reads with SizeLimitInputStream to prevent oversized decompression.
server/src/main/java/org/eclipse/openvsx/util/FileUtil.java Deletes partial files on writer failure to avoid poisoning the cache path.
server/src/main/java/org/eclipse/openvsx/cache/CacheConfig.java Changes web resource cache eviction from entry-count to total-bytes using maximumWeight + FileSizeWeigher.
server/src/main/java/org/eclipse/openvsx/cache/FileSizeWeigher.java Implements a Caffeine weigher based on actual on-disk file size.
server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java Updates test wiring for the new WebResourceService constructor parameter.
server/src/test/java/org/eclipse/openvsx/adapter/WebResourceServiceTest.java Adds coverage for oversized declared entries and “lying” zip headers (bounded copy + no partial file left behind).
server/src/test/java/org/eclipse/openvsx/util/FileUtilTest.java Adds coverage for partial-write cleanup and retry behavior.
server/src/test/java/org/eclipse/openvsx/cache/FileSizeWeigherTest.java Adds coverage for size-based weighing behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Caffeine can't combine maximumSize and maximumWeight on one cache, but the
same effect is achievable with a single maximumWeight cache by giving every
entry a floor weight: FileSizeWeigher now charges each cached file at least
ceil(maxTotalSize / maxEntries) of the weight budget, even if its real size
is smaller.

This restores the previously-removed ovsx.caching.files-webresource.max-size
(entry count, default 150) alongside max-total-size (bytes, default 2 GiB)
introduced in this PR, so the cache respects both limits at once: an entry
larger than its floor share is still weighed by its real size (byte budget
holds), while every entry claims at least a 1/maxEntries share of the budget
(entry count can never exceed maxEntries).
@netomi

netomi commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit (452f705) that restores ovsx.caching.files-webresource.max-size (entry count, default 150 — the property this PR removed) alongside the new .max-total-size (bytes, default 2 GiB), so the cache respects both at once.

Caffeine doesn't allow combining maximumSize and maximumWeight on the same cache, but the same effect is achievable with a single maximumWeight cache by giving FileSizeWeigher a floor weight: every cached file is now charged at least ceil(maxTotalSize / maxEntries) of the weight budget, even if its real size is smaller.

  • An entry larger than its floor share is still weighed by its real size, so the byte budget (maxTotalSize) still holds.
  • Every entry claims at least a 1/maxEntries share of the budget, so the entry count can never exceed maxEntries regardless of how small individual files are.

maxEntries <= 0 falls back to floorWeight = 0 (pure byte-weighing, no count constraint) rather than dividing by zero.

Added two tests to FileSizeWeigherTest covering the floor behavior (a tiny file charged its floor share; a file above the floor still weighed by its actual size) and updated the existing ones for the new constructor. Full affected suite (FileSizeWeigherTest, WebResourceServiceTest, VSCodeAPITest, FileUtilTest) still passes — 58/58.

🤖 Generated with Claude Code

ArchiveUtil.MAX_ENTRY_SIZE (32 MB) was designed for the handful of
known-small metadata files (README, package.json, icon, ...) extracted at
publish time. /vscode/unpkg/{namespace}/{extension}/{version}/{path} serves
arbitrary files bundled in the VSIX -- e.g. a WASM binary -- which can
legitimately be much larger and were only ever bounded by the whole-package
upload limit. Reusing that constant here risked rejecting a file that
published successfully.

ovsx.caching.files-webresource.max-file-size now defaults to -1
(unbounded), matching pre-#2060 behavior exactly: no size check, no
SizeLimitInputStream wrapping. Operators who want the protection this PR
introduced (bounding decompressed size against a lying or oversized zip
header) can opt in with a positive byte value, same as before.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

server/src/test/java/org/eclipse/openvsx/cache/FileSizeWeigherTest.java:40

  • This test relies on a hard-coded absolute path (/does/not/exist) to be missing. That can be brittle across environments (different root layouts, Windows, containers) and may accidentally exist; prefer using @TempDir and referencing a guaranteed-nonexistent path under it (e.g., tmpDir.resolve("missing")) to make the test deterministic.
    void testWeighsMissingFileAsMax() {
        var path = Path.of("/does/not/exist");

        assertThat(weigher.weigh("key", path)).isEqualTo(Integer.MAX_VALUE);
    }

server/src/main/java/org/eclipse/openvsx/cache/CacheConfig.java:81

  • The PR description calls this a config rename from ovsx.caching.files-webresource.max-size to ...max-total-size, but the code still reads ...max-size (now as an entry-count limit) and adds ...max-total-size as a new knob. This is not a pure rename and could be a breaking config change for operators; consider introducing a new max-entries property (keeping max-size as a deprecated alias) and/or updating documentation/PR description to match the actual behavior.
            @Value("${ovsx.caching.files-webresource.tti:PT1H}") Duration timeToIdle,
            @Value("${ovsx.caching.files-webresource.max-size:150}") long maxEntries,
            // 2 GiB total; each entry's weight is its file size in bytes rather than a flat 1, so
            // this bounds the cache's disk footprint instead of just the number of cached files.
            @Value("${ovsx.caching.files-webresource.max-total-size:2147483648}") long maxTotalSize

netomi added 2 commits August 24, 2026 14:15
CI intermittently failed with java.lang.OutOfMemoryError: Java heap space
during unrelated Spring test context bootstrapping (e.g. SitemapControllerTest,
JacksonConfigDtoTest) -- not caused by this PR's changes, but the same run
passed twice earlier with identical content, so it's resource pressure on
the existing 4096m ceiling rather than a deterministic bug. The suite caches
many distinct @SpringBootTest/@WebMvcTest contexts, which adds up across the
whole test class list; 4 GB was occasionally too tight for that.
@netomi
netomi requested review from gnugomez and removed request for shblue21 August 24, 2026 14:39
@netomi
netomi merged commit 4a62ae0 into main Aug 24, 2026
5 checks passed
@netomi
netomi deleted the fix/web-resource-decompression-limit branch August 24, 2026 18:35
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.

4 participants