You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
test(security): real-process regression suite for the bsdtar extraction guard [DEVA11Y-484]
Adds local integration tests (no mocks) that exercise the decompression-bomb
guards against real curl/bsdtar/head and the real Swift watchdog, plus hardens
the guard itself based on what the tests surfaced.
Guard hardening (Plugins/BrowserStackAccessibilityLint.swift):
- The watchdog now also terminates bsdtar on an entry-count ceiling, closing the
"millions of tiny files" bomb that stays small on disk (previously only
locateExecutable caught it, after the fact).
- Added a post-extraction footprint check so detection is deterministic on fast
disks: a bomb that finishes decompressing within a single 200ms poll interval
is now caught and cleaned up rather than slipping past the live watchdog.
- Refactored the guard into a self-contained, marked block of free functions so
it can be mirrored and drift-checked.
Tests (scripts/test/, run via run_tests.sh):
- Shell: extracts the REAL download_binary from bash/zsh/fish verbatim and runs it
against a local server (only the hardcoded URL is redirected, via a curl shim).
- Swift: a mirror harness compiles the guard block verbatim and drives real
curl/bsdtar; check_drift.sh fails CI if the mirror diverges from the plugin
(SwiftPM command plugins can't be imported by a test target).
- Scenarios: legit (downloads/extracts/runs), 400MB bomb, 20k-entry bomb,
oversized (>100MB) download, corrupt archive, multi-file, missing URL.
- Fixtures are bounded (≤400MB, gitignored) and bomb tests use a small cap, so a
regressed guard can never exhaust the disk. Full run ~9s, disk usage flat.
- CI: .github/workflows/extraction-guard-tests.yml runs the suite on macOS for PRs
touching the download/extract path.
53/53 assertions green locally; real production artifact (34MB/64MB) verified to
pass through the new extraction path and run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
// Catch a bomb that completed within a single watchdog poll interval (fast disk).
294
+
if !limitState.exceeded,let reason =footprintExceeded(at: directory, maxBytes:Self.maxDecompressedBytes, maxEntries:Self.maxArchiveEntries){
295
+
limitState.markExceeded(reason)
296
+
}
295
297
if limitState.exceeded {
296
298
try? fileManager.removeItem(at: directory)
297
-
forwardExit(code:1, message:"BrowserStack CLI archive exceeds the maximum allowed decompressed size of \(Self.maxDecompressedBytes /(1024*1024)) MB. Aborting to prevent disk exhaustion.")
299
+
forwardExit(code:1, message:"BrowserStack CLI archive rejected: \(limitState.reason). Aborting to prevent disk exhaustion.")
throwPluginError("Failed to launch bsdtar: \(error.localizedDescription)")
359
328
}
360
329
330
+
// Catch a bomb that completed within a single watchdog poll interval (fast disk).
331
+
if !limitState.exceeded,let reason =footprintExceeded(at: directory, maxBytes:Self.maxDecompressedBytes, maxEntries:Self.maxArchiveEntries){
332
+
limitState.markExceeded(reason)
333
+
}
361
334
if limitState.exceeded {
362
335
try? fileManager.removeItem(at: directory)
363
-
forwardExit(code:1, message:"BrowserStack CLI archive exceeds the maximum allowed decompressed size of \(Self.maxDecompressedBytes /(1024*1024)) MB. Aborting to prevent disk exhaustion.")
336
+
forwardExit(code:1, message:"BrowserStack CLI archive rejected: \(limitState.reason). Aborting to prevent disk exhaustion.")
364
337
}
365
338
366
339
if process.terminationReason !=.exit || process.terminationStatus !=0{
0 commit comments