git: bbf93227fe9e - main - igc: Recover from fatal internal memory errors

From: Kevin Bowling <kbowling_at_FreeBSD.org>
Date: Wed, 12 Aug 2026 19:40:07 UTC
The branch main has been updated by kbowling:

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

commit bbf93227fe9ee1f38b7db5477398c3564e5c84bc
Author:     Kevin Bowling <kbowling@FreeBSD.org>
AuthorDate: 2026-08-12 04:12:49 +0000
Commit:     Kevin Bowling <kbowling@FreeBSD.org>
CommitDate: 2026-08-12 19:39:54 +0000

    igc: Recover from fatal internal memory errors
    
    I225 and I226 report uncorrectable internal memory errors through
    ICR.FER and identify the affected region in PEIND.  Depending on the
    region, hardware stops transmit or all PCIe and DMA traffic until the
    port is reset and reinitialized.
    
    Enable the fatal error interrupt and capture its read clear status in
    the interrupt filter.  Mask the cause while an iflib reset is pending,
    report the affected memory regions, and expose per region indication
    counters.
    
    PCIe region parity failures require a different recovery order from a
    normal reset: assert DEV_RST, wait at least 3 ms, disable PCIe master
    requests, clear PCIEERRSTS, and then reinitialize the port.  Follow that
    sequence before entering the normal reset path and clear the remaining
    LAN status afterward.
    
    The I225/I226 PBECCSTS layout is unrelated to the PCH layout previously
    copied into the igc headers.  Replace those unused definitions with the
    I225/I226 memory error register definitions.
    
    Hardware validation used an I225-IT revision 3 and a debug kernel that
    wrote only the documented self-clearing injection bits.  It did not
    synthesize interrupt or status state.
    
    Coverage, notably DMA and Mgmt are not fully testable in my setup:
        Region  Observed hardware status          Result
        LAN     PEIND 0x1, LANPERRSTS 0x200       Reset and recovered
        PCIe    PEIND 0x4, PCIEERRSTS 0x8         Reset and recovered
        DMA     DRPARC injection read back zero   DFT-gated on test NIC
        Mgmt    Host debug strap unavailable      Not injectable
    
    The repeated LAN and PCIe tests recovered without a panic or watchdog.
    A PCIe-to-LAN sequence also verified that reset-time PEIND indications
    are drained before FER is unmasked.
    
    MFC after:      2 weeks
    Sponsored by:   BBOX.io
---
 sys/dev/igc/if_igc.c      | 226 +++++++++++++++++++++++++++++++++++++++++++++-
 sys/dev/igc/if_igc.h      |  10 ++
 sys/dev/igc/igc_defines.h |  25 ++++-
 sys/dev/igc/igc_regs.h    |   9 +-
 4 files changed, 258 insertions(+), 12 deletions(-)

diff --git a/sys/dev/igc/if_igc.c b/sys/dev/igc/if_igc.c
index 359cd1d6dc39..add391f9195f 100644
--- a/sys/dev/igc/if_igc.c
+++ b/sys/dev/igc/if_igc.c
@@ -127,6 +127,10 @@ static void	igc_if_intr_enable(if_ctx_t);
 static void	igc_if_intr_disable(if_ctx_t);
 static int	igc_if_rx_queue_intr_enable(if_ctx_t, uint16_t);
 static int	igc_if_tx_queue_intr_enable(if_ctx_t, uint16_t);
+static void	igc_handle_fatal_error_intr(struct igc_softc *, u32);
+static bool	igc_handle_fatal_error_admin(struct igc_softc *);
+static void	igc_prepare_fatal_error_reset(struct igc_softc *);
+static void	igc_finish_fatal_error_reset(struct igc_softc *);
 static void	igc_if_multi_set(if_ctx_t);
 static void	igc_if_update_admin_status(if_ctx_t);
 static void	igc_apply_i225_ipg_workaround(struct igc_softc *);
@@ -162,6 +166,13 @@ static void	igc_enable_wakeup(if_ctx_t);
 
 int		igc_intr(void *);
 
