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

Reply via email to