git: a64205b1354d - main - hn: Track VF association and wait for datapath switch completion

From: Kevin Bowling <kbowling_at_FreeBSD.org>
Date: Tue, 15 Sep 2026 02:01:03 UTC
The branch main has been updated by kbowling:

URL: https://cgit.FreeBSD.org/src/commit/?id=a64205b1354d67c280670859c47a18e80f740803

commit a64205b1354d67c280670859c47a18e80f740803
Author:     Kevin Bowling <kbowling@FreeBSD.org>
AuthorDate: 2026-09-14 23:06:15 +0000
Commit:     Kevin Bowling <kbowling@FreeBSD.org>
CommitDate: 2026-09-15 02:00:41 +0000

    hn: Track VF association and wait for datapath switch completion
    
    Handle the host's VF association notifications instead of ignoring them.
    Require an allocated association before switching to the MAC-matched VF,
    and track notification generations so a withdrawal during initialization
    cannot enable an obsolete handoff.  Defer association and address-event
    work to the VF taskqueue; the receive channel must remain available to
    deliver switch completions.
    
    Request and wait for the empty VMBus completion for SET_DATAPATH, checking
    submission and channel-revocation failures.  Enable transparent VF
    transmit and select its link status only after the switch completes.
    Use the existing transaction lifetime and revocation handling, without a
    timeout that could leave a late completion referencing a freed request.
    
    Restore synthetic capabilities, TSO limits and hardware-assist flags on
    fallback, targeting hn rather than the departing VF.  Block transparent
    transmit during handoff and after association withdrawal.  Separate the
    attach delay from saved-setting readiness, permit delayed association to
    trigger initialization, and require a fresh handoff after NVS reattach.
    Stop and detach still close local VF access if returning to the synthetic
    path fails; such failures are logged, not reported as a successful switch.
    
    MFC after:      2 weeks
    Sponsored by:   BBOX.io
---
 sys/dev/hyperv/netvsc/hn_nvs.c   |  48 +++++-
 sys/dev/hyperv/netvsc/hn_nvs.h   |   2 +-
 sys/dev/hyperv/netvsc/if_hn.c    | 333 ++++++++++++++++++++++++++++-----------
 sys/dev/hyperv/netvsc/if_hnreg.h |   8 +-
 sys/dev/hyperv/netvsc/if_hnvar.h |  10 +-
 5 files changed, 300 insertions(+), 101 deletions(-)

diff --git a/sys/dev/hyperv/netvsc/hn_nvs.c b/sys/dev/hyperv/netvsc/hn_nvs.c
index 4cb78e487a41..e5b885f76e64 100644
--- a/sys/dev/hyperv/netvsc/hn_nvs.c
+++ b/sys/dev/hyperv/netvsc/hn_nvs.c
@@ -40,6 +40,7 @@
 #include <sys/socket.h>
 #include <sys/systm.h>
 #include <sys/taskqueue.h>
+#include <machine/atomic.h>
 
 #include <vm/vm.h>
 #include <vm/vm_extern.h>
@@ -741,14 +742,49 @@ hn_nvs_send_rndis_ctrl(struct vmbus_channel *chan,
 	    sndc, gpa, gpa_cnt);
 }
 
-void
+int
 hn_nvs_set_datapath(struct hn_softc *sc, uint32_t path)
 {
-	struct hn_nvs_datapath dp;
+	struct hn_nvs_datapath *dp;
+	struct hn_nvs_sendctx sndc;
+	struct vmbus_xact *xact;
+	size_t resplen;
+	int error;
 
-	memset(&dp, 0, sizeof(dp));
-	dp.nvs_type = HN_NVS_TYPE_SET_DATAPATH;
-	dp.nvs_active_path = path;
+	if (path == HN_NVS_DATAPATH_VF &&
+	    !(atomic_load_acq_int(&sc->hn_vf_assoc) & HN_VF_ASSOC_ALLOCATED))
+		return (EAGAIN);
+
+	xact = vmbus_xact_get(sc->hn_xact, sizeof(*dp));
+	if (xact == NULL) {
+		error = ENXIO;
+		goto failed;
+	}
+	dp = vmbus_xact_req_data(xact);
+	memset(dp, 0, sizeof(*dp));
+	dp->nvs_type = HN_NVS_TYPE_SET_DATAPATH;
+	dp->nvs_active_path = path;
+	hn_nvs_sendctx_init(&sndc, hn_nvs_sent_xact, xact);
+	vmbus_xact_activate(xact);
+	error = hn_nvs_send(sc->hn_prichan, VMBUS_CHANPKT_FLAG_RC,
+	    dp, sizeof(*dp), &sndc);
+	if (error) {
+		vmbus_xact_deactivate(xact);
+	} else {
+		/* No NVS response: the empty VMBus completion confirms the switch. */
+		vmbus_chan_xact_wait(sc->hn_prichan, xact, &resplen,
+		    HN_CAN_SLEEP(sc));
+		if (vmbus_chan_is_revoked(sc->hn_prichan))
+			error = ENXIO;
+		else if (resplen != 0)
+			error = EIO;
+	}
+	vmbus_xact_put(xact);
+	if (error == 0)
+		return (0);
 
-	hn_nvs_req_send(sc, &dp, sizeof(dp));
+failed:
+	if_printf(sc->hn_ifp, "datapath switch to %s failed: %d\n",
+	    path == HN_NVS_DATAPATH_VF ? "VF" : "synthetic", error);
+	return (error);
 }
