Re: [PATCH v6] vhost: fix use-after-free in fdset during shutdown

Stephen Hemminger
2026-02-05T01:17:28Z

On Thu, 5 Feb 2026 00:05:02 +0100
Yehor Malikov <malikovyehor@gmail.com> wrote:

> From: Yehor Malikov <Yehor.Malikov@solidigm.com>
>
> The fdset_event_dispatch thread runs in a loop checking the destroy
> flag after each epoll_wait iteration. During process exit,
> rte_eal_cleanup() frees hugepages memory while the fdset thread is
> still running, causing use-after-free when accessing the fdset
> structure.
>
> Add fdset_deinit() function to stop the dispatch thread by setting
> the destroy flag and waiting for thread completion. Resource cleanup
> (epoll fd, fdsets array, memory) is intentionally skipped since the
> function is only called during process exit when the OS will reclaim
> all resources anyway, avoiding potential deadlocks from mutex
> operations in destructor context.
>
> Use symmetric RTE_FINI destructors for both vhost-user and VDUSE fdsets
> to ensure proper cleanup before EAL teardown.
>
> Fixes: e68a6feaa3b3 ("vhost: improve fdset initialization")
>
> Signed-off-by: Yehor Malikov <Yehor.Malikov@solidigm.com>

I understand why this is important but not sure if it will work as
you expect. I asked AI to help give a better explanation (so don't trust it
to be right).

Good question. Yes, there are potential concerns:

**RTE_FINI ordering is undefined.** There's no guarantee about the order destructors run relative to each other or to EAL cleanup. The commit message says the goal is to stop the fdset thread "before EAL teardown" but `RTE_FINI` doesn't actually guarantee that - it just registers an `__attribute__((destructor))` function.

**Possible scenarios:**

1. **EAL cleanup runs first** - If `rte_eal_cleanup()` frees hugepages before the `RTE_FINI` destructor runs, we're back to the original bug. The destructor would then call `fdset_deinit()` on already-freed memory.

2. **Destructor runs during active vhost operations** - If the application is still doing vhost work when exit starts, the destructor could stop the dispatch thread while callbacks are expected to run. The `busy` flag helps with `fdset_del()` synchronization, but abrupt thread termination during shutdown could leave things inconsistent.

3. **Fork concerns** - After `fork()`, the child has the same destructor registered but the dispatch thread doesn't exist in the child (threads aren't inherited). `rte_thread_join()` on an invalid/non-existent thread ID could behave unexpectedly.

**What would be more robust:**

The proper fix would be to integrate with EAL cleanup directly - register a callback via `rte_eal_cleanup_register()` (if that exists) or have vhost explicitly clean up its fdsets in a documented shutdown sequence before `rte_eal_cleanup()` is called, rather than relying on destructor ordering.

Worth raising with the author: has this been tested with different linking scenarios (static vs dynamic) and confirmed the destructor actually runs before EAL teardown in practice?