+enum igc_fatal_error_state {
+	IGC_FATAL_ERROR_NONE,
+	IGC_FATAL_ERROR_CAPTURING,
+	IGC_FATAL_ERROR_DETECTED,
+	IGC_FATAL_ERROR_RESET_REQUESTED,
+};
+
 /* MSI-X handlers */
 static int	igc_if_msix_intr_assign(if_ctx_t, int);
 static int	igc_msix_link(void *);
@@ -1120,6 +1131,8 @@ igc_intr(void *arg)
 	if (reg_icr & IGC_ICR_RXO)
 		sc->rx_overruns++;
 
+	igc_handle_fatal_error_intr(sc, reg_icr);
+
 	igc_neweitr(sc, que, rxr);
 
 	return (FILTER_SCHEDULE_THREAD);
@@ -1185,8 +1198,13 @@ igc_msix_link(void *arg)
 	if (reg_icr & (IGC_ICR_RXSEQ | IGC_ICR_LSC)) {
 		igc_handle_link(sc->ctx);
 	}
+	igc_handle_fatal_error_intr(sc, reg_icr);
 
-	IGC_WRITE_REG(&sc->hw, IGC_IMS, IGC_IMS_LSC);
+	reg_icr = IGC_IMS_LSC;
+	if (atomic_load_acq_32(&sc->fatal_error_state) ==
+	    IGC_FATAL_ERROR_NONE)
+		reg_icr |= IGC_IMS_FER;
+	IGC_WRITE_REG(&sc->hw, IGC_IMS, reg_icr);
 	IGC_WRITE_REG(&sc->hw, IGC_EIMS, sc->link_mask);
 
 	return (FILTER_HANDLED);
@@ -1202,6 +1220,93 @@ igc_handle_link(void *context)
 	iflib_admin_intr_deferred(ctx);
 }
 
+/*
+ * Fatal internal memory errors stop some or all device traffic.  Capture the
+ * read-clear indication before handing recovery to the iflib admin task.
+ */
+static void
+igc_handle_fatal_error_intr(struct igc_softc *sc, u32 icr)
+{
+	struct igc_hw *hw;
+	u32 lanerr, mngerr, pcieerr, peind;
+
+	if ((icr & IGC_ICR_FER) == 0)
+		return;
+
+	hw = &sc->hw;
+	IGC_WRITE_REG(hw, IGC_IMC, IGC_IMS_FER);
+	if (!atomic_cmpset_32(&sc->fatal_error_state,
+	    IGC_FATAL_ERROR_NONE, IGC_FATAL_ERROR_CAPTURING))
+		return;
+
+	peind = IGC_READ_REG(hw, IGC_PEIND) & IGC_PEIND_FATAL_MASK;
+	pcieerr = IGC_READ_REG(hw, IGC_PCIEERRSTS) &
+	    IGC_PCIEERRSTS_FATAL_MASK;
+	lanerr = IGC_READ_REG(hw, IGC_LANPERRSTS) &
+	    IGC_LANPERRSTS_RETX_BUF;
+	mngerr = IGC_READ_REG(hw, IGC_MNGPARSTS) &
+	    IGC_MNGPARSTS_FATAL_MASK;
+	if (pcieerr != 0)
+		peind |= IGC_PEIND_PCIE_PARITY_FATAL;
+	if (lanerr != 0)
+		peind |= IGC_PEIND_LANPORT_PARITY_FATAL;
+
+	sc->fatal_error_peind = peind;
+	sc->fatal_error_pcie = pcieerr;
+	sc->fatal_error_lan = lanerr;
+	sc->fatal_error_mng = mngerr;
+	atomic_store_rel_32(&sc->fatal_error_state,
+	    IGC_FATAL_ERROR_DETECTED);
+	iflib_admin_intr_deferred(sc->ctx);
+}
+
+static bool
+igc_handle_fatal_error_admin(struct igc_softc *sc)
+{
+	u32 peind;
+
+	if (!atomic_cmpset_acq_32(&sc->fatal_error_state,
+	    IGC_FATAL_ERROR_DETECTED, IGC_FATAL_ERROR_RESET_REQUESTED))
+		return (atomic_load_acq_32(&sc->fatal_error_state) !=
+		    IGC_FATAL_ERROR_NONE);
+
+	peind = sc->fatal_error_peind;
+	if (peind & IGC_PEIND_LANPORT_PARITY_FATAL)
+		sc->fatal_error_lan_count++;
+	if (peind & IGC_PEIND_MNG_PARITY_FATAL)
+		sc->fatal_error_mng_count++;
+	if (peind & IGC_PEIND_PCIE_PARITY_FATAL)
+		sc->fatal_error_pcie_count++;
+	if (peind & IGC_PEIND_DMA_PARITY_FATAL)
+		sc->fatal_error_dma_count++;
+	if (peind == 0)
+		sc->fatal_error_unknown_count++;
+
+	device_printf(sc->dev,
+	    "fatal internal memory error: PEIND %#x, PCIEERRSTS %#x, "
+	    "LANPERRSTS %#x, MNGPARSTS %#x\n",
+	    peind, sc->fatal_error_pcie, sc->fatal_error_lan,
+	    sc->fatal_error_mng);
+	/* Management-memory recovery is owned by management firmware. */
+	if (peind != 0 && (peind & IGC_PEIND_HOST_FATAL_MASK) == 0) {
+		sc->fatal_error_peind = 0;
+		sc->fatal_error_pcie = 0;
+		sc->fatal_error_lan = 0;
+		sc->fatal_error_mng = 0;
+		atomic_store_rel_32(&sc->fatal_error_state,
+		    IGC_FATAL_ERROR_NONE);
+		IGC_WRITE_REG(&sc->hw, IGC_IMS, IGC_IMS_FER);
+		IGC_WRITE_FLUSH(&sc->hw);
+		return (true);
+	}
+
+	device_printf(sc->dev, "requesting reset after memory error\n");
+	iflib_request_reset(sc->ctx);
+	/* Re-enter the admin task so it observes the reset request. */
+	iflib_admin_intr_deferred(sc->ctx);
+	return (true);
+}
+
 /*********************************************************************
  *
  *  Media Ioctl callback
@@ -1446,6 +1551,9 @@ igc_if_update_admin_status(if_ctx_t ctx)
 	device_t dev = iflib_get_dev(ctx);
 	u32 link_check, thstat, ctrl;
 
+	if (igc_handle_fatal_error_admin(sc))
+		return;
+
 	link_check = thstat = ctrl = 0;
 	/* Get the cached link value or read phy for real */
 	switch (hw->phy.media_type) {
@@ -1501,10 +1609,93 @@ igc_if_stop(if_ctx_t ctx)
 	INIT_DEBUGOUT("igc_if_stop: begin");
 
 	igc_led_restore(sc);
+	igc_prepare_fatal_error_reset(sc);
 	igc_reset_hw(&sc->hw);
+	igc_finish_fatal_error_reset(sc);
 	IGC_WRITE_REG(&sc->hw, IGC_WUC, 0);
 }
 
