mirror of
https://github.com/arendst/Tasmota.git
synced 2026-09-11 17:23:18 +00:00
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:
+2
-1
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user