git: 923b9a80c21f - main - ice: Isolate VFs after malicious-driver detection

From: Kevin Bowling <kbowling_at_FreeBSD.org>
Date: Fri, 18 Sep 2026 00:46:42 UTC
The branch main has been updated by kbowling:

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

commit 923b9a80c21ff178ceb4ffb23374e9ef924adb66
Author:     Kevin Bowling <kbowling@FreeBSD.org>
AuthorDate: 2026-08-19 10:49:34 +0000
Commit:     Kevin Bowling <kbowling@FreeBSD.org>
CommitDate: 2026-09-18 00:45:23 +0000

    ice: Isolate VFs after malicious-driver detection
    
    Consume the per-function MDD latches to attribute transmit and receive
    events to the offending VF.  Treat the global debug registers only as
    the last-cause diagnostic, add the missing Tx data-protection cause, and
    select the E830 TCLAN register addresses when required.
    
    Block every virtchnl request from an offending VF, reset it, and leave
    its queues and interrupt mappings unconfigured.  Most MDD classes stop a
    queue, but Tx data protection only drops the offending packet; the reset
    makes the reported blocked state an actual DMA fence for every class.
    Complete VFR without restoring resources so a later physical FLR can
    create a new reset edge and recover the function.
    
    Complete VFR before restoring queue and interrupt mappings.  E810 does
    not retain mapping writes while VFSWR remains asserted; retaining the
    original hardware order prevents an immediate post-attach VFR from
    leaving queue-map enable clear.
    
    E810 also sets a parent PF_MDET latch for an event attributed by a
    VP_MDET latch to one of its VFs.  Do not reinitialize the PF for those
    event classes.  This prevents a hostile VF from flapping PF and sibling
    traffic while retaining recovery for an unattributed PF queue event.
    
    Provide an opt-in auto-reset policy for operators who prefer
    availability to persistent isolation.  Clear reset induced anti-spoof
    latches before releasing a rebuilt VF.
    
    This follows the MDD attribution and reset semantics in section
    9.2.2.2.1 of the Intel E810 Datasheet.  The per-VF register coverage and
    E830 TCLAN selection match the Intel ICE driver, while the default
    fail-closed policy follows ixl(4).
    
    Validated on E810 with four four-queue FreeBSD iavf VFs.  An invalid
    tail write blocked only the offender, incremented its Tx MDD counter,
    caused no PF reinitialization, and left a sibling at 50/50 successful
    pings.  Physical FLR restored the offender and a PF reset rebuilt it
    without a spurious anti-spoof block.  With auto-reset enabled, the
    offender resumed after the reset while its sibling completed 60/60
    pings; all queue-map enable bits remained set after the second VFR.
    
    MFC after:      2 weeks
    Sponsored by:   BBOX.io
    Differential Revision:  https://reviews.freebsd.org/D59028
---
 share/man/man4/ice.4             |  15 +++-
 sys/dev/ice/ice_common_sysctls.h |  21 +++++
 sys/dev/ice/ice_hw_autogen.h     |   2 +
 sys/dev/ice/ice_iflib.h          |   1 +
 sys/dev/ice/ice_iov.c            | 184 +++++++++++++++++++++++++++++++++++----
 sys/dev/ice/ice_iov.h            |   6 ++
 sys/dev/ice/ice_lib.c            | 179 +++++++++++++++++++++++++------------
 sys/dev/ice/ice_lib.h            |  12 +++
 8 files changed, 346 insertions(+), 74 deletions(-)

diff --git a/share/man/man4/ice.4 b/share/man/man4/ice.4
index c8924e2de8f9..e3246db08689 100644
--- a/share/man/man4/ice.4
+++ b/share/man/man4/ice.4
@@ -32,7 +32,7 @@
 .\"
 .\" * Other names and brands may be claimed as the property of others.
 .\"
-.Dd August 27, 2026
+.Dd September 5, 2026
 .Dt ICE 4
 .Os
 .Sh NAME
@@ -46,6 +46,7 @@ In
 .Xr loader.conf 5 :
 .Cd if_ice_load
 .Cd hw.ice.enable_health_events
+.Cd hw.ice.mdd_auto_reset_vf
 .Cd hw.ice.irdma
 .Cd hw.ice.irdma_max_msix
 .Cd hw.ice.debug.enable_tx_fc_filter
@@ -1039,6 +1040,18 @@ Enabled by default.
 If enabled, when the driver receives a firmware health event message, it will
 print out a description of the event to the kernel message buffer and if
 applicable, possible actions to take to remedy it.