diff --git a/sys/dev/hyperv/netvsc/hn_nvs.h b/sys/dev/hyperv/netvsc/hn_nvs.h
index 6d37b2b5ab1d..adc4b3b97019 100644
--- a/sys/dev/hyperv/netvsc/hn_nvs.h
+++ b/sys/dev/hyperv/netvsc/hn_nvs.h
@@ -98,7 +98,7 @@ void		hn_nvs_sent_xact(struct hn_nvs_sendctx *sndc,
 int		hn_nvs_send_rndis_ctrl(struct vmbus_channel *chan,
 		    struct hn_nvs_sendctx *sndc, struct vmbus_gpa *gpa,
 		    int gpa_cnt);
-void		hn_nvs_set_datapath(struct hn_softc *sc, uint32_t path);
+int		hn_nvs_set_datapath(struct hn_softc *sc, uint32_t path);
 
 extern struct hn_nvs_sendctx	hn_nvs_sendctx_none;
 
diff --git a/sys/dev/hyperv/netvsc/if_hn.c b/sys/dev/hyperv/netvsc/if_hn.c
index 118f4e42ce4c..a36656b89b76 100644
--- a/sys/dev/hyperv/netvsc/if_hn.c
+++ b/sys/dev/hyperv/netvsc/if_hn.c
@@ -289,8 +289,10 @@ static void			hn_ifnet_lnkevent(void *, if_t, int);
 static bool			hn_ismyvf(const struct hn_softc *,
 				    const if_t);
 static void			hn_rxvf_change(struct hn_softc *,
-				    if_t, bool);
+				    if_t);
 static void			hn_rxvf_set(struct hn_softc *, if_t);
+static void			hn_rxvf_change_locked(struct hn_softc *, if_t,
+				    bool);
 static void			hn_rxvf_set_task(void *, int);
 static void			hn_xpnt_vf_input(if_t, struct mbuf *);
 static int			hn_xpnt_vf_iocsetflags(struct hn_softc *);
@@ -298,10 +300,13 @@ static int			hn_xpnt_vf_iocsetcaps(struct hn_softc *,
 				    struct ifreq *);
 static void			hn_xpnt_vf_saveifflags(struct hn_softc *);
 static bool			hn_xpnt_vf_isready(struct hn_softc *);
+static bool			hn_xpnt_vf_caninit(struct hn_softc *);
 static void			hn_xpnt_vf_setready(struct hn_softc *);
+static void			hn_xpnt_vf_restore(struct hn_softc *);
+static void			hn_xpnt_vf_deactivate(struct hn_softc *);
 static void			hn_xpnt_vf_init_taskfunc(void *, int);
 static void			hn_xpnt_vf_init(struct hn_softc *);
-static void			hn_xpnt_vf_setenable(struct hn_softc *);
+static bool			hn_xpnt_vf_setenable(struct hn_softc *);
 static void			hn_xpnt_vf_setdisable(struct hn_softc *, bool);
 static void			hn_vf_rss_fixup(struct hn_softc *, bool);
 static void			hn_vf_rss_restore(struct hn_softc *);
@@ -1175,11 +1180,25 @@ hn_ismyvf(const struct hn_softc *sc, const if_t ifp)
 }
 
 static void
