Skip to content

[wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew()#13983

Merged
bdraco merged 5 commits intodevfrom
fix-esp8266-dhcp-state-corruption
Feb 14, 2026
Merged

[wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew()#13983
bdraco merged 5 commits intodevfrom
fix-esp8266-dhcp-state-corruption

Conversation

@bdraco
Copy link
Member

@bdraco bdraco commented Feb 13, 2026

What does this implement/fix?

Remove a cargo-culted dhcp_renew() call that corrupts lwIP's DHCP state machine on every ESP8266 WiFi connection attempt.

Origin

The dhcp_renew() loop in wifi_apply_hostname_() was copied from the ESP8266 Arduino core's LwipIntf::hostname() in commit 072b2c4 (Dec 2019). That function was designed for a different use case: changing the hostname on an already-connected system with multiple interfaces (WiFi + Ethernet), where you need to inform each interface's DHCP server of the new name.

Why it's unnecessary in ESPHome

The hostname is fixed at compile time and never changes at runtime. Setting intf->hostname is sufficient — lwIP automatically includes it in DHCP DISCOVER/REQUEST packets via LWIP_NETIF_HOSTNAME. There is no need to call dhcp_renew() to propagate a hostname change that never happens.

wifi_apply_hostname_() is only called from:

  • setup_() — during initial setup before WiFi connects
  • wifi_sta_connect_() — during the connection sequence, before wifi_station_connect()

In neither case is a dhcp_renew() appropriate.

How it causes damage

lwIP's dhcp_renew() unconditionally sets the DHCP state to RENEWING (dhcp.c:1159) before attempting to send, and never rolls back on failure. When called on a disconnected interface:

  1. DHCP state is corrupted from INIT → RENEWING
  2. When WiFi later connects, dhcp_network_changed() sees RENEWING and calls dhcp_reboot() instead of dhcp_discover()
  3. dhcp_reboot() broadcasts a DHCP REQUEST for IP 0.0.0.0 (no valid lease)
  4. Most routers handle this gracefully (NAK → fallback), but some routers enter a persistent bad state for the device's MAC address, requiring a router restart

Fix

Remove the dhcp_renew() loop entirely. Keep the intf->hostname assignment which is the only part that matters.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Developer breaking change (an API change that could break external components)
  • Code quality improvements to existing code or addition of tests
  • Other

Related issue or feature (if applicable):

Pull request in esphome-docs with documentation (if applicable):

  • N/A (no user-facing configuration changes)

Test Environment

  • ESP32
  • ESP32 IDF
  • ESP8266
  • RP2040
  • BK72xx
  • RTL87xx
  • LN882x
  • nRF52840

Example entry for config.yaml:

# No configuration changes — this is a bugfix in the WiFi connection internals

Checklist:

  • The code change is tested and works locally.
  • Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

wifi_apply_hostname_() calls dhcp_renew() on all interfaces with DHCP
data, including when WiFi is not yet connected. lwIP's dhcp_renew()
unconditionally sets the DHCP state to RENEWING (line 1159 in dhcp.c)
before attempting to send, and never rolls back the state on failure.

This corrupts the DHCP state machine: when WiFi later connects and
dhcp_network_changed() is called, it sees RENEWING state and calls
dhcp_reboot() instead of dhcp_discover(). dhcp_reboot() sends a
broadcast DHCP REQUEST for IP 0.0.0.0 (since no lease was ever
obtained), which can put some routers into a persistent bad state
that requires a router restart to clear.

This bug has existed since commit 072b2c4 (Dec 2019, "Add ESP8266
core v2.6.2") and affects every ESP8266 WiFi connection attempt. Most
routers handle the bogus DHCP REQUEST gracefully (NAK then fallback
to DISCOVER), but affected routers get stuck and refuse connections
from the device until restarted.

Fix: guard the dhcp_renew() call with netif_is_link_up() so it only
runs when the interface actually has an active link. The hostname is
still set on the netif regardless, so it will be included in DHCP
packets when the connection is established normally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:

external_components:
  - source: github://pr#13983
    components: [wifi]
    refresh: 1h

(Added by the PR bot)

@codecov-commenter
Copy link

codecov-commenter commented Feb 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.11%. Comparing base (903971d) to head (46cc9c0).
⚠️ Report is 13 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev   #13983      +/-   ##
==========================================
+ Coverage   74.08%   74.11%   +0.02%     
==========================================
  Files          55       55              
  Lines       11588    11588              
  Branches     1577     1577              
==========================================
+ Hits         8585     8588       +3     
+ Misses       2600     2598       -2     
+ Partials      403      402       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 13, 2026

Memory Impact Analysis

Components: wifi
Platform: esp8266-ard

Metric Target Branch This PR Change
RAM 28,656 bytes 28,656 bytes ➡️ +0 bytes (0.00%)
Flash 323,359 bytes 323,239 bytes 📉 ✅ -120 bytes (-0.04%)
📊 Component Memory Breakdown
Component Target Flash PR Flash Change
[esphome]wifi 16,851 bytes 16,737 bytes 📉 ✅ -114 bytes (-0.68%)
🔍 Symbol-Level Changes (click to expand)

Changed Symbols

Symbol Target Size PR Size Change
esphome::wifi::WiFiComponent::wifi_apply_hostname_()::__pstr__ 69 bytes 20 bytes 📉 -49 bytes (-71.01%)
esphome::wifi::WiFiComponent::wifi_apply_hostname_() 114 bytes 69 bytes 📉 -45 bytes (-39.47%)

Note: This analysis measures static RAM and Flash usage only (compile-time allocation).
Dynamic memory (heap) cannot be measured automatically.
⚠️ You must test this PR on a real device to measure free heap and ensure no runtime memory issues.

This analysis runs automatically when components change. Memory usage is measured from a representative test configuration.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a 6-year-old bug in ESP8266 WiFi connection code where dhcp_renew() was called before the interface had an active link, corrupting lwIP's DHCP state machine and causing persistent connection issues with some routers.

Changes:

  • Added netif_is_link_up(intf) guard to prevent dhcp_renew() from being called before WiFi connection is established
  • Added comprehensive comment explaining the bug, root cause in lwIP, and consequences
  • Preserves hostname setting behavior while preventing state machine corruption

@bdraco
Copy link
Member Author

bdraco commented Feb 13, 2026

I saw this all the time but my router doesn't care about the
[W][wifi_esp8266:220]: wifi_apply_hostname_(XXX): lwIP error -16 on interface st (index 0)
error.

This fixes it.

I spent an hour looking for it and claude found it in 20 minutes on the 2nd attempt

@bdraco bdraco marked this pull request as ready for review February 13, 2026 20:18
@bdraco
Copy link
Member Author

bdraco commented Feb 13, 2026

It was 100% reproducible on esp8266 if the first wifi connection fails. To reproduce, wrap in foil on the first attempt and unwrap.

@bdraco
Copy link
Member Author

bdraco commented Feb 13, 2026

My awful test setup
IMG_0628

@bdraco bdraco added this to the 2026.2.0b2 milestone Feb 13, 2026
@bdraco bdraco marked this pull request as draft February 13, 2026 20:34
netif_is_link_up() is insufficient — if wifi_station_connect()
completes quickly (e.g. fast_connect), the setup() call at line 710
could reach dhcp_renew() with link up but DHCP still in SELECTING
or REQUESTING state, causing the same state corruption.

Check dhcp->state == DHCP_STATE_BOUND directly to ensure dhcp_renew()
is only called when there is an actual lease to renew.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jesserockz jesserockz removed this from the 2026.2.0b2 milestone Feb 14, 2026
DHCP_STATE_BOUND alone is insufficient — during reconnection, DHCP
can remain BOUND from a previous connection while the link is down
(wifi_disconnect_() doesn't stop DHCP). Both conditions are needed:
DHCP must be BOUND and the interface must have link.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bdraco
Copy link
Member Author

bdraco commented Feb 14, 2026

likely original code modeled after esp8266/Arduino#6680

bdraco and others added 2 commits February 14, 2026 05:22
The dhcp_renew() loop was cargo-culted from Arduino ESP8266's
WiFi.hostname() which was designed for changing the hostname on
an already-connected system with multiple interfaces (WiFi+Ethernet).

In ESPHome, wifi_apply_hostname_() is only called from:
  - setup_() — before WiFi connects (link never up)
  - wifi_sta_connect_() — after wifi_disconnect_() (link always down)

The hostname is fixed at compile time and never changes at runtime.
Setting intf->hostname is sufficient — lwIP automatically includes
it in DHCP DISCOVER/REQUEST packets via LWIP_NETIF_HOSTNAME.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cannot confirm wifi_station_disconnect() synchronously clears the
lwIP netif LINK_UP flag on ESP8266 NONOS SDK. The comment doesn't
need to make claims about link state since the fix is simply that
the hostname never changes at runtime, making dhcp_renew() pointless.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@bdraco bdraco marked this pull request as ready for review February 14, 2026 12:31
@bdraco
Copy link
Member Author

bdraco commented Feb 14, 2026

thanks

@bdraco bdraco merged commit 36776b4 into dev Feb 14, 2026
35 checks passed
@bdraco bdraco deleted the fix-esp8266-dhcp-state-corruption branch February 14, 2026 15:21
@github-actions github-actions bot locked and limited conversation to collaborators Feb 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants