Wire's protobuf decoders did not consistently validate attacker-controlled length-delimited sizes against the current reader bounds before computing cursor, limit, or pointer positions.
In the Kotlin runtime, ProtoAdapter.decode(ByteArray) and ProtoAdapter.decode(ByteString) use the ProtoReader32 fast path implemented by ByteArrayProtoReader32. In ByteArrayProtoReader32.internalNextLengthDelimited(), Wire read an untrusted varint length into an Int and rejected only negative values. A length such as 2147483647 is non-negative, so it passed that check, but pos + length overflowed the signed 32-bit cursor and produced a negative limit. The following if (limit > pushedLimit) guard did not catch this because the overflowed value was negative.
That invalid limit then reached string, bytes, skip, and scalar-reading paths as an invalid byte count or invalid range. Instead of failing as a checked decode error such as IOException, malformed input could throw unchecked runtime exceptions including IllegalArgumentException and ArrayIndexOutOfBoundsException. Applications commonly treat malformed protobuf input as an expected decode failure; unchecked runtime exceptions escaping that boundary can crash request handling or the process.
The original report is a sibling of the negative-length skipped-group bug fixed as CVE-2026-45799. It is not the same bug. The length in this advisory is positive, and the overflow occurs when setting a length-delimited message limit, not only when skipping a group.
While auditing for the same bug class, related boundary flaws were also found and fixed:
ProtoReader now validates logical message limits before varint, fixed32, fixed64, and skip operations. The originally reported byte-array overflow payload did not reproduce as the same signed overflow in ProtoReader, because that reader tracks positions as Long, but the streaming reader still needed consistent current-message-limit enforcement.ReadBuffer.readVarint() read pointer.pointee before checking that one byte remained. A tag-only varint field could read past the end of the buffer.ReadBuffer.verifyAdditional(count:) formed pointer.advanced(by: count) before proving the requested count fit within the remaining buffer, so pointer arithmetic ran before the bounds were established. (The distinct Swift negative-length skipGroup() crash is tracked separately as GHSA-86wm-r4c5-2rc9 / CVE-2026-61695; this advisory covers the positive/oversized-length boundary failures.)UInt64 varint size to Int without exactness or availability checks. On platforms where the value is not representable, this could trap.The fix enforces a single invariant across the hardened readers: every decoded or skipped byte count must be non-negative and no larger than the remaining bytes in the current logical message limit before any cursor, pointer, limit, allocation, or slice is advanced.
An attacker who can supply protobuf bytes to an application using affected Wire decoders can trigger a denial of service by causing decode to fail with unchecked runtime failures or traps rather than normal malformed-input decode errors.
Known impact:
Attack requirements:
Most directly affected Kotlin entry points:
ProtoAdapter.decode(ByteArray)ProtoAdapter.decode(ByteString)Adjacent Kotlin path hardened by this fix:
ProtoAdapter.decode(BufferedSource)ProtoReaderAffected Swift entry points:
ProtoDecoder and ProtoReader APIs when decoding attacker-controlled Data or buffers.These payloads are intentionally small and should be treated as malformed protobuf input. After the fix, they must fail with normal decode errors such as IOException, EOFException, or ProtoDecoder.Error.unexpectedEndOfData, not unchecked runtime exceptions, traps, out-of-bounds reads, or large allocations.
Hex:
0A FF FF FF FF 07
Meaning:
0A: field 1, length-delimitedFF FF FF FF 07: varint length 2147483647Pre-fix behavior observed through Person.ADAPTER.decode(byteArray):
java.lang.IllegalArgumentException: startIndex: 6 > endIndex: -2147483643
Expected fixed behavior:
IOException / EOFException
Hex:
1A FF FF FF FF 07
Meaning:
1A: field 3, length-delimitedFF FF FF FF 07: varint length 2147483647Pre-fix behavior observed:
ArrayIndexOutOfBoundsException
Expected fixed behavior:
IOException / EOFException
Hex:
0B 0A FF FF FF FF 07 0C
Meaning:
0B: start group, field 10A: nested field 1, length-delimitedFF FF FF FF 07: varint length 21474836470C: end group, field 1Expected fixed behavior:
IOException / EOFException
Hex:
02 0D 05 00 00 00
Meaning:
02: outer length-delimited message length is 2 bytes0D: nested field 1, fixed3205 00 00 00: enough bytes remain in the underlying source, but not inside the current logical message limitExpected fixed behavior:
EOFException
This covers the invariant that scalar reads must not cross the current length-delimited message boundary even when the underlying source has more bytes available.
Hex:
08
Meaning:
08: field 1, varintPre-fix risk:
ReadBuffer.readVarint() could dereference pointer.pointee before verifying that a byte remained.Expected fixed behavior:
ProtoDecoder.Error.unexpectedEndOfData
Hex:
12 FF FF FF FF 07
Meaning:
12: field 2, length-delimitedFF FF FF FF 07: varint length 2147483647Pre-fix risk:
Expected fixed behavior:
ProtoDecoder.Error.unexpectedEndOfData
Hex:
0A FF FF FF FF 07
Meaning:
0A: field 1, length-delimited packed repeated fieldFF FF FF FF 07: varint length 2147483647Pre-fix risk:
Expected fixed behavior:
ProtoDecoder.Error.unexpectedEndOfData
Hex:
FF FF FF FF FF FF FF FF FF 01
Meaning:
UInt64.maxPre-fix risk:
ProtoDecoder.decodeSizeDelimited(_:from:) converted the untrusted UInt64 to Int without exactness checking.Expected fixed behavior:
ProtoDecoder.Error.unexpectedEndOfData
The vulnerable code mixed three operations that must remain separate:
In the vulnerable paths, step 3 happened before step 2 was complete. For Kotlin ByteArrayProtoReader32, this caused signed integer wraparound in pos + length. For Swift, related pointer and allocation operations could be performed before proving the requested bytes existed.
The fix centralizes checked cursor and pointer advancement.
Kotlin changes:
ByteArrayProtoReader32 now validates constructor invariants for pos and limit.ByteArrayProtoReader32 now uses shared helpers to:
skip,ProtoReader now mirrors the same logical-boundary model for:
Swift changes:
ReadBuffer now computes checked end pointers only after confirming count >= 0 and count <= remaining.ReadBuffer.readVarint() verifies one byte remains before each byte dereference.ReadBuffer.readBuffer(count:), readData(count:), readFixed32(), and readFixed64() compute the checked new pointer before reading and advancing.ProtoReader.beginMessage() validates nested message lengths before storing a message-end pointer.ProtoDecoder.decodeSizeDelimited(_:from:) converts sizes with Int(exactly:) and verifies that the full message bytes exist before constructing a child buffer.Fixed in PR #3635:
25ebcabb9ab7f12d1d77af75ecbc51726fddc015The recommended remediation is to upgrade to a patched release.
Partial mitigations if an immediate upgrade is not possible:
Data directly to affected decoders without an outer size cap and exception/error boundary.These mitigations reduce exposure but do not fully fix the parser bugs.
A crash or error may contain one of the following symptoms when processing malformed protobuf bytes:
IllegalArgumentException: startIndex: 6 > endIndex: -2147483643
ArrayIndexOutOfBoundsException
IndexOutOfBoundsException
unexpected unchecked RuntimeException during ProtoAdapter.decode(ByteArray)
Swift trap during Int conversion from an untrusted protobuf size
Swift unexpected pointer/buffer failure while reading malformed varints or length-delimited values
The absence of these exact messages does not prove safety. Any unchecked exception, trap, or process crash while decoding malformed length-delimited protobuf input should be investigated.
Regression tests added:
ProtoReader32Test.lengthDelimitedRejectsPositiveLengthOverflowProtoReader32Test.fixed32CannotReadPastLengthDelimitedLimitProtoReaderTest.fixed32CannotReadPastLengthDelimitedLimitProtoReaderTests.testReadVarintRejectsMissingValueProtoReaderTests.testNestedMessageRejectsOversizedLengthProtoReaderTests.testPackedRepeatedRejectsOversizedLengthBeforePreallocationProtoDecoderTests.testDecodeSizeDelimitedRejectsUnrepresentableSizeFocused verification command:
./gradlew :wire-runtime:jvmTest :wire-runtime-swift:test
Expected result:
BUILD SUCCESSFUL
This is a distinct vulnerability from the negative-length issues. It is a
different bug class — a positive, non-negative length (for example
2147483647) that passes the existing length < 0 check but still overflows
the signed 32-bit cursor or crosses the current message boundary — and it has
a separate fix (PR #3635, not the negative-length PRs).
CVE-2026-45799 / GHSA-7xpr-hc2w-34m9 fixed the original Kotlin/JVM
negative-length skipped-group crash (Wire 6.3.0). The non-negative
overflow described here was not covered by that check and remained
exploitable through 6.4.4.GHSA-86wm-r4c5-2rc9 / CVE-2026-61695 covers the Swift negative-length
skipGroup() crash (PR #3616). The Swift hardening in this advisory
(PR #3635) instead addresses positive/oversized-length overflow, buffer
over-read, and unrepresentable-size conversions in the Swift readers.{
"cwe_ids": [
"CWE-190"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-17T14:52:52Z",
"nvd_published_at": "2026-09-16T19:17:24Z",
"severity": "HIGH"
}