+/*
+ * A PCIe-region parity failure stops PCIe and DMA traffic.  Intel requires a
+ * device reset before master disable in this case, unlike the normal reset
+ * path, which disables the bus master first.
+ */
+static void
+igc_prepare_fatal_error_reset(struct igc_softc *sc)
+{
+	struct igc_hw *hw;
+	s32 error;
+	u32 ctrl, pcieerr;
+	int i;
+
+	if (atomic_load_acq_32(&sc->fatal_error_state) ==
+	    IGC_FATAL_ERROR_NONE)
+		return;
+
+	hw = &sc->hw;
+	pcieerr = sc->fatal_error_pcie |
+	    (IGC_READ_REG(hw, IGC_PCIEERRSTS) & IGC_PCIEERRSTS_FATAL_MASK);
+	if ((sc->fatal_error_peind & IGC_PEIND_PCIE_PARITY_FATAL) == 0 &&
+	    pcieerr == 0)
+		return;
+
+	ctrl = IGC_READ_REG(hw, IGC_CTRL);
+	IGC_WRITE_REG(hw, IGC_CTRL, ctrl | IGC_CTRL_DEV_RST);
+	/* Do not access device registers for at least 3 ms after DEV_RST. */
+	msec_delay(3);
+	for (i = 0; i < AUTO_READ_DONE_TIMEOUT; i++) {
+		if ((IGC_READ_REG(hw, IGC_EECD) & IGC_EECD_AUTO_RD) != 0 &&
+		    (IGC_READ_REG(hw, IGC_STATUS) & IGC_STATUS_RST_DONE) != 0)
+			break;
+		msec_delay(1);
+	}
+	if (i == AUTO_READ_DONE_TIMEOUT)
+		device_printf(sc->dev,
+		    "device reset did not complete during parity recovery\n");
+	error = igc_disable_pcie_master_generic(hw);
+	if (error != IGC_SUCCESS)
+		device_printf(sc->dev,
+		    "PCIe master disable failed during parity recovery: %d\n",
+		    error);
+	pcieerr |= IGC_READ_REG(hw, IGC_PCIEERRSTS) &
+	    IGC_PCIEERRSTS_FATAL_MASK;
+	if (pcieerr != 0)
+		IGC_WRITE_REG(hw, IGC_PCIEERRSTS, pcieerr);
+}
+
+static void
+igc_finish_fatal_error_reset(struct igc_softc *sc)
+{
+	struct igc_hw *hw;
+	u32 lanerr, pcieerr;
+
+	if (atomic_load_acq_32(&sc->fatal_error_state) ==
+	    IGC_FATAL_ERROR_NONE)
+		return;
+
+	hw = &sc->hw;
+	pcieerr = sc->fatal_error_pcie |
+	    (IGC_READ_REG(hw, IGC_PCIEERRSTS) & IGC_PCIEERRSTS_FATAL_MASK);
+	if (pcieerr != 0)
+		IGC_WRITE_REG(hw, IGC_PCIEERRSTS, pcieerr);
+	lanerr = sc->fatal_error_lan |
+	    (IGC_READ_REG(hw, IGC_LANPERRSTS) & IGC_LANPERRSTS_RETX_BUF);
+	if (lanerr != 0)
+		IGC_WRITE_REG(hw, IGC_LANPERRSTS, lanerr);
+	/*
+	 * DEV_RST can relatch PEIND from a subordinate status register
+	 * before that register is cleared.  Drain the recovered indication
+	 * before unmasking FER so a later error is not misattributed.
+	 */
+	(void)IGC_READ_REG(hw, IGC_PEIND);
+
+	sc->fatal_error_peind = 0;
+	sc->fatal_error_pcie = 0;
+	sc->fatal_error_lan = 0;
+	sc->fatal_error_mng = 0;
+	atomic_store_rel_32(&sc->fatal_error_state, IGC_FATAL_ERROR_NONE);
+}
+
 /*
  * I225/I226 have three configurable LED outputs.  DPDK uses LED1 for
  * adapter identification; retain that convention and preserve the OEM's
@@ -2569,9 +2760,13 @@ igc_if_intr_enable(if_ctx_t ctx)
 		IGC_WRITE_REG(hw, IGC_EIAC, mask);
 		IGC_WRITE_REG(hw, IGC_EIAM, mask);
 		IGC_WRITE_REG(hw, IGC_EIMS, mask);
-		IGC_WRITE_REG(hw, IGC_IMS, IGC_IMS_LSC);
+		mask = IGC_IMS_LSC;
 	} else
-		IGC_WRITE_REG(hw, IGC_IMS, IMS_ENABLE_MASK);
+		mask = IMS_ENABLE_MASK;
+	if (atomic_load_acq_32(&sc->fatal_error_state) ==
+	    IGC_FATAL_ERROR_NONE)
+		mask |= IGC_IMS_FER;
+	IGC_WRITE_REG(hw, IGC_IMS, mask);
 	IGC_WRITE_FLUSH(hw);
 }
 
@@ -2901,8 +3096,10 @@ igc_add_hw_stats(struct igc_softc *sc)
 	struct sysctl_oid_list *child = SYSCTL_CHILDREN(tree);
 	struct igc_hw_stats *stats = &sc->stats;
 
-	struct sysctl_oid *eee_node, *stat_node, *queue_node, *int_node;
-	struct sysctl_oid_list *eee_list, *stat_list, *queue_list, *int_list;
+	struct sysctl_oid *eee_node, *memerr_node, *stat_node, *queue_node,
+	    *int_node;
+	struct sysctl_oid_list *eee_list, *memerr_list, *stat_list, *queue_list,
+	    *int_list;
 
 #define QUEUE_NAME_LEN 32
 	char namebuf[QUEUE_NAME_LEN];
@@ -2931,6 +3128,25 @@ igc_add_hw_stats(struct igc_softc *sc)
 	SYSCTL_ADD_UINT(ctx, child, OID_AUTO, "fc_low_water",
 	    CTLFLAG_RD, &sc->hw.fc.low_water, 0,
 	    "Flow Control Low Watermark");
+	memerr_node = SYSCTL_ADD_NODE(ctx, child, OID_AUTO, "memory_errors",
+	    CTLFLAG_RD | CTLFLAG_MPSAFE, NULL,
+	    "Internal memory error indications");
+	memerr_list = SYSCTL_CHILDREN(memerr_node);
+	SYSCTL_ADD_UQUAD(ctx, memerr_list, OID_AUTO, "fatal_lan",
+	    CTLFLAG_RD, &sc->fatal_error_lan_count,
+	    "Fatal LAN-port memory error indications");
+	SYSCTL_ADD_UQUAD(ctx, memerr_list, OID_AUTO, "fatal_management",
+	    CTLFLAG_RD, &sc->fatal_error_mng_count,
+	    "Fatal management-memory error indications");
+	SYSCTL_ADD_UQUAD(ctx, memerr_list, OID_AUTO, "fatal_pcie",
+	    CTLFLAG_RD, &sc->fatal_error_pcie_count,
+	    "Fatal PCIe memory error indications");
+	SYSCTL_ADD_UQUAD(ctx, memerr_list, OID_AUTO, "fatal_dma",
+	    CTLFLAG_RD, &sc->fatal_error_dma_count,
+	    "Fatal DMA memory error indications");
+	SYSCTL_ADD_UQUAD(ctx, memerr_list, OID_AUTO, "fatal_unknown",
+	    CTLFLAG_RD, &sc->fatal_error_unknown_count,
+	    "Fatal memory errors without a reported region");
 	eee_node = SYSCTL_ADD_NODE(ctx, child, OID_AUTO, "eee",
 	    CTLFLAG_RD | CTLFLAG_MPSAFE, NULL,
 	    "Energy Efficient Ethernet statistics");
diff --git a/sys/dev/igc/if_igc.h b/sys/dev/igc/if_igc.h
index c98f17a97ca3..8b35e1f59a60 100644
--- a/sys/dev/igc/if_igc.h
+++ b/sys/dev/igc/if_igc.h
@@ -414,6 +414,16 @@ struct igc_softc {
 	unsigned long	dropped_pkts;
 	unsigned long	link_irq;
 	unsigned long	rx_overruns;
+	u32		fatal_error_state;
+	u32		fatal_error_peind;
+	u32		fatal_error_pcie;
+	u32		fatal_error_lan;
+	u32		fatal_error_mng;
+	uint64_t	fatal_error_lan_count;
+	uint64_t	fatal_error_mng_count;
+	uint64_t	fatal_error_pcie_count;
+	uint64_t	fatal_error_dma_count;
+	uint64_t	fatal_error_unknown_count;
 	struct igc_hw_stats stats;
 	u16		vf_ifp;
 };
diff --git a/sys/dev/igc/igc_defines.h b/sys/dev/igc/igc_defines.h
index 885f0799f6a6..1856c86b91a8 100644
--- a/sys/dev/igc/igc_defines.h
+++ b/sys/dev/igc/igc_defines.h
@@ -254,6 +254,7 @@
 #define IGC_STATUS_LAN_INIT_DONE	0x00000200 /* Lan Init Compltn by NVM */
 #define IGC_STATUS_PHYRA		0x00000400 /* PHY Reset Asserted */
 #define IGC_STATUS_GIO_MASTER_ENABLE	0x00080000 /* Master request status */
