git: b68f4667612f - main - aq(4): PHY thermal-shutdown handling and correctness fixes

Adrian Chadd <[email protected]> Sun, 02 Aug 2026 23:10:33 +0000
Newsgroups gmane.os.freebsd.devel.cvs.src
Message-ID <6a6fce69.42365.17d7cea__41538.4618494258$1785712350$gmane$org@gitrepo.freebsd.org>
The branch main has been updated by adrian:

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

commit b68f4667612ff8cde2fefc94f8b31377b128ea7e
Author:     Nick Price <[email protected]>
AuthorDate: 2026-08-02 22:44:33 +0000
Commit:     Adrian Chadd <[email protected]>
CommitDate: 2026-08-02 22:44:33 +0000

    aq(4): PHY thermal-shutdown handling and correctness fixes
    
    Fold the thermal-protection work and the correctness fixes that landed
    alongside it.
    
    Report and auto-recover from PHY thermal shutdown.  The Atlantic PHYs can
    autonomously shut down on over-temperature, latching global fault 0x8007
    and dropping the link; Atlantic 2 ships this armed, Atlantic 1 disabled.
    Arm it on Atlantic 1 at interface init (1E.C478.A via the MAC's MDIO
    controller), and recover from a trip automatically: the admin-status poll
    detects the fault, logs the shutdown limit and measured temperature, and
    holds the link down until the PHY cools, then restores it -- Atlantic 1
    needs a PHY reset (1E.2681.0) with the MAC firmware running plus a full
    re-init, Atlantic 2 recovers on the re-init alone.  New firmware ops
    get_phy_fault, phy_reset, thermal_arm, and get_thermal_limit back the
    state machine in aq_if_update_admin_status().
    
    Make that Atlantic 1 thermal MDIO path address-correct and fail-safe.
    The direct-MDIO helpers hardcoded the Clause-45 port address to 0, but it
    is strap-selectable: on a board whose PHY answers elsewhere every thermal
    op targeted nothing, so arming silently no-oped and the post-trip reset
    never cleared the latch.  Discover the address by scanning ports 0..31
    for a PMA/PMD identifier and form it as (phy_id << 5) | mmd, marking it
    valid only when a PHY actually answers.  aq_fw2x_phy_read also returned 0
    on a semaphore timeout, indistinguishable from a real 1E.C478 == 0, so
    thermal_arm could zero live provisioning bits; give the read an error
    return and gate thermal_arm and get_thermal_limit on it.
    
    Bound the multicast filter slot index.  aq_mc_filter_apply() programmed
    slot count + 1 and bailed only at count == AQ_HW_MAC_MAX (33), one
    address too late, so a 33rd entry raced in between the if_llmaddr_count()
    snapshot and the if_foreach_llmaddr() walk drove an out-of-bounds MMIO
    write to slot 33.  Fire the guard at AQ_HW_MAC_MAX - 1, and also reject
    index >= AQ_HW_MAC_MAX in aq_hw_mac_addr_set() where the slot becomes an
    RPF register offset.
    
    Correctness and safety fixes: initialize the sysctl context in
    attach_pre so the iflib fail-path detach cannot sysctl_ctx_free() an
    uninitialized list (a page fault when MSI/MSI-X is denied); range-check
    the Atlantic 2 action-resolver table index, taken verbatim from a
    firmware-supplied base, before writing the ART registers; and accumulate
    statistics deltas as unsigned, since AQ_SDELTA discarded a forward delta
    of 2^31 or more at 10G across a stretched admin poll.
    
    Signed-off-by: Nick Price <[email protected]>
    
    Reviewed by:    adrian
    Differential Revision:  https://reviews.freebsd.org/D58435
---
 sys/dev/aq/aq2_fw.c    |  51 ++++++++++++++-
 sys/dev/aq/aq2_hw.h    |  12 ++++
 sys/dev/aq/aq_device.h |  10 +++
 sys/dev/aq/aq_fw.h     |  15 +++++
 sys/dev/aq/aq_fw2x.c   | 167 ++++++++++++++++++++++++++++++++++++++++++++++++-
 sys/dev/aq/aq_hw.c     |  13 +++-
 sys/dev/aq/aq_hw.h     |   4 ++
 sys/dev/aq/aq_irq.c    | 103 +++++++++++++++++++++++++++++-
 sys/dev/aq/aq_main.c   |  14 ++++-
 9 files changed, 380 insertions(+), 9 deletions(-)