-hn_rxvf_change(struct hn_softc *sc, if_t ifp, bool rxvf)
+hn_rxvf_change(struct hn_softc *sc, if_t ifp)
+{
+	struct rm_priotracker pt;
+
+	/* Address events can run on the channel that delivers completions. */
+	rm_rlock(&sc->hn_vf_lock, &pt);
+	if (sc->hn_vf_ifp == ifp)
+		taskqueue_enqueue_timeout(sc->hn_vf_taskq, &sc->hn_vf_init, 0);
+	rm_runlock(&sc->hn_vf_lock, &pt);
+}
+
+static void
+hn_rxvf_change_locked(struct hn_softc *sc, if_t ifp, bool rxvf)
 {
 	if_t hn_ifp;
+	u_int assoc, old_flags;
+	int error;
 
-	HN_LOCK(sc);
+	HN_LOCK_ASSERT(sc);
 
 	if (!(sc->hn_flags & HN_FLAG_SYNTH_ATTACHED))
 		goto out;
@@ -1187,6 +1206,10 @@ hn_rxvf_change(struct hn_softc *sc, if_t ifp, bool rxvf)
 	if (!hn_ismyvf(sc, ifp))
 		goto out;
 	hn_ifp = sc->hn_ifp;
+	assoc = atomic_load_acq_int(&sc->hn_vf_assoc);
+	if (rxvf && !(assoc & HN_VF_ASSOC_ALLOCATED))
+		goto out;
+	old_flags = sc->hn_flags;
 
 	if (rxvf) {
 		if (sc->hn_flags & HN_FLAG_RXVF)
@@ -1205,10 +1228,33 @@ hn_rxvf_change(struct hn_softc *sc, if_t ifp, bool rxvf)
 			hn_set_rxfilter(sc, NDIS_PACKET_TYPE_NONE);
 	}
 
-	hn_nvs_set_datapath(sc,
+	/* Prepare receive routing before the host enables the VF path. */
+	if (rxvf)
+		hn_rxvf_set(sc, ifp);
+	error = hn_nvs_set_datapath(sc,
 	    rxvf ? HN_NVS_DATAPATH_VF : HN_NVS_DATAPATH_SYNTH);
+	if (error == 0 && rxvf &&
+	    assoc != atomic_load_acq_int(&sc->hn_vf_assoc)) {
+		hn_nvs_set_datapath(sc, HN_NVS_DATAPATH_SYNTH);
+		error = EAGAIN;
+	}
+	if (error) {
+		sc->hn_flags = old_flags;
+		hn_rxvf_set(sc, (old_flags & HN_FLAG_RXVF) ? ifp : NULL);
+		if ((old_flags & HN_FLAG_RXVF) ||
+		    (if_getdrvflags(hn_ifp) & IFF_DRV_RUNNING))
+			hn_rxfilter_config(sc);
+		else
+			hn_set_rxfilter(sc, NDIS_PACKET_TYPE_NONE);
+		if (error == EAGAIN)
+			taskqueue_enqueue_timeout(sc->hn_vf_taskq,
+			    &sc->hn_vf_init, hz);
+		goto out;
+	}
+	sc->hn_vf_active_assoc = rxvf ? assoc : 0;
 
-	hn_rxvf_set(sc, rxvf ? ifp : NULL);
+	if (!rxvf)
+		hn_rxvf_set(sc, NULL);
 
 	if (rxvf) {
 		hn_vf_rss_fixup(sc, true);
@@ -1229,7 +1275,7 @@ hn_rxvf_change(struct hn_softc *sc, if_t ifp, bool rxvf)
 		    rxvf ? "to" : "from", if_name(ifp));
 	}
 out:
-	HN_UNLOCK(sc);
+	return;
 }
 
 static void
@@ -1238,14 +1284,14 @@ hn_ifnet_event(void *arg, if_t ifp, int event)
 
 	if (event != IFNET_EVENT_UP && event != IFNET_EVENT_DOWN)
 		return;
-	hn_rxvf_change(arg, ifp, event == IFNET_EVENT_UP);
+	hn_rxvf_change(arg, ifp);
 }
 
 static void
 hn_ifaddr_event(void *arg, if_t ifp)
 {
 
-	hn_rxvf_change(arg, ifp, if_getflags(ifp) & IFF_UP);
+	hn_rxvf_change(arg, ifp);
 }
 
 static int
@@ -1638,7 +1684,7 @@ hn_xpnt_vf_setready(struct hn_softc *sc)
 	/*
 	 * Mark the VF ready.
 	 */
-	sc->hn_vf_rdytick = 0;
+	sc->hn_vf_ready = true;
 
 	/*
 	 * Save information for restoration.
@@ -1709,24 +1755,67 @@ hn_xpnt_vf_setready(struct hn_softc *sc)
 static bool
 hn_xpnt_vf_isready(struct hn_softc *sc)
 {
+	u_int assoc;
 
 	HN_LOCK_ASSERT(sc);
 
-	if (!hn_xpnt_vf || sc->hn_vf_ifp == NULL)
-		return (false);
+	assoc = atomic_load_acq_int(&sc->hn_vf_assoc);
+	return (hn_xpnt_vf && sc->hn_vf_ifp != NULL && sc->hn_vf_ready &&
+	    (assoc & HN_VF_ASSOC_ALLOCATED) &&
+	    sc->hn_vf_active_assoc == assoc);
+}
 
-	if (sc->hn_vf_rdytick == 0)
-		return (true);
+static bool
+hn_xpnt_vf_caninit(struct hn_softc *sc)
+{
 
-	if (sc->hn_vf_rdytick > ticks)
-		return (false);
+	HN_LOCK_ASSERT(sc);
+	return (hn_xpnt_vf && sc->hn_vf_ifp != NULL &&
+	    (atomic_load_acq_int(&sc->hn_vf_assoc) & HN_VF_ASSOC_ALLOCATED) &&
+	    (int)(ticks - sc->hn_vf_rdytick) >= 0);
+}
 
-	/* Mark VF as ready. */
-	hn_xpnt_vf_setready(sc);
-	return (true);
+static void
+hn_xpnt_vf_restore(struct hn_softc *sc)
+{
+	if_t ifp = sc->hn_ifp;
+
+	HN_LOCK_ASSERT(sc);
+	if (!sc->hn_vf_ready)
+		return;
+	if_setcapabilities(ifp, sc->hn_saved_caps);
+	if_sethwtsomax(ifp, sc->hn_saved_tsomax);
+	if_sethwtsomaxsegcount(ifp, sc->hn_saved_tsosegcnt);
+	if_sethwtsomaxsegsize(ifp, sc->hn_saved_tsosegsz);
+	if_setcapenable(ifp, sc->hn_saved_capenable);
+	if_sethwassist(ifp, sc->hn_saved_hwassist);
+	sc->hn_vf_ready = false;
 }
 
 static void
