lwip2 time-wait.patch: TCP_TW_LIMIT can abort the pcb that tcp_process() is still handling -> double free, heap corruption
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 52/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- c
調査の方向性
Start with tools/sdk/lwip2/builder/patches/time-wait.patch and the tcp_process() paths in lwIP's tcp_in.c for FIN_WAIT_1, FIN_WAIT_2, and CLOSING. Trace TCP_TW_LIMIT through tcp_kill_timewait() and the tcp_input() closed-connection branch, then use the two-client reproduction and free tracer described in the issue. Done means the TIME_WAIT limit no longer frees the PCB still being processed and the reproduction shows no double free or heap corruption.
索引モデルが issue の本文から書いたものです。
説明
Basic Infos
- This issue complies with the issue POLICY doc.
- I have read the documentation at readthedocs and the issue is not addressed there.
- I have tested that the issue is present in current master branch (aka latest git). (Not run on master, but
tools/sdk/lwip2/builderon master points at esp82xx-nonos-linklayer 4087efd, whosepatches/time-wait.patchis byte-identical to the one in 3.1.2.) - I have searched the issue tracker for a similar issue.
- If there is a stack dump, I have decoded it.
- I have filled out all fields below.
Platform
- Hardware: ESP-12 (NodeMCU v2 style board)
- Core Version: 3.1.2 (framework-arduinoespressif8266 3.30102.0), NONOS SDK 2.2.2-dev(38a443e)
- Development Env: Platformio
- Operating System: Windows
Settings in IDE
- Module: Nodemcu (nodemcuv2)
- Flash Mode: dio
- Flash Size: 4MB (eagle.flash.4m2m.ld)
- lwip Variant: v2 Lower Memory (PlatformIO default)
- Reset Method: nodemcu
- Flash Frequency: 40Mhz
- CPU Frequency: 80Mhz
- Upload Using: OTA
Problem Description
tools/sdk/lwip2/builder/patches/time-wait.patch adds TCP_TW_LIMIT(MEMP_NUM_TCP_PCB_TIME_WAIT) straight after TCP_REG(&tcp_tw_pcbs, pcb) in tcp_process() (FIN_WAIT_1, FIN_WAIT_2 and CLOSING cases). When the TIME_WAIT list grows past the limit (MEMP_NUM_TCP_PCB_TIME_WAIT, 5 by default), the macro calls tcp_kill_timewait(), which tcp_abort()s the pcb with the largest tcp_ticks - pcb->tmr.
That pcb can be the one tcp_process() has just moved to TIME_WAIT. Once the application has closed a connection, TF_RXCLOSED is set and tcp_process() stops refreshing pcb->tmr:
// tcp_in.c (lwIP 2.1.3), start of tcp_process()
if ((pcb->flags & TF_RXCLOSED) == 0) {
/* Update the PCB (in)activity timer unless rx is closed (see tcp_shutdown) */
pcb->tmr = tcp_ticks;
}
So a pcb that sat in FIN_WAIT_2 waiting for the peer's FIN keeps the timestamp from when we closed it, and it is usually the "oldest" entry in the TIME_WAIT list by the time the FIN arrives.
tcp_kill_timewait() frees it, but tcp_process() doesn't return ERR_ABRT, so tcp_input() carries on using the freed pcb. If the segment carried data (for example a browser re-using a keep-alive connection that the server has already closed), tcp_input() goes down the "received data although already closed" branch and calls tcp_abort(pcb) a second time:
// tcp_in.c (lwIP 2.1.3) ~line 494, line 497 after the patch
if (pcb->flags & TF_RXCLOSED) {
/* received data although already closed -> abort (send RST) ... */
pbuf_free(recv_data);
tcp_abort(pcb);
goto aborted;
}
The result is a double free of the tcp_pcb. Before the second free the block has often been handed out again, so the second tcp_abort() corrupts the new owner. That surfaces much later as unrelated exceptions: wild pointers in wDev_ProcessFiq, umm_assimilate_up LoadStoreError, illegal instructions. We had about a dozen of these over four months on a device running ESP8266WebServer + PubSubClient + HTTPClient before tracking this down.
Conditions: a TIME_WAIT list already at the limit (many short HTTP connections closed by the ESP side), plus a peer that sends data + FIN on a connection the ESP has already closed. Two browsers/pollers on an ESP8266WebServer at the same time reproduced it in 38 minutes.
How it was caught
-Wl,--wrap=free -Wl,--wrap=vPortFree, with a wrapper that checks the umm block header's free bit (UMM_FREELIST_MASK) before each free. It keeps a small history of recent frees with a stack scan and abort()s on a double free. Decoded with the matching ELF:
Double free of 0x3fff75a4 (tcp_pcb), first freed 78 us before the second
1st free:
0x4023f780: mem_free at lwip2-src/src/core/mem.c:237
0x4023f80d: memp_free at lwip2-src/src/core/memp.c:447
0x40240374: tcp_free at lwip2-src/src/core/tcp.c:217
0x40240964: tcp_abandon at lwip2-src/src/core/tcp.c:583
0x40240a68: tcp_abort at lwip2-src/src/core/tcp.c:641
0x40240b2d: tcp_kill_timewait at lwip2-src/src/core/tcp.c:1809
0x40242bab: tcp_process at lwip2-src/src/core/tcp_in.c:1020 <- TCP_TW_LIMIT, FIN_WAIT_2 -> TIME_WAIT
(inlined by) tcp_input at lwip2-src/src/core/tcp_in.c:438
2nd free:
0x4023f780: mem_free at lwip2-src/src/core/mem.c:237
0x4023f80d: memp_free at lwip2-src/src/core/memp.c:447
0x40240374: tcp_free at lwip2-src/src/core/tcp.c:217
0x402409ff: tcp_abandon at lwip2-src/src/core/tcp.c:622
0x40240a68: tcp_abort at lwip2-src/src/core/tcp.c:641
0x40242cd7: tcp_input at lwip2-src/src/core/tcp_in.c:497 <- "received data although already closed"
0x40247d59: ip4_input at lwip2-src/src/core/ipv4/ip4.c:1467
0x4023ee21: ethernet_input_LWIP2 at lwip2-src/src/netif/ethernet.c:188
0x4026788b: ethernet_input at glue-esp/lwip-esp.c:373
Without the tracer, the same bug crashed later inside the allocator, e.g. umm_assimilate_up <- umm_free_core <- umm_free <- mem_free <- memp_free <- ... tcp_free, with EXCVADDR computed from a free-list index that already had UMM_FREELIST_MASK set.
Suggested fix
Apply the limit before the pcb joins the TIME_WAIT list, so tcp_kill_timewait() can never choose it:
TCP_RMV_ACTIVE(pcb);
pcb->state = TIME_WAIT;
- TCP_REG(&tcp_tw_pcbs, pcb);
- TCP_TW_LIMIT(MEMP_NUM_TCP_PCB_TIME_WAIT);
+ TCP_TW_LIMIT(MEMP_NUM_TCP_PCB_TIME_WAIT - 1);
+ TCP_REG(&tcp_tw_pcbs, pcb);
(in all three places). An alternative is a tcp_kill_timewait() variant that skips a given pcb.
Workaround for sketches
Remove all TIME_WAIT pcbs from loop(), so the list never grows past the limit and the patch never has to choose (this is d-a-v's tcpCleanup() from #4213):
#include <lwip/tcp.h>
#include <lwip/priv/tcp_priv.h>
void loop() {
server.handleClient();
while (tcp_tw_pcbs) tcp_abort(tcp_tw_pcbs); // TIME_WAIT pcbs: just unlinked + freed
...
}
With this in place (and the tracer still active), the same two-client test ran for 90 minutes with no double free and no crash: 3,204 HTTP requests, and the workaround removed 2,448 TIME_WAIT pcbs (250-300 every 10 minutes). Without it, the same test hit the double free after 38 minutes. It only narrows the window: 6+ connections entering TIME_WAIT inside a single loop() pass could still hit the bug.
MCVE Sketch
Not yet reduced to a minimal sketch. It was reproduced on a full application (ESP8266WebServer with ~40 JSON routes, PubSubClient, SoftwareSerial). The trigger is two HTTP clients at once: one polling 4 URLs in parallel every 10 s over keep-alive connections, and one loading a page plus 10 API calls (6 in parallel) every 30 s. From the analysis above, a minimal repro should be:
ESP8266WebServeransweringGET /(it closes after each response).- A client making 6+ short connections to fill TIME_WAIT.
- Another client that keeps a connection open after the server's FIN, then sends a request plus FIN on it.
Debug Messages
Not captured on serial; the decode above is from a crash record written by custom_crash_callback.
- 主要言語
- C++
- スター
- 16.7k
- フォーク
- 13.1k
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
esp8266/Arduino のほかの issue
-
Typo in Scan Class Documentation対応中かも @MukundaKatta が 174 日前に担当しました。 オープン
難易度 1/5 1時間未満 初心者へのやさしさ 70/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
-
難易度 3/5 1〜2日 初心者へのやさしさ 65/100
-
難易度 4/5 3〜5日 初心者へのやさしさ 42/100
-
[compiler Warnings]: Compile with `-Wconversion` and `-Wsign-conversion` brings up many Warningsオープン
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
esp8266/Arduino の issue をすべて見る
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 73/100
EchoTools/nevr-runtime#116 ·
メンテナーはふだん 1 日以内に返信
-
code-quality libc++
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
llvm/llvm-project#229284 ·
メンテナーはふだん 1 日以内に返信
-
test-issue
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
llvm/offload-test-suite#1557 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
iOS: hidden scale bar invalidates its intrinsic content size on every layout pass of MLNMapViewオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
maplibre/maplibre-native#4723 ·
メンテナーはふだん 1 日以内に返信