On 12.08.26 13:29, Denis V. Lunev wrote:
s->reply is documented as protected by s->receive_mutex, but the cookie is cleared without it once the owning request has consumed its chunk. A waiter in nbd_receive_replies() inspects the very same field under the mutex, and does so with two separate loads:if (s->reply.cookie != 0) { ind2 = COOKIE_TO_INDEX(s->reply.cookie); assert(!s->requests[ind2].receiving); Nothing keeps those two loads consistent. If the owner clears the cookie in between, the second one reads 0, COOKIE_TO_INDEX() turns it into an index of -1, and s->requests[] is accessed out of bounds: Assertion `!s->requests[ind2].receiving' failed. (gdb) p cookie $1 = 8 (gdb) p s->reply.cookie $2 = 0 (gdb) p &((NBDClientRequest *)s->requests)[-1].receiving $3 = (_Bool *) 0x5555558416c0 (gdb) p &s->in_flight $4 = (unsigned int *) 0x5555558416c0 (gdb) p s->in_flight $5 = 8 The cookie we wait for is 8, yet reply.cookie reads 0 one line after it was found non-zero, so the index is -1. requests[-1].receiving lands on in_flight, which is non-zero while requests are outstanding, and that is what the assertion trips over. Hitting this requires two coroutines of one NBD node to run in different threads, as there is no yield point between the two loads for the owner to squeeze into. A multiqueue configuration provides exactly that, with the virtqueues of one disk spread over several iothreads. Note that a compiler is free to merge the two loads into one, in which case the race is invisible, so builds with reduced optimization are much more likely to trip over it. Accessing s->reply without the mutex is fine for the coroutine that owns the reply: a non-zero cookie makes the field private to it. Releasing that ownership is not, as it races with the waiters which are explicitly allowed to look at the cookie. Clear it under the mutex, in the same critical section as the wakeup, and make nbd_recv_coroutines_wake() caller-locked, as CoMutex is not recursive. It has a single caller. The added acquisition cannot block behind the header read in nbd_receive_replies(), because that path is only reachable with reply.cookie == 0 while we still own a non-zero cookie. Merging the clear with the wakeup also keeps a newcomer from starting a header read in between, which would stall this already completed request for the duration of that read. There is no cookie to own when we get here after an error, and then a newcomer can indeed be inside that read. It does not hold us for long either, as the channel has been shut down before the error was reported, so the read it sits in returns right away. Fixes: 4ddb5d2fde ("block/nbd: drop connection_co") Signed-off-by: Denis V. Lunev<[email protected]> Cc: Eric Blake<[email protected]> Cc: Vladimir Sementsov-Ogievskiy<[email protected]>
Reviewed-by: Vladimir Sementsov-Ogievskiy <[email protected]> -- Best regards, Vladimir