diff --git a/sys/dev/aq/aq2_fw.c b/sys/dev/aq/aq2_fw.c
index a9353a9cfa96..9dc477dcf8e9 100644
--- a/sys/dev/aq/aq2_fw.c
+++ b/sys/dev/aq/aq2_fw.c
@@ -42,6 +42,7 @@ static int aq2_fw_get_mode(struct aq_hw *hw, enum aq_hw_fw_mpi_state *mode,
 static int aq2_fw_get_mac_addr(struct aq_hw *hw, uint8_t *mac);
 static int aq2_fw_get_stats(struct aq_hw *hw, struct aq_hw_stats *stats);
 static int aq2_fw_get_temp(struct aq_hw *hw, int *temp_mc);
+static int aq2_fw_get_thermal_limit(struct aq_hw *hw, int *limit_mc);
 
 /* Coherent OUT-window read, bracketed by the transaction id. */
 static int
@@ -490,7 +491,7 @@ aq2_fw_get_temp(struct aq_hw *hw, int *temp_mc)
 		return (ENXIO);
 	}
 
-	/* F/W reports whole degrees Celsius. */
+	/* F/W reports whole degrees Celsius, signed. */
 	*temp_mc = (int)(int8_t)((raw &
 	    AQ2_FW_INTERFACE_OUT_PHY_TEMPERATURE) >>
 	    AQ2_FW_INTERFACE_OUT_PHY_TEMPERATURE_S) * 1000;
@@ -498,6 +499,50 @@ aq2_fw_get_temp(struct aq_hw *hw, int *temp_mc)
 	return (0);
 }
 
