On Thu, 5 Nov 2026 12:20:26 +0100
Sriram Yagnaraman <[email protected]> wrote:

> The control channel can be torn down from two threads: the EAL interrupt
> thread (memif_intr_handler on peer disconnect/error) and an application
> thread (dev_stop/dev_close -> memif_disconnect). Both race to unregister
> the callback and free the channel, which memif_pmd_autotest SIGSEGVs on
> during EAL teardown (seen on amd64 and arm64).
> 
> Rework teardown around a single owner:
> 
>   - memif_disconnect() clears pmd->cc under cc_lock; whoever clears it
>     owns the free.
>   - Device teardown (link down, flags, queue eventfds, regions) moves to
>     memif_disconnect_queues(), run by whichever thread owns the
>     disconnect, so flag/link state is left consistent for reconnect.
>   - memif_cc_release() frees the channel: unregister_pending() on the
>     interrupt thread (deferred free, drops the fd from epoll after
>     dispatch), unregister_sync() on an application thread.
>   - memif_intr_handler() self-unregisters on disconnect so the EOF
>     (level-triggered) socket fd cannot re-fire (no livelock).
> 
> Reported-by: Stephen Hemminger <[email protected]>
> Suggested-by: Stephen Hemminger <[email protected]>
> Signed-off-by: Sriram Yagnaraman <[email protected]>
> ---

AI found an error here..



[PATCH v2] net/memif: redesign control channel teardown

Error: the application thread no longer waits for the handler once the
handler has taken ownership, so the reported SIGSEGV is still reachable.

  owned = (pmd->cc == cc);
  if (owned)
          pmd->cc = NULL;
  ...
  if (owned)
          memif_disconnect_queues(cc->dev);

Peer hangs up, handler clears pmd->cc and runs
memif_disconnect_queues() after dropping cc_lock. Meanwhile dev_stop
-> memif_disconnect() finds cc == NULL, skips memif_cc_release() and
calls memif_disconnect_queues(dev) concurrently. memif_free_regions()
then does munmap/close/rte_free on the same regions from two threads.
dev_close followed by port release frees dev->data while the handler
is still inside memif_disconnect_queues(cc->dev), which is the original
memif_pmd_autotest crash. The "Idempotent ... safe to run from
whichever thread" comment is not true; idempotent is not thread safe.

Do the device teardown under cc_lock in both paths, and have the
handler touch only cc after it drops the lock:

  /* handler */
  rte_spinlock_lock(&pmd->cc_lock);
  owned = (pmd->cc == cc);
  if (owned) {
          pmd->cc = NULL;
          memif_disconnect_queues(cc->dev);
  }
  rte_spinlock_unlock(&pmd->cc_lock);
  rte_intr_callback_unregister_pending(cc->intr_handle,
          memif_intr_handler, cc,
          owned ? memif_intr_unregister_handler : NULL);

  /* memif_disconnect(), after memif_cc_release() */
  rte_spinlock_lock(&pmd->cc_lock);
  memif_disconnect_queues(dev);
  rte_spinlock_unlock(&pmd->cc_lock);

Error: pmd->cc is set to NULL while the handler may still be running
and the handler dereferences pmd->cc.

  cc = pmd->cc;
  pmd->cc = NULL;

The application thread then waits in rte_intr_callback_unregister_sync()
on the assumption the handler is safe. It is not: memif_msg_receive()
on HELLO/INIT/ADD_REGION/ADD_RING/CONNECT calls memif_msg_enq_ack(),
memif_msg_enq_init(), memif_msg_enq_add_region(),
memif_msg_enq_add_ring(), memif_msg_enq_connect() and
memif_msg_enq_connected(), all of which do

  struct memif_msg_queue_elt *e = memif_msg_enq(pmd->cc);

and memif_msg_enq() does TAILQ_INSERT_TAIL(&cc->msg_queue, ...) on
NULL. Pass the channel down instead of re-reading pmd->cc on the
interrupt thread, e.g. memif_msg_enq_ack(cc) using cc->dev for pmd.
The channel stays valid until unregister_sync() returns.

Error: memif_cc_release() leaks the channel when memif_disconnect()
runs on the interrupt thread for a source that is not dispatching.

  if (rte_thread_is_intr()) {
          ret = rte_intr_callback_unregister_pending(ih,

memif_bad_desc_disconnect() is an EAL alarm, and alarms run on the
interrupt thread. The active source is the alarm timerfd, not the
control socket, so unregister_pending() returns -EAGAIN. pmd->cc is
already NULL, the callback stays registered and cc is never freed.
When the peer later closes, the handler sees owned == false and
passes a NULL free callback, so cc and its fd leak; if the port was
released first, the handler dereferences cc->dev->data after free.
The old code tried rte_intr_callback_unregister() first. Keep that:

  if (rte_thread_is_intr()) {
          ret = rte_intr_callback_unregister(ih,
                          memif_intr_handler, cc);
          if (ret == -EAGAIN)
                  ret = rte_intr_callback_unregister_pending(ih,
                                  memif_intr_handler, cc,
                                  memif_intr_unregister_handler);
          else if (ret > 0)
                  memif_intr_unregister_handler(ih, cc);
  }

Warning: on a protocol error the peer no longer gets the DISCONNECT
message with the reason.

  memif_disconnect_queues(cc->dev);

memif_msg_receive() queues a disconnect ("Invalid message size",
"Unknown message type", "Incorrect secret", ...) and returns -1, which
goes straight to the disconnect label. The old code called
memif_disconnect(), which dropped the other queued messages and sent
the DISCONNECT. The new handler path never calls
memif_msg_send_from_queue(), and memif_intr_unregister_handler() frees
the queued message. The peer now sees a bare EOF. Flush the
disconnect message in the owned branch before the teardown.

Warning: v1 had "Fixes: 09c7e63a71f9" and "Cc: [email protected]"; v2
dropped both. The race is in released versions; restore them.

Info: pre-existing, not introduced by this patch. The single-owner
model needs every write of pmd->cc under cc_lock, but
memif_msg_receive_init() still does

  pmd->cc = cc;

without the lock. A server port being stopped can have a new channel
attached after memif_disconnect() has already looked at pmd->cc.

Reply via email to