@@ -25,16 +25,29 @@ public class USBSubmitProcessor {
2525 let device : USBDevice ?
2626 }
2727
28- private var activeURBs : [ UInt32 : TrackedURB ] = [ : ]
28+ /// Identifies a request. A sequence number alone does not.
29+ ///
30+ /// USB/IP numbers requests per connection, and the Linux client opens one connection
31+ /// per attached device, so every client starts again at 1. Tracking by bare sequence
32+ /// number put all of them in one namespace: a second client's first request looked
33+ /// like a duplicate of the first client's, and attaching two devices from a single
34+ /// machine was enough to trigger it. Devid distinguishes them, since a connection
35+ /// carries exactly one imported device.
36+ private struct URBKey : Hashable {
37+ let devid : UInt32
38+ let seqnum : UInt32
39+ }
40+
41+ private var activeURBs : [ URBKey : TrackedURB ] = [ : ]
2942
30- /// Sequence numbers accepted and not yet answered, claimed before the URB exists so
31- /// that an UNLINK arriving during setup has something to find.
32- private var reservedSeqnums : Set < UInt32 > = [ ]
43+ /// Requests accepted and not yet answered, claimed before the URB exists so that an
44+ /// UNLINK arriving during setup has something to find.
45+ private var reserved : Set < URBKey > = [ ]
3346
34- /// Sequence numbers the client withdrew. A transfer already in IOKit's hands may
35- /// still complete after the abort, and its result must be dropped rather than sent:
36- /// the client has been told the request is dead and is not expecting a reply.
37- private var cancelledSeqnums : Set < UInt32 > = [ ]
47+ /// Requests the client withdrew. A transfer already in IOKit's hands may still
48+ /// complete after the abort, and its result must be dropped rather than sent: the
49+ /// client has been told the request is dead and is not expecting a reply.
50+ private var cancelled : Set < URBKey > = [ ]
3851 private let urbQueue = DispatchQueue ( label: " com.usbipd.mac.urb " , attributes: . concurrent)
3952
4053 /// Device communicator for executing USB transfers
@@ -106,6 +119,20 @@ public class USBSubmitProcessor {
106119 do {
107120 try validateSubmitRequest ( request)
108121 try await checkConcurrentRequestLimit ( devid: request. devid)
122+
123+ // Claim the request before doing anything that can block.
124+ //
125+ // Registration used to happen after the device had been resolved, which
126+ // means after an IOKit enumeration. A client that submits a read and cancels
127+ // it a millisecond later — probe-rs drains the IN endpoint exactly that way
128+ // before its first command — sent the UNLINK into that window, found no such
129+ // URB, and was told the request had already completed. It had not: it went
130+ // on to run, and later returned data for a request the client had abandoned.
131+ //
132+ // This sits inside the block above so that a rejected claim is answered.
133+ // Thrown from outside it, a duplicate reached ServerCoordinator, which logs
134+ // and sends nothing, and the client waited for a reply that never came.
135+ try await reserveSequenceNumber ( devid: request. devid, seqnum: request. seqnum)
109136 } catch {
110137 logger. warning ( " Rejected SUBMIT request " , context: [
111138 " seqnum " : String ( request. seqnum) ,
@@ -126,19 +153,6 @@ public class USBSubmitProcessor {
126153 )
127154 }
128155
129- // Claim the sequence number before doing anything that can block.
130- //
131- // Registration used to happen after the device had been resolved, which means
132- // after an IOKit enumeration. A client that submits a read and cancels it a
133- // millisecond later — probe-rs drains the IN endpoint exactly that way before
134- // its first command — sent the UNLINK into that window, found no such URB, and
135- // was told the request had already completed. It had not: it went on to run,
136- // and later returned data for a request the client had abandoned.
137- //
138- // Only the sequence number is known this early, which is all the cancel path
139- // needs to reserve the slot; the full URB replaces this entry below.
140- try await reserveSequenceNumber ( request. seqnum)
141-
142156 // Prefer the device's own description of the endpoint. CMD_SUBMIT has no
143157 // transfer-type field, so anything derived from the request alone is a guess —
144158 // and the interval-based guess below misreads every bulk endpoint that
@@ -194,17 +208,17 @@ public class USBSubmitProcessor {
194208 // The data is dropped with it. That is the honest outcome: it belongs to a
195209 // transfer the client cancelled, and handing it to the next read would put
196210 // one command's answer in front of another's.
197- if await wasCancelled ( request. seqnum) {
211+ if await wasCancelled ( devid : request . devid , seqnum : request. seqnum) {
198212 logger. info ( " Discarding result of a cancelled request " , context: [
199213 " seqnum " : String ( request. seqnum) ,
200214 " actualLength " : String ( result. actualLength)
201215 ] )
202- await removeActiveURB ( request. seqnum)
216+ await removeActiveURB ( devid : request . devid , seqnum : request. seqnum)
203217 return Data ( )
204218 }
205219
206220 // Remove URB from tracking
207- await removeActiveURB ( request. seqnum)
221+ await removeActiveURB ( devid : request . devid , seqnum : request. seqnum)
208222
209223 return try USBIPMessageEncoder . encodeUSBSubmitResponse (
210224 seqnum: response. seqnum,
@@ -221,11 +235,11 @@ public class USBSubmitProcessor {
221235 } catch {
222236 // Aborting a pipe makes the transfer in flight fail, so a cancelled request
223237 // usually arrives here rather than above. It is silent for the same reason.
224- if await wasCancelled ( request. seqnum) {
238+ if await wasCancelled ( devid : request . devid , seqnum : request. seqnum) {
225239 logger. info ( " Cancelled request ended without a reply " , context: [
226240 " seqnum " : String ( request. seqnum)
227241 ] )
228- await removeActiveURB ( request. seqnum)
242+ await removeActiveURB ( devid : request . devid , seqnum : request. seqnum)
229243 return Data ( )
230244 }
231245
@@ -238,7 +252,7 @@ public class USBSubmitProcessor {
238252 let errorResponse = createErrorResponse ( from: request, error: error)
239253
240254 // Remove URB from tracking
241- await removeActiveURB ( request. seqnum)
255+ await removeActiveURB ( devid : request . devid , seqnum : request. seqnum)
242256
243257 return try USBIPMessageEncoder . encodeUSBSubmitResponse (
244258 seqnum: errorResponse. seqnum,
@@ -324,12 +338,13 @@ public class USBSubmitProcessor {
324338 /// carries the duplicate check that used to live in `addActiveURB`, which is the
325339 /// right place for it: a repeat of a sequence number is a client error whether or
326340 /// not the first one has finished being prepared.
327- private func reserveSequenceNumber( _ seqnum: UInt32 ) async throws {
341+ private func reserveSequenceNumber( devid : UInt32 , seqnum: UInt32 ) async throws {
328342 try urbQueue. sync ( flags: . barrier) {
329- guard !reservedSeqnums. contains ( seqnum) else {
343+ let key = URBKey ( devid: devid, seqnum: seqnum)
344+ guard !reserved. contains ( key) else {
330345 throw USBRequestError . duplicateRequest
331346 }
332- reservedSeqnums . insert ( seqnum )
347+ reserved . insert ( key )
333348 }
334349 }
335350
@@ -338,34 +353,37 @@ public class USBSubmitProcessor {
338353 // Barrier: this mutates shared state on a concurrent queue. Plain .sync
339354 // lets writers run simultaneously and corrupts the dictionary.
340355 urbQueue. sync ( flags: . barrier) {
341- activeURBs [ urb. seqnum] = TrackedURB ( urb: urb, status: . pending, device: device)
356+ activeURBs [ URBKey ( devid: urb. devid, seqnum: urb. seqnum) ] =
357+ TrackedURB ( urb: urb, status: . pending, device: device)
342358 }
343359 }
344360
345361 /// Whether the client withdrew this request while it was running.
346- func wasCancelled( _ seqnum: UInt32 ) async -> Bool {
347- return urbQueue. sync { cancelledSeqnums . contains ( seqnum) }
362+ func wasCancelled( devid : UInt32 , seqnum: UInt32 ) async -> Bool {
363+ return urbQueue. sync { cancelled . contains ( URBKey ( devid : devid , seqnum: seqnum ) ) }
348364 }
349365
350366 /// Remove URB from active tracking
351- private func removeActiveURB( _ seqnum: UInt32 ) async {
367+ private func removeActiveURB( devid : UInt32 , seqnum: UInt32 ) async {
352368 // Barrier: this mutates shared state on a concurrent queue. Plain .sync
353369 // lets writers run simultaneously and corrupts the dictionary.
354370 urbQueue. sync ( flags: . barrier) {
355- activeURBs. removeValue ( forKey: seqnum)
356- reservedSeqnums. remove ( seqnum)
357- cancelledSeqnums. remove ( seqnum)
371+ let key = URBKey ( devid: devid, seqnum: seqnum)
372+ activeURBs. removeValue ( forKey: key)
373+ reserved. remove ( key)
374+ cancelled. remove ( key)
358375 }
359376 }
360377
361378 /// Update URB status
362- private func updateURBStatus( _ seqnum: UInt32 , status: URBStatus ) async {
379+ private func updateURBStatus( devid : UInt32 , seqnum: UInt32 , status: URBStatus ) async {
363380 // Barrier: this mutates shared state on a concurrent queue. Plain .sync
364381 // lets writers run simultaneously and corrupts the dictionary.
365382 urbQueue. sync ( flags: . barrier) {
366- if var entry = activeURBs [ seqnum] {
383+ let key = URBKey ( devid: devid, seqnum: seqnum)
384+ if var entry = activeURBs [ key] {
367385 entry. status = status
368- activeURBs [ seqnum ] = entry
386+ activeURBs [ key ] = entry
369387 }
370388 }
371389 }
@@ -377,7 +395,7 @@ public class USBSubmitProcessor {
377395 }
378396
379397 // Update URB status
380- await updateURBStatus ( urb. seqnum, status: . inProgress)
398+ await updateURBStatus ( devid : urb . devid , seqnum : urb. seqnum, status: . inProgress)
381399
382400 // Execute based on transfer type
383401 switch urb. transferType {
@@ -567,23 +585,27 @@ public class USBSubmitProcessor {
567585 return urbQueue. sync { activeURBs. count }
568586 }
569587
570- /// Cancel URB by sequence number (for UNLINK support)
571- public func cancelURB( _ seqnum: UInt32 ) async -> Bool {
588+ /// Cancel a request (for UNLINK support).
589+ ///
590+ /// Takes the devid as well as the sequence number: numbering restarts per
591+ /// connection, so a sequence number on its own could name another client's request.
592+ public func cancelURB( devid: UInt32 , seqnum: UInt32 ) async -> Bool {
572593 // Barrier: this mutates shared state on a concurrent queue. Plain .sync
573594 // lets writers run simultaneously and corrupts the dictionary.
574595 let claimed : CancelClaim = urbQueue. sync ( flags: . barrier) {
575596 // A reservation is enough. Requiring an entry in activeURBs meant a request
576597 // still being set up could not be cancelled, and the client was told its URB
577598 // had already completed when in fact it had not started.
578- guard reservedSeqnums. contains ( seqnum) else {
599+ let key = URBKey ( devid: devid, seqnum: seqnum)
600+ guard reserved. contains ( key) else {
579601 return CancelClaim ( reserved: false , urb: nil , device: nil )
580602 }
581- cancelledSeqnums . insert ( seqnum )
582- guard var entry = activeURBs [ seqnum ] else {
603+ cancelled . insert ( key )
604+ guard var entry = activeURBs [ key ] else {
583605 return CancelClaim ( reserved: true , urb: nil , device: nil )
584606 }
585607 entry. status = . cancelled
586- activeURBs [ seqnum ] = entry
608+ activeURBs [ key ] = entry
587609 return CancelClaim ( reserved: true , urb: entry. urb, device: entry. device)
588610 }
589611
0 commit comments