+/* interface-in thermal_shutdown.shutdown_temperature, whole degC. */
+static int
+aq2_fw_get_thermal_limit(struct aq_hw *hw, int *limit_mc)
+{
+	uint32_t v;
+	int temp_c;
+
+	v = AQ_READ_REG(hw, AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_REG);
+	temp_c = (v & AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_TEMP) >>
+	    AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_TEMP_S;
+	if (temp_c == 0 || temp_c == 0xff)
+		return (ENXIO);
+	*limit_mc = temp_c * 1000;
+
+	return (0);
+}
+
+static int
+aq2_fw_get_phy_fault(struct aq_hw *hw, uint16_t *fault)
+{
+	uint32_t health, code;
+	int err;
+
+	err = aq2_fw_interface_buffer_read(hw,
+	    AQ2_FW_INTERFACE_OUT_PHY_HEALTH_MONITOR_REG, &health,
+	    sizeof(health));
+	if (err != 0)
+		return (err);
+
+	if ((health & AQ2_FW_INTERFACE_OUT_PHY_HEALTH_MONITOR_FAULT) == 0) {
+		*fault = 0;
+		return (0);
+	}
+
+	err = aq2_fw_interface_buffer_read(hw,
+	    AQ2_FW_INTERFACE_OUT_PHY_FAULT_CODE_REG, &code, sizeof(code));
+	if (err != 0)
+		return (err);
+
+	*fault = (uint16_t)(code & AQ2_FW_INTERFACE_OUT_PHY_FAULT_CODE);
+
+	return (0);
+}
+
 const struct aq_firmware_ops aq2_fw_ops = {
 	.reset = aq2_fw_reset,
 	.set_mode = aq2_fw_set_mode,
@@ -505,5 +550,9 @@ const struct aq_firmware_ops aq2_fw_ops = {
 	.get_mac_addr = aq2_fw_get_mac_addr,
 	.get_stats = aq2_fw_get_stats,
 	.get_temp = aq2_fw_get_temp,
+	.get_phy_fault = aq2_fw_get_phy_fault,
+	.phy_reset = NULL,	/* A2 clears thermal shutdown on its own reset */
+	.thermal_arm = NULL,	/* A2 firmware ships thermal shutdown armed */
+	.get_thermal_limit = aq2_fw_get_thermal_limit,
 	.led_control = NULL,
 };
diff --git a/sys/dev/aq/aq2_hw.h b/sys/dev/aq/aq2_hw.h
index d707e2dad51e..1f68a9c4a1d0 100644
--- a/sys/dev/aq/aq2_hw.h
+++ b/sys/dev/aq/aq2_hw.h
@@ -194,6 +194,11 @@ aq_is_atlantic2(uint16_t device_id)
 #define  AQ2_FW_INTERFACE_IN_LINK_OPTIONS_LINK_RENEGOTIATE (1 << 1)
 #define  AQ2_FW_INTERFACE_IN_LINK_OPTIONS_LINK_UP	(1 << 0)
 
+#define AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_REG	0x12020
+#define  AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_ENABLE	(1 << 0)
+#define  AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_TEMP	0x0000ff00	/* whole degC */
+#define  AQ2_FW_INTERFACE_IN_THERMAL_SHUTDOWN_TEMP_S	8
+
 #define AQ2_FW_INTERFACE_IN_REQUEST_POLICY_REG	0x12a58
 #define  AQ2_FW_INTERFACE_IN_REQUEST_POLICY_MCAST_QUEUE_OR_TC	0x00800000
 #define  AQ2_FW_INTERFACE_IN_REQUEST_POLICY_MCAST_RX_QUEUE_TC_INDEX 0x007c0000
@@ -231,9 +236,13 @@ aq_is_atlantic2(uint16_t device_id)
 
 #define AQ2_FW_INTERFACE_OUT_PHY_HEALTH_MONITOR_REG	0x13620
 #define  AQ2_FW_INTERFACE_OUT_PHY_HEALTH_MONITOR_READY	0x00000001
+#define  AQ2_FW_INTERFACE_OUT_PHY_HEALTH_MONITOR_FAULT	0x00000002
 #define  AQ2_FW_INTERFACE_OUT_PHY_TEMPERATURE		0x0000ff00
 #define  AQ2_FW_INTERFACE_OUT_PHY_TEMPERATURE_S		8
 
+#define AQ2_FW_INTERFACE_OUT_PHY_FAULT_CODE_REG		0x13624
+#define  AQ2_FW_INTERFACE_OUT_PHY_FAULT_CODE		0x0000ffff
+
 #define AQ2_FW_INTERFACE_OUT_STATS_REG		0x13700
 
 #define AQ2_FW_INTERFACE_OUT_LINK_STATUS_REG	0x13014
@@ -260,6 +269,9 @@ aq_is_atlantic2(uint16_t device_id)
 #define AQ2_RPF_ACT_ART_REQ_MASK_REG(i)		(0x14004 + (i) * 0x10)
 #define AQ2_RPF_ACT_ART_REQ_ACTION_REG(i)	(0x14008 + (i) * 0x10)
 
+/* Action-resolver table depth: 16 enable sections of 8 rows. */
+#define AQ2_ART_TABLE_SIZE			128
+
 /*
  * L2 unicast filter MSW register (shared with Atlantic 1).  Atlantic 2 sets
  * the TAG field so unicast frames are classified into the resolver table.
diff --git a/sys/dev/aq/aq_device.h b/sys/dev/aq/aq_device.h
index 6c2453d8277c..1ace227974a5 100644
--- a/sys/dev/aq/aq_device.h
+++ b/sys/dev/aq/aq_device.h
@@ -139,6 +139,16 @@ struct aq_dev {
 	uint32_t          tx_rings_count;
 	uint32_t          rx_rings_count;
 	bool              linkup;
+	uint16_t          phy_fault_last;	/* last fault code reported */
+	enum aq_thermal_state {
+		AQ_THERMAL_NORMAL = 0,	/* no thermal shutdown pending */
+		AQ_THERMAL_COOLING,	/* shut down; waiting to cool */
+		AQ_THERMAL_SETTLING,	/* PHY reset; waiting to re-init */
+	}                 thermal_state;
+	int               thermal_settle;
+	int               thermal_retry_ticks;	/* earliest tick to retry at */
+	int               thermal_temp_mc;	/* temp at the last shutdown/cool */
+	int               thermal_recover_mc;	/* recover once cooled to here */
 	int               media_active;
 
 	struct aq_hw_stats  last_stats;
diff --git a/sys/dev/aq/aq_fw.h b/sys/dev/aq/aq_fw.h
index ee45aedfc4e1..a7e880df51a7 100644
--- a/sys/dev/aq/aq_fw.h
+++ b/sys/dev/aq/aq_fw.h
@@ -71,9 +71,24 @@ struct aq_firmware_ops
 	/* Reports millidegrees Celsius. */
 	int (*get_temp)(struct aq_hw* hw, int* temp_mc);
 
+	/* Reports the PHY global fault code; zero means no fault. */
+	int (*get_phy_fault)(struct aq_hw* hw, uint16_t* fault);
+
+	/* Resets the PHY (clears a latched thermal shutdown). */
+	int (*phy_reset)(struct aq_hw* hw);
+
+	/* Arms the firmware autonomous thermal shutdown (A2 ships armed). */
+	int (*thermal_arm)(struct aq_hw* hw);
+
+	/* Reports the PHY high-temperature shutdown threshold (millidegrees C). */
+	int (*get_thermal_limit)(struct aq_hw* hw, int* limit_mc);
+
 	int (*led_control)(struct aq_hw* hw, uint32_t mode);
 };
 