+hn_xpnt_vf_deactivate(struct hn_softc *sc)
+{
+
+	HN_LOCK_ASSERT(sc);
+	/* Do not send VF-formatted packets through synthetic during handoff. */
+	rm_wlock(&sc->hn_vf_lock);
+	sc->hn_xvf_flags |= HN_XVFFLAG_SWITCHING;
+	rm_wunlock(&sc->hn_vf_lock);
+	hn_xpnt_vf_setdisable(sc, false);
+	if (sc->hn_vf_active_assoc != 0) {
+		/* Close local VF transmit even if returning to synthetic fails. */
+		hn_nvs_set_datapath(sc, HN_NVS_DATAPATH_SYNTH);
+		sc->hn_vf_active_assoc = 0;
+		hn_vf_rss_restore(sc);
+	}
+	hn_xpnt_vf_restore(sc);
+	rm_wlock(&sc->hn_vf_lock);
+	sc->hn_xvf_flags &= ~HN_XVFFLAG_SWITCHING;
+	rm_wunlock(&sc->hn_vf_lock);
+	hn_resume_mgmt(sc);
+}
+
+static bool
 hn_xpnt_vf_setenable(struct hn_softc *sc)
 {
 	int i;
@@ -1735,11 +1824,17 @@ hn_xpnt_vf_setenable(struct hn_softc *sc)
 
 	/* NOTE: hn_vf_lock for hn_transmit()/hn_qflush() */
 	rm_wlock(&sc->hn_vf_lock);
+	if (sc->hn_vf_active_assoc != atomic_load_acq_int(&sc->hn_vf_assoc)) {
+		rm_wunlock(&sc->hn_vf_lock);
+		return (false);
+	}
 	sc->hn_xvf_flags |= HN_XVFFLAG_ENABLED;
+	sc->hn_xvf_flags &= ~HN_XVFFLAG_SWITCHING;
 	rm_wunlock(&sc->hn_vf_lock);
 
 	for (i = 0; i < sc->hn_rx_ring_cnt; ++i)
 		sc->hn_rx_ring[i].hn_rx_flags |= HN_RX_FLAG_XPNT_VF;
+	return (true);
 }
 
 static void
