patch: guard against crafted patches that panic the applier#32668
Conversation
A file-creation hunk header with no body (e.g. @@ -0,0 +0,0 @@) parses to a Hunk with an empty parts vec, and apply() indexed parts[0] unconditionally. Use .first() and treat empty parts the same as a missing hunk: the newly-created file stays empty. Separately, the Vec capacity estimate in apply_patch() computed file_lines + patched_len - original_len via i64 and unwrapped the usize conversion. A patch whose header claims to delete more lines than the target file actually has drove that negative and panicked before the real bounds checks ran. The result is only a capacity hint, so use saturating arithmetic; the existing per-part bounds checks then surface the mismatch as EINVAL.
|
Updated 10:17 AM PT - Jun 24th, 2026
❌ @robobun, your commit a125b21 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 32668That installs a local version of the PR into your bun-32668 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
WalkthroughTwo safety fixes in the Rust patch engine: ChangesMalformed patch safety fixes
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/bun/patch/patch.test.ts`:
- Line 545: The test fixture setup still uses tempDirWithFiles for directories
that need disposal, so those temp directories can leak in persistent runners.
Update the affected cases in patch.test.ts to use tempDir with using for the
fixture directory, and when spawning the process pass String(dir) so the
temporary path is used correctly. Make the same change in the other matching
test case as well, keeping the cleanup behavior aligned with tempDir and the
existing test helpers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4661a69e-e766-4a78-8352-3214731e3ed4
📒 Files selected for processing (2)
src/patch/lib.rstest/js/bun/patch/patch.test.ts
|
Linked #21932 in the description: that crash is Resolved the coderabbit note about CI status: the diff is green; |
There was a problem hiding this comment.
LGTM — minimal defensive hardening that swaps two panic paths for graceful handling, with regression tests for both.
Extended reasoning...
Overview
This PR touches two files: src/patch/lib.rs (the Rust patch applier used by bun install for patchedDependencies) and test/js/bun/patch/patch.test.ts. The Rust change is two surgical edits: (1) replace unconditional hunk.parts[0] indexing in the FileCreation arm with hunk.parts.first() + a let-else continue, so a zero-body @@ -0,0 +0,0 @@ hunk yields an empty file instead of an index-out-of-bounds panic; (2) replace the usize::try_from(i64 - i64).unwrap() capacity precomputation in apply_patch with saturating_add/saturating_sub, so a header that overstates deletions clamps to 0 instead of panicking on negative-to-usize conversion. Two new subprocess tests cover each case.
Security risks
None introduced — this is hardening that removes two DoS-via-panic vectors reachable from untrusted patchedDependencies input. The file-creation path still goes through the existing is_safe_patch_path guard before openat, and the empty-parts continue lands after the file is already created/truncated, which is the correct semantics for a 0-line "new file" hunk. The saturated lines_count is used only as a Vec::with_capacity hint; correctness is enforced by the per-part bounds checks that already return EINVAL during the splice loop.
Level of scrutiny
Low. Both edits are behavior-preserving for well-formed patches: .first() is identical to [0] when the vec is non-empty, and saturating arithmetic is identical to the old i64 path when no underflow occurs. The only new behavior is on previously-panicking inputs, where we now get an empty file or a catchable EINVAL.
Other factors
All prior review threads are resolved (CodeRabbit withdrew its tempDir suggestion; my earlier cargo fmt nit was addressed by the autofix commit b74309c). The bug-hunting system found nothing. The musl CI build failures point at scripts/build/ci.ts build infra, not at this change, and a retrigger commit is already on the branch. No CODEOWNERS entry covers src/patch/.
Repro
Two inputs crash
bun install(andpatchInternals.apply) on current canary:Both patches parse successfully (they pass
verify_integrity), so they reachPatchFile::apply. A maliciouspatchedDependenciesentry can crashbun installthis way.Cause
PatchFile::applyforFileCreationindexedhunk.parts[0]unconditionally. A header like@@ -0,0 +0,0 @@with no body produces a hunk whosepartsvec is empty (the integrity check passes because both lengths are 0).apply_patchprecomputed the capacity for thelinesvec asusize::try_from(i64(count) + patched_len - original_len).unwrap(). When the header'soriginal.lenexceeds the actual file's line count, that goes negative and theusizeconversion panics before the real per-part bounds checks run.Fix
hunk.parts.first()and treat an empty parts vec the same ashunk: None: the newly-created file stays empty.lines_countvalue is only aVec::with_capacityhint; switch to saturating arithmetic so a lying header clamps to 0 instead of panicking. The existing bounds checks during the splice then surface the mismatch as a catchableEINVAL.Verification
bun bd test test/js/bun/patch/patch.test.ts: 25 pass, including the two newapply > malformed patches do not crash > ...cases. Both new tests fail withexitCode: 132/ panic output underUSE_SYSTEM_BUN=1.test/regression/issue/patch-bounds-check.test.tsstill passes (3/3).Fixes #21932 (crash in
PatchFile.applyduringbun installwhen apatchedDependenciesentry targets a package version whose files no longer match the hunk headers).