Skip to content

Fix crash in nested decoder reuse when codingPath is shorter than expected#5

Open
someotherkyle wants to merge 2 commits into
fumoboy007:mainfrom
someotherkyle:main
Open

Fix crash in nested decoder reuse when codingPath is shorter than expected#5
someotherkyle wants to merge 2 commits into
fumoboy007:mainfrom
someotherkyle:main

Conversation

@someotherkyle
Copy link
Copy Markdown

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:

  • Array iteration reuses decoder chains across elements
  • Optional.init(from:) goes through singleValueContainer → compoundValue(T) → reuseNestedDecoder(for: nil), which
    truncates the cached decoder's codingPath via removeSubrange
  • A subsequent decode at the same or deeper level reuses the shortened decoder with a codingPathIndexToReplace that now
    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:

  • Array elements with optional compound fields and generic Decodable dispatch
  • Deeply nested structures (Page → PageValue → Order → OrderData → Item → fields) mirroring real-world usage
  • Mixed nil/non-nil optional presence across array elements and pages
  • Generic string fields forced through the compoundValue → reuseNestedDecoder path

All 175 tests pass (171 existing + 4 new).

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.

1 participant