Skip to content

Fix possible crash in UdpContext host mocking. - #9312

Merged
mcspr merged 2 commits into
esp8266:masterfrom
everslick:patch-1
Mar 23, 2026
Merged

Fix possible crash in UdpContext host mocking.#9312
mcspr merged 2 commits into
esp8266:masterfrom
everslick:patch-1

Conversation

@everslick

Copy link
Copy Markdown
Contributor

I saw this crash in UdpContext.h happen after some hours of regular NTP requests....

[17:38:51] ADE:  voltage spike: phase A, factor=0.81, cycles=1
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
[17:41:14] ADE:  voltage spike: phase A, factor=0.86, cycles=2
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
[17:43:49] ADE:  voltage spike: phase B, factor=1.12, cycles=3
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) =====> UdpServer port: 2390 <=====
[17:44:31] NTP:  waiting for response from '81.169.233.252' timed out

Program received signal SIGSEGV, Segmentation fault.
0x000055555556848c in std::_Function_base::_M_empty() const ()
(gdb) where
#0  0x000055555556848c in std::_Function_base::_M_empty() const ()
#1  0x00005555555685be in std::function<void ()>::operator bool() const ()
#2  0x00005555555684b8 in UdpContext::mock_cb() ()
#3  0x00005555555683af in check_incoming_udp() ()
#4  0x0000555555567fa6 in main ()

I tracked it down to UdpContext::mock_cb() which is calling a std::function callback that's been destroyed thus used-after-free. I believe what happens is:

  1. UdpContext::unref() calls delete this when refcount hits 0 (line 56)
  2. The destructor (~UdpContext(), line 47) is empty — it never calls register_udp(_sock, nullptr) to remove itself from the global udps map
  3. check_incoming_udp() iterates udps, finds the dangling pointer, calls mock_cb() on freed memory
  4. _on_rx (a destroyed std::function) → segfault in _M_empty()

The disconnect() method already does the right thing (lines 78-84: closes socket + unregisters), but it's never called before the object is destroyed via unref().

The fix is to make the destructor clean up properly.

I tested intensely, no crashes since.

I saw this crash in UdpContext.h happen after some hours of regular NTP requests....

```
[17:38:51] ADE:  voltage spike: phase A, factor=0.81, cycles=1
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
[17:41:14] ADE:  voltage spike: phase A, factor=0.86, cycles=2
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) ClientContext::connect: ::connect(): Connection refused
[17:43:49] ADE:  voltage spike: phase B, factor=1.12, cycles=3
(mock) ClientContext::connect: ::connect(): Connection refused
(mock) =====> UdpServer port: 2390 <=====
[17:44:31] NTP:  waiting for response from '81.169.233.252' timed out

Program received signal SIGSEGV, Segmentation fault.
0x000055555556848c in std::_Function_base::_M_empty() const ()
(gdb) where
#0  0x000055555556848c in std::_Function_base::_M_empty() const ()
esp8266#1  0x00005555555685be in std::function<void ()>::operator bool() const ()
esp8266#2  0x00005555555684b8 in UdpContext::mock_cb() ()
esp8266#3  0x00005555555683af in check_incoming_udp() ()
esp8266#4  0x0000555555567fa6 in main ()
```

I tracked it down to UdpContext::mock_cb() which is calling a std::function callback that's been destroyed thus used-after-free. I believe  what happens is:
  1. UdpContext::unref() calls delete this when refcount hits 0 (line 56)
  2. The destructor (~UdpContext(), line 47) is empty — it never calls register_udp(_sock, nullptr) to remove itself from the global udps map
  3. check_incoming_udp() iterates udps, finds the dangling pointer, calls mock_cb() on freed memory
  4. _on_rx (a destroyed std::function) → segfault in _M_empty()

The disconnect() method already does the right thing (lines 78-84: closes socket + unregisters), but it's never called before the object is destroyed via  unref().

The fix is to make the destructor clean up properly.

I tested intensely, no crashes since.
@mcspr
mcspr merged commit f946290 into esp8266:master Mar 23, 2026
28 checks passed
@everslick
everslick deleted the patch-1 branch March 23, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants