git: c13dfbcf43b4 - main - iavf: Reconcile VLAN filters with acknowledged requests

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

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

commit c13dfbcf43b4c7a5ba0204ad0a0c3bf34eb54be3
Author:     Kevin Bowling <kbowling@FreeBSD.org>
AuthorDate: 2026-11-02 00:39:54 +0000
Commit:     Kevin Bowling <kbowling@FreeBSD.org>
CommitDate: 2026-11-03 02:05:51 +0000

    iavf: Reconcile VLAN filters with acknowledged requests
    
    Track desired and acknowledged VLAN membership separately.  Retain
    pending deletions and uncertain state after failed requests instead of
    losing the work when a message is submitted.  Make repeated registration
    idempotent.
    
    Adapt DPDK's synchronous, single-VID virtchnl request shape and the
    bounded desired-state reconciliation used by igbv and ixv.  Serialize
    VLAN requests under the existing virtchnl mutex, with filter-list
    changes under the iflib context lock.  Preserve an unresolved
    transaction so a late reply cannot be mistaken for a newer operation,
    including after the desired state changes.
    
    Reconcile live membership changes without restarting healthy queues.
    Limit each pass to 32 requests and a one-second reply deadline,
    continuing live batches from the admin task.  Down or recovering VFs
    apply desired state at initialization; initialization batches continue
    through the existing retry callout without rediscovering the mailbox or
    losing acknowledged progress. Completed PF rejections retain pending
    work without resetting unrelated traffic.  An unresolved request invokes
    mailbox recovery when running and prevents queue enable during
    initialization.
    
    Delete nonzero VLAN filters before additions so released capacity is
    reused in the same reconciliation pass. Add VID 0 first and delete it
    last, retaining untagged admission if a nonzero deletion is rejected.
    After a confirmed VF reset, replay desired membership.  After
    AdminQ-only rediscovery, retain deletion tombstones and reconcile
    uncertain state because the PF may still hold the old filters.
    
    MFC after:      2 weeks
    Sponsored by:   BBOX.io
---
 sys/dev/iavf/iavf_iflib.h     |  12 +-
 sys/dev/iavf/iavf_lib.c       |  44 +++++--
 sys/dev/iavf/iavf_lib.h       |   2 +-
 sys/dev/iavf/iavf_vc_common.c | 290 ++++++++++++++++++++++++++----------------
 sys/dev/iavf/iavf_vc_common.h |   4 +-
 sys/dev/iavf/if_iavf_iflib.c  |  78 +++++++++---
 6 files changed, 284 insertions(+), 146 deletions(-)

diff --git a/sys/dev/iavf/iavf_iflib.h b/sys/dev/iavf/iavf_iflib.h
index 3b4de1c8bca7..498c4d2a9a6d 100644
--- a/sys/dev/iavf/iavf_iflib.h
+++ b/sys/dev/iavf/iavf_iflib.h
@@ -311,7 +311,10 @@ SLIST_HEAD(mac_list, iavf_mac_filter);
 struct iavf_vlan_filter {
 	SLIST_ENTRY(iavf_vlan_filter)  next;
 	u16     vlan;
-	u16     flags;
+	bool    desired;
+	bool    applied;
+	bool    uncertain;
+	bool    rejected;	/* Do not retry a NACK within the same bounded pass. */
 };
 
 /**
@@ -362,6 +365,13 @@ struct iavf_sc {
 	struct mac_list		*mac_filters;
 	struct vlan_list	*vlan_filters;
 
+	/* One VLAN transaction, protected by vc_mtx (lists use the CTX lock). */
+	enum virtchnl_ops	vlan_op;
+	enum virtchnl_status_code vlan_status;
+	u16			vlan_id;
+	bool			vlan_done;
+	u32			vlan_batch_pending;	/* Published to retry callout. */
+
 	/* Virtual comm channel */
 	struct virtchnl_vf_resource *vf_res;
 	struct virtchnl_vsi_resource *vsi_res;