+/* PHY global fault codes, register 1E.C850. */
+#define	AQ_PHY_FAULT_THERMAL_SHUTDOWN	0x8007
+
 /* aq_fw1x/aq_fw2x: Atlantic 1 firmware ABIs; aq2_fw: Atlantic 2 (AQC11x). */
 extern const struct aq_firmware_ops aq_fw1x_ops;
 extern const struct aq_firmware_ops aq_fw2x_ops;
diff --git a/sys/dev/aq/aq_fw2x.c b/sys/dev/aq/aq_fw2x.c
index 0f4bb7a89906..be1baa93c2ab 100644
--- a/sys/dev/aq/aq_fw2x.c
+++ b/sys/dev/aq/aq_fw2x.c
@@ -88,7 +88,7 @@ enum aq_fw2x_caps_hi {
 	CAPS_HI_TEMPERATURE,
 	CAPS_HI_DOWNSHIFT,
 	CAPS_HI_PTP_AVB_EN,
-	CAPS_HI_MEDIA_DETECT,
+	CAPS_HI_THERMAL_SHUTDOWN,
 	CAPS_HI_LINK_DROP,
 	CAPS_HI_SLEEP_PROXY,
 	CAPS_HI_WOL,
@@ -499,6 +499,167 @@ aq_fw2x_get_temp(struct aq_hw* hw, int* temp_mc)
 	return (0);
 }
 
