Neutralize lwIP SNTP client to prevent heap drain on WiFi reconnects (#24593)

The ESP8266 SDK automatically invokes sntp_init() via
netif_sta_status_callback on every WiFi reconnect. This results in repeated
allocations of UDP PCBs (udp_new()), leading to heap exhaustion and
potential lwIP timeout list corruption due to unmatched sntp_stop() calls.

Tasmota does not use the lwIP SNTP client and relies solely on WifiGetNtp()
for time synchronization. To avoid unnecessary resource usage, wrap and
neutralize sntp_init() and sntp_stop() with no-op implementations.

This prevents UDP PCB allocations and avoids issues caused by repeated
initialization during reconnect cycles.

Additionally, a PlatformIO post-build script has been added to validate
that the linker wraps are correctly applied. The script runs after ELF
generation and uses xtensa-lx106-elf-nm to inspect symbols, ensuring that
the __wrap_sntp_init and __wrap_sntp_stop functions are present in the
final firmware. If the wrappers are missing, the build fails.

Confirmed via firmware disassembly:
- Single call site in netif_sta_status_callback
- Requires linker wraps: --wrap=sntp_init,--wrap=sntp_stop

Fixes: #24566
This commit is contained in:
the-way-of-A-Wild
2026-03-26 10:42:02 +01:00
committed by GitHub
parent 4384f9c98a
commit 86bc033c86
4 changed files with 122 additions and 1 deletions
+2 -1
View File
@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
### Changed
- ESP8266 platform update from 2026.02.00 to 2026.03.00 (#24547)
- ESP8266 use wrapped symbols for sntp_init and sntp_stop (#24566)
- ESP32 Platform from 2025.02.30 to 2026.03.30, Framework (Arduino Core) from v3.1.9 to v3.1.10 and IDF from v5.3.4.251226 to v5.3.4.260127 (#24547)
- Matter don't advertize IPv6 global address, only link-local (#24563)
- ESP32-C5/C6/P4 Platform from 2025.03.30 to 2026.03.50, Framework (Arduino Core) from v3.1.10 to v3.3.7 and IDF from v5.3.4.260127 to v5.5.3+ (#24567)
@@ -18,7 +19,7 @@ All notable changes to this project will be documented in this file.
### Fixed
- Athom esp32 2-3-4 gang change led behaviour after firmware update (#24509)
- ESP8266 heap drain and exception 29 when DHCP provides NTP server (#24515)
- ESP8266 heap drain and exception 29 when DHCP provides NTP server (#24515,#24566)
- NeoPool possible IntegerDivideByZero (#24578)
### Removed
@@ -0,0 +1,94 @@
Import("env")
import subprocess
import sys
import os
def parse_nm_symbols(nm_output):
symbols = set()
for line in nm_output.splitlines():
parts = line.strip().split()
if len(parts) >= 3:
symbols.add(parts[-1])
return symbols
def check_sntp_wrap(target, source, env):
print("SNTP WRAP SYMBOL CHECK: Checking firmware")
board = env.BoardConfig()
mcu = board.get("build.mcu", "esp32").lower()
if mcu != "esp8266":
print("SNTP WRAP SYMBOL CHECK: Skipped for MCU =", mcu)
return
firmware = str(target[0])
if not os.path.exists(firmware):
print("ERROR: Firmware ELF not found:", firmware)
sys.exit(1)
else:
print("Firmware ELF found:", firmware)
nm = "xtensa-lx106-elf-nm"
result = subprocess.run(
[nm, "-n", firmware],
capture_output=True,
text=True
)
if result.returncode != 0:
print("ERROR: nm failed to execute")
print(result.stderr)
sys.exit(1)
symbols = parse_nm_symbols(result.stdout)
required = [
"__wrap_sntp_init",
"__wrap_sntp_stop",
]
forbidden = [
"sntp_init",
"sntp_stop",
]
found_required = [s for s in required if s in symbols]
missing_required = [s for s in required if s not in symbols]
found_forbidden = [s for s in forbidden if s in symbols]
print("Required wrap symbols:")
for s in required:
status = "OK" if s in symbols else "MISSING"
print(" ", status + ":", s)
print("Forbidden original symbols:")
if found_forbidden:
for s in found_forbidden:
print(" FORBIDDEN:", s)
else:
print(" None")
if missing_required or found_forbidden:
print("FAILED: lwIP SNTP wrap symbol check failed.")
if missing_required:
print("Missing required wrap symbols:")
for s in missing_required:
print(" MISSING:", s)
if found_forbidden:
print("Forbidden original symbols detected:")
for s in found_forbidden:
print(" FORBIDDEN:", s)
sys.exit(1)
print("PASSED: All lwIP SNTP wrap symbols are present and original symbols are not found.")
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", check_sntp_wrap)
+4
View File
@@ -84,6 +84,7 @@ extra_scripts = post:pio-tools/name-firmware.py
post:pio-tools/metrics-firmware.py
pre:pio-tools/custom_target.py
; post:pio-tools/obj-dump.py
post:pio-tools/check-wrapped-lwip-sntp-calls.py
${scripts_defaults.extra_scripts}
; *** remove undesired all warnings
build_unflags = ${tasmota.build_unflags}
@@ -123,6 +124,9 @@ build_flags = ${esp_defaults.build_flags}
-DMIMETYPE_MINIMAL
; uncomment the following to enable TLS with 4096 RSA certificates
;-DUSE_4K_RSA
; Prevent lwIP SNTP client from heap drain in case of repeated calls to sntp_init() and sntp_stop() from SDK/lwIP
-Wl,--wrap=sntp_init
-Wl,--wrap=sntp_stop
lib_ignore = ESP8266Audio
ESP8266SAM
ESP8266LLMNR
@@ -290,4 +290,26 @@ uint32_t HwRandom(void) {
#undef _RAND_ADDR
}
/*********************************************************************************************\
* Neutralize lwIP SNTP client to prevent heap drain on WiFi reconnects
*
* The ESP8266 SDK autonomously calls sntp_init() via netif_sta_status_callback
* on every WiFi reconnect, allocating a UDP PCB via udp_new() each time.
* Tasmota manages NTP entirely via WifiGetNtp() and has no use for the lwIP
* SNTP client. These wrappers replace both functions with NOPs, preventing
* UDP PCB heap allocation and lwIP timeout list corruption from unpaired
* sntp_stop() calls.
*
* Confirmed by disassembly of firmware.elf - single call site at netif_sta_status_callback
* Source: liblwip2-1460.a(sntp.o) - requires -Wl,--wrap=sntp_init,--wrap=sntp_stop
\*********************************************************************************************/
extern "C" void __wrap_sntp_init(void) {
// Prevent lwIP SNTP client from starting on WiFi reconnects and causing heap drain and timeout list corruption
}
extern "C" void __wrap_sntp_stop(void) {
// Prevent lwIP SNTP client from stopping on WiFi disconnects and causing timeout list corruption
}
#endif // ESP8266