+.It Va hw.ice.mdd_auto_reset_vf
+Automatically reconstruct and release a VF after malicious-driver detection
+resets it.
+This is disabled by default so a non-cooperative VF is reset without restoring
+its queues and remains unable to issue traffic until an externally initiated
+VF function-level reset, PF reset, or SR-IOV configuration recreation
+reconstructs it.
+Enabling this tunable favors availability but allows a persistently faulty or
+hostile VF to resume after each reset.
+An individual PF can override the global setting with the
+.Va dev.ice.#.mdd_auto_reset_vf
+loader tunable.
 .It Va hw.ice.irdma
 Set to 1 to enable the RDMA client interface, required by the
 .Xr irdma 4
diff --git a/sys/dev/ice/ice_common_sysctls.h b/sys/dev/ice/ice_common_sysctls.h
index 37a50296ea81..5224e367f95c 100644
--- a/sys/dev/ice/ice_common_sysctls.h
+++ b/sys/dev/ice/ice_common_sysctls.h
@@ -93,6 +93,21 @@ bool ice_enable_tx_lldp_filter = true;
  */
 bool ice_enable_health_events = true;
 
+#ifdef PCI_IOV
+/**
+ * @var ice_mdd_auto_reset_vf
+ * @brief reconstruct and release a VF after a malicious-driver reset
+ *
+ * Global default for automatically reconstructing and releasing a VF after
+ * hardware reports a malicious-driver event and the driver resets it. Leave
+ * this disabled unless availability is more important than keeping a
+ * persistently faulty VF blocked.
+ *
+ * @remark each PF has a separate sysctl which can override this value.
+ */
+bool ice_mdd_auto_reset_vf = false;
+#endif
+
 /**
  * @var ice_tx_balance_en
  * @brief boolean permitting the 5-layer scheduler topology enablement
@@ -133,6 +148,12 @@ SYSCTL_BOOL(_hw_ice, OID_AUTO, enable_health_events, CTLFLAG_RDTUN,
 	    &ice_enable_health_events, 0,
 	    "Enable FW health event reporting globally");
 
+#ifdef PCI_IOV
+SYSCTL_BOOL(_hw_ice, OID_AUTO, mdd_auto_reset_vf, CTLFLAG_RDTUN,
+	    &ice_mdd_auto_reset_vf, 0,
+	    "Automatically restore VFs after an MDD reset");
+#endif
+
 SYSCTL_BOOL(_hw_ice, OID_AUTO, irdma, CTLFLAG_RDTUN, &ice_enable_irdma, 0,
 	    "Enable iRDMA client interface");
 
diff --git a/sys/dev/ice/ice_hw_autogen.h b/sys/dev/ice/ice_hw_autogen.h
index 3f2778d91a4b..cecef6bbe959 100644
--- a/sys/dev/ice/ice_hw_autogen.h
+++ b/sys/dev/ice/ice_hw_autogen.h
@@ -5508,6 +5508,7 @@
 #define GL_MDET_TX_PQM_VALID_S			31
 #define GL_MDET_TX_PQM_VALID_M			BIT(31)
 #define GL_MDET_TX_TCLAN			0x000FC068 /* Reset Source: CORER */
+#define E830_GL_MDET_TX_TCLAN			0x000FCCC0
 #define GL_MDET_TX_TCLAN_QNUM_S			0
 #define GL_MDET_TX_TCLAN_QNUM_M			MAKEMASK(0x7FFF, 0)
 #define GL_MDET_TX_TCLAN_VF_NUM_S		15
@@ -5539,6 +5540,7 @@
 #define PF_MDET_TX_PQM_VALID_S			0
 #define PF_MDET_TX_PQM_VALID_M			BIT(0)
 #define PF_MDET_TX_TCLAN			0x000FC000 /* Reset Source: CORER */
+#define E830_PF_MDET_TX_TCLAN			0x000FCC00
 #define PF_MDET_TX_TCLAN_VALID_S		0
 #define PF_MDET_TX_TCLAN_VALID_M		BIT(0)
 #define PF_MDET_TX_TDPU				0x00040800 /* Reset Source: CORER */
diff --git a/sys/dev/ice/ice_iflib.h b/sys/dev/ice/ice_iflib.h
index fe1f53f4cb68..ab576ec1d896 100644
--- a/sys/dev/ice/ice_iflib.h
+++ b/sys/dev/ice/ice_iflib.h
@@ -351,6 +351,7 @@ struct ice_softc {
 #ifdef PCI_IOV
 	struct ice_vf *vfs;
 	u16 num_vfs;
+	bool mdd_auto_reset_vf;
 #endif
 	struct ice_resmgr os_imgr;
 	/* For mirror interface */
diff --git a/sys/dev/ice/ice_iov.c b/sys/dev/ice/ice_iov.c
index 315950b6f8f8..d86044acfc64 100644
--- a/sys/dev/ice/ice_iov.c
+++ b/sys/dev/ice/ice_iov.c
@@ -59,6 +59,8 @@ static int ice_iov_configure_mac_anti_spoof(struct ice_softc *sc,
 static int ice_iov_restore_vf_host_config(struct ice_softc *sc,
     struct ice_vf *vf);
 static void ice_iov_clear_vf_queue_state(struct ice_vf *vf);
+static void ice_iov_complete_vf_reset(struct ice_softc *sc,
+    struct ice_vf *vf, bool restore_mapping);
 static void ice_iov_ready_vf(struct ice_softc *sc, struct ice_vf *vf);
 static int ice_reset_vf(struct ice_softc *sc, struct ice_vf *vf,
 			bool trigger_reset, bool release_vf);
@@ -702,6 +704,117 @@ ice_iov_handle_vflr(struct ice_softc *sc)
 	}
 }
 
+/**
+ * ice_iov_handle_mdd - Attribute malicious-driver events to VFs
+ * @sc: device softc structure
+ *
+ * Consume every per-VF MDD latch. Block further virtchnl requests and reset a
+ * newly blocked VF without restoring its queues, so even event classes which
+ * only drop the offending packet cannot continue traffic. An optional policy
+ * reconstructs and releases the VF immediately instead.
+ *
+ * @returns a mask of enum ice_mdd_source_bits attributed to configured or
+ * unconfigured VFs of this PF.
+ */
+u32
+ice_iov_handle_mdd(struct ice_softc *sc)
+{
+	static const struct timeval log_interval = { 2, 0 };
+	struct ice_hw *hw = &sc->hw;
+	struct virtchnl_pf_event event = {};
+	struct ice_vf *vf;
+	u32 reg, sources, vf_sources, tx_events, rx_events, vf_flags;
+	bool newly_blocked;
+	int error;
+
+	event.event = VIRTCHNL_EVENT_RESET_IMPENDING;
+	event.severity = PF_EVENT_SEVERITY_CERTAIN_DOOM;
+	vf_sources = 0;
+	for (int i = 0; i < sc->num_vfs; i++) {
+		vf = &sc->vfs[i];
+		sources = 0;
+		tx_events = 0;
+		rx_events = 0;
+
+		reg = rd32(hw, VP_MDET_TX_PQM(vf->vf_num));
+		if ((reg & VP_MDET_TX_PQM_VALID_M) != 0) {
+			wr32(hw, VP_MDET_TX_PQM(vf->vf_num), 0xffff);
+			sources |= ICE_MDD_TX_PQM;
+			tx_events++;
+		}
+		reg = rd32(hw, VP_MDET_TX_TCLAN(vf->vf_num));
+		if ((reg & VP_MDET_TX_TCLAN_VALID_M) != 0) {
+			wr32(hw, VP_MDET_TX_TCLAN(vf->vf_num), 0xffff);
+			sources |= ICE_MDD_TX_TCLAN;
+			tx_events++;
+		}
+		reg = rd32(hw, VP_MDET_TX_TDPU(vf->vf_num));
+		if ((reg & VP_MDET_TX_TDPU_VALID_M) != 0) {
+			wr32(hw, VP_MDET_TX_TDPU(vf->vf_num), 0xffff);
+			sources |= ICE_MDD_TX_TDPU;
+			tx_events++;
+		}
+		reg = rd32(hw, VP_MDET_RX(vf->vf_num));
+		if ((reg & VP_MDET_RX_VALID_M) != 0) {
+			wr32(hw, VP_MDET_RX(vf->vf_num), 0xffff);
+			sources |= ICE_MDD_RX;
+			rx_events++;
+		}
+		if (tx_events == 0 && rx_events == 0)
+			continue;
+		vf_sources |= sources;
+
+		vf_flags = atomic_load_acq_32(&vf->vf_flags);
+		if ((vf_flags & VF_FLAG_ENABLED) == 0 || vf->vsi == NULL)
+			continue;
+		vf->mdd_tx_events += tx_events;
+		vf->mdd_rx_events += rx_events;
+		newly_blocked = (vf_flags & VF_FLAG_MDD_BLOCKED) == 0;
+		atomic_set_32(&vf->vf_flags, VF_FLAG_MDD_BLOCKED);
+
+		if (ratecheck(&vf->last_mdd_log, &log_interval)) {
+			device_printf(sc->dev,
+			    "malicious-driver event from VF-%d "
+			    "(tx %ju, rx %ju); %s\n", vf->vf_num,
+			    (uintmax_t)vf->mdd_tx_events,
+			    (uintmax_t)vf->mdd_rx_events,
+			    sc->mdd_auto_reset_vf && newly_blocked ?
+			    "resetting VF" : "VF remains blocked");
+		}
+		if (!newly_blocked)
+			continue;
+
+		/* Ignore notification failure; reset does not require VF help. */
+		if (sc->mdd_auto_reset_vf &&
+		    (vf_flags & VF_FLAG_INITIALIZED) != 0 &&
+		    ice_check_sq_alive(hw, &hw->mailboxq)) {
+			(void)ice_aq_send_msg_to_vf(hw, vf->vf_num,
+			    VIRTCHNL_OP_EVENT, VIRTCHNL_STATUS_SUCCESS,
+			    (u8 *)&event, sizeof(event), NULL);
+		}
+		/*
+		 * TDPU MDD drops only the offending packet. Reset the entire VF so
+		 * the software blocked state always means that traffic is actually
+		 * fenced. The opt-in policy reconstructs its queues immediately.
+		 */
+		error = ice_reset_vf(sc, vf, true, sc->mdd_auto_reset_vf);
+		if (error != 0) {
+			device_printf(sc->dev,
+			    "failed to quiesce MDD-blocked VF-%d: %d\n",
+			    vf->vf_num, error);
+		} else if (!sc->mdd_auto_reset_vf) {
+			/*
+			 * Complete VFR without restoring queues. This leaves the VF
+			 * inactive and DMA-fenced, but permits a later physical FLR to
+			 * create a new reset edge and recover it.
+			 */
+			ice_iov_complete_vf_reset(sc, vf, false);
+		}
+	}
+	ice_flush(hw);
+	return (vf_sources);
+}
+
 /**
  * ice_iov_notify_vfs_reset - Notify initialized VFs of an impending reset
  * @sc: device softc structure
@@ -745,37 +858,73 @@ ice_iov_clear_vf_queue_state(struct ice_vf *vf)
 }
 
 /**
- * ice_iov_ready_vf - Setup VF interrupts and mark it as ready
+ * ice_iov_clear_vf_mdd - Clear hardware MDD latches for a reset VF
  * @sc: device softc structure
  * @vf: driver's VF structure for the VF to update
  *
- * Clears VF reset triggering bit, sets up the PF<->VF interrupt
- * mapping and marks the VF as active in the HW so that the VF
- * driver can use it.
+ * Function reset can generate a spurious anti-spoof MDD indication. Consume
+ * all per-VF latches before releasing reset so it cannot re-block a VF which
+ * has just been reconstructed successfully.
  */
 static void
-ice_iov_ready_vf(struct ice_softc *sc, struct ice_vf *vf)
+ice_iov_clear_vf_mdd(struct ice_softc *sc, struct ice_vf *vf)
 {
 	struct ice_hw *hw = &sc->hw;
-	u32 reg;
 
-	/* A VF or PF reset discards all queue configuration and state. */
-	ice_iov_clear_vf_queue_state(vf);
+	wr32(hw, VP_MDET_TX_PQM(vf->vf_num), 0xffff);
+	wr32(hw, VP_MDET_TX_TCLAN(vf->vf_num), 0xffff);
+	wr32(hw, VP_MDET_TX_TDPU(vf->vf_num), 0xffff);
+	wr32(hw, VP_MDET_RX(vf->vf_num), 0xffff);
+	ice_flush(hw);
+}
+
+/**
+ * ice_iov_complete_vf_reset - Complete a VF reset
+ * @sc: device softc structure
+ * @vf: driver's VF structure for the VF to update
+ * @restore_mapping: restore the VF queue and interrupt mappings
+ *
+ * Clear VFSWR after the hardware drain, optionally restore the VF mappings,
+ * and then publish VFACTIVE. The mapping registers do not retain writes made
+ * while VFSWR remains asserted. Callers may instead leave a software-blocked
+ * VF with no queue or interrupt mappings.
+ */
+static void
+ice_iov_complete_vf_reset(struct ice_softc *sc, struct ice_vf *vf,
+    bool restore_mapping)
+{
+	struct ice_hw *hw = &sc->hw;
+	u32 reg;
 
-	/* Clear the triggering bit */
 	reg = rd32(hw, VPGEN_VFRTRIG(vf->vf_num));
 	reg &= ~VPGEN_VFRTRIG_VFSWR_M;
 	wr32(hw, VPGEN_VFRTRIG(vf->vf_num), reg);
-
-	/* Setup VF interrupt allocation and mapping */
-	ice_iov_setup_intr_mapping(sc, vf);
-
-	/* Indicate to the VF that reset is done */
+	if (restore_mapping)
+		ice_iov_setup_intr_mapping(sc, vf);
 	wr32(hw, VFGEN_RSTAT(vf->vf_num), VIRTCHNL_VFR_VFACTIVE);
-
 	ice_flush(hw);
 }
 