+static int
+aq_fw2x_get_phy_fault(struct aq_hw* hw, uint16_t* fault)
+{
+	uint32_t raw;
+	int err;
+
+	mtx_lock(&hw->fw_mtx);
+	err = aq_hw_fw_downld_dwords(hw, hw->mbox_addr +
+	    offsetof(struct aq_fw2x_mailbox, phy_h_bit), &raw, 1);
+	mtx_unlock(&hw->fw_mtx);
+
+	if (err != 0)
+		return (err);
+
+	*fault = (uint16_t)(raw >> 16);
+
+	return (0);
+}
+
+/* PHY MDIO access: MMD register read/write via the MAC's MDIO controller. */
+#define AQ_MDIO_IFACE(n)	(0x280 + (((n) - 1) * 4))
+#define AQ_MDIO_BUSY		0x80000000u	/* iface2 bit 31 */
+#define AQ_MDIO_EXECUTE		0x00008000u	/* iface2 bit 15 */
+#define AQ_MDIO_OP_S		12		/* iface2 bits 13:12 */
+#define AQ_MDIO_OP_ADDR		3
+#define AQ_MDIO_OP_READ		1
+#define AQ_MDIO_OP_WRITE	2
+#define AQ_MDIO_PHYADDR_MSK	0x3ffu
+#define AQ_FW_SM_MDIO		0		/* cpu semaphore index */
+
+#define AQ_PHY_ID_MAX		32		/* MDIO port addresses to scan */
+#define AQ_MDIO_MMD_PMAPMD	0x01		/* PMA/PMD MMD */
+#define AQ_PHY_ID2_REG		0x0003		/* PMA/PMD Device Identifier 2 */
+
+#define AQ_PHY_MMD_GLOBAL	0x1e
+#define AQ_PHY_RESET_REG	0x2681		/* 1E.2681.0 = PHY hard reset */
+#define  AQ_PHY_RESET		0x0001
+#define AQ_PHY_THERMAL_CTRL_REG	0xc478		/* 1E.C478 thermal control */
+#define  AQ_PHY_THERMAL_SD_EN	0x0400		/* .A thermalShutdownEnable */
+
+static uint16_t
+aq_fw2x_mdio_op(struct aq_hw* hw, uint16_t mmd, uint16_t addr, int write,
+    uint16_t data)
+{
+	uint32_t pa = (((uint32_t)hw->phy_id & 0x1f) << 5) | (mmd & 0x1f);
+
+	AQ_WRITE_REG(hw, AQ_MDIO_IFACE(4), addr);
+	AQ_WRITE_REG(hw, AQ_MDIO_IFACE(2),
+	    AQ_MDIO_EXECUTE | (AQ_MDIO_OP_ADDR << AQ_MDIO_OP_S) | pa);
+	AQ_HW_WAIT_FOR((AQ_READ_REG(hw, AQ_MDIO_IFACE(2)) & AQ_MDIO_BUSY) == 0,
+	    10, 10000);
+	if (write) {
+		AQ_WRITE_REG(hw, AQ_MDIO_IFACE(3), data);
+		AQ_WRITE_REG(hw, AQ_MDIO_IFACE(2),
+		    AQ_MDIO_EXECUTE | (AQ_MDIO_OP_WRITE << AQ_MDIO_OP_S) | pa);
+	} else {
+		AQ_WRITE_REG(hw, AQ_MDIO_IFACE(2),
+		    AQ_MDIO_EXECUTE | (AQ_MDIO_OP_READ << AQ_MDIO_OP_S) | pa);
+	}
+	AQ_HW_WAIT_FOR((AQ_READ_REG(hw, AQ_MDIO_IFACE(2)) & AQ_MDIO_BUSY) == 0,
+	    10, 10000);
+	return ((uint16_t)AQ_READ_REG(hw, AQ_MDIO_IFACE(5)));
+}
+
+/* MDIO is serialized against the F/W by cpu semaphore 0. */
+static void
+aq_fw2x_phy_write(struct aq_hw* hw, uint16_t mmd, uint16_t addr, uint16_t data)
+{
+	if (AQ_HW_WAIT_FOR(reg_glb_cpu_sem_get(hw, AQ_FW_SM_MDIO) == 1U,
+	    10, 10000) != 0)
+		return;
+	aq_fw2x_mdio_op(hw, mmd, addr, 1, data);
+	reg_glb_cpu_sem_set(hw, 1U, AQ_FW_SM_MDIO);
+}
+
+static int
+aq_fw2x_phy_read(struct aq_hw* hw, uint16_t mmd, uint16_t addr, uint16_t* val)
+{
+	if (AQ_HW_WAIT_FOR(reg_glb_cpu_sem_get(hw, AQ_FW_SM_MDIO) == 1U,
+	    10, 10000) != 0)
+		return (ETIMEDOUT);
+	*val = aq_fw2x_mdio_op(hw, mmd, addr, 0, 0);
+	reg_glb_cpu_sem_set(hw, 1U, AQ_FW_SM_MDIO);
+
+	return (0);
+}
+
+/* Discover the PHY's MDIO port address; it is strap-selectable, not fixed at 0. */
+static bool
+aq_fw2x_init_phy_id(struct aq_hw* hw)
+{
+	uint16_t val;
+	uint8_t id;
+
+	for (id = 0; id < AQ_PHY_ID_MAX; id++) {
+		hw->phy_id = id;
+		if (aq_fw2x_phy_read(hw, AQ_MDIO_MMD_PMAPMD, AQ_PHY_ID2_REG,
+		    &val) == 0 && val != 0xffff)
+			return (true);
+	}
+	hw->phy_id = 0;
+	return (false);
+}
+
+/* Called with fw_mtx held; the port address is needed by every MDIO caller. */
+static void
+aq_fw2x_phy_id_probe(struct aq_hw* hw)
+{
+	if (!hw->phy_id_valid && aq_fw2x_init_phy_id(hw))
+		hw->phy_id_valid = true;
+}
+
+/* PHY hard reset (1E.2681.0): clears a latched thermal shutdown a MAC reset cannot. */
+static int
+aq_fw2x_phy_reset(struct aq_hw* hw)
+{
+	mtx_lock(&hw->fw_mtx);
+	aq_fw2x_phy_id_probe(hw);
+	aq_fw2x_phy_write(hw, AQ_PHY_MMD_GLOBAL, AQ_PHY_RESET_REG, AQ_PHY_RESET);
+	mtx_unlock(&hw->fw_mtx);
+
+	return (0);
+}
+
+/* Arm autonomous thermal shutdown (1E.C478.A), cleared by any PHY reset. */
+static int
+aq_fw2x_thermal_arm(struct aq_hw* hw)
+{
+	uint16_t ctrl;
+
+	mtx_lock(&hw->fw_mtx);
+	aq_fw2x_phy_id_probe(hw);
+	if (aq_fw2x_phy_read(hw, AQ_PHY_MMD_GLOBAL, AQ_PHY_THERMAL_CTRL_REG,
+	    &ctrl) == 0 && ctrl != 0xffff && (ctrl & AQ_PHY_THERMAL_SD_EN) == 0)
+		aq_fw2x_phy_write(hw, AQ_PHY_MMD_GLOBAL,
+		    AQ_PHY_THERMAL_CTRL_REG, ctrl | AQ_PHY_THERMAL_SD_EN);
+	mtx_unlock(&hw->fw_mtx);
+
+	return (0);
+}
+
+/* 1E.C421 high-temp shutdown threshold, degrees C in Q8.8 fixed point. */
+#define AQ_PHY_THERMAL_HIGH_REG	0xc421
+static int
+aq_fw2x_get_thermal_limit(struct aq_hw* hw, int* limit_mc)
+{
+	uint16_t raw;
+	int err;
+
+	mtx_lock(&hw->fw_mtx);
+	aq_fw2x_phy_id_probe(hw);
+	err = aq_fw2x_phy_read(hw, AQ_PHY_MMD_GLOBAL, AQ_PHY_THERMAL_HIGH_REG,
+	    &raw);
+	mtx_unlock(&hw->fw_mtx);
+	if (err != 0 || raw == 0 || raw == 0xffff)
+		return (ENXIO);
+	*limit_mc = (int)(int16_t)raw * 1000 / 256;
+
+	return (0);
+}
+
 static int
 aq_fw2x_led_control(struct aq_hw* hw, uint32_t onoff)
 {
@@ -526,6 +687,10 @@ const struct aq_firmware_ops aq_fw2x_ops =
 	.get_mac_addr = aq_fw2x_get_mac_addr,
 	.get_stats = aq_fw2x_get_stats,
 	.get_temp = aq_fw2x_get_temp,
+	.get_phy_fault = aq_fw2x_get_phy_fault,
+	.phy_reset = aq_fw2x_phy_reset,
+	.thermal_arm = aq_fw2x_thermal_arm,
+	.get_thermal_limit = aq_fw2x_get_thermal_limit,
 
 	.led_control = aq_fw2x_led_control,
 };
