mshv_vtl: detach poll waiter on POLLFREE

MSHV_SET_POLL_FILE keeps a poll wait entry after the ioctl returns. A
file reference does not always pin the registered waitqueue:
signalfd_poll() uses current->sighand->signalfd_wqh, whose lifetime is
tied to the task.

signalfd_cleanup() sends POLLFREE before the sighand is freed. The MSHV
callback currently leaves its entry linked, so __wake_up_pollfree()
warns and a later replacement calls remove_wait_queue() through a stale
wqh pointer. A source-equivalent KASAN run reproduced:

  BUG: KASAN: slab-use-after-free in _raw_spin_lock_irqsave
  _raw_spin_lock_irqsave
  remove_wait_queue
  harness_ioctl [mshv_pollfree_harness]

The object belonged to sighand_cache and had been released through
slab_free_after_rcu_debug() after __cleanup_sighand().

On POLLFREE, unlink the entry under the provider waitqueue lock. Preserve
the existing CPU cancellation before publishing a NULL wqh, since a NULL
load lets a racing replacement reuse the persistent poll state. Use a
matching acquire load under RCU when removing a normal registration.

The callback takes no mutex and does not sleep, so the existing
mutex-to-waitqueue-lock order is unchanged.

An LLM assisted the source audit, fix development, and validation of this
issue.

Fixes: 7bfe3b8ea6 ("Drivers: hv: Introduce mshv_vtl driver")
Cc: stable@vger.kernel.org
Signed-off-by: Daehyeon Ko <4ncienth@gmail.com>
Reviewed-by: Naman Jain <namjain@linux.microsoft.com>
Signed-off-by: Wei Liu <wei.liu@kernel.org>
This commit is contained in:
Daehyeon Ko 2026-10-19 17:43:48 +09:00 committed by Wei Liu
parent 60de22022f
commit efc5dc4c15

View File

@ -17,6 +17,7 @@
#include <linux/entry-virt.h>
#include <linux/eventfd.h>
#include <linux/poll.h>
#include <linux/rcupdate.h>
#include <linux/file.h>
#include <linux/vmalloc.h>
#include <asm/debugreg.h>
@ -448,12 +449,35 @@ static void mshv_vtl_cancel(int cpu)
static int mshv_vtl_poll_file_wake(wait_queue_entry_t *wait, unsigned int mode, int sync, void *key)
{
struct mshv_vtl_poll_file *poll_file = container_of(wait, struct mshv_vtl_poll_file, wait);
__poll_t pollflags = key_to_poll(key);
if (pollflags & POLLFREE)
list_del_init(&wait->entry);
mshv_vtl_cancel(poll_file->cpu);
if (pollflags & POLLFREE) {
/* Publish teardown last; pair with acquire in the removal path. */
smp_store_release(&poll_file->wqh, NULL);
}
return 0;
}
static void mshv_vtl_poll_file_remove_wait_queue(struct mshv_vtl_poll_file *poll_file)
{
wait_queue_head_t *wqh;
/* POLLFREE providers RCU-delay the waitqueue free. */
rcu_read_lock();
/* Pair with the release store in the POLLFREE callback. */
wqh = smp_load_acquire(&poll_file->wqh);
if (wqh)
remove_wait_queue(wqh, &poll_file->wait);
rcu_read_unlock();
WRITE_ONCE(poll_file->wqh, NULL);
}
static void mshv_vtl_ptable_queue_proc(struct file *file, wait_queue_head_t *wqh, poll_table *pt)
{
struct mshv_vtl_poll_file *poll_file = container_of(pt, struct mshv_vtl_poll_file, pt);
@ -490,9 +514,7 @@ static int mshv_vtl_ioctl_set_poll_file(struct mshv_vtl_set_poll_file __user *us
mutex_lock(&mshv_vtl_poll_file_lock);
if (poll_file->wqh)
remove_wait_queue(poll_file->wqh, &poll_file->wait);
poll_file->wqh = NULL;
mshv_vtl_poll_file_remove_wait_queue(poll_file);
old_file = poll_file->file;
poll_file->file = file;