Skip to content

Fix single-AsyncIterator crash in multi-frame gRPC request handlers - #955

Open
erneestoc wants to merge 1 commit into
facebook:mainfrom
erneestoc:fix/grpc-single-async-iterator
Open

Fix single-AsyncIterator crash in multi-frame gRPC request handlers#955
erneestoc wants to merge 1 commit into
facebook:mainfrom
erneestoc:fix/grpc-single-async-iterator

Conversation

@erneestoc

@erneestoc erneestoc commented Aug 24, 2026

Copy link
Copy Markdown

Summary

GRPCAsyncRequestStream is backed by NIOThrowingAsyncSequenceProducer, which fatal-errors if more than one AsyncIterator is ever created from it:

NIOThrowingAsyncSequenceProducer allows only a single AsyncIterator to be created

The AsyncSequence.requiredNext helper (Companion/Utility/AsyncSequence+Extension.swift) reads via first(where:), and every first(where:) call constructs a fresh iterator; a for try await … in requestStream loop makes one too. Any handler that combines two such reads on the same request stream aborts the whole companion on the second read.

Affected RPC handlers (directly, or via the shared MultisourceFileReader):

Handler Second iterator comes from Command
InstallMethodHandler payload frames + trailing for try await … in requestStream idb install
RecordMethodHandler start, then stop idb video / screen recording
LaunchMethodHandler start, then stop idb launch --wait-for
TailMethodHandler start, then stop file tail
XctraceRecordMethodHandler start, then stop idb xctrace record
InstrumentsRunMethodHandler start, then stop idb instruments
DapMethodHandler start, then consumeElements loop idb dap
ReplMethodHandler start, then serve loop idb repl
VideoStreamMethodHandler start, then client-cancel loop idb video-stream
PushMethodHandler / AddMediaMethodHandler MultisourceFileReader reads requiredNext then loops the rest idb file push, idb add-media

Fix

Introduce a shared SingleIteratorRequestStream (Companion/Utility/) that makes one iterator per RPC and exposes next() / requiredNext against it. Each multi-frame handler wraps its requestStream once and routes every read through the wrapper; handlers that delegate to a helper (dap, repl, push, add-media) thread the wrapper into the helper, and MultisourceFileReader now takes the wrapper as well. The trailing for try await request in requestStream loops become while let request = try await stream.next() on the same iterator.

The requiredNext extension is kept for the many genuine single-read handlers, now with a doc-comment warning about the single-shot hazard.

Notes

  • Reproduced with grpc-swift 1.23.1 / current SwiftNIO. The trap is inherent to grpc-swift's single-iterator contract, so this reproduces wherever a recent grpc-swift/NIO is used; please confirm against the internal pin.
  • Verified the companion builds (Xcode 26.2 / Swift 6.2) and that idb video now records a valid MP4 instead of aborting.
  • No behavior change for the single-read handlers (debugserver, hid, video-stream-single-frame paths, etc.).

@meta-cla

meta-cla Bot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @erneestoc!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

grpc-swift's GRPCAsyncRequestStream is backed by
NIOThrowingAsyncSequenceProducer, which fatal-errors if more than one
AsyncIterator is ever created from it:

    NIOThrowingAsyncSequenceProducer allows only a single AsyncIterator
    to be created

The AsyncSequence.requiredNext helper reads via first(where:), and every
first(where:) call makes a new iterator. `for try await ... in requestStream`
likewise makes one. Any handler that combines two such reads on the same
request stream therefore crashes the whole companion on the second read.
This affects, directly or via a shared helper:

  - install               (destination + payload frames)
  - record / `idb video`  (start, stop)
  - launch --wait-for     (start, stop)
  - tail                  (start, stop)
  - xctrace record        (start, stop)
  - instruments run       (start, stop)
  - dap                   (start, then consumeElements loop)
  - repl                  (start, then serve loop)
  - video-stream          (start, then client-cancel loop)
  - push / add-media      (via MultisourceFileReader, which reads
                           requiredNext then loops the remaining frames)

Introduce a shared SingleIteratorRequestStream that owns one iterator per
RPC, and route every multi-frame handler's reads through it. Handlers that
delegate to a helper (dap/repl/push/add-media) thread the wrapper into the
helper; MultisourceFileReader now takes the wrapper too. The trailing
`for try await request in requestStream` loops become
`while let request = try await stream.next()` on the same iterator.
Single-read handlers keep AsyncSequence.requiredNext, now with a
doc-comment warning about the single-shot hazard.

Reproduced with grpc-swift 1.23.1 / current SwiftNIO: the companion aborts
the moment any of these RPCs reads its second frame.
@erneestoc
erneestoc force-pushed the fix/grpc-single-async-iterator branch from 52e4e23 to 5eef451 Compare August 24, 2026 02:14
@meta-cla meta-cla Bot added the CLA Signed label Aug 24, 2026
erneestoc added a commit to erneestoc/rules_idb that referenced this pull request Aug 24, 2026
…handlers

The build patch previously wrapped only Install and Record. grpc-swift's
GRPCAsyncRequestStream fatal-errors on a second AsyncIterator, which every
handler that reads more than one request frame hits -- via requiredNext
twice, or requiredNext plus a `for try await ... in requestStream` loop.

Extend the shared SingleIteratorRequestStream to Launch, Tail, Xctrace,
Instruments, Dap, Repl, and VideoStream, and refactor MultisourceFileReader
to take it too (which is what made Push and AddMedia crash). Move the wrapper
into its own Companion/Utility/SingleIteratorRequestStream.swift and add a
doc-warning to AsyncSequence.requiredNext. Upstreamed as facebook/idb#955.
@erneestoc

erneestoc commented Aug 24, 2026

Copy link
Copy Markdown
Author

@lawrencelomax been working on rules_idb for bazel to allow me to run more than a few concurrent simulators for my test runs (xcodebuild test-without-building is using too much memory for some reason). Let me know if there's something I can help with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant