Fix crash in nested decoder reuse when codingPath is shorter than expected#5
Open
someotherkyle wants to merge 2 commits into
Open
Fix crash in nested decoder reuse when codingPath is shorter than expected#5someotherkyle wants to merge 2 commits into
someotherkyle wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a bounds check in reuseNestedDecoder to prevent a fatal crash (Range requires lowerBound <= upperBound) when a
cached nested decoder's codingPath is shorter than the parent decoder's codingPath.count.
Problem
reset() calls codingPath.replaceSubrange(codingPathIndexToReplace..., with:) without verifying that
codingPathIndexToReplace <= codingPath.count. When the reusable nested decoder's codingPath has been shortened by a prior
decode cycle — specifically through the nil-key path in singleValue's compoundValue(for: nil) which calls removeSubrange
— the index can exceed the array bounds, producing a fatal error.
This occurs in production when decoding deeply nested structures (7+ levels) where:
truncates the cached decoder's codingPath via removeSubrange
exceeds its codingPath.count
The crash is intermittent because it depends on the specific nesting depth, the presence/absence of optional compound
fields, and whether the decoder reuse chain was populated by prior elements with different depth characteristics.
Stack trace from production
Fatal error: Range requires lowerBound <= upperBound
frame #3: PartialRangeFrom<τ_0_0>.relative(to:)
frame #4: RangeReplaceableCollection.replaceSubrange(_:with:)
frame #5: MessagePackValueToSwiftValueDecoder.reset(codingPathIndexToReplace=7)
frame #6: MessagePackValueToSwiftValueDecoder.reuseNestedDecoder(codingKey="binloc")
frame #7: MessagePackValueToSwiftValueDecoder.compoundValue<String, String>
Fix
A single guard condition added to reuseNestedDecoder in MessagePackValueToSwiftValueDecoder.swift:
if let reusableNestedDecoder,
reusableNestedDecoder.codingPath.count >= codingPath.count // added
{
When the cached decoder's codingPath is too short to safely call replaceSubrange(codingPath.count..., ...), the decoder
falls through to create a fresh instance instead of crashing. This trades a small amount of reuse for correctness.
Tests
Added NestedDecoderReuseCodingPathTests.swift with 4 tests exercising the decoder reuse optimization across:
All 175 tests pass (171 existing + 4 new).