On 7/16/26 7:13 PM, Stefano Garzarella wrote:
> On Thu, Jul 16, 2026 at 06:39:48PM +0300, Andrey Drobyshev wrote:
>> On 7/16/26 11:57 AM, Stefano Garzarella wrote:
>>> On Tue, Jul 14, 2026 at 06:16:37PM +0300, Andrey Drobyshev wrote:
>>>> vhost_vq_work_queue() only holds the RCU read lock while it dereferences
>>>> vq->worker and queues work on it. vhost_workers_free() however clears
>>>> the vq->worker pointers and immediately frees the workers, without
>>>> waiting for a grace period. A caller that fetched the worker right
>>>> before the pointer was cleared can therefore still be queueing work on
>>>> it while it is freed. And even when the queueing itself wins the race,
>>>> the work is never run, so its VHOST_WORK_QUEUED bit stays set and all
>>>> future attempts to queue it are silently skipped.
>>>>
>>>> None of the current callers can actually hit this: net and scsi stop
>>>> their virtqueues before the workers are freed, and vsock unhashes the
>>>> device and does synchronize_rcu() of its own in vhost_vsock_dev_release()
>>>> before the workers go away. But the upcoming VHOST_RESET_OWNER support
>>>> in vhost-vsock keeps the device hashed while its workers are freed, so
>>>> the lockless send/cancel paths become able to race with the teardown.
>>>>
>>>> Close this the way vhost_worker_killed() already does: clear the
>>>> vq->worker pointers, wait for a grace period, run whatever the last
>>>> readers may have queued, and only then free the workers. The
>>>> synchronize_rcu() is skipped if the device has no workers, so cleanup of
>>>> devices which never got an owner stays cheap.
>>>>
>>>
>>> Do we need a Fixes tag for this?
>>>
>>
>> I'm guessing it should be:
>>
>> Fixes: 228a27cf78af ("vhost: Allow worker switching while work is queueing")
>>
>>> Thanks for pointing out that the issue wasn't occurring, but I think we
>>> should add it because it's a sneaky problem we discovered by chance.
>>> IMO the code should already have `synchronize_rcu()` after
>>> `rcu_assign_pointer()` loop.
>>>
>>> @Michael, what do you think?
>>>
>>>> Suggested-by: Stefano Garzarella <[email protected]>
>>>> Signed-off-by: Andrey Drobyshev <[email protected]>
>>>> ---
>>>> drivers/vhost/vhost.c | 15 +++++++++++++++
>>>> 1 file changed, 15 insertions(+)
>>>>
>>>> diff --git a/drivers/vhost/vhost.c b/drivers/vhost/vhost.c
>>>> index 4c525b3e16ea..0d1414d40f4e 100644
>>>> --- a/drivers/vhost/vhost.c
>>>> +++ b/drivers/vhost/vhost.c
>>>> @@ -729,6 +729,21 @@ static void vhost_workers_free(struct vhost_dev *dev)
>>>>
>>>> for (i = 0; i < dev->nvqs; i++)
>>>> rcu_assign_pointer(dev->vqs[i]->worker, NULL);
>>>> +
>>>> + /*
>>>> + * vhost_vq_work_queue() reads vq->worker under rcu_read_lock(), so a
>>>> + * caller that fetched a worker before we cleared the pointers above
>>>> + * may still be about to queue work on it. Wait for those RCU readers
>>>> + * to finish before freeing the worker, then run whatever they queued
>>>> + * so nothing is left with VHOST_WORK_QUEUED set. Mirrors
>>>> + * vhost_worker_killed().
>>>> + */
>>>> + if (!xa_empty(&dev->worker_xa)) {
>>>> + synchronize_rcu();
>>>> + xa_for_each(&dev->worker_xa, i, worker)
>>>> + vhost_run_work_list(worker);
>>>> + }
>>>> +
>>>
>>> Following sashiko review [1], I tried to undersand why we need this, but
>>> TBH I'm really confused. That said, this seems wrong also because it
>>> will work only with vhost_tasks, and not with kthreads.
>>>
>>> IIUC vhost_worker_killed() will be called anyway when calling
>>> vhost_worker_destroy(). For vhost_tasks, it will call
>>> vhost_task_do_stop() that calls vhost_task_stop(). This sets
>>> VHOST_TASK_FLAGS_STOP and wait the worker on vtsk->exited before freeing
>>> stuff. The worker breaks the loop and calls vtsk->handle_sigkill() that
>>> is exactly vhost_worker_killed() you mentioned we are mirroring here.
>>>
>>
>> Hmm, are we sure it's the case for our codepath? Looking at the
>> vhost_task loop function:
>>
>>> static int vhost_task_fn(void *data)
>>> {
>>> for (;;) {
>>> if (signal_pending(current)) {
>>> if (get_signal(&ksig))
>>> break;
>>> }
>>> ...
>>> if (test_bit(VHOST_TASK_FLAGS_STOP, &vtsk->flags)) {
>>> __set_current_state(TASK_RUNNING);
>>> break;
>>> }
>>> did_work = vtsk->fn(vtsk->data);
>>> ...
>>> }
>>>
>>> ...
>>>
>>> if (!test_bit(VHOST_TASK_FLAGS_STOP, &vtsk->flags)) {
>>> set_bit(VHOST_TASK_FLAGS_KILLED, &vtsk->flags);
>>> vtsk->handle_sigkill(vtsk->data);
>>> }
>>> ...
>>> }
>>
>> AFAICT, we exit the loop in 2 cases: signal delivery or STOP bit
>> setting. Like you said, STOP is set by vhost_task_stop. E.g. for our
>> RESET_OWNER case:
>>
>> vhost_vsock_reset_owner()
>> vhost_dev_reset_owner()
>> vhost_dev_cleanup()
>> vhost_workers_free()
>> vhost_worker_destroy()
>> vhost_task_stop() // for vhost_task_ops backend
>> set_bit(VHOST_TASK_FLAGS_STOP)
>>
>> So, first of all, actual work by .fn() callback is done after the exit
>> checks, therefore we skip it - no chance to drain there.
>>
>> Secondly, the handle_sigkill() callback is deliberately NOT called in
>> the STOP case and only called on fatal signal delivery. And for
>> vhost_task backend the .handle_sigkill() callback is exactly
>> vhost_worker_killed().
>>
>> So my understanding is: if we only call synchronize_rcu() here and leave
>> this path undrained, then whatever work which was put by send_pkt() for
>> the worker currently being freed - will be lost. Please correct me if
>> I'm wrong.
>
> Yep, your right. But what will be the issue of loosing them?
>
> IIUC we are not loosing any data, just avoiding some works that will be
> handled later when/if will set a new owner.
>
But will it actually be handled?
vhost_transport_send_pkt() // called on every packet send
virtio_vsock_skb_queue_tail(&send_pkt_queue, skb) // add skb to list
vhost_vq_work_queue(&send_pkt_work) // try to arm the work
vhost_worker_queue()
if (!test_and_set_bit(VHOST_WORK_QUEUED, &work->flags)) {
llist_add(&worker->work_list)
}
So send_pkt_queue is a list of skbs, it lives on the vhost_vsock device
state, and survives RESET_OWNER. In that sense you're probably right
that we aren't loosing any data.
There's also send_pkt_work object, also living on the vhost_vsock device
state. So we're accumulating skbs, and then send_pkg_work gets put in
the worker task list - but only if it's NOT already armed in there, i.e.
QUEUED bit is unset. And the bit gets cleared by the workload callback
- for vhost_task backend it's vhost_run_work_list().
The most important thing is WHERE this piece of work is being put. That
is worker->work_list - this list does not survive RESET_OWNER, as we
free the worker in vhost_workers_free().
Now imagine we have RESET_OWNER racing with send_pkt. In
vhost_workers_free() we acquire ptr to a worker but not NULL'ify it yet.
Then on the send_pkt path we arm the send_pkt_work, set the QUEUED bit,
and place it on the work_list of a DYING worker. Then the worker gets
freed. Now we have send_pkt_work (a singleton struct) with QUEUED set
in its flags, and with no worker to walk through this piece of work and
clear this flag. As a result - send_pkt_work can't be placed in the
list of any other worker, because it doesn't pass the "if
(!test_and_set_bit(QUEUED)" check. Thus no new packets can be
processed, and the connection is stalled.
Does this make sense?
>>
>> That said, I agree that vhost_run_work_list() will only work with
>> vhost_task backend, not with kthreads backend. If we do
>> vhost_worker_flush() instead - I guess it'll keep the drain here, yet
>> become backend-agnostic. I.e.:
>>
>>> + if (!xa_empty(&dev->worker_xa)) {
>>> + synchronize_rcu();
>>> + xa_for_each(&dev->worker_xa, i, worker)
>>> + vhost_worker_flush(worker);
>>> + }
>>
>> With the last 2 lines being equivalent to just calling
>> vhost_dev_flush(dev). And once we become backend-agnostic here, I'm
>> guessing the warning reported by Sashiko should be dealt with as well.
>
> I'd avoid `if !xa_empty(&dev->worker_xa)` at all, and call
> synchronize_rcu() in any case.
>
Agreed.
> About vhost_dev_flush(), we are calling it in several places, and maybe
> we should re-check them. E.g. we call in vhost_vsock_flush(), but it's
> also called by vhost_dev_stop(), maybe we can avoid to call
> vhost_vsock_flush() if we call vhost_dev_stop().
>
> I'm not sure we really need another one here, but if you think some
> other works can be queued between the vhost_dev_stop() and the
> synchronize_rcu() we are adding here, then okay, it may have sense.
>
Note that in our particular case we're gonna do:
vhost_workers_free()
vhost_dev_flush() // the flush we're planning to add
xa_for_each(&dev->worker_xa, i, worker)
vhost_worker_destroy(dev, worker)
xa_destroy(&dev->worker_xa)
So we walk through the XArray, destroy workers in it one by one, then
destroy the XArray itself. Then the next time we call
vhost_dev_flush(), e.g. from vhost_dev_stop() or wherever else, it tries
iterating over the XArray which no longer exists - which is gonna be a
no-op.
Now, we can reach vhost_workers_free() via (at least) 2 paths:
RESET_OWNER and device release path. On the former the flush is needed
as I illustrated above. On the latter it's indeed redundant but is
cheap as it's a no-op.
Andrey
> Thanks,
> Stefano
>