@@ -1764,11 +1859,20 @@ static void
 hn_xpnt_vf_init(struct hn_softc *sc)
 {
 	int error;
+	u_int assoc;
 
 	HN_LOCK_ASSERT(sc);
 
 	KASSERT((sc->hn_xvf_flags & HN_XVFFLAG_ENABLED) == 0,
 	    ("%s: transparent VF was enabled", if_name(sc->hn_ifp)));
+	if (!hn_xpnt_vf_caninit(sc))
+		return;
+	assoc = atomic_load_acq_int(&sc->hn_vf_assoc);
+	rm_wlock(&sc->hn_vf_lock);
+	sc->hn_xvf_flags |= HN_XVFFLAG_SWITCHING;
+	rm_wunlock(&sc->hn_vf_lock);
+	if (!sc->hn_vf_ready)
+		hn_xpnt_vf_setready(sc);
 
 	if (bootverbose) {
 		if_printf(sc->hn_ifp, "try bringing up %s\n",
@@ -1784,7 +1888,7 @@ hn_xpnt_vf_init(struct hn_softc *sc)
 	if (error) {
 		if_printf(sc->hn_ifp, "bringing up %s failed: %d\n",
 		    if_name(sc->hn_vf_ifp), error);
-		return;
+		goto failed;
 	}
 
 	/*
@@ -1797,7 +1901,11 @@ hn_xpnt_vf_init(struct hn_softc *sc)
 	 * NOTE:
 	 * Datapath setting must happen _after_ bringing the VF up.
 	 */
-	hn_nvs_set_datapath(sc, HN_NVS_DATAPATH_VF);
+	error = hn_nvs_set_datapath(sc, HN_NVS_DATAPATH_VF);
+	if (error)
+		goto failed;
+	sc->hn_vf_active_assoc = assoc;
+	hn_suspend_mgmt(sc);
 
 	/*
 	 * NOTE:
@@ -1807,7 +1915,17 @@ hn_xpnt_vf_init(struct hn_softc *sc)
 	hn_vf_rss_fixup(sc, true);
 
 	/* Mark transparent mode VF as enabled. */
-	hn_xpnt_vf_setenable(sc);
+	if (!hn_xpnt_vf_setenable(sc)) {
+		error = EAGAIN;
+		goto failed;
+	}
+	if_link_state_change(sc->hn_ifp, if_getlinkstate(sc->hn_vf_ifp));
+	return;
+
+failed:
+	hn_xpnt_vf_deactivate(sc);
+	if (error == EAGAIN)
+		taskqueue_enqueue_timeout(sc->hn_vf_taskq, &sc->hn_vf_init, hz);
 }
 
 static void
@@ -1816,17 +1934,30 @@ hn_xpnt_vf_init_taskfunc(void *xsc, int pending __unused)
 	struct hn_softc *sc = xsc;
 
 	HN_LOCK(sc);
+	if (sc->hn_detaching)
+		goto done;
 
 	if ((sc->hn_flags & HN_FLAG_SYNTH_ATTACHED) == 0)
 		goto done;
 	if (sc->hn_vf_ifp == NULL)
 		goto done;
+	if (!hn_xpnt_vf) {
+		if ((sc->hn_flags & HN_FLAG_RXVF) && sc->hn_vf_active_assoc !=
+		    atomic_load_acq_int(&sc->hn_vf_assoc))
+			hn_rxvf_change_locked(sc, sc->hn_vf_ifp, false);
+		hn_rxvf_change_locked(sc, sc->hn_vf_ifp,
+		    (if_getflags(sc->hn_vf_ifp) & IFF_UP) != 0);
+		goto done;
+	}
+	if (sc->hn_vf_active_assoc != 0 && sc->hn_vf_active_assoc !=
+	    atomic_load_acq_int(&sc->hn_vf_assoc))
+		hn_xpnt_vf_deactivate(sc);
 	if (sc->hn_xvf_flags & HN_XVFFLAG_ENABLED)
 		goto done;
-
-	if (sc->hn_vf_rdytick != 0) {
-		/* Mark VF as ready. */
-		hn_xpnt_vf_setready(sc);
+	if ((int)(ticks - sc->hn_vf_rdytick) < 0) {
+		taskqueue_enqueue_timeout(sc->hn_vf_taskq, &sc->hn_vf_init,
+		    sc->hn_vf_rdytick - ticks);
+		goto done;
 	}
 
 	if (if_getdrvflags(sc->hn_ifp) & IFF_DRV_RUNNING) {
@@ -1913,11 +2044,6 @@ hn_ifnet_attevent(void *xsc, if_t ifp)
 		sc->hn_vf_input = if_getinputfn(ifp);
 		if_setinputfn(ifp, hn_xpnt_vf_input);
 
-		/*
-		 * Stop link status management; use the VF's.
-		 */
-		hn_suspend_mgmt(sc);
-
 		/*
 		 * Give VF sometime to complete its attach routing.
 		 */
@@ -1926,6 +2052,8 @@ hn_ifnet_attevent(void *xsc, if_t ifp)
 
 		taskqueue_enqueue_timeout(sc->hn_vf_taskq, &sc->hn_vf_init,
 		    wait_ticks);
+	} else {
+		taskqueue_enqueue_timeout(sc->hn_vf_taskq, &sc->hn_vf_init, 0);
 	}
 done:
 	HN_UNLOCK(sc);
@@ -1962,38 +2090,20 @@ hn_ifnet_detevent(void *xsc, if_t ifp)
 
 		KASSERT(sc->hn_vf_input != NULL, ("%s VF input is not saved",
 		    if_name(sc->hn_ifp)));
+		if (sc->hn_flags & HN_FLAG_SYNTH_ATTACHED)
+			hn_xpnt_vf_deactivate(sc);
+		else
+			hn_xpnt_vf_restore(sc);
 		if_setinputfn(ifp, sc->hn_vf_input);
 		sc->hn_vf_input = NULL;
-
-		if ((sc->hn_flags & HN_FLAG_SYNTH_ATTACHED) &&
-		    (sc->hn_xvf_flags & HN_XVFFLAG_ENABLED))
-			hn_nvs_set_datapath(sc, HN_NVS_DATAPATH_SYNTH);
-
-		if (sc->hn_vf_rdytick == 0) {
-			/*
-			 * The VF was ready; restore some settings.
-			 */
-			if_setcapabilities(ifp, sc->hn_saved_caps);
-
-			if_sethwtsomax(ifp, sc->hn_saved_tsomax);
-			if_sethwtsomaxsegcount(sc->hn_ifp,
-			    sc->hn_saved_tsosegcnt);
-			if_sethwtsomaxsegsize(ifp, sc->hn_saved_tsosegsz);
-
-			if_setcapenable(ifp, sc->hn_saved_capenable);
-			if_sethwassist(ifp, sc->hn_saved_hwassist);
-		}
-
-		if (sc->hn_flags & HN_FLAG_SYNTH_ATTACHED) {
-			/*
-			 * Restore RSS settings.
-			 */
+	} else if (sc->hn_flags & HN_FLAG_SYNTH_ATTACHED) {
+		hn_rxvf_change_locked(sc, ifp, false);
+		/* Never leave receive routing pointing at a departing VF. */
+		if (sc->hn_flags & HN_FLAG_RXVF) {
+			sc->hn_flags &= ~HN_FLAG_RXVF;
+			sc->hn_vf_active_assoc = 0;
+			hn_rxvf_set(sc, NULL);
 			hn_vf_rss_restore(sc);
-
-			/*
-			 * Resume link status management, which was suspended
-			 * by hn_ifnet_attevent().
-			 */
 			hn_resume_mgmt(sc);
 		}
 	}
@@ -2023,7 +2133,8 @@ hn_ifnet_lnkevent(void *xsc, if_t ifp, int link_state)
 {
 	struct hn_softc *sc = xsc;
 
-	if (sc->hn_vf_ifp == ifp)
+	if (sc->hn_vf_ifp == ifp &&
+	    (sc->hn_xvf_flags & HN_XVFFLAG_ENABLED))
 		if_link_state_change(sc->hn_ifp, link_state);
 }
 
@@ -2130,17 +2241,13 @@ hn_attach(device_t dev)
 	TIMEOUT_TASK_INIT(sc->hn_mgmt_taskq0, &sc->hn_netchg_status, 0,
 	    hn_netchg_status_taskfunc, sc);
 
-	if (hn_xpnt_vf) {
-		/*
-		 * Setup taskqueue for VF tasks, e.g. delayed VF bringing up.
-		 */
-		sc->hn_vf_taskq = taskqueue_create("hn_vf", M_WAITOK,
-		    taskqueue_thread_enqueue, &sc->hn_vf_taskq);
-		taskqueue_start_threads(&sc->hn_vf_taskq, 1, PI_NET, "%s vf",
-		    device_get_nameunit(dev));
-		TIMEOUT_TASK_INIT(sc->hn_vf_taskq, &sc->hn_vf_init, 0,
-		    hn_xpnt_vf_init_taskfunc, sc);
-	}
+	/* Association work must not block the channel delivering completions. */
+	sc->hn_vf_taskq = taskqueue_create("hn_vf", M_WAITOK,
+	    taskqueue_thread_enqueue, &sc->hn_vf_taskq);
+	taskqueue_start_threads(&sc->hn_vf_taskq, 1, PI_NET, "%s vf",
+	    device_get_nameunit(dev));
+	TIMEOUT_TASK_INIT(sc->hn_vf_taskq, &sc->hn_vf_init, 0,
+	    hn_xpnt_vf_init_taskfunc, sc);
 
 	/*
 	 * Allocate ifnet and setup its name earlier, so that if_printf
@@ -2490,6 +2597,11 @@ hn_detach(device_t dev)
 		vmbus_xact_ctx_orphan(sc->hn_xact);
 	}
 
+	HN_LOCK(sc);
+	sc->hn_detaching = true;
+	HN_UNLOCK(sc);
+	taskqueue_drain_timeout(sc->hn_vf_taskq, &sc->hn_vf_init);
+
 	if (sc->hn_ifaddr_evthand != NULL)
 		EVENTHANDLER_DEREGISTER(ifaddr_event, sc->hn_ifaddr_evthand);
 	if (sc->hn_ifnet_evthand != NULL)
@@ -3772,6 +3884,16 @@ hn_ioctl(if_t ifp, u_long cmd, caddr_t data)
 		 * Suspend this interface before the synthetic parts
 		 * are ripped.
 		 */
+		if (sc->hn_vf_ready)
+			hn_xpnt_vf_deactivate(sc);
+		if (sc->hn_flags & HN_FLAG_RXVF) {
+			hn_rxvf_change_locked(sc, sc->hn_vf_ifp, false);
+			if (sc->hn_flags & HN_FLAG_RXVF) {
+				error = EIO;
+				HN_UNLOCK(sc);
+				break;
+			}
+		}
 		hn_suspend(sc);
 
 		/*
@@ -3825,15 +3947,10 @@ hn_ioctl(if_t ifp, u_long cmd, caddr_t data)
 		 */
 		hn_resume(sc);
 
-		if ((sc->hn_flags & HN_FLAG_RXVF) ||
-		    (sc->hn_xvf_flags & HN_XVFFLAG_ENABLED)) {
-			/*
-			 * Since we have reattached the NVS part,
-			 * change the datapath to VF again; in case
-			 * that it is lost, after the NVS was detached.
-			 */
-			hn_nvs_set_datapath(sc, HN_NVS_DATAPATH_VF);
-		}
+		/* Reattach requires a fresh association and acknowledged switch. */
+		if (sc->hn_vf_ifp != NULL)
+			taskqueue_enqueue_timeout(sc->hn_vf_taskq,
+			    &sc->hn_vf_init, 0);
 
 		HN_UNLOCK(sc);
 		break;
@@ -4056,19 +4173,16 @@ hn_stop(struct hn_softc *sc, bool detaching)
 	/* Disable polling. */
 	hn_polling(sc, 0);
 
-	if (sc->hn_xvf_flags & HN_XVFFLAG_ENABLED) {
+	if (sc->hn_vf_ready || (sc->hn_xvf_flags & HN_XVFFLAG_ENABLED)) {
 		KASSERT(sc->hn_vf_ifp != NULL,
 		    ("%s: VF is not attached", if_name(ifp)));
 
-		/* Mark transparent mode VF as disabled. */
-		hn_xpnt_vf_setdisable(sc, false /* keep hn_vf_ifp */);
-
 		/*
 		 * NOTE:
 		 * Datapath setting must happen _before_ bringing
 		 * the VF down.
 		 */
-		hn_nvs_set_datapath(sc, HN_NVS_DATAPATH_SYNTH);
+		hn_xpnt_vf_deactivate(sc);
 
 		/*
 		 * Bring the VF down.
@@ -4119,7 +4233,7 @@ hn_init_locked(struct hn_softc *sc)
 	/* Clear TX 'suspended' bit. */
 	hn_resume_tx(sc, sc->hn_tx_ring_inuse);
 
-	if (hn_xpnt_vf_isready(sc)) {
+	if (hn_xpnt_vf_caninit(sc)) {
 		/* Initialize transparent VF. */
 		hn_xpnt_vf_init(sc);
 	}
@@ -5953,10 +6067,18 @@ hn_transmit(if_t ifp, struct mbuf *m)
 	struct hn_tx_ring *txr;
 	int error, idx = 0;
 
-	if (sc->hn_xvf_flags & HN_XVFFLAG_ENABLED) {
+	if (sc->hn_xvf_flags & (HN_XVFFLAG_ENABLED | HN_XVFFLAG_SWITCHING)) {
 		struct rm_priotracker pt;
 
 		rm_rlock(&sc->hn_vf_lock, &pt);
+		if ((sc->hn_xvf_flags & HN_XVFFLAG_SWITCHING) ||
+		    ((sc->hn_xvf_flags & HN_XVFFLAG_ENABLED) &&
+		    sc->hn_vf_active_assoc != atomic_load_acq_int(&sc->hn_vf_assoc))) {
+			rm_runlock(&sc->hn_vf_lock, &pt);
+			m_freem(m);
+			if_inc_counter(ifp, IFCOUNTER_OQDROPS, 1);
+			return (ENETDOWN);
+		}
 		if (__predict_true(sc->hn_xvf_flags & HN_XVFFLAG_ENABLED)) {
 			struct mbuf *m_bpf = NULL;
 			int obytes, omcast;
@@ -6472,6 +6594,9 @@ hn_synth_attach(struct hn_softc *sc, int mtu)
 	/*
 	 * Attach the primary channel _before_ attaching NVS and RNDIS.
 	 */
+	atomic_store_rel_int(&sc->hn_vf_assoc,
+	    (atomic_load_int(&sc->hn_vf_assoc) + HN_VF_ASSOC_GENINC) &
+	    ~HN_VF_ASSOC_ALLOCATED);
 	error = hn_chan_attach(sc, sc->hn_prichan);
 	if (error)
 		goto failed;
@@ -6961,14 +7086,14 @@ hn_resume(struct hn_softc *sc)
 		hn_resume_data(sc);
 
 	/*
-	 * Don't resume link status change if VF is attached/activated.
+	 * Don't resume link status change if VF is activated.
 	 * - In the non-transparent VF mode, the synthetic device marks
 	 *   link down until the VF is deactivated; i.e. VF is down.
 	 * - In transparent VF mode, VF's media status is used until
-	 *   the VF is detached.
+	 *   the VF is deactivated.
 	 */
 	if ((sc->hn_flags & HN_FLAG_RXVF) == 0 &&
-	    !(hn_xpnt_vf && sc->hn_vf_ifp != NULL))
+	    !(sc->hn_xvf_flags & HN_XVFFLAG_ENABLED))
 		hn_resume_mgmt(sc);
 
 	/*
@@ -7389,6 +7514,29 @@ hn_nvs_handle_notify(struct hn_softc *sc, const struct vmbus_chanpkt_hdr *pkt)
 	}
 	hdr = VMBUS_CHANPKT_CONST_DATA(pkt);
 
+	if (hdr->nvs_type == HN_NVS_TYPE_VFASSOC_NOTE) {
+		const struct hn_nvs_vfassoc *assoc;
+		u_int state;
+
+		if (VMBUS_CHANPKT_DATALEN(pkt) < sizeof(*assoc)) {
+			if_printf(sc->hn_ifp, "short VF association notification\n");
+			return;
+		}
+		assoc = (const struct hn_nvs_vfassoc *)hdr;
+		if (assoc->nvs_alloc > 1) {
+			if_printf(sc->hn_ifp, "invalid VF association notification\n");
+			return;
+		}
+		/* Preserve withdrawals even when the worker coalesces notices. */
+		state = (atomic_load_int(&sc->hn_vf_assoc) + HN_VF_ASSOC_GENINC) &
+		    ~HN_VF_ASSOC_ALLOCATED;
+		atomic_store_rel_int(&sc->hn_vf_assoc, state | assoc->nvs_alloc);
+		if (bootverbose)
+			if_printf(sc->hn_ifp, "VF %u %s\n", assoc->nvs_serial,
+			    assoc->nvs_alloc ? "associated" : "withdrawn");
+		taskqueue_enqueue_timeout(sc->hn_vf_taskq, &sc->hn_vf_init, 0);
+		return;
+	}
 	if (hdr->nvs_type == HN_NVS_TYPE_TXTBL_NOTE) {
 		/* Useless; ignore */
 		return;
@@ -7565,7 +7713,8 @@ hn_chan_callback(struct vmbus_channel *chan, void *xrxr)
 			break;
 
 		case VMBUS_CHANPKT_TYPE_INBAND:
-			hn_nvs_handle_notify(sc, pkt);
+			if (chan == sc->hn_prichan)
+				hn_nvs_handle_notify(sc, pkt);
 			break;
 
 		default:
diff --git a/sys/dev/hyperv/netvsc/if_hnreg.h b/sys/dev/hyperv/netvsc/if_hnreg.h
index bc6256bb375b..3698ac237607 100644
--- a/sys/dev/hyperv/netvsc/if_hnreg.h
+++ b/sys/dev/hyperv/netvsc/if_hnreg.h
@@ -140,7 +140,13 @@ CTASSERT(sizeof(struct hn_nvs_ndis_init) >= HN_NVS_REQSIZE_MIN);
 #define HN_NVS_DATAPATH_SYNTH		0
 #define HN_NVS_DATAPATH_VF		1
 
-/* No response */
+struct hn_nvs_vfassoc {
+	uint32_t	nvs_type;	/* HN_NVS_TYPE_VFASSOC_NOTE */
+	uint32_t	nvs_alloc;
+	uint32_t	nvs_serial;
+} __packed;
+
+/* Empty VMBus completion, no NVS response. */
 struct hn_nvs_datapath {
 	uint32_t	nvs_type;	/* HN_NVS_TYPE_SET_DATAPATH */
 	uint32_t	nvs_active_path;/* HN_NVS_DATAPATH_* */
diff --git a/sys/dev/hyperv/netvsc/if_hnvar.h b/sys/dev/hyperv/netvsc/if_hnvar.h
index f46adcbf9245..fb545b8b2bc8 100644
--- a/sys/dev/hyperv/netvsc/if_hnvar.h
+++ b/sys/dev/hyperv/netvsc/if_hnvar.h
@@ -264,7 +264,11 @@ struct hn_softc {
 	/*
 	 * Transparent VF delayed initialization.
 	 */
-	int			hn_vf_rdytick;	/* ticks, 0 == ready */
+	int			hn_vf_rdytick;	/* end of VF attach delay */
+	bool			hn_vf_ready;	/* saved synthetic settings */
+	bool			hn_detaching;	/* hn_lock */
+	u_int			hn_vf_assoc;	/* atomic generation + allocated */
+	u_int			hn_vf_active_assoc; /* confirmed VF generation */
 	struct taskqueue	*hn_vf_taskq;
 	struct timeout_task	hn_vf_init;
 
@@ -300,6 +304,10 @@ struct hn_softc {
 
 #define HN_XVFFLAG_ENABLED		0x0001
 #define HN_XVFFLAG_ACCBPF		0x0002
+#define HN_XVFFLAG_SWITCHING		0x0004
+
+#define HN_VF_ASSOC_ALLOCATED		0x0001
+#define HN_VF_ASSOC_GENINC		0x0002
 
 #define HN_NO_SLEEPING(sc)			\
 do {						\