Summary
The decrypted-RX path in wireguardif.c calls ip_input() directly from the caller's task (the task that runs wireguardif_network_rx / the wg manager task), not from lwIP's tcpip_thread. lwIP's core is not thread-safe: entering it from two threads concurrently corrupts pbuf refcounts. Under sustained TCP traffic through the tunnel this reliably triggers Assertion "pbuf_free: p->ref > 0" and reboots the device mid-transfer.
Environment
- ESP32-S3 (N16R8), ESP-IDF v5.3
- microlink @ 216da33 (also present on current
main)
- Reproduced with a TCP proxy on the tailnet IP relaying a ~600 KB stream (LPR print job)
Root cause
components/microlink/components/wireguard_lwip/src/wireguardif.c line 542: after decrypt, the pbuf is handed to ip_input(pbuf, device->netif) on the calling task. Meanwhile tcpip_thread is processing TCP for the same netif (acks for the outbound leg of the proxy). The race double-frees a sent-segment pbuf:
Assertion "pbuf_free: p->ref > 0" failed at line 753 in .../lwip/src/core/pbuf.c
Note: ml_wg_mgr.c already sets netif->input = tcpip_input, i.e. the integration intends all input to be serialized through the tcpip mailbox — the direct ip_input() call in wireguardif.c bypasses that.
Fix
Route the decrypted pbuf through netif->input (== tcpip_input) instead of calling ip_input() directly, so the lwIP core is only ever entered from tcpip_thread. ~8-line change; PR follows.
Verification
On hardware (ESP32-S3): before the fix, a real print job crashed the board mid-job every time (truncated output, reboot). After the fix, multi-hundred-KB jobs relay to completion; the node also survived overnight idle + WAN modem restart and printed first try afterwards.
Summary
The decrypted-RX path in
wireguardif.ccallsip_input()directly from the caller's task (the task that runswireguardif_network_rx/ the wg manager task), not from lwIP'stcpip_thread. lwIP's core is not thread-safe: entering it from two threads concurrently corrupts pbuf refcounts. Under sustained TCP traffic through the tunnel this reliably triggersAssertion "pbuf_free: p->ref > 0"and reboots the device mid-transfer.Environment
main)Root cause
components/microlink/components/wireguard_lwip/src/wireguardif.cline 542: after decrypt, the pbuf is handed toip_input(pbuf, device->netif)on the calling task. Meanwhiletcpip_threadis processing TCP for the same netif (acks for the outbound leg of the proxy). The race double-frees a sent-segment pbuf:Note:
ml_wg_mgr.calready setsnetif->input = tcpip_input, i.e. the integration intends all input to be serialized through the tcpip mailbox — the directip_input()call inwireguardif.cbypasses that.Fix
Route the decrypted pbuf through
netif->input(==tcpip_input) instead of callingip_input()directly, so the lwIP core is only ever entered fromtcpip_thread. ~8-line change; PR follows.Verification
On hardware (ESP32-S3): before the fix, a real print job crashed the board mid-job every time (truncated output, reboot). After the fix, multi-hundred-KB jobs relay to completion; the node also survived overnight idle + WAN modem restart and printed first try afterwards.