Repository navigation
fix: limit decompressed size when serving vscode/unpkg web resources - #2060
Conversation
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>
There was a problem hiding this comment.
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 Largeresponse when the declared size exceeds the configured maximum. - Ensure
FileUtil.writeSyncdeletes 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).
|
Pushed a follow-up commit (452f705) that restores Caffeine doesn't allow combining
Added two tests to 🤖 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.
There was a problem hiding this comment.
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@TempDirand 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-sizeto...max-total-size, but the code still reads...max-size(now as an entry-count limit) and adds...max-total-sizeas a new knob. This is not a pure rename and could be a breaking config change for operators; consider introducing a newmax-entriesproperty (keepingmax-sizeas 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
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.
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 underjava.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:
ovsx.caching.files-webresource) evicted by entry count, not total size, so it did nothing to bound disk usage from oversized cached files.FileUtil.writeSyncwould then treat that leftover file as "already written," permanently blocking retries at that cache path.Fix
WebResourceService.writeBinaryFilecan reject an entry whose declared size exceedsovsx.caching.files-webresource.max-file-sizebefore extracting anything, returning413 Content Too Large. The actual copy is also bounded bySizeLimitInputStreamagainst 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/unpkgserves arbitrary files bundled in a VSIX (e.g. a WASM binary), which can legitimately be much larger than the handful of known-small metadata filesArchiveUtil.MAX_ENTRY_SIZEwas 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.writeSyncnow 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.webResourceCachebean switched frommaximumSize(entry count) tomaximumWeight, weighing each cached file by its actual byte size viaFileSizeWeigher, bounded byovsx.caching.files-webresource.max-total-size(default 2 GiB). It also still respectsovsx.caching.files-webresource.max-size(entry count, default 150, restored):FileSizeWeigherfloors every entry's weight toceil(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 a1/max-sizeshare of the budget (entry count can never exceedmax-size) — a singlemaximumWeightcache enforcing both limits at once, including for zero-byte files.Testing
WebResourceServiceTest,FileUtilTest,FileSizeWeigherTestcover: 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 inFileUtil.writeSync, and the weigher's floor behavior (small/empty files charged their floor share, large files still charged their real size).VSCodeAPITest,ArchiveUtilTest, and the Azure/AWS/GCS storage test suites still pass. Fulladapter/cache/utilsuites: 88/88.