micropython / micropython/micropython

esp32: network.LAN() leaks MAC/PHY/netif and GPIO reservations when esp_eth_driver_install fails — Ethernet unrecoverable until reboot

Open
#19,457 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug port-esp32
Dominant language
C
Stars
22.1k
Forks
9k
Avg merge
6d 4h
Merged PRs (30d)
16

Description

[!NOTE]
AI generated comments — this issue was investigated, tested and written with AI assistance (Claude, Anthropic). All findings and reproductions were run and verified on real hardware (ESP32-PICO-V3-02 + LAN8710A over RMII) by the reporter.

Port, board and/or hardware

esp32 port — ESP32-PICO-V3-02 + Microchip LAN8710A (RMII, MDC=GPIO8, MDIO=GPIO7). The leak itself is target-independent; the "GPIO reserved" consequence applies to all IDF >= 5.5-based builds.

MicroPython version

v1.27.0 (official ESP32_GENERIC-SPIRAM build, ESP-IDF v5.5.1) and current master — ports/esp32/network_lan.c, error path after esp_eth_driver_install().

Reproduction

Make the first network.LAN() fail for any transient reason (in our case a misconfigured pad; a PHY whose power rail is not up yet fails the same way), then retry:

import network
from machine import Pin, mem32

# put the SMI pads in a state that makes PHY init fail (any transient
# PHY problem reproduces the same sequence):
mdc, mdio = Pin(8, Pin.OPEN_DRAIN), Pin(7, Pin.OPEN_DRAIN)

try:
    network.LAN(mdc=mdc, mdio=mdio, phy_type=network.PHY_LAN8720, phy_addr=0)
except OSError as e:
    print("attempt 1:", e)     # OSError: esp_eth_driver_install failed

# fix the pads completely — clear open-drain:
mem32[0x3FF44088 + 4*8] &= ~4
mem32[0x3FF44088 + 4*7] &= ~4

try:
    network.LAN(mdc=Pin(8), mdio=Pin(7), phy_type=network.PHY_LAN8720, phy_addr=0)
except OSError as e:
    print("attempt 2:", e)     # OSError: esp_eth_driver_install failed with invalid argument

Observed output on real hardware:

attempt 1: esp_eth_driver_install failed
attempt 2: esp_eth_driver_install failed with invalid argument

Attempt 2 fails even though the underlying problem is fixed, and keeps failing until a hard reset.

Expected behaviour

A failed network.LAN() should release everything it allocated, so that a later call can succeed once the transient condition clears (applications need to be able to retry PHY bring-up).

Observed behaviour / analysis

In network_lan.c, when esp_eth_driver_install() returns an error the code raises OSError immediately with no cleanup:

  • the MAC object from esp_eth_mac_new_esp32() is never deleted — on IDF >= 5.5 it holds esp_gpio_reserve() reservations for all EMAC pins (0, 7, 8, 19, 21, 22, 25, 26, 27 on classic ESP32);
  • the PHY object leaks;
  • the esp_netif created just before leaks;
  • the ETH/IP event handlers stay registered.

On the next network.LAN() call, esp_eth_mac_new_esp32() logs GPIO x is already reserved, returns NULL, and esp_eth_driver_install() is invoked with a NULL mac -> ESP_ERR_INVALID_ARG. Ethernet is therefore permanently dead until reboot after any single transient init failure.

Why this matters for other users

Any transient PHY bring-up failure (slow power rail, oscillator not ready, marginal pad state) turns into a permanent Ethernet outage that only a reboot clears — and the retry attempts fail with a different, misleading error ("invalid argument"), sending users down the wrong debugging path entirely. Field devices that could self-heal with a simple retry loop instead need watchdog reboots. Cleaning up the error path makes network.LAN() failures recoverable and the error reports truthful, sparing other users the trap we fell into.

Suggested fix

In the error path after esp_eth_driver_install() (and the earlier failure points): call mac->del(mac) and self->phy->del(self->phy), destroy the netif, and unregister the event handlers before raising.

Upstream status (ESP-IDF)

The condition that made our first install fail was reported upstream and root-caused in espressif/esp-idf#18756 — full analysis update: https://github.com/espressif/esp-idf/issues/18756#issuecomment-4934987955 (stale open-drain pad state that IDF <= 5.4 used to clean up inside the EMAC driver and >= 5.5 no longer does; Espressif has been asked to consider hardening emac_esp_gpio_matrix_init()). This MicroPython issue is independent of how ESP-IDF resolves that: whatever makes esp_eth_driver_install() fail once, the missing cleanup turns it into a permanent failure. See also the companion MicroPython issue about Pin(mode=-1): #19456.

Code of Conduct

Yes, I agree

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in ports/esp32/network_lan.c at the error path after esp_eth_driver_install(), then trace the earlier failure paths and ownership of the MAC, PHY, netif, and event handlers. Run the supplied network.LAN() retry reproduction on the stated ESP32 hardware or equivalent setup. Done means a transient install failure releases its resources so a later network.LAN() call can succeed without rebooting.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
embedded-iot, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.