【lwIP】Bug #1 | accept()でクラッシュする原因|二重解放バグ
(English Version Here.)
このnoteでは、lwIPで実際に起こり得る不具合・脆弱性・再現方法・最小修正の考え方やデバッグのノウハウをシリーズで解説しています。
▶ 日本語記事一覧はこちら:lwIPトラブル対策室|日本語記事一覧
■ lwIPでこんな現象に遭遇していませんか?
TCPサーバー機能が、突然 ASSERTで停止する
`netconn state error` などのASSERTメッセージが出ている
特定のタイミングでの接続で ハングアップ・フリーズ が発生する
ネットワークスキャンやポートスキャン実行中に発生しやすい
lwIPに存在する既知の不具合が原因の可能性があります。
■ 発生条件
lwIPバージョン: 2.1.2(またはそれ以前のバージョン)
影響を受ける構成: Socket APIでTCPサーバー起動
■ この記事で分かること
実際に再現できる具体手順
nmap コマンドを使った再現方法とTCPパケットの条件
Python スクリプトを使った代替再現方法
内部動作を追ったコード解析
accept() のエラーパスで発生する Double Free の原因
内部でメモリ解放が二重実行される仕組み
Use-After-Free が ASSERT停止や HardFault(異常例外)に繋がる理由
回避する方法
最小限の修正パッチ

■ 問題の概要
lwIP 2.1.2 以前の `sockets.c` に実装されている `lwip_accept()` 関数には、エラー処理のコードパスに メモリの二重解放(Double Free)が発生するバグ があります。
このバグにより、 解放済みメモリへのアクセス(Use-After-Free) が発生する可能性があり、ビルド設定やタイミングによってはクラッシュやASSERT停止につながります。
原因は `lwip_accept()` のエラー経路にあります。接続エラーを検出した際、内部で確保した `newconn`(`struct netconn`)の解放処理(`netconn_delete()`)が2回実行されます。
■ 再現手順
注意(実施前に必ずご確認ください): 以降の再現手順は、自身が管理する検証環境・隔離ネットワーク内でのみ実施してください。第三者が管理する機器やネットワークへの無断スキャンは、実害の有無にかかわらず不正アクセス禁止法に抵触するおそれがあります。必ず対象機器の管理権限を持つ立場で、許可された範囲内でのみ実施してください。
前提環境
lwIP 2.1.2(またはそれ以前のバージョン)
TCPポートをLISTEN中(例:HTTPサーバーとしてポート80)
Socket API(lwip_accept)を使用している
TCPフルコネクトスキャンを実行できる環境(nmap 等)
手順
Step 1: ターゲットが正常動作していることを確認する
ping <ターゲットのIPアドレス>pingが返ってくることを確認します。
Step 2: nmapでTCPフルコネクトスキャンを実行する
nmap とは: ネットワーク上のホストやポートの状態を調査するオープンソースのポートスキャナーです。Linux / macOS / Windows で動作します。`-sT`(TCPフルコネクト)や `-sS`(SYNスキャン)など複数のスキャン方式をサポートしています。
nmap -sT -p <オープンポート> <ターゲットのIPアドレス>`-sT`:TCPフルコネクトスキャン(3ウェイハンドシェイクを完了させたうえで即RST)
`-p <オープンポート>`:サーバーがLISTENしているポートを指定(例:`-p 80`)
注意: このバグはタイミング依存のため、1回のスキャンで必ず再現するわけではありません。再現しない場合は繰り返し実行してください。以下のように複数回ループさせると効率的です。
for i in $(seq 1 10); do
nmap -sT -p <オープンポート> <ターゲットのIPアドレス>
sleep 1
done
Step 3: クラッシュの確認
`LWIP_NOASSERT` が未定義の場合、ASSERTが発火します。upstream lwIP 2.1.2 のデフォルト実装では以下の形式で出力されます:
Assertion "netconn state error" failed at line xxxx in api_msg.cただし上記は `LWIP_PLATFORM_ASSERT` の実装に依存するため、表示形式はプラットフォームにより異なります。
それ以外の症状として、下記が発生:
TCPスレッドの停止
ハングアップ
ウォッチドッグリセット(搭載している場合)
Step 4: クラッシュの再現性確認
再現しない場合はStep 2のスキャンを繰り返してください。クラッシュが発生した時点でターゲットが停止します。
nmap が使えない環境向けに、`SO_LINGER` を用いたPythonスクリプトによる代替再現手順も有料パート(修正方法の後)に記載しています。
■ 原因・対策
ここまでで、発生条件・問題の概要・再現手順を公開しました。
次に、ソースコード解析の入口として、問題の全体像が分かる最小限の部分だけ紹介します。
詳細なコード追跡・修正方法・修正後の確認方法・公式対応状況は以降の有料パートで解説します。
同様の現象で困っている方、出荷済み製品の修正対応が必要な方に向けて、すぐに使用できる具体的な内容まで解説しています。
またlwIPの全体像や、各ベンダーSDKごとの差異、なぜlwIPの不具合情報が重要なのか、といった背景については、導入記事で整理しています。
【lwIP】よくある不具合と対策まとめ|組み込みTCP/IPトラブル事例集
■ ソースコード解析
lwIP 2.1.2 ソースコード内の `sockets.c` の `lwip_accept()` 関数を追います。
【解析1】lwip_accept() の正常フロー
`accept()` が呼ばれると `lwip_accept()` が実行されます。正常時の処理フローは次の通りです:
// sockets.c lwip_accept()
// 1. 接続待ちキューから新しい接続を取り出す
err = netconn_accept(sock->conn, &newconn);
// 2. ソケット番号を確保し、newconn と紐付ける
newsock = alloc_socket(newconn, 1); // newsock != -1 → 成功
nsock = &sockets[newsock - LWIP_SOCKET_OFFSET];
// ↑ この時点で nsock->conn = newconn が設定される(alloc_socket内)
// 3. 接続先のIPアドレスとポートを取得する
err = netconn_peer(newconn, &naddr, &port); // 通常は ERR_OK
// 4. 正常終了
return newsock;【解析2】バグが潜むエラーパス
`netconn_peer()` が失敗したときのコードを見てください:
ここから先は
¥ 500
この記事が気に入ったらチップで応援してみませんか?