diff --git a/sys/dev/aq/aq_hw.c b/sys/dev/aq/aq_hw.c
index ca733ffa7e3d..a7dae250c66e 100644
--- a/sys/dev/aq/aq_hw.c
+++ b/sys/dev/aq/aq_hw.c
@@ -598,13 +598,20 @@ static int
 aq2_art_filter_set(struct aq_hw *hw, uint32_t idx, uint32_t tag,
     uint32_t mask, uint32_t action)
 {
+	idx += hw->art_filter_base_index;
+	if (idx >= AQ2_ART_TABLE_SIZE) {
+		device_printf(hw->dev,
+		    "ART index %u out of range (firmware base %u)\n", idx,
+		    hw->art_filter_base_index);
+		return (EINVAL);
+	}
+
 	if (AQ_HW_WAIT_FOR(reg_glb_cpu_sem_get(hw, AQ2_ART_SEM_INDEX) == 1U,
 	    10U, 1000U) != 0) {
 		device_printf(hw->dev, "ART semaphore timeout, idx %u\n", idx);
 		return (EBUSY);
 	}
 
-	idx += hw->art_filter_base_index;
 	AQ_WRITE_REG(hw, AQ2_RPF_ACT_ART_REQ_TAG_REG(idx), tag);
 	AQ_WRITE_REG(hw, AQ2_RPF_ACT_ART_REQ_MASK_REG(idx), mask);
 	AQ_WRITE_REG(hw, AQ2_RPF_ACT_ART_REQ_ACTION_REG(idx), action);
@@ -739,6 +746,10 @@ aq_hw_mac_addr_set(struct aq_hw *hw, uint8_t *mac_addr, uint8_t index)
 		err = EINVAL;
 		goto err_exit;
 	}
+	if (index >= AQ_HW_MAC_MAX) {
+		err = EINVAL;
+		goto err_exit;
+	}
 	h = (mac_addr[0] << 8) | (mac_addr[1]);
 	l = (mac_addr[2] << 24) | (mac_addr[3] << 16) | (mac_addr[4] << 8) |
 	    mac_addr[5];
diff --git a/sys/dev/aq/aq_hw.h b/sys/dev/aq/aq_hw.h
index c0888f03ab23..1156b33c1904 100644
--- a/sys/dev/aq/aq_hw.h
+++ b/sys/dev/aq/aq_hw.h
@@ -195,6 +195,10 @@ struct aq_hw {
 	uint32_t chip_features;
 	uint64_t fw_caps;
 
+	/* Atlantic 1: MDIO port address of the PHY, discovered once. */
+	uint8_t phy_id;
+	bool phy_id_valid;
+
 	bool lro_enabled;
 
 	uint32_t mbox_addr;
diff --git a/sys/dev/aq/aq_irq.c b/sys/dev/aq/aq_irq.c
index ac3b8a66a900..325a957b1e8f 100644
--- a/sys/dev/aq/aq_irq.c
+++ b/sys/dev/aq/aq_irq.c
@@ -51,6 +51,7 @@ __FBSDID("$FreeBSD$");
 #include "aq_dbg.h"
 #include "aq_hw.h"
 #include "aq_hw_llh.h"
+#include "aq_fw.h"
 
 int
 aq_update_hw_stats(struct aq_dev *aq_dev)
@@ -63,9 +64,7 @@ aq_update_hw_stats(struct aq_dev *aq_dev)
 		return (0);
 
 #define AQ_SDELTA(_N_) do { \
-	int32_t _d = (int32_t)(stats._N_ - aq_dev->last_stats._N_); \
-	if (_d > 0) \
-		aq_dev->curr_stats._N_ += _d; \
+	aq_dev->curr_stats._N_ += stats._N_ - aq_dev->last_stats._N_; \
 } while (0)
 	if (aq_dev->linkup) {
 		AQ_SDELTA(uprc);
@@ -115,6 +114,100 @@ aq_update_hw_stats(struct aq_dev *aq_dev)
 }
 
 