+#define IGC_STATUS_RST_DONE		0x00200000 /* Device reset complete */
 #define IGC_STATUS_2P5_SKU		0x00001000 /* Val of 2.5GBE SKU strap */
 #define IGC_STATUS_2P5_SKU_OVER	0x00002000 /* Val of 2.5GBE SKU Over */
 #define IGC_STATUS_PCIM_STATE		0x40000000 /* PCIm function state */
@@ -427,11 +428,25 @@
 
 #define IGC_PBS_16K		IGC_PBA_16K
 
-/* Uncorrectable/correctable ECC Error counts and enable bits */
-#define IGC_PBECCSTS_CORR_ERR_CNT_MASK		0x000000FF
-#define IGC_PBECCSTS_UNCORR_ERR_CNT_MASK	0x0000FF00
-#define IGC_PBECCSTS_UNCORR_ERR_CNT_SHIFT	8
-#define IGC_PBECCSTS_ECC_ENABLE			0x00010000
+/* I225/I226 memory error status bits. */
+#define IGC_PEIND_LANPORT_PARITY_FATAL	0x00000001
+#define IGC_PEIND_MNG_PARITY_FATAL	0x00000002
+#define IGC_PEIND_PCIE_PARITY_FATAL	0x00000004
+#define IGC_PEIND_DMA_PARITY_FATAL	0x00000008
+#define IGC_PEIND_FATAL_MASK		0x0000000F
+#define IGC_PEIND_HOST_FATAL_MASK	(IGC_PEIND_LANPORT_PARITY_FATAL | \
+	IGC_PEIND_PCIE_PARITY_FATAL | IGC_PEIND_DMA_PARITY_FATAL)
+
+#define IGC_PBECCSTS_ECC_ENABLE		0x00000001
+#define IGC_PBECCSTS_CORR_ERR		0x00000004
+
+#define IGC_PCIEERRSTS_FATAL_MASK	0x00000078
+#define IGC_PCIEECCSTS_TX_WR_DATA	0x00000010
+#define IGC_PCIEECCSTS_RETRY_BUF		0x00000020
+#define IGC_PCIEECCSTS_CORR_MASK		0x00000030
+
+#define IGC_LANPERRSTS_RETX_BUF		0x00000200
+#define IGC_MNGPARSTS_FATAL_MASK		0x00000003
 
 #define IFS_MAX			80
 #define IFS_MIN			40
diff --git a/sys/dev/igc/igc_regs.h b/sys/dev/igc/igc_regs.h
index f4690e24c2af..849b13b2adf2 100644
--- a/sys/dev/igc/igc_regs.h
+++ b/sys/dev/igc/igc_regs.h
@@ -376,8 +376,13 @@
 #define IGC_FCRTC		0x02170 /* Flow Control Rx high watermark */
 #define IGC_PCIEMISC		0x05BB8 /* PCIE misc config register */
 
-/* PCIe Parity Status Register */
-#define IGC_PCIEERRSTS		0x05BA8
+/* Memory error detection registers */
+#define IGC_PEIND		0x01084 /* Parity and ECC Indication - RC */
+#define IGC_PBECCSTS		0x0245C /* Packet Buffer ECC Status */
+#define IGC_PCIEERRSTS		0x05BA8 /* PCIe Parity Status - RW1C */
+#define IGC_PCIEECCSTS		0x05BAC /* PCIe ECC Status - RW1C */
+#define IGC_LANPERRSTS		0x05F58 /* LAN Port Parity Status - RW1C */
+#define IGC_MNGPARSTS		0x08F24 /* Management Parity Status */
 
 #define IGC_PROXYS		0x5F64 /* Proxying Status */
 #define IGC_PROXYFC		0x5F60 /* Proxying Filter Control */