usb: let only one party reply for a seqnum - #7
Open
potpiemuncher wants to merge 1 commit into
Open
Conversation
USBIP_CMD_UNLINK and the async IN completion goroutines both race for the right to answer a request, and only the UNLINK handler was claiming it. It looks the sequence up under pendingMu, deletes it, and replies -ECONNRESET only when it was the one to find the entry. Both completion paths deleted unconditionally, discarded the result, and then wrote a RET_SUBMIT regardless; writeRet applies no ownership test of its own. So an UNLINK arriving after a completion goroutine's last context check and before it takes the lock produces two replies for one seqnum: the UNLINK answers -ECONNRESET and cancels, then the completion writes RET_SUBMIT anyway. The client is left holding a completion for a URB it has already unlinked. On the Windows client that is a response for a request the driver no longer owns, which is the input its request-lifetime paths are least robust against. Extracted the removal into claimPending, which returns whether this caller was the one that removed the entry, and routed all three sites through it so the rule lives in one place. The UNLINK handler keeps its existing semantics - deleting a key that is absent is a no-op, so folding its conditional delete into the helper changes nothing - and the two completion paths now return silently when they lose the race. Found by code reading while auditing lifecycle invariants downstream; not reproduced, and the window is narrow. The early-exit paths at the cancelled and timed-out branches were already correct in deleting without writing, which is why this reads as an oversight in the two success paths rather than a missing concept. Tests cover the claim directly, including 64 goroutines contending for one seqnum across 200 rounds under -race, where exactly one must win. Negative control: with the pre-fix semantics restored all four fail, the contention test reporting 64 winners. go build, go vet and the full go test suite pass; gofmt clean.
Owner
|
I have looked into this, and determined that it was due to a process trying to take over the USB stack, the process on my machine was Citrix Workspace. It is a work application, and I've added safeguards into my app to prevent this, but it is definitely still an issue upstream of USBIP |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
USBIP_CMD_UNLINKand the async IN completion goroutines can currently both write a reply for the same seqnum, so a client can receiveRET_UNLINK(-ECONNRESET)andRET_SUBMITfor one request.Found by code reading against
main@308e9b2while auditing lifecycle invariants for a downstream project. Not reproduced — the window is narrow and I have no failing trace. Reporting and fixing it because of what the consequence looks like on the client side, and because the fix is small. (Issues are disabled on this repo, so a PR seemed the right channel — happy to reshape it however you prefer.)The asymmetry
The UNLINK handler already claims ownership properly — it only answers
-ECONNRESETif it was the one to remove the entry, otherwisestatus = 0meaning "already completed":Both async IN completion paths delete unconditionally, discard the result, and write regardless —
server.go:967-971(ISO-IN) and the same shape atserver.go:1013-1018(generic IN).writeRetapplies no ownership test of its own; it serialises onwriteMuand writes aRET_SUBMITfor whatever seqnum it is handed.The interleaving
The ISO-IN goroutine checks
urbCtxat933,949and955, then takespendingMuat967. If an UNLINK arrives in the gap between that last context check and the lock:RET_UNLINK(-ECONNRESET).RET_SUBMIT.Cancelling the context doesn't prevent it, because there's no further
urbCtxcheck after955.Why it seemed worth fixing unreproduced
On the Windows client this lands in
usbip2_ude, and aRET_SUBMITfor a request the driver has already unlinked is a response for a request it no longer owns — the same shape as the defect class behind vadimgrn/usbip-win2#181 (root-caused since to ausbip2_filteroverrun, with separate request-lifetime work in #182). I'm not claiming this path causes those crashes — only that it produces exactly the input those paths are least robust against, and it's cheap not to.The change
Extracted the removal into
claimPending, which reports whether the caller was the one that removed the entry, and routed all three sites through it so the rule lives in one place:The UNLINK handler keeps its exact semantics — deleting an absent key is a no-op, so folding its conditional delete into the helper changes nothing. The two completion paths now return silently when they lose the race, with a debug line.
Credit where it's due
The stranding half is clearly deliberate —
server.go:929carries// Cancellation must never strand a later URB behind this one., and the early-exit paths at935,951and958correctly delete without writing. Only the two success paths write without checking, which is why this reads as an oversight rather than a missing concept.Verification
go build ./...,go vet, fullgo test ./...— all pass;gofmtclean.pending_test.go: four tests covering the claim, including 64 goroutines contending for one seqnum across 200 rounds under-race, where exactly one must win.64 winners, want exactly 1. Restored and re-verified green.