+#define	AQ_THERMAL_HYSTERESIS_MC	18000	/* recover this far below the limit */
+#define	AQ_THERMAL_RECOVER_MC	90000	/* fallback when the limit is unreadable */
+#define	AQ_THERMAL_SETTLE_POLLS	5	/* ~5 s for the PHY reset to settle */
+#define	AQ_THERMAL_RETRY_SECS	60	/* minimum spacing between recoveries */
+
+/* Temperature here is post-trip; the PHY is already dropping to low power. */
+static void
+aq_thermal_report_shutdown(struct aq_dev *aq_dev)
+{
+	struct aq_hw *hw = &aq_dev->hw;
+	int temp_mc, limit_mc;
+	bool have_temp, have_limit;
+
+	have_temp = hw->fw_ops->get_temp(hw, &temp_mc) == 0;
+	have_limit = hw->fw_ops->get_thermal_limit(hw, &limit_mc) == 0;
+	if (have_temp)
+		aq_dev->thermal_temp_mc = temp_mc;
+	/* The F/W also exposes a cold_temperature hysteresis point. */
+	aq_dev->thermal_recover_mc = have_limit ?
+	    limit_mc - AQ_THERMAL_HYSTERESIS_MC : AQ_THERMAL_RECOVER_MC;
+
+	if (have_temp && have_limit)
+		device_printf(aq_dev->dev, "PHY thermal shutdown; "
+		    "limit %d C, temp %d C; holding link down until it cools\n",
+		    limit_mc / 1000, temp_mc / 1000);
+	else if (have_temp)
+		device_printf(aq_dev->dev, "PHY thermal shutdown; "
+		    "temp %d C; holding link down until it cools\n",
+		    temp_mc / 1000);
+	else
+		device_printf(aq_dev->dev, "PHY thermal shutdown; "
+		    "holding link down until it cools\n");
+}
+
+/* Recover after cooldown: A1 needs a PHY reset then re-init, A2 re-inits alone. */
+static void
+aq_thermal_poll(struct aq_dev *aq_dev)
+{
+	struct aq_hw *hw = &aq_dev->hw;
+	uint16_t fault;
+	int temp_mc;
+
+	switch (aq_dev->thermal_state) {
+	case AQ_THERMAL_NORMAL:
+		if (aq_dev->linkup)
+			return;
+		/* The F/W raises the fault a poll after it drops the link. */
+		if (hw->fw_ops->get_phy_fault(hw, &fault) != 0 || fault == 0)
+			return;
+		/* Report each code once; do not mask a later shutdown. */
+		if (fault == aq_dev->phy_fault_last)
+			return;
+		aq_dev->phy_fault_last = fault;
+		if (fault != AQ_PHY_FAULT_THERMAL_SHUTDOWN) {
+			device_printf(aq_dev->dev,
+			    "PHY fault 0x%04x\n", fault);
+			return;
+		}
+		aq_thermal_report_shutdown(aq_dev);
+		aq_dev->thermal_state = AQ_THERMAL_COOLING;
+		return;
+
+	case AQ_THERMAL_COOLING:
+		if (hw->fw_ops->get_temp(hw, &temp_mc) != 0 ||
+		    temp_mc > aq_dev->thermal_recover_mc)
+			return;
+		aq_dev->thermal_temp_mc = temp_mc;
+		if (hw->fw_ops->phy_reset != NULL) {
+			hw->fw_ops->phy_reset(hw);
+			aq_dev->thermal_settle = 0;
+			aq_dev->thermal_state = AQ_THERMAL_SETTLING;
+			return;
+		}
+		/* No PHY reset needed (A2): re-init below restores the link. */
+		break;
+
+	case AQ_THERMAL_SETTLING:
+		if (++aq_dev->thermal_settle < AQ_THERMAL_SETTLE_POLLS)
+			return;
+		break;
+	}
+
+	/* Space attempts out: recovery costs a re-init and a renegotiation. */
+	if ((int)(ticks - aq_dev->thermal_retry_ticks) < 0)
+		return;
+	aq_dev->thermal_retry_ticks = ticks + AQ_THERMAL_RETRY_SECS * hz;
+
+	device_printf(aq_dev->dev, "PHY cooled to %d C; restoring "
+	    "link\n", aq_dev->thermal_temp_mc / 1000);
+	aq_dev->thermal_state = AQ_THERMAL_NORMAL;
+	iflib_request_reset(aq_dev->ctx);
+	iflib_admin_intr_deferred(aq_dev->ctx);
+}
+
 void
 aq_if_update_admin_status(if_ctx_t ctx)
 {
@@ -129,6 +222,7 @@ aq_if_update_admin_status(if_ctx_t ctx)
 		device_printf(aq_dev->dev, "atlantic: link UP: speed=%d\n", link_speed);
 
 		aq_dev->linkup = 1;
+		aq_dev->phy_fault_last = 0;
 
 		/* turn on/off RX Pause in RPB */
 		rpb_rx_xoff_en_per_tc_set(hw, fc_neg.fc_rx, 0);
@@ -151,6 +245,9 @@ aq_if_update_admin_status(if_ctx_t ctx)
 		aq_mediastatus_update(aq_dev, link_speed, &fc_neg);
 	}
 