+/**
+ * ice_iov_ready_vf - Setup VF interrupts and mark it as ready
+ * @sc: device softc structure
+ * @vf: driver's VF structure for the VF to update
+ *
+ * Clears VF reset triggering bit, sets up the PF<->VF interrupt
+ * mapping and marks the VF as active in the HW so that the VF
+ * driver can use it.
+ */
+static void
+ice_iov_ready_vf(struct ice_softc *sc, struct ice_vf *vf)
+{
+	/* A VF or PF reset discards all queue configuration and state. */
+	ice_iov_clear_vf_queue_state(vf);
+	ice_iov_clear_vf_mdd(sc, vf);
+	atomic_clear_32(&vf->vf_flags, VF_FLAG_MDD_BLOCKED);
+
+	ice_iov_complete_vf_reset(sc, vf, true);
+}
+
 /**
  * ice_iov_rebuild_vf - Rebuild a VF VSI after a PF or device reset
  * @sc: device softc structure
@@ -983,6 +1132,8 @@ ice_reset_vf(struct ice_softc *sc, struct ice_vf *vf, bool trigger_reset,
 	}
 
 	if (!release_vf) {
+		/* Discard any anti-spoof MDD indication caused by the reset. */
+		ice_iov_clear_vf_mdd(sc, vf);
 		atomic_clear_32(&vf->vf_flags, VF_FLAG_RESET_FAILED);
 		return (0);
 	}
@@ -2599,6 +2750,9 @@ ice_vc_handle_vf_msg(struct ice_softc *sc, struct ice_rq_event_info *event)
 	vf_flags = atomic_load_acq_32(&vf->vf_flags);
 	if ((vf_flags & VF_FLAG_ENABLED) == 0 || vf->vsi == NULL)
 		return;
