メインコンテンツへスキップ
見出し画像

【lwIP】News #8|U-Bootのwgetで受信上限を超えると何が起きるか|tcp_abort()後の戻り値がuse-after-freeを招く

    U-Boot の lwIP 連携コード(`net/lwip/wget.c`)が `altcp_abort()` の直後に `ERR_BUF` を返しています。lwIP が求めているのは `ERR_ABRT` です。平文HTTPの経路をソースで追うと、解放済みの `tcp_pcb` へ書き込もうとする箇所まで到達します。U-Boot を使っていない製品でも、コールバックの中で接続を abort した後に `ERR_ABRT` 以外を返しているコードや、非同期のコールバックにスタック変数を渡しているコードがあれば、同種の寿命問題が起こり得ます。公開のメーリングリストで修正が提案されている段階の情報を整理します。

    このnoteでは、lwIPで実際に起こり得る不具合・脆弱性・再現方法・最小修正の考え方やデバッグのノウハウをシリーズで解説しています。
    ▶ 日本語記事一覧はこちら:lwIPトラブル対策室|日本語記事一覧

    本記事は「News」として、lwIPに関する速報・重要な動向をお届けします。News記事は、基本的に外部で公表された情報の整理・注意喚起が中心です。


    ■ 今回のニュース概要

    U-Boot の `wget` と `dns` コマンドについて、メモリ寿命の問題として報告し、修正を提案するパッチが、U-Boot のメーリングリストに投稿されています。確認した投稿は次の3件です。

    投稿者が異なる別々のシリーズで、扱う箇所が一部重なっています。Hilliard 氏のシリーズが Jalayeri 氏のシリーズを参照して書かれた可能性を排除できないため、本記事では「独立した2件の検証」とは数えません。

    なお、U-Boot の lwIP 統合に関与する Jerome Forissier 氏が、Jalayeri 氏の3本すべてにレビュー返信をしています(問題1 / 問題2 / 問題3。内容は後述します)。

    本記事の確度と、調査の基準

    • 公開のメーリングリストで議論中のパッチを追跡した情報です

    • 参照した投稿はパッチ投稿であり、セキュリティアドバイザリではありません

    • 2026年9月5日に NVD・CVE.org・GitHub Advisory Database・CERT/CC を「U-Boot lwIP wget use-after-free」「Das U-Boot」「httpc_recv_cb」「altcp_abort ERR_ABRT」で検索した範囲では、対応する公開済みのCVE・VU・GHSAは確認できませんでした(採番されていないことの証明ではありません。なお CERT/CC にある GNU wget の記録は、U-Boot の `wget` コマンドとは別件です)

    • 実機での動作確認・クラッシュ再現は行っていません。

    証拠の強さは、3件で異なります。

    • 問題1:筆者がソースコード上の到達経路を最後まで確認しました

    • 問題2:コンテキストがローカル変数であること、および lwIP 側が接続設定をポインタで保持していることを確認しました。中断後にコールバックが走る経路は報告者の説明です

    • 問題3:スタック変数であること、および lwIP の公開APIに取消手段が無いことを確認しました。実際に発火する経路は追っていません

    ソースは、U-Boot の GitHub ミラーの `main` ブランチを2026年9月5日に取得したものです。取得時点の先頭は、コミット `c46bd5b`(作者日時 2026-08-28 15:06:00 UTC、コミッター日時 2026-09-04 22:55:10 UTC = 日本時間9月5日07:55)でした。既定ブランチは `master` ではなく `main` です。同じ確認時点で `master` は v2026.07 のままだったため、そちらを見ると7月時点の状態になります。

    したがって、問題1の `ERR_ABRT` への変更は、この基準コミットの時点では取り込まれていません(`c46bd5b`)。同コミットの `net/lwip/wget.c` で確認できます。一方、10本シリーズ全体がどう採否されるかは、このスナップショットでは判定できません。

    lwIP 側のコードも、U-Boot がツリー内に同梱している実体(`lib/lwip/lwip/` 配下)を読んでいます(別リポジトリを参照する形ではなく、U-Boot のツリーにソースが入っています)。同梱 lwIP の `CHANGELOG` は、最後に記載されている安定版の見出しが `STABLE-2.2.0` です(その上に `(git master)` の節があるため、正確なリリース版までは特定していません)。


    ■ この記事が対象とする方

    自社アプリケーションについて確認が必要な方

    • lwIP の raw API(`tcp_recv()` や `tcp_abort()` などのコールバックを自分で書く方式)を使っている

    • `altcp` や lwIP の HTTPクライアントなど、コールバックを登録するアプリケーション層を組み込んでいる

    • 非同期のDNS解決を自分で書いている

    自社アプリでは不要でも、別の確認が要る方

    自社アプリケーションが socket API や netconn API だけを使っていて、raw API のコールバックを直接登録していない場合、この戻り値を自社アプリ自身が返す箇所はありません。ただし、該当する U-Boot の `wget` / `dns` 機能を製品で使っている場合と、ベンダーSDK・ブートローダ・自社ミドルウェアの lwIP 統合コードを保守している場合(そこに raw API のコールバックが書かれていることがあります)は別に確認してください。


    ■ 問題1:altcp_abort() の後に ERR_BUF を返す(lwIP が求めるのは ERR_ABRT)

    lwIP が文書化している約束

    U-Boot 同梱の `lib/lwip/lwip/src/core/tcp.c` にある `tcp_abort()` のコメントです。

     * Aborts the connection by sending a RST (reset) segment to the remote
     * host. The pcb is deallocated. This function never fails.
     *
     * ATTENTION: When calling this from one of the TCP callbacks, make
     * sure you always return ERR_ABRT (and never return ERR_ABRT otherwise
     * or you will risk accessing deallocated memory or memory leaks!

    `src/include/lwip/tcp.h` の受信コールバック型 `tcp_recv_fn` には、その逆側の注意が書かれています。

     *            Only return ERR_ABRT if you have called tcp_abort from within the
     *            callback function!

    つまり `tcp.c` が「abort したら必ず `ERR_ABRT` を返せ」、`tcp.h` が「abort していないなら `ERR_ABRT` を返すな」です。隠れた仕様ではなく、実装とヘッダの両方に注意が書かれています。

    守らないと、lwIP が何をするか

    同梱の `lib/lwip/lwip/src/core/tcp_in.c` です。

              /* Notify application that data has been received. */
              TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
              if (err == ERR_ABRT) {
                ...
                goto aborted;
              }
    
              /* If the upper layer can't receive this data, store it */
              if (err != ERR_OK) {
                ...
                pcb->refused_data = recv_data;

    戻り値が `ERR_ABRT` なら中断処理へ進みます。`ERR_OK` なら正常系です。そのどちらでもない値が返ると、中断されないまま後続処理へ進みます。具体的には `pcb->refused_data` への代入です。

    コールバックの中で `tcp_abort()` が呼ばれていた場合、この `pcb` はすでに解放されています。lwIP はそれを知る手段を持ちません。abort したかどうかを lwIP に伝えるのは、この戻り値だけです。

    U-Boot 側のコード

    `net/lwip/wget.c` の `httpc_recv_cb()` です。

    	for (buf = pbuf; buf; buf = buf->next) {
    		if (store_block(ctx, buf->payload, buf->len) < 0) {
    			altcp_abort(pcb);
    			ret = ERR_BUF;
    			goto out;
    		}
    	}
    	altcp_recved(pcb, pbuf->tot_len);
    	ret = ERR_OK;
    out:
    	pbuf_free(pbuf);
    	return ret;

    `altcp_abort(pcb)` を呼んだ直後に `ERR_BUF` を返しています。

    どの経路を通るか(平文HTTPの場合)

    `altcp` は素のTCPとTLSを切り替えられる層なので、どちらを通るかで話が変わります。U-Boot は接続設定を `memset(&conn, 0, sizeof(conn));` でゼロクリアしており、`altcp_allocator` は NULL のままです。HTTPS のときだけ `conn.altcp_allocator = &tls_allocator;` を設定します。

    したがって平文HTTPでは、素のTCPバックエンドを通ります。以下は平文HTTPの経路です。HTTPS の場合は TLS 側の実装を経由するため、本記事では追っていません。

    同梱lwIPで確認した経路は次のとおりです。

    1. `http_client.c` の `httpc_tcp_recv()` が、ユーザーのコールバックの戻り値をそのまま返す

    2. `altcp_tcp.c` の `altcp_tcp_recv()` も、そのまま返す

    3. `altcp.c` の `altcp_abort()` は `conn->fns->abort(conn)` へ振り分ける

    4. 素のTCPバックエンドでは `altcp_tcp.c` の `altcp_tcp_abort()` が `tcp_abort(pcb)` を呼び、`tcp_pcb` を解放する

    5. `ERR_BUF` が `tcp_in.c` の分岐へ戻り、`goto aborted` に入らないまま `pcb->refused_data` への代入に進む

    1 の箇所には、lwIP 自身がこの状況を想定したコメントを書いています。

        if (req->recv_fn != NULL) {
          /* directly return here: the connection might already be aborted from the callback! */
          return req->recv_fn(req->callback_arg, pcb, p, r);

    4 の `altcp_tcp_abort()` は、`conn->state` から `tcp_pcb` を取り出して `tcp_abort(pcb)` を呼ぶだけの関数です。

    つまり、解放済みの `tcp_pcb` を通じて `refused_data` へ書き込もうとします。

    この結論は、報告者が [PATCH v2 1/3] のコミットメッセージに書いている内容と一致しました。

    lwIP's receive-callback contract requires ERR_ABRT once tcp_abort() has been called. On any other return value tcp_input() keeps using the freed pcb (for example it stores the segment in pcb->refused_data), a use-after-free.

    筆者が独自にソースを追った結果も、同じ `pcb->refused_data` に行き着きました。このパッチのレビュー状況は後述します。

    なお U-Boot 側は戻る前に `pbuf_free(pbuf)` を呼んでおり、`recv_data` に渡されるその pbuf の参照を手放しています。それでも `ERR_BUF` を返すため、lwIP 側は「上位層が受け取れなかったので保持しておく」と解釈します。所有権の受け渡しが食い違った状態です(`pbuf_free()` は参照カウントを減らす関数なので、その時点で実体まで解放されているかは条件によります)。

    C言語では解放後のアクセスは未定義動作なので、実機で必ず特定のアドレスに書き込まれると断定はできません。確認できたのは、ソース上ここに到達する経路があることです。

    どういうときに、この経路に入るのか

    `store_block()` が負値を返したときだけです。

    	if (wget_info->buffer_size && wget_info->buffer_size < ctx->size + len)
    		return -1;

    失敗条件は2つあります。1つ目がこの上限判定で、受信済みサイズ+今回の長さが設定されたバッファ上限を超えた場合です。2つ目は、LMB が有効で `set_bootdev` のとき、`store_addr + len` がオーバーフローするか `lmb_read_check()` が失敗する(予約済みメモリ領域への書き込みになる)場合です。

    1つ目は受信量で決まります。`buffer_size` が設定されていて、応答本文がその上限を超えて受信コールバックまで届けば、この条件に到達します。2つ目は受信量だけでなく、格納先アドレス、`set_bootdev` の設定、LMB の予約領域との位置関係にも依存します。

    到達可能性は、次の範囲で書きます。

    機器の側から `wget` で取りに行った先のサーバーが返す応答量は、1つ目の失敗条件に到達させる要因になります。ただし、外部の任意のホストから一方的に到達できるものではありません。また、解放後の書き込みが実機上でどのような結果になるかは確認していません。

    いつから入ったコードか

    U-Boot の各タグで `net/lwip/wget.c` を読み比べました。

    v2025.01 と v2025.04 には `store_block()` も `altcp_abort()` もなく、受信データをそのまま `memcpy()` していました。v2025.07 までの間に、上限チェックを行う `store_block()` と、その失敗時の `altcp_abort()` + `ERR_BUF` が追加されています。v2026.07(2026年9月5日時点の最新の正式リリース。`v2026.10-rc3` は正式リリース前のリリース候補版)と `main` の `c46bd5b` も同じままです。

    両者が同じコミットで入ったかは確認していません。確認できたのは、同じリリースの範囲で現れているという事実です。安全側に倒す変更と、戻り値の不一致とが、同じリリース範囲で現れている形で、lwIP の raw API では戻り値そのものが制御信号なので、エラー処理の分岐を足すときには注意が必要な箇所です。


    ■ 問題2と問題3:コールバックに渡した引数の寿命

    残り2件は、問題1とは別の種類です。問題1が「PCBを破棄した後の戻り値」の話なのに対し、こちらは「コールバックに渡した引数の生存期間とキャンセル」の話です。

    Jalayeri 氏は [PATCH v2 2/3] で、`wget_do_request()` が転送コンテキストをスタック上に置いているため、名前解決の途中で中断して受信ループを抜けた後に、保留されていたDNS参照が解決されて接続が確立し、消滅したスタックフレームに対して `httpc_recv_cb()` が走る、と説明しています。これは報告者の説明です。筆者がソースで確認したのは、`wget_do_request()` の `struct wget_ctx ctx;` がローカル変数であることまでで、中断経路でコールバックが解除されるかどうかは追っていません。

    一方、報告者が同じ投稿で挙げているもう1つの寿命問題については、その前提が lwIP 側のソースで確認できました。接続設定を受け取る `httpc_state_t` は、設定をコピーせずポインタで保持しています。

    フィールドの宣言は `const httpc_connection_t *conn_settings;` で、構造体そのものではなくポインタです。

    lwIP はこのポインタを後から `result_fn` や `headers_done_fn` の呼び出しで参照します。U-Boot はこの設定(`conn`)を `wget_handle_request()` のローカル変数として渡しています(転送コンテキスト `ctx` のほうは `wget_do_request()` のローカル変数です)。転送コンテキストとは別に、接続設定そのものもスタック上に置かれていることになります。

    `dns` コマンド側も同じ形です。`net/lwip/dns.c` の `dns_loop()` が `struct dns_cb_arg dns_cb_arg = { };` をスタック上に確保し、そのアドレスを `dns_gethostbyname(name, &ipaddr, dns_cb, &dns_cb_arg)` に渡しています。中断時は `ctrlc()` を見てループを抜け、その後 `sys_untimeout(do_dns_tmr, NULL)` でDNS再送タイマーは解除しています。解除されていないのは、`dns_gethostbyname()` に登録したコールバックの側です。

    なぜ解除されないのかを、lwIP 側で確認しました。`dns_gethostbyname()` に渡したコールバックと引数は、lwIP 内部のテーブルに保持されます。

    内部の要求テーブル `dns_req_entry` が、コールバック(`found`)とその引数(`arg`)をそのまま保持します。

    そして `dns.h` の公開APIには、進行中の1件の解決を呼び出し側から取り消す関数がありません。`ERR_INPROGRESS` で保留状態になった要求では、通常、`dns_call_found()` が完了を通知した後に `found` がクリアされます。確認した主な通知経路では、解決の成功時だけでなく、応答のエラー、`DNS_MAX_RETRIES` 到達によるタイムアウト、DNSサーバーが無効になった場合にも、`NULL` を渡して呼ばれます。いずれにせよ「通知されたら消える」のであって、「呼び出し側が抜けたから消える」経路は用意されていません。

    報告者も [PATCH v2 3/3] で同じことを書いています。

    lwIP has no way to cancel a pending lookup

    非同期のDNS解決を途中で放棄しうるコードでは、ここが効きます。`dns_gethostbyname()` のドキュメントは、`ERR_INPROGRESS` が返った場合に限りコールバックが呼ばれると明記しています。その場合、渡した引数はコールバックが呼ばれる可能性がなくなるまで有効でなければなりません。静的領域や十分に長寿命なコンテキストを渡している場合は問題になりません。短命なスタック変数を渡すと危険になる、という話です。

    なお、タイムアウト経路は `dns_tmr()` が呼ばれることが前提です。U-Boot 側は登録したタイムアウトハンドラに対して `sys_untimeout()` を呼び、netif も削除して戻ります。そのため、関数から戻った後に実際にコールバックが発火する条件までは追っていません。

    なお、問題2と問題3について報告者が示している引き金は、コンソールでの中断操作(Ctrl-C)です。到達条件と脅威モデルが問題1とは異なります。


    ■ 報告された問題と修正方法は、分けて評価されています

    問題1のパッチには `Reviewed-by: Jerome Forissier` が付きました(2026年8月14日)。問題1の契約違反と修正方向については、報告者・U-Boot 側のレビュアー・筆者の見解が一致しています。ただし `Reviewed-by:` はパッチのレビュー承認であり、実機再現の証明ではありません。

    一方、問題2と問題3への返信は、提案された修正の設計を対象としています。返信だけから、問題の存在自体が独立に検証されたと判断することはできません。指摘の要点は「後から来るコールバックに後始末を任せる方式は、そのコールバックが走る保証がない」というもので、問題3への返信ではこう述べられています。

    ...this relies on a later DNS callback to reclaim the heap context, but that callback is not guaranteed to run.

    議論は2026年8月20日時点でも続いていました。

    また Hilliard 氏の10本シリーズの説明には、同梱している lwIP 側の変更について次の記述があります。

    The corresponding vendored lwIP changes are kept as separate patches for upstream submission.

    上流へ提出するために別パッチとして分けてあるという説明です。lwIP 本体側にも変更が提案される可能性がありますが、実際に提出・採用されたかどうかは確認できていません。

    本記事が修正の設計そのものに踏み込まないのは、この理由です。問題1については見解が一致していますが、問題2と問題3の直し方は、まだ議論の途中です。


    ■ 確認できていないこと

    • 実機での発現結果。問題1についてソース上の到達経路は追いましたが、実行してクラッシュ等を観測してはいません

    • HTTPS(TLSバックエンド)を使う場合の経路

    • `c46bd5b` より後の U-Boot 本流での取り込み状況

    • 問題2・問題3で、実際にコールバックが発火する経路

    • 3件が最終的に何本の修正になるか。シリーズが再編されれば数え方も変わります

    • 上流の lwIP へ実際に取消APIが提案・採用されるかどうか

    • 実際に出荷されている機器のうち、どれだけがこの経路を有効にしているか。`wget` の利用可否、`buffer_size` の設定、LMB の有効・無効で条件が変わります

    修正の設計そのものは、まだレビュー中で確定した内容として紹介できないため、本記事では扱いません。最終的な修正内容・対象リリース・後方移植の有無は、U-Boot のコミット履歴・リリースノート・メーリングリストでご確認ください。本記事の内容は2026年9月5日時点のものです。


    ■ 自社の統合コードで確認できること

    1. abort した後のコールバック戻り値を確認する

    `tcp_abort()` / `altcp_abort()` をコールバックの中で呼んでいる箇所が、何を返しているかを見ます。

    lwIP が求めているのは `ERR_ABRT` です。逆に、abort していないのに `ERR_ABRT` を返している箇所も見てください。こちらは解放漏れの方向の問題になります。

    2. コールバック引数の確保場所を確認する

    コールバックに渡している `arg` が、どこに確保されたものかを見ます。

    スタック変数のアドレスを渡していないか。渡している場合、その関数から抜けるより先にコールバックが必ず解除されると言い切れるか。中断・タイムアウト・エラー終了の経路もすべて含めて確認します。名前解決の途中で抜ける経路は見落としやすい場所です。

    3. ベンダーSDKやブートローダの統合コードも対象に入っているか

    upstream lwIP の変更履歴を追っていても、統合層のコードは視野に入りません。以前の News #3(ESP-IDF 独自のDHCPサーバー実装の境界チェック欠落、CVE-2026-45160)と共通するのはこの一点だけです。ただし News #3 は CVE・影響版・修正版が確定しており、今回は採番が未確認で、シリーズ全体の取り込みも未確定です。確度は同じではありません。


    ■ 本記事の取り扱いについて(免責事項)

    本記事は、公開されているメーリングリストの投稿と、公開されているソースコードをもとに整理したものです。特定の製品・組織を批判する意図はありません。

    U-Boot および lwIP は、多くの開発者の貢献によって保守されているOSSです。今回取り上げた箇所は、いずれも公開の場で報告され、公開の場で修正が提案されているものです。

    本記事の情報を利用したことによって生じたいかなる損害についても、筆者は責任を負いません。実際の製品への影響判断・対策の実施は、必ずご自身の環境で検証のうえ行ってください。ご自身が権限を持たない機器・ネットワークに対する試験は行わないでください。


    ■ 最後に

    lwIP の raw API では、コールバックの戻り値そのものが制御信号です。`ERR_ABRT` は「エラーコードのひとつ」ではなく、「この接続はもう触るな」という宣言です。

    そして、コールバックに渡した引数は、そのコールバックが呼ばれる可能性がなくなるまで生きていなければなりません。DNS解決についてはこれが特に厳しく、`ERR_INPROGRESS` が返った要求を個別に取り消す公開APIがありません。抜けた後にコールバックが走り得るかを確認したうえで、引数の寿命を設計する必要があります。

    戻り値の約束と、引数の寿命。今回報告されている3件は、いずれも lwIP を組み込んだ側の寿命管理に関する指摘です。そして問題1が関わるのは、lwIP が `tcp.c` に `ATTENTION` として書いている注意そのものです。

    自社の統合コードで `tcp_abort()` を呼んでいる箇所と、コールバックに渡している引数の置き場所。この2点を一度確認していただければ、この記事の役目は果たせたと思います。

    lwIPは無料で手軽に製品に組み込める反面、利用する際にはバージョンや設定、既知の問題を把握しておくことが重要です。
    また、商用製品のような保証付きサポートが前提ではないOSSであるため、最終的にはメーカー自身で不具合や脆弱性に対応しなければなりません。
    そのため対応コストが製品出荷後に増大するリスクについても考慮しておく必要があります。

    OSSであるlwIPには多くの利点がありますが、長期保守やサポート体制が重要な製品では、商用スタックを選択するケースもあります。

    ただし、すでにlwIPで開発中の製品や出荷済みの製品を今から置き換えることは現実的ではありません。
    そういった方に向けて、本記事の情報が、

    • 不具合や脆弱性の早期発見

    • 原因特定の時間短縮

    • 対応コストの低減

    の参考になれば幸いです。

    今後も、lwIPの問題に対して実用的な情報を公開します。

    ▶ 日本語記事一覧はこちら:lwIPトラブル対策室|日本語記事一覧

    関連記事・新着のお知らせ

     
     
    猫とお酒とギター大好きな組み込みエンジニアです。 lwIPの不具合解析や、ネットワークトラブル対策を実務ベースで共有しています。世の中のネットワーク機器の不具合を少しでも減らしていければと思っています。

    あなたへのおすすめ