diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d97c22e1..159393421 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pio-tools/check-wrapped-lwip-sntp-calls.py b/pio-tools/check-wrapped-lwip-sntp-calls.py new file mode 100644 index 000000000..d7de9ff7b --- /dev/null +++ b/pio-tools/check-wrapped-lwip-sntp-calls.py @@ -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) diff --git a/platformio.ini b/platformio.ini index 470c3b843..b94b5f99e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/tasmota/tasmota_support/support_esp8266.ino b/tasmota/tasmota_support/support_esp8266.ino index 171cf91bd..13ac4171a 100644 --- a/tasmota/tasmota_support/support_esp8266.ino +++ b/tasmota/tasmota_support/support_esp8266.ino @@ -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 \ No newline at end of file