+	/* Only a reset outside this dispatcher may release an MDD-blocked VF. */
+	if ((vf_flags & VF_FLAG_MDD_BLOCKED) != 0)
+		return;
 
 	/*
 	 * Permit only reset negotiation while VF hardware state is unsafe.
diff --git a/sys/dev/ice/ice_iov.h b/sys/dev/ice/ice_iov.h
index fc78169172d2..35f5edcb5acd 100644
--- a/sys/dev/ice/ice_iov.h
+++ b/sys/dev/ice/ice_iov.h
@@ -73,6 +73,7 @@ enum ice_vf_flags {
 	VF_FLAG_INITIALIZED		= BIT(5),
 	VF_FLAG_REBUILD_FAILED		= BIT(6),
 	VF_FLAG_RESET_FAILED		= BIT(7),
+	VF_FLAG_MDD_BLOCKED		= BIT(8),
 };
 
 struct ice_vf_mac_filter {
@@ -110,6 +111,10 @@ struct ice_vf {
 	u32 txq_configured;
 	u32 rxq_configured;
 	u32 rxq_enabled;
+
+	u64 mdd_tx_events;
+	u64 mdd_rx_events;
+	struct timeval last_mdd_log;
 };
 
 #define ICE_PCIE_DEV_STATUS			0xAA
@@ -134,6 +139,7 @@ int ice_iov_rebuild_vf(struct ice_softc *sc, struct ice_vsi *vsi);
 void ice_iov_uninit(struct ice_softc *sc);
 
 void ice_iov_handle_vflr(struct ice_softc *sc);
+u32 ice_iov_handle_mdd(struct ice_softc *sc);
 void ice_iov_notify_vfs_reset(struct ice_softc *sc);
 int ice_iov_quiesce_vfs_for_reset(struct ice_softc *sc);
 
diff --git a/sys/dev/ice/ice_lib.c b/sys/dev/ice/ice_lib.c
index 9d634b6a7658..679def5c0e6d 100644
--- a/sys/dev/ice/ice_lib.c
+++ b/sys/dev/ice/ice_lib.c
@@ -5968,6 +5968,13 @@ ice_add_device_tunables(struct ice_softc *sc)
 			CTLFLAG_RDTUN, &sc->enable_health_events, 0,
 			"Enable FW health event reporting for this PF");
 
+#ifdef PCI_IOV
+	sc->mdd_auto_reset_vf = ice_mdd_auto_reset_vf;
+	SYSCTL_ADD_BOOL(ctx, ctx_list, OID_AUTO, "mdd_auto_reset_vf",
+	    CTLFLAG_RDTUN, &sc->mdd_auto_reset_vf, 0,
+	    "Automatically restore VFs after an MDD reset");
+#endif
+
 	/* Add a node to track VSI sysctls. Keep track of the node in the
 	 * softc so that we can hook other sysctls into it later. This
 	 * includes both the VSI statistics, as well as potentially dynamic
@@ -8395,11 +8402,22 @@ ice_init_link_events(struct ice_softc *sc)
 	return (0);
 }
 
-#ifndef GL_MDET_TX_TCLAN
-/* Temporarily use this redefinition until the definition is fixed */
-#define GL_MDET_TX_TCLAN	E800_GL_MDET_TX_TCLAN
-#define PF_MDET_TX_TCLAN	E800_PF_MDET_TX_TCLAN
-#endif /* !defined(GL_MDET_TX_TCLAN) */
+static u32
+ice_gl_mdet_tx_tclan(struct ice_hw *hw)
+{
+
+	return (ice_is_e830(hw) ? E830_GL_MDET_TX_TCLAN :
+	    GL_MDET_TX_TCLAN);
+}
+
+static u32
+ice_pf_mdet_tx_tclan(struct ice_hw *hw)
+{
+
+	return (ice_is_e830(hw) ? E830_PF_MDET_TX_TCLAN :
+	    PF_MDET_TX_TCLAN);
+}
+
 /**
  * ice_handle_mdd_event - Handle possibly malicious events
  * @sc: the device softc
@@ -8412,98 +8430,143 @@ void
 ice_handle_mdd_event(struct ice_softc *sc)
 {
 	struct ice_hw *hw = &sc->hw;
-	bool mdd_detected = false, request_reinit = false;
 	device_t dev = sc->dev;
-	u32 reg;
+	u32 pf_sources, reg, tclan_reg, vf_sources;
+	bool request_reinit;
 
 	if (!ice_testandclear_state(&sc->state, ICE_STATE_MDD_PENDING))
 		return;
 
-	reg = rd32(hw, GL_MDET_TX_TCLAN);
+	pf_sources = 0;
+	vf_sources = 0;
+	tclan_reg = ice_gl_mdet_tx_tclan(hw);
+	reg = rd32(hw, tclan_reg);
 	if (reg & GL_MDET_TX_TCLAN_VALID_M) {
-		u8 pf_num  = (reg & GL_MDET_TX_TCLAN_PF_NUM_M) >> GL_MDET_TX_TCLAN_PF_NUM_S;
-		u16 vf_num = (reg & GL_MDET_TX_TCLAN_VF_NUM_M) >> GL_MDET_TX_TCLAN_VF_NUM_S;
-		u8 event   = (reg & GL_MDET_TX_TCLAN_MAL_TYPE_M) >> GL_MDET_TX_TCLAN_MAL_TYPE_S;
-		u16 queue  = (reg & GL_MDET_TX_TCLAN_QNUM_M) >> GL_MDET_TX_TCLAN_QNUM_S;
+		u8 pf_num = (reg & GL_MDET_TX_TCLAN_PF_NUM_M) >>
+		    GL_MDET_TX_TCLAN_PF_NUM_S;
+		u16 vf_num = (reg & GL_MDET_TX_TCLAN_VF_NUM_M) >>
+		    GL_MDET_TX_TCLAN_VF_NUM_S;
+		u8 event = (reg & GL_MDET_TX_TCLAN_MAL_TYPE_M) >>
+		    GL_MDET_TX_TCLAN_MAL_TYPE_S;
+		u16 queue = (reg & GL_MDET_TX_TCLAN_QNUM_M) >>
+		    GL_MDET_TX_TCLAN_QNUM_S;
 
-		device_printf(dev, "Malicious Driver Detection Tx Descriptor check event '%s' on Tx queue %u PF# %u VF# %u\n",
-			      ice_mdd_tx_tclan_str(event), queue, pf_num, vf_num);
+		device_printf(dev,
+		    "malicious-driver Tx descriptor event '%s' on queue %u, "
+		    "PF %u, VF %u\n", ice_mdd_tx_tclan_str(event), queue,
+		    pf_num, vf_num);
 
 		/* Only clear this event if it matches this PF, that way other
 		 * PFs can read the event and determine VF and queue number.
 		 */
 		if (pf_num == hw->pf_id)
-			wr32(hw, GL_MDET_TX_TCLAN, 0xffffffff);
-
-		mdd_detected = true;
+			wr32(hw, tclan_reg, 0xffffffff);
 	}
 
 	/* Determine what triggered the MDD event */
 	reg = rd32(hw, GL_MDET_TX_PQM);
 	if (reg & GL_MDET_TX_PQM_VALID_M) {
-		u8 pf_num  = (reg & GL_MDET_TX_PQM_PF_NUM_M) >> GL_MDET_TX_PQM_PF_NUM_S;
-		u16 vf_num = (reg & GL_MDET_TX_PQM_VF_NUM_M) >> GL_MDET_TX_PQM_VF_NUM_S;
-		u8 event   = (reg & GL_MDET_TX_PQM_MAL_TYPE_M) >> GL_MDET_TX_PQM_MAL_TYPE_S;
-		u16 queue  = (reg & GL_MDET_TX_PQM_QNUM_M) >> GL_MDET_TX_PQM_QNUM_S;
+		u8 pf_num = (reg & GL_MDET_TX_PQM_PF_NUM_M) >>
+		    GL_MDET_TX_PQM_PF_NUM_S;
+		u16 vf_num = (reg & GL_MDET_TX_PQM_VF_NUM_M) >>
+		    GL_MDET_TX_PQM_VF_NUM_S;
+		u8 event = (reg & GL_MDET_TX_PQM_MAL_TYPE_M) >>
+		    GL_MDET_TX_PQM_MAL_TYPE_S;
+		u16 queue = (reg & GL_MDET_TX_PQM_QNUM_M) >>
+		    GL_MDET_TX_PQM_QNUM_S;
 
-		device_printf(dev, "Malicious Driver Detection Tx Quanta check event '%s' on Tx queue %u PF# %u VF# %u\n",
-			      ice_mdd_tx_pqm_str(event), queue, pf_num, vf_num);
+		device_printf(dev,
+		    "malicious-driver Tx quanta event '%s' on queue %u, "
+		    "PF %u, VF %u\n", ice_mdd_tx_pqm_str(event), queue,
+		    pf_num, vf_num);
 
 		/* Only clear this event if it matches this PF, that way other
 		 * PFs can read the event and determine VF and queue number.
 		 */
 		if (pf_num == hw->pf_id)
 			wr32(hw, GL_MDET_TX_PQM, 0xffffffff);
+	}
+
+	reg = rd32(hw, GL_MDET_TX_TDPU);
+	if (reg & GL_MDET_TX_TDPU_VALID_M) {
+		u8 pf_num = (reg & GL_MDET_TX_TDPU_PF_NUM_M) >>
+		    GL_MDET_TX_TDPU_PF_NUM_S;
+		u16 vf_num = (reg & GL_MDET_TX_TDPU_VF_NUM_M) >>
+		    GL_MDET_TX_TDPU_VF_NUM_S;
+		u8 event = (reg & GL_MDET_TX_TDPU_MAL_TYPE_M) >>
+		    GL_MDET_TX_TDPU_MAL_TYPE_S;
+		u16 queue = (reg & GL_MDET_TX_TDPU_QNUM_M) >>
+		    GL_MDET_TX_TDPU_QNUM_S;
 
-		mdd_detected = true;
+		device_printf(dev,
+		    "malicious-driver Tx data event %#x on queue %u, "
+		    "PF %u, VF %u\n", event, queue, pf_num, vf_num);
+		if (pf_num == hw->pf_id)
+			wr32(hw, GL_MDET_TX_TDPU, 0xffffffff);
 	}
 
 	reg = rd32(hw, GL_MDET_RX);
 	if (reg & GL_MDET_RX_VALID_M) {
-		u8 pf_num  = (reg & GL_MDET_RX_PF_NUM_M) >> GL_MDET_RX_PF_NUM_S;
-		u16 vf_num = (reg & GL_MDET_RX_VF_NUM_M) >> GL_MDET_RX_VF_NUM_S;
-		u8 event   = (reg & GL_MDET_RX_MAL_TYPE_M) >> GL_MDET_RX_MAL_TYPE_S;
-		u16 queue  = (reg & GL_MDET_RX_QNUM_M) >> GL_MDET_RX_QNUM_S;
+		u8 pf_num = (reg & GL_MDET_RX_PF_NUM_M) >>
+		    GL_MDET_RX_PF_NUM_S;
+		u8 event = (reg & GL_MDET_RX_MAL_TYPE_M) >>
+		    GL_MDET_RX_MAL_TYPE_S;
+		u16 queue = (reg & GL_MDET_RX_QNUM_M) >>
+		    GL_MDET_RX_QNUM_S;
 
-		device_printf(dev, "Malicious Driver Detection Rx event '%s' on Rx queue %u PF# %u VF# %u\n",
-			      ice_mdd_rx_str(event), queue, pf_num, vf_num);
+		/*
+		 * E810 Datasheet section 9.2.2.2.1 says only the queue field in
+		 * GL_MDET_RX is valid.  VP_MDET_RX provides VF attribution.
+		 */
+		device_printf(dev,
+		    "malicious-driver Rx event '%s' on queue %u, PF %u\n",
+		    ice_mdd_rx_str(event), queue, pf_num);
 
 		/* Only clear this event if it matches this PF, that way other
-		 * PFs can read the event and determine VF and queue number.
+		 * PFs can read the event and determine the queue number.
 		 */
 		if (pf_num == hw->pf_id)
 			wr32(hw, GL_MDET_RX, 0xffffffff);
-
-		mdd_detected = true;
 	}
 