+	if (hw->fw_ops->get_phy_fault != NULL)
+		aq_thermal_poll(aq_dev);
+
 	aq_update_hw_stats(aq_dev);
 }
 
diff --git a/sys/dev/aq/aq_main.c b/sys/dev/aq/aq_main.c
index 7c3a88740f3b..4fe9aad42a71 100644
--- a/sys/dev/aq/aq_main.c
+++ b/sys/dev/aq/aq_main.c
@@ -338,6 +338,8 @@ aq_if_attach_pre(if_ctx_t ctx)
 	softc = iflib_get_softc(ctx);
 	rc = 0;
 
+	sysctl_ctx_init(&softc->aq_sysctl_ctx);
+
 	softc->ctx = ctx;
 	softc->dev = iflib_get_dev(ctx);
 	softc->media = iflib_get_media(ctx);
@@ -374,6 +376,8 @@ aq_if_attach_pre(if_ctx_t ctx)
 	hw->fc.fc_rx = 1;
 	hw->fc.fc_tx = 1;
 	softc->linkup = 0U;
+	/* Set here, not in aq_if_init(): a recovery re-init must not reset it. */
+	softc->thermal_retry_ticks = ticks;
 
 	softc->dbg_level = AQ_DBG_LEVEL_DEFAULT;
 	softc->dbg_categories = AQ_DBG_CATEGORIES_DEFAULT;
@@ -745,6 +749,8 @@ aq_if_init(if_ctx_t ctx)
 
 	atomic_store_rel_long(&hw->flags, 0);
 
+	softc->phy_fault_last = 0;
+	softc->thermal_state = AQ_THERMAL_NORMAL;
 	hw->tx_rings_count = softc->tx_rings_count;
 
 	err = aq_hw_init(&softc->hw, softc->hw.mac_addr, softc->msix,
@@ -755,6 +761,10 @@ aq_if_init(if_ctx_t ctx)
 		return;
 	}
 
+	/* aq_hw_init reloads the PHY, resetting the thermal-shutdown arming. */
+	if (hw->fw_ops->thermal_arm != NULL)
+		hw->fw_ops->thermal_arm(hw);
+
 	aq_if_media_status(ctx, &ifmr);
 
 	aq_update_vlan_filters(softc);
@@ -864,7 +874,7 @@ aq_mc_filter_apply(void *arg, struct sockaddr_dl *dl, u_int count)
 	struct aq_hw *hw = &softc->hw;
 	uint8_t *mac_addr = NULL;
 
-	if (count == AQ_HW_MAC_MAX)
+	if (count >= AQ_HW_MAC_MAX - 1)
 		return (0);
 
 	mac_addr = LLADDR(dl);
@@ -1425,8 +1435,6 @@ aq_add_stats_sysctls(struct aq_dev *softc)
 #define QUEUE_NAME_LEN 32
 	char                    namebuf[QUEUE_NAME_LEN];
 
-	/* Own these oids so aq_if_detach can drain and free them in order. */
-	sysctl_ctx_init(ctx);
 	/* RSS configuration */
 	SYSCTL_ADD_PROC(ctx, child, OID_AUTO, "print_rss_config",
 	    CTLTYPE_STRING | CTLFLAG_RD, softc, 0,