diff --git a/sys/dev/iavf/iavf_lib.c b/sys/dev/iavf/iavf_lib.c
index f4f14f799f7d..505f42e49c77 100644
--- a/sys/dev/iavf/iavf_lib.c
+++ b/sys/dev/iavf/iavf_lib.c
@@ -918,8 +918,8 @@ iavf_sysctl_sw_filter_list(SYSCTL_HANDLER_ARGS)
 	else {
 		SLIST_FOREACH(v, sc->vlan_filters, next) {
 			sbuf_printf(buf,
-			    "%d, flags %#06x",
-			    v->vlan, v->flags);
+			    "%d, desired %d, applied %d, uncertain %d",
+			    v->vlan, v->desired, v->applied, v->uncertain);
 			/* don't print '\n' for last entry */
 			if (++ftl_counter != ftl_len)
 				sbuf_printf(buf, "\n");
@@ -1447,18 +1447,34 @@ iavf_baudrate_from_link_speed(struct iavf_sc *sc)
  * @sc: device private softc
  * @vtag: the VLAN id to filter
  *
- * Allocate a new VLAN filter structure and insert it into the VLAN list.
+ * Record a desired membership, retaining any unacknowledged operation.
+ * Returns true only when a new membership was recorded.
  */
-void
+bool
 iavf_add_vlan_filter(struct iavf_sc *sc, u16 vtag)
 {
-	struct iavf_vlan_filter	*v;
+	struct iavf_vlan_filter *v, *first;
 
+	SLIST_FOREACH(v, sc->vlan_filters, next) {
+		if (v->vlan == vtag) {
+			v->rejected = false;
+			if (v->desired)
+				return (false);
+			v->desired = true;
+			return (true);
+		}
+	}
 	v = (struct iavf_vlan_filter *)malloc(sizeof(struct iavf_vlan_filter),
 	    M_IAVF, M_WAITOK | M_ZERO);
-	SLIST_INSERT_HEAD(sc->vlan_filters, v, next);
+	/* Admit untagged traffic before the first nonzero membership. */
+	first = SLIST_FIRST(sc->vlan_filters);
+	if (vtag != 0 && first != NULL && first->vlan == 0)
+		SLIST_INSERT_AFTER(first, v, next);
+	else
+		SLIST_INSERT_HEAD(sc->vlan_filters, v, next);
 	v->vlan = vtag;
-	v->flags = IAVF_FILTER_ADD;
+	v->desired = true;
+	return (true);
 }
 
 /**
@@ -1466,9 +1482,9 @@ iavf_add_vlan_filter(struct iavf_sc *sc, u16 vtag)
  * @sc: device private softc
  * @vtag: the VLAN id to delete
  *
- * Marks all VLAN filters matching the given vtag for deletion.
+ * Clear the desired membership, keeping the entry until deletion is confirmed.
  *
- * @returns the number of filters marked for deletion.
+ * @returns one if the membership changed, or zero if already absent.
  *
  * @remark the filters are not removed immediately, but will be removed from
  * the list by another function that synchronizes over the virtchnl interface.
@@ -1477,16 +1493,16 @@ int
 iavf_mark_del_vlan_filter(struct iavf_sc *sc, u16 vtag)
 {
 	struct iavf_vlan_filter	*v;
-	int i = 0;
 
 	SLIST_FOREACH(v, sc->vlan_filters, next) {
-		if (v->vlan == vtag) {
-			v->flags = IAVF_FILTER_DEL;
-			++i;
+		if (v->vlan == vtag && v->desired) {
+			v->desired = false;
+			v->rejected = false;
+			return (1);
 		}
 	}
 
-	return (i);
+	return (0);
 }
 
 /**
diff --git a/sys/dev/iavf/iavf_lib.h b/sys/dev/iavf/iavf_lib.h
index fd707051aef8..1a1f2bac7ee5 100644
--- a/sys/dev/iavf/iavf_lib.h
+++ b/sys/dev/iavf/iavf_lib.h
@@ -473,7 +473,7 @@ struct iavf_mac_filter *
 struct iavf_mac_filter *
     iavf_get_mac_filter(struct iavf_sc *sc);
 u64 iavf_baudrate_from_link_speed(struct iavf_sc *sc);
-void iavf_add_vlan_filter(struct iavf_sc *sc, u16 vtag);
+bool iavf_add_vlan_filter(struct iavf_sc *sc, u16 vtag);
 int iavf_mark_del_vlan_filter(struct iavf_sc *sc, u16 vtag);
 int iavf_disable_queues_with_retries(struct iavf_sc *);
 
diff --git a/sys/dev/iavf/iavf_vc_common.c b/sys/dev/iavf/iavf_vc_common.c
index 94fdb402bc30..a605ec0901bf 100644
--- a/sys/dev/iavf/iavf_vc_common.c
+++ b/sys/dev/iavf/iavf_vc_common.c
@@ -376,134 +376,198 @@ iavf_disable_queues(struct iavf_sc *sc)
 	    (u8 *)&vqs, sizeof(vqs)));
 }
 
-/**
- * iavf_add_vlans - Add VLAN filters
- * @sc: device softc
- *
- * Scan the Filter List looking for vlans that need
- * to be added, then create the data to hand to the AQ
- * for handling.
- *
- * @returns zero on success, or an error code on failure.
- */
-int
-iavf_add_vlans(struct iavf_sc *sc)
+/* Bound control-plane work even when the PF acknowledges slowly. */
+#define IAVF_VLAN_SYNC_BATCH	32
+#define IAVF_VLAN_SYNC_TIMEOUT	SBT_1S
+
+/* The VC task owns no filter-list pointers and must not acquire CTX. */
+static void
+iavf_vlan_completion(struct iavf_sc *sc, enum virtchnl_ops op,
+    enum virtchnl_status_code status)
 {
-	struct virtchnl_vlan_filter_list *v;
-	struct iavf_vlan_filter *f, *ftmp;
-	device_t dev = sc->dev;
-	int i = 0, cnt = 0;
-	u32 len;
 
-	/* Get count of VLAN filters to add */
-	SLIST_FOREACH(f, sc->vlan_filters, next) {
-		if (f->flags & IAVF_FILTER_ADD)
-			cnt++;
-	}
+	IAVF_VC_LOCK_ASSERT(sc);
+	if (sc->vlan_op != op || sc->vlan_done)
+		return;
+	sc->vlan_status = status;
+	sc->vlan_done = true;
+	wakeup(&sc->vlan_op);
+}
 
-	if (!cnt) /* no work... */
-		return (ENOENT);
+/* Apply an acknowledgement to its original VID and direction, under CTX. */
+static int
+iavf_vlan_result(struct iavf_sc *sc)
+{
+	struct iavf_vlan_filter *f;
+	int error;
 
-	len = sizeof(struct virtchnl_vlan_filter_list) +
-	      (cnt * sizeof(u16));
+	IAVF_VC_LOCK_ASSERT(sc);
+	if (sc->vlan_op == VIRTCHNL_OP_UNKNOWN)
+		return (0);
+	if (!sc->vlan_done)
+		return (ETIMEDOUT);
 
-	if (len > IAVF_AQ_BUF_SZ) {
-		device_printf(dev, "%s: Exceeded Max AQ Buf size\n",
-			__func__);
-		return (EFBIG);
+	error = sc->vlan_status == VIRTCHNL_STATUS_SUCCESS ? 0 : EIO;
+	SLIST_FOREACH(f, sc->vlan_filters, next) {
+		if (f->vlan != sc->vlan_id)
+			continue;
+		if (error == 0) {
+			f->applied = sc->vlan_op == VIRTCHNL_OP_ADD_VLAN;
+			f->uncertain = false;
+		} else {
+			/* The PF need not have rolled back every internal step. */
+			f->uncertain = true;
+			f->rejected = true;
+		}
+		break;
 	}
-
-	v = (struct virtchnl_vlan_filter_list *)malloc(len, M_IAVF, M_NOWAIT | M_ZERO);
-	if (!v) {
-		device_printf(dev, "%s: unable to allocate memory\n",
-			__func__);
-		return (ENOMEM);
+	KASSERT(f != NULL, ("VLAN transaction lost its filter"));
+	if (error == 0 && !f->desired && !f->applied) {
+		SLIST_REMOVE(sc->vlan_filters, f, iavf_vlan_filter, next);
+		free(f, M_IAVF);
 	}
+	sc->vlan_op = VIRTCHNL_OP_UNKNOWN;
+	return (error);
+}
 
-	v->vsi_id = sc->vsi_res->vsi_id;
-	v->num_elements = cnt;
+/*
+ * Discard transactions only after replacing the AdminQ and completing resource
+ * discovery.  An AdminQ-only restart does not prove that PF filters vanished;
+ * preserve deletion tombstones in that case.  A completed VF reset does.
+ */
+void
+iavf_reset_vlan_filters(struct iavf_sc *sc, bool reset)
+{
+	struct iavf_vlan_filter *f, *tmp;
 
-	/* Scan the filter array */
-	SLIST_FOREACH_SAFE(f, sc->vlan_filters, next, ftmp) {
-                if (f->flags & IAVF_FILTER_ADD) {
-                        bcopy(&f->vlan, &v->vlan_id[i], sizeof(u16));
-			f->flags = IAVF_FILTER_USED;
-                        i++;
-                }
-                if (i == cnt)
-                        break;
+	sx_assert(iflib_ctx_lock_get(sc->vsi.ctx), SA_XLOCKED);
+	IAVF_VC_LOCK(sc);
+	sc->vlan_op = VIRTCHNL_OP_UNKNOWN;
+	sc->vlan_done = false;
+	IAVF_VC_UNLOCK(sc);
+	atomic_store_rel_32(&sc->vlan_batch_pending, 0);
+	SLIST_FOREACH_SAFE(f, sc->vlan_filters, next, tmp) {
+		f->rejected = false;
+		f->uncertain = !reset;
+		if (!reset)
+			continue;
+		f->applied = false;
+		if (!f->desired) {
+			SLIST_REMOVE(sc->vlan_filters, f, iavf_vlan_filter, next);
+			free(f, M_IAVF);
+		}
 	}
-
-	iavf_send_pf_msg(sc, VIRTCHNL_OP_ADD_VLAN, (u8 *)v, len);
-	free(v, M_IAVF);
-	/* add stats? */
-	return (0);
 }
 
-/**
- * iavf_del_vlans - Delete VLAN filters
- * @sc: device softc
+/*
+ * Keep one VID per request so a partial batch failure cannot hide which
+ * memberships were acknowledged.  The caller serializes membership changes
+ * under CTX; the independent VC task can deliver replies while we wait.
  *
- * Scan the Filter Table looking for vlans that need
- * to be removed, then create the data to hand to the AQ
- * for handling.
- *
- * @returns zero on success, or an error code on failure.
+ * EIO is a completed PF rejection, retained for a later control-plane pass.
+ * EAGAIN leaves a bounded batch's tail pending without losing prior progress.
+ * An incomplete transaction must prevent queue enable: virtchnl replies carry
+ * only an opcode, so a retry cannot safely be matched after a timeout.
+ * Live callers enter mailbox recovery rather than submit an ambiguous retry.
  */
 int
-iavf_del_vlans(struct iavf_sc *sc)
+iavf_sync_vlans(struct iavf_sc *sc)
 {
-	struct virtchnl_vlan_filter_list *v;
-	struct iavf_vlan_filter *f, *ftmp;
-	device_t dev = sc->dev;
-	int i = 0, cnt = 0;
-	u32 len;
-
-	/* Get count of VLAN filters to delete */
-	SLIST_FOREACH(f, sc->vlan_filters, next) {
-		if (f->flags & IAVF_FILTER_DEL)
-			cnt++;
-	}
-
-	if (!cnt) /* no work... */
-		return (ENOENT);
-
-	len = sizeof(struct virtchnl_vlan_filter_list) +
-	      (cnt * sizeof(u16));
-
-	if (len > IAVF_AQ_BUF_SZ) {
-		device_printf(dev, "%s: Exceeded Max AQ Buf size\n",
-			__func__);
-		return (EFBIG);
+	struct {
+		struct virtchnl_vlan_filter_list list;
+		u16 pad;	/* The legacy message validator counts the array twice. */
+	} msg;
+	struct iavf_vlan_filter *f, *tmp;
+	sbintime_t deadline;
+	unsigned int pass, sent;
+	int error, first_error;
+
+	sx_assert(iflib_ctx_lock_get(sc->vsi.ctx), SA_XLOCKED);
+	if (iavf_test_state(&sc->state, IAVF_STATE_RESET_PENDING) ||
+	    iavf_test_state(&sc->state, IAVF_STATE_RESET_RECOVERY) ||
+	    iavf_test_state(&sc->state, IAVF_STATE_VC_POLLING) ||
+	    !iavf_reset_is_complete(&sc->hw) ||
+	    !iavf_check_asq_alive(&sc->hw))
+		return (ENXIO);
+
+	deadline = getsbinuptime() + IAVF_VLAN_SYNC_TIMEOUT;
+	if (atomic_load_acq_32(&sc->vlan_batch_pending) == 0) {
+		SLIST_FOREACH(f, sc->vlan_filters, next)
+			f->rejected = false;
 	}
+	atomic_store_rel_32(&sc->vlan_batch_pending, 0);
+	IAVF_VC_LOCK(sc);
+	error = iavf_vlan_result(sc);
+	if (error != 0 && error != EIO)
+		goto out;
+	first_error = error;
 
-	v = (struct virtchnl_vlan_filter_list *)
-	    malloc(len, M_IAVF, M_NOWAIT | M_ZERO);
-	if (!v) {
-		device_printf(dev, "%s: unable to allocate memory\n",
-			__func__);
-		return (ENOMEM);
-	}
-
-	v->vsi_id = sc->vsi_res->vsi_id;
-	v->num_elements = cnt;
-
-	/* Scan the filter array */
-	SLIST_FOREACH_SAFE(f, sc->vlan_filters, next, ftmp) {
-                if (f->flags & IAVF_FILTER_DEL) {
-                        bcopy(&f->vlan, &v->vlan_id[i], sizeof(u16));
-                        i++;
-                        SLIST_REMOVE(sc->vlan_filters, f, iavf_vlan_filter, next);
-                        free(f, M_IAVF);
-                }
-                if (i == cnt)
-                        break;
+	sent = 0;
+	/*
+	 * Free nonzero VLAN capacity before adding desired memberships.  VID 0
+	 * stays at the head of the list, so it is still the first addition.
+	 * Delete it last, preserving untagged admission if other work failed.
+	 */
+	for (pass = 0; pass < 3; pass++) {
+		if (pass == 2 && first_error != 0)
+			break;
+		SLIST_FOREACH_SAFE(f, sc->vlan_filters, next, tmp) {
+			if (f->desired) {
+				if (pass != 1)
+					continue;
+			} else if (pass != (f->vlan == 0 ? 2 : 0))
+				continue;
+			if (f->rejected) {
+				first_error = EIO;
+				continue;
+			}
+			if (!f->uncertain && f->desired == f->applied) {
+				if (!f->desired) {
+					SLIST_REMOVE(sc->vlan_filters, f,
+					    iavf_vlan_filter, next);
+					free(f, M_IAVF);
+				}
+				continue;
+			}
+			if (sent == IAVF_VLAN_SYNC_BATCH ||
+			    getsbinuptime() >= deadline) {
+				atomic_store_rel_32(&sc->vlan_batch_pending, 1);
+				error = EAGAIN;
+				goto out;
+			}
+			bzero(&msg, sizeof(msg));
+			msg.list.vsi_id = sc->vsi_res->vsi_id;
+			msg.list.num_elements = 1;
+			msg.list.vlan_id[0] = f->vlan;
+			sc->vlan_id = f->vlan;
+			sc->vlan_op = f->desired ? VIRTCHNL_OP_ADD_VLAN :
+			    VIRTCHNL_OP_DEL_VLAN;
+			sc->vlan_done = false;
+			sent++;
+			error = iavf_send_pf_msg(sc, sc->vlan_op, (u8 *)&msg,
+			    sizeof(msg));
+			if (error != 0) {
+				/* A send timeout can occur after posting the command. */
+				error = ETIMEDOUT;
+				goto out;
+			}
+			while (!sc->vlan_done) {
+				error = msleep_sbt(&sc->vlan_op, &sc->vc_mtx, 0,
+				    "iavfvlan", deadline, 0, C_ABSOLUTE);
+				if (error != 0 && !sc->vlan_done) {
+					error = ETIMEDOUT;
+					goto out;
+				}
+			}
+			error = iavf_vlan_result(sc);
+			if (error != 0)
+				first_error = error;
+		}
 	}
-
-	iavf_send_pf_msg(sc, VIRTCHNL_OP_DEL_VLAN, (u8 *)v, len);
-	free(v, M_IAVF);
-	/* add stats? */
-	return (0);
+	error = first_error;
+out:
+	IAVF_VC_UNLOCK(sc);
+	return (error);
 }
 
 /**
@@ -917,13 +981,13 @@ iavf_vc_send_cmd(struct iavf_sc *sc, uint32_t request)
 		return iavf_add_ether_filters(sc);
 
 	case IAVF_FLAG_AQ_ADD_VLAN_FILTER:
-		return iavf_add_vlans(sc);
+		return iavf_sync_vlans(sc);
 
 	case IAVF_FLAG_AQ_DEL_MAC_FILTER:
 		return iavf_del_ether_filters(sc);
 
 	case IAVF_FLAG_AQ_DEL_VLAN_FILTER:
-		return iavf_del_vlans(sc);
+		return iavf_sync_vlans(sc);
 
 	case IAVF_FLAG_AQ_CONFIGURE_QUEUES:
 		return iavf_configure_queues(sc);
@@ -1273,6 +1337,10 @@ iavf_vc_completion(struct iavf_sc *sc,
 		return;
 	}
 
+	if (v_opcode == VIRTCHNL_OP_ADD_VLAN ||
+	    v_opcode == VIRTCHNL_OP_DEL_VLAN)
+		iavf_vlan_completion(sc, v_opcode, v_retval);
+
 	rss_op = 0;
 	if (v_opcode == VIRTCHNL_OP_CONFIG_RSS_KEY)
 		rss_op = IAVF_FLAG_AQ_CONFIG_RSS_KEY;
diff --git a/sys/dev/iavf/iavf_vc_common.h b/sys/dev/iavf/iavf_vc_common.h
index 1b569148e0cc..d90ec35d2f43 100644
--- a/sys/dev/iavf/iavf_vc_common.h
+++ b/sys/dev/iavf/iavf_vc_common.h
@@ -48,11 +48,11 @@ int iavf_verify_api_ver_retries(struct iavf_sc *, u32);
 int iavf_send_api_ver(struct iavf_sc *sc);
 int iavf_enable_queues(struct iavf_sc *sc);
 int iavf_disable_queues(struct iavf_sc *sc);
-int iavf_add_vlans(struct iavf_sc *sc);
+int iavf_sync_vlans(struct iavf_sc *sc);
+void iavf_reset_vlan_filters(struct iavf_sc *sc, bool reset);
 int iavf_send_vf_config_msg(struct iavf_sc *sc);
 int iavf_get_vf_config(struct iavf_sc *sc);
 int iavf_get_vf_config_retries(struct iavf_sc *, u32);
-int iavf_del_vlans(struct iavf_sc *sc);
 int iavf_add_ether_filters(struct iavf_sc *sc);
 int iavf_del_ether_filters(struct iavf_sc *sc);
 int iavf_request_reset(struct iavf_sc *sc);
diff --git a/sys/dev/iavf/if_iavf_iflib.c b/sys/dev/iavf/if_iavf_iflib.c
index 5b077a4bdfac..304347316a75 100644
--- a/sys/dev/iavf/if_iavf_iflib.c
+++ b/sys/dev/iavf/if_iavf_iflib.c
@@ -93,9 +93,11 @@ static void	 iavf_if_stop(if_ctx_t ctx);
 static bool	 iavf_if_needs_restart(if_ctx_t, enum iflib_restart_event);
 
 static void	iavf_mbx_lost(struct iavf_sc *);
+static void	iavf_update_vlan_filters(struct iavf_sc *);
 static void	iavf_enter_reset_recovery(struct iavf_sc *);
 static void	iavf_mbx_retry_detach(struct iavf_sc *);
 static void	iavf_mbx_retry_failed(if_ctx_t);
+static void	iavf_init_retry(if_ctx_t, bool);
 static void	iavf_mbx_retry_prepare(struct iavf_sc *);
 static void	iavf_mbx_retry_stop(struct iavf_sc *);
 static void	iavf_mbx_retry_succeeded(struct iavf_sc *);
@@ -776,7 +778,8 @@ iavf_mbx_retry_callout(void *arg)
 
 	sc = arg;
 	if (atomic_readandclear_32(&sc->mbx_retry_pending) == 0 ||
-	    atomic_load_acq_32(&sc->mbx_ready) != 0 ||
+	    (atomic_load_acq_32(&sc->mbx_ready) != 0 &&
+	    atomic_load_acq_32(&sc->vlan_batch_pending) == 0) ||
 	    iflib_in_detach(sc->vsi.ctx))
 		return;
 	ifp = iflib_get_ifp(sc->vsi.ctx);
@@ -963,6 +966,14 @@ iavf_quiesce_queues(struct iavf_sc *sc)
 
 static void
 iavf_mbx_retry_failed(if_ctx_t ctx)
+{
+
+	iavf_init_retry(ctx, false);
+}
+
+/* Retry either a failed mailbox or a bounded, still-healthy filter batch. */
+static void
+iavf_init_retry(if_ctx_t ctx, bool mailbox_ready)
 {
 	struct iavf_sc *sc;
 	struct iavf_vsi *vsi;
@@ -972,7 +983,13 @@ iavf_mbx_retry_failed(if_ctx_t ctx)
 
 	sc = iavf_sc_from_ctx(ctx);
 	vsi = &sc->vsi;
-	atomic_store_rel_32(&sc->mbx_ready, 0);
+	atomic_store_rel_32(&sc->mbx_ready, mailbox_ready);
+	if (mailbox_ready) {
+		atomic_store_rel_32(&sc->vc_reinit_required, 0);
+		iavf_clear_state(&sc->state, IAVF_STATE_RESET_RECOVERY);
+	} else {
+		atomic_store_rel_32(&sc->vlan_batch_pending, 0);
+	}
 	iavf_clear_state(&sc->state, IAVF_STATE_RUNNING);
 	sc->link_up = false;
 	if (vsi->link_active) {
@@ -985,11 +1002,12 @@ iavf_mbx_retry_failed(if_ctx_t ctx)
 	if (!sc->mbx_retry_initialized ||
 	    (if_getflags(ifp) & IFF_UP) == 0)
 		return;
-	stage = sc->mbx_retry_stage;
+	stage = mailbox_ready ? 0 : sc->mbx_retry_stage;
 	if (stage >= nitems(iavf_mbx_retry_delay))
 		stage = nitems(iavf_mbx_retry_delay) - 1;
 	delay = iavf_mbx_retry_delay[stage];
-	if (sc->mbx_retry_stage + 1 < nitems(iavf_mbx_retry_delay))
+	if (!mailbox_ready &&
+	    sc->mbx_retry_stage + 1 < nitems(iavf_mbx_retry_delay))
 		sc->mbx_retry_stage++;
 	atomic_set_32(&sc->mbx_retry_pending, 1);
 	callout_reset_sbt(&sc->mbx_retry, delay, 0,
@@ -1127,6 +1145,8 @@ iavf_reestablish_vc(struct iavf_sc *sc)
 	 * replacement AdminQ is established.  This permits ordinary AdminQ
 	 * processing while making the following quiesce check a no-op.
 	 */
+	iavf_reset_vlan_filters(sc,
+	    iavf_test_state(&sc->state, IAVF_STATE_RESET_RECOVERY));
 	if (iavf_test_state(&sc->state, IAVF_STATE_RESET_RECOVERY)) {
 		atomic_store_rel_32(&sc->queues_enabled, 0);
 		iavf_clear_state(&sc->state, IAVF_STATE_RESET_RECOVERY);
@@ -1145,16 +1165,11 @@ static void
 iavf_replay_filters(struct iavf_sc *sc)
 {
 	struct iavf_mac_filter *mac;
-	struct iavf_vlan_filter *vlan;
 
 	SLIST_FOREACH(mac, sc->mac_filters, next) {
 		if ((mac->flags & IAVF_FILTER_DEL) == 0)
 			mac->flags |= IAVF_FILTER_ADD | IAVF_FILTER_USED;
 	}
-	SLIST_FOREACH(vlan, sc->vlan_filters, next) {
-		if ((vlan->flags & IAVF_FILTER_DEL) == 0)
-			vlan->flags = IAVF_FILTER_ADD;
-	}
 }
 
 /**
@@ -1232,10 +1247,19 @@ iavf_if_init(if_ctx_t ctx)
 		iavf_replay_filters(sc);
 	if (!error || error == EEXIST || replay_filters)
 		iavf_send_vc_msg(sc, IAVF_FLAG_AQ_ADD_MAC_FILTER);
-	if (replay_filters)
-		iavf_send_vc_msg(sc, IAVF_FLAG_AQ_ADD_VLAN_FILTER);
 	iflib_set_mac(ctx, hw->mac.addr);
 
+	/* Finish filter changes before queued traffic can reach PF anti-spoof. */
+	error = iavf_sync_vlans(sc);
+	if (error == EAGAIN) {
+		/* Continue a bounded batch without forgetting acknowledged work. */
+		iavf_init_retry(ctx, true);
+		return;
+	}
+	if (error != 0 && error != EIO)
+		goto fail;
+	/* A completed policy rejection must not reset unrelated traffic forever. */
+
 	/* Prepare the queues for operation */
 	iavf_init_queues(vsi);
 
@@ -1817,6 +1841,8 @@ iavf_if_update_admin_status(if_ctx_t ctx)
 	}
 
 	iavf_process_adminq(sc, &pending);
+	if (atomic_load_acq_32(&sc->vlan_batch_pending) != 0)
+		iavf_update_vlan_filters(sc);
 	iavf_update_link_status(sc);
 
 	/*
@@ -1970,6 +1996,24 @@ iavf_if_timer(if_ctx_t ctx, uint16_t qid)
 	iavf_request_stats(sc);
 }
 
+/* Reconcile live memberships without restarting otherwise healthy queues. */
+static void
+iavf_update_vlan_filters(struct iavf_sc *sc)
+{
+	int error;
+
+	/* Down and recovering VFs replay their desired state at initialization. */
+	if (!iflib_is_running(sc->vsi.ctx) ||
+	    atomic_load_acq_32(&sc->mbx_ready) == 0 ||
+	    atomic_load_acq_32(&sc->vc_reinit_required) != 0)
+		return;
+	error = iavf_sync_vlans(sc);
+	if (error == EAGAIN)
+		iflib_admin_intr_deferred(sc->vsi.ctx);
+	else if (error != 0 && error != EIO)
+		iavf_mbx_lost(sc);
+}
+
 /**
  * iavf_if_vlan_register - Register a VLAN
  * @ctx: iflib context
@@ -1990,11 +2034,10 @@ iavf_if_vlan_register(if_ctx_t ctx, u16 vtag)
 	if (vsi->num_vlans == 0)
 		iavf_add_vlan_filter(sc, 0);
 
-	iavf_add_vlan_filter(sc, vtag);
+	if (iavf_add_vlan_filter(sc, vtag))
+		++vsi->num_vlans;
 
-	++vsi->num_vlans;
-
-	iavf_send_vc_msg(sc, IAVF_FLAG_AQ_ADD_VLAN_FILTER);
+	iavf_update_vlan_filters(sc);
 }
 
 /**
@@ -2022,7 +2065,7 @@ iavf_if_vlan_unregister(if_ctx_t ctx, u16 vtag)
 		i += iavf_mark_del_vlan_filter(sc, 0);
 
 	if (i > 0)
-		iavf_send_vc_msg(sc, IAVF_FLAG_AQ_DEL_VLAN_FILTER);
+		iavf_update_vlan_filters(sc);
 }
 
 /**
@@ -2085,7 +2128,8 @@ iavf_if_needs_restart(if_ctx_t ctx __unused, enum iflib_restart_event event)
 {
 	switch (event) {
 	case IFLIB_RESTART_VLAN_CONFIG:
-		return (true);
+		/* Membership changes use the mailbox without resetting queues. */
+		return (false);
 	default:
 		return (false);
 	}