-	/* Now, confirm that this event actually affects this PF, by checking
-	 * the PF registers.
-	 */
-	if (mdd_detected) {
-		reg = rd32(hw, PF_MDET_TX_TCLAN);
-		if (reg & PF_MDET_TX_TCLAN_VALID_M) {
-			wr32(hw, PF_MDET_TX_TCLAN, 0xffff);
-			sc->soft_stats.tx_mdd_count++;
-			request_reinit = true;
-		}
-
-		reg = rd32(hw, PF_MDET_TX_PQM);
-		if (reg & PF_MDET_TX_PQM_VALID_M) {
-			wr32(hw, PF_MDET_TX_PQM, 0xffff);
-			sc->soft_stats.tx_mdd_count++;
-			request_reinit = true;
-		}
-
-		reg = rd32(hw, PF_MDET_RX);
-		if (reg & PF_MDET_RX_VALID_M) {
-			wr32(hw, PF_MDET_RX, 0xffff);
-			sc->soft_stats.rx_mdd_count++;
-			request_reinit = true;
-		}
+	/* Per-function latches provide authoritative PF/VF attribution. */
+	tclan_reg = ice_pf_mdet_tx_tclan(hw);
+	reg = rd32(hw, tclan_reg);
+	if (reg & PF_MDET_TX_TCLAN_VALID_M) {
+		wr32(hw, tclan_reg, 0xffff);
+		sc->soft_stats.tx_mdd_count++;
+		pf_sources |= ICE_MDD_TX_TCLAN;
+	}
+	reg = rd32(hw, PF_MDET_TX_PQM);
+	if (reg & PF_MDET_TX_PQM_VALID_M) {
+		wr32(hw, PF_MDET_TX_PQM, 0xffff);
+		sc->soft_stats.tx_mdd_count++;
+		pf_sources |= ICE_MDD_TX_PQM;
+	}
+	reg = rd32(hw, PF_MDET_TX_TDPU);
+	if (reg & PF_MDET_TX_TDPU_VALID_M) {
+		wr32(hw, PF_MDET_TX_TDPU, 0xffff);
+		sc->soft_stats.tx_mdd_count++;
+		pf_sources |= ICE_MDD_TX_TDPU;
+	}
+	reg = rd32(hw, PF_MDET_RX);
+	if (reg & PF_MDET_RX_VALID_M) {
+		wr32(hw, PF_MDET_RX, 0xffff);
+		sc->soft_stats.rx_mdd_count++;
+		pf_sources |= ICE_MDD_RX;
 	}
 
-	/* TODO: Implement logic to detect and handle events caused by VFs. */
+#ifdef PCI_IOV
+	vf_sources = ice_iov_handle_mdd(sc);
+#endif
+	/*
+	 * E810 sets the parent PF_MDET latch for events attributed by a
+	 * VP_MDET latch to one of its VFs. Recover the PF only for event
+	 * classes which were not attributed to a VF. TDPU drops only the
+	 * offending packet and does not stop a queue.
+	 */
+	request_reinit = (pf_sources & ~vf_sources &
+	    (ICE_MDD_TX_PQM | ICE_MDD_TX_TCLAN | ICE_MDD_RX)) != 0;
 
 	/* request that the upper stack re-initialize the Tx/Rx queues */
 	if (request_reinit)
diff --git a/sys/dev/ice/ice_lib.h b/sys/dev/ice/ice_lib.h
index 6c6f93b97228..912e6f690a95 100644
--- a/sys/dev/ice/ice_lib.h
+++ b/sys/dev/ice/ice_lib.h
@@ -72,6 +72,13 @@
 
 #include "ice_rss.h"
 
+enum ice_mdd_source_bits {
+	ICE_MDD_TX_PQM			= BIT(0),
+	ICE_MDD_TX_TCLAN		= BIT(1),
+	ICE_MDD_TX_TDPU			= BIT(2),
+	ICE_MDD_RX			= BIT(3),
+};
+
 /* Hide debug sysctls unless INVARIANTS is enabled */
 #ifdef INVARIANTS
 #define ICE_CTLFLAG_DEBUG 0
@@ -122,6 +129,11 @@ extern bool ice_enable_tx_lldp_filter;
 /* global sysctl indicating whether FW health status events should be enabled */
 extern bool ice_enable_health_events;
 
+#ifdef PCI_IOV
+/* reconstruct and release a VF automatically after an MDD reset */
+extern bool ice_mdd_auto_reset_vf;
+#endif
+
 /* global sysctl indicating whether to enable 5-layer scheduler topology */
 extern bool ice_tx_balance_en;