Skip to content

deadline超過時にRST_STREAM(CANCEL)でstreamのみ閉じてpersistent connectionを温存 - #29

Merged
dkkoma merged 21 commits into
mainfrom
codex/issue-deadline-rst-stream-keep-connection
Jul 13, 2026
Merged

deadline超過時にRST_STREAM(CANCEL)でstreamのみ閉じてpersistent connectionを温存#29
dkkoma merged 21 commits into
mainfrom
codex/issue-deadline-rst-stream-keep-connection

Conversation

@dkkoma

@dkkoma dkkoma commented Jul 10, 2026

Copy link
Copy Markdown
Owner

概要

deadline超過(read poll timeout)を接続レベル障害ではなくstream-scopedな失敗として扱い、RST_STREAM(CANCEL) を当該streamへ送出してpersistent connectionを温存する。C-core / grpc-go と同挙動。

Issue: docs/issues/open/2026-07-08-deadline-rst-stream-keep-connection.md

従来はunaryのsocket timeoutで mark_connection_dead により接続ごと破棄しており、FrankenPHP worker等のpersistent connection前提の用途では1回のDEADLINE_EXCEEDEDごとにTCP+TLSハンドシェイクをやり直していた。またRST_STREAMを送らないためサーバー側はクライアント消失まで処理を継続し得た。

変更内容

  • cancel_grpc_call_stream() を追加 (src/transport.c): stream単位の RST_STREAM(CANCEL) submit + flush を共通化。unaryのsocket timeout分岐 (src/unary_call.c) を接続破棄から本helperへ置き換え
  • streaming cancel経路の潜在バグ修正: 既存の cancel_active_server_streaming_call_state / server_streaming_call_terminate_with_cancel は期限切れのcall deadlineをRST書き込みのwrite deadlineに使っており、deadline経路ではRSTが書けず即接続破棄になっていた。RST flushは専用の50ms grace deadline(pending frame一式のflush上限、超過時は従来どおり接続破棄)で行う
  • setup_deadline_abs_us のscope整理: connection setup完了時に0へクリア。従来は接続作成時のcall deadlineがconnection-scopedなwrite fallbackに残留し、温存した接続上のdeadlineなし後続コールが即ETIMEDOUTになった(timeout時に接続を破棄していた従来挙動では露見しなかった)
  • timeout後の接続温存に伴う残骸クリア: RST送出成功時に connection->last_error_detail / last_io_errno / last_ssl_error をクリアし、後続コールのstatus detailsへの漏れを防止
  • locally_cancelled flag追加: 自送出RSTでmid-messageに閉じたstreamがtruncated-body判定で malformed_response_frame 偽陽性になるのを除外(status taxonomyは timed_out 優先で従来どおりDEADLINE_EXCEEDED)
  • トレース: wire.frame_out にRST_STREAMの error_code を追加(inbound側と対称)
  • PHPT 033追加: unary / streaming のdeadline超過後に (1) ワイヤ上のRST_STREAM(CANCEL=8)がtimeoutしたcallのstreamに帰属、(2) 後続コールが persistent_reused=true + OK、(3) deadlineなしin-flight streamingが並行コールのdeadline超過を生き延びる、を固定

Non-Goals (issue記載どおり)

  • deadline検出精度の変更(pollベース現行方式は維持)
  • write block中のタイムアウト救済(frame境界を保証できないため従来どおり接続破棄)

レビュー

HTTP/2/gRPCドメインモデルレビュー実施: docs/reviews/issues/2026-07-11-deadline-rst-keep-connection-domain-review.md

  • 初回: Blocker 0 / High 1 / Medium 1 / Low 3 / Design Decision 1 → 全件修正(caeac40)
  • 再レビュー: 全6件adequate、新規指摘なし(all clear)

検証

  • tools/test/check-phpt.sh: 18/18 PASS(スイート3回連続、新規033は単体3回もPASS)
  • tools/test/check-c-unit.sh: protocol_core / status_core / transport_core PASS
  • PHPUnit統合テスト: 31 tests / 116 assertions OK
  • tools/test/check-c-static-analysis.sh: pass
  • トレース実測: timeout時にRST_STREAM(error_code=8)送出、直後のコールが persistent_reused=true で成功

🤖 Generated with Claude Code

dkkoma and others added 3 commits July 11, 2026 00:14
- cancel_grpc_call_stream()を追加し、unaryのsocket timeout分岐をmark_connection_deadから置き換え
- streaming cancel経路が期限切れcall deadlineでRSTを書けていなかった潜在バグを修正 (RST書き込みは50ms grace deadline)
- persistent connection reuse時にsetup_deadline_abs_usを採用コールのdeadlineへ更新 (期限切れdeadline残留による後続コール即ETIMEDOUTを解消)
- wire.frame_outトレースにRST_STREAMのerror_codeを追加
- PHPT 033: timeout後のRST_STREAM(CANCEL)ワイヤ観測と同一persistent connection再利用を固定

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- REVIEW-20260711-001 (High): setup_deadline_abs_usをconnection setup完了時に0へクリアし、reuse時のdeadline上書きを撤回。connection-scopedなwrite fallbackが他コールの期限切れdeadlineを借用して並行streamを殺すscope違反を解消
- REVIEW-20260711-002 (Medium): RST送出成功で接続を温存する場合にconnection側のlast_error_detail / last_io_errno / last_ssl_errorをクリアし、後続コールのstatus detailsへの漏れを防止
- REVIEW-20260711-003 (Low): grace deadlineが「pending frame一式のflush上限」であることをコメントとissue Decision Logに明記
- REVIEW-20260711-004 (Low): locally_cancelled flagを追加し、自送出RSTでmid-messageに閉じたstreamのtruncated-body誤判定(malformed_response_frame偽陽性)を除外
- REVIEW-20260711-005 (Low): PHPT 033のtimeout/delayマージンを300ms/2000msへ拡大し、RST_STREAMのstream帰属assertionを追加
- REVIEW-20260711-006 (DD): SPEC §4.2のreuse安全性根拠をnghttp2 closed-stream処理主体に書き分け
- PHPT 033に並行ケースを追加: deadlineなしin-flight streamingが同一connection上の別コールのdeadline超過を生き延びることを固定

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…D 1 → 全件Fixed、再レビューall clear)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Jul 10, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 76.97842% with 32 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/transport.c 75.49% 25 Missing ⚠️
src/unary_call.c 66.67% 5 Missing ⚠️
src/server_streaming_call.c 88.24% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@dkkoma

dkkoma commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

敵対的レビュー結果です。元 issue の scope 内で、merge 前に High 1件 / Medium 1件の対応を推奨します。あわせて回帰テストの Low 1件があります。

High — RST flush失敗後は dead/fatal session を全ownerでterminalにする

対象: src/transport.c:337

このflushがpartial/failedになるとconnectionはdeadになりますが、同じconnectionを保持する別のserver-streaming callは、次のpullでsend_pending_h2_frames()connection_recv()を再実行できます。mark_connection_dead()はflagを立てるだけで、stream_owner_count > 0ならfd/sessionのallocationは残るため、partial HTTP/2 frame後のsessionを再駆動したり、deadlineなしcallが待ち続けたりし得ます。

dead/fatal-session後は全ownerからsocket/nghttp2 I/Oを禁止してterminalへ遷移させ、RST submit・0-byte/partial flush失敗をfault injectionで固定してください。drainingは一律拒否せず、GOAWAYでadmit済みのstreamは完了まで継続できる必要があります。

Medium — 64KiB backlog時のconnection reuse保証を満たすか、仕様を限定する

対象: docs/SPEC.md:92

cancel済みstreamのresponse DATAが64KiB到着済みのケースでは、RST送出後のpreflightが65,536 bytesを読んだ時点で上限に達し、EAGAIN boundaryを確認せずconnectionをdrainingにします。message_count=1000 / payload_bytes=65536 / timeout=300msのserver streamで最初のyield後500ms停止する再現では、RSTは1本出ましたがfollow-upはSTATUS_OKかつpersistent_reused=false、connection prefaceは2本でした。

未読bytes中のGOAWAYを処理してから新規HEADERSを許すbounded adoptionを実装するか、reuseをbest-effortと明記してcap fallbackをテストしてください。現記述は実装の保証範囲を超えています。

Low — survivorをtimeout RST後までactiveに残す

対象: tests/phpt/033-deadline-rst-stream-connection-reuse.phpt:66

3 messagesを100ms間隔で送る現在の条件では、残りのDATA/trailersが約200msで処理され、300ms deadlineのRSTより約100ms先にsurvivorがcloseしています。そのため、RST時点のmultiplex、RST後のread/WINDOW_UPDATE、deadline scopeの非漏洩を検証できていません。survivorのdelayをdeadlineより長くし、terminal HEADERSがRSTより後であること、survivor宛RSTがないこと、最終STATUS_OKをassertしてください。

補足: MAX_CONCURRENT_STREAMS到達中の未送信requestへidle RSTが出るという候補は、raw fixtureでnghttp2がpending requestを内部cancelしwire RSTを出さないことを確認できたため、指摘から除外しました。

dkkoma and others added 2 commits July 11, 2026 14:03
…t明記 / multiplexテスト強化

- REVIEW-20260711-007 (High): connection_io_allowed()を追加し、server streaming pullループ先頭でdead connectionをterminal化。send_pending_h2_frames_with_deadlineにもdead時の早期エラーreturnを追加し、partial frame後のsession再駆動とdeadlineなしcallのhangを防止。drainingはGOAWAY admit済みstreamの完走を許す
- REVIEW-20260711-008 (Medium): SPEC §4.2にreuseがbest-effortであること(preflight drain cap 64KiB超過時はdraining→新規接続fallback)を明記。PHPT 035でbacklog超過時のfallback安全性(follow-up OK / persistent_reused=false / preface 2本)を固定
- REVIEW-20260711-009 (Low): PHPT 033 part 4のsurvivor delayを700msへ拡大し、survivor宛RSTなし・terminal HEADERSが並行RST後・全message受信OKをワイヤ順序でassert
- fixture :50066追加 (1本目のstreamへmessage送出後保持、2本目のrequestでTCP切断): PHPT 034でdead-under-another-owner不変条件(dead後のwire I/Oゼロ)を固定

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

3件とも妥当と判断し、0480479 で対応しました。レビュー記録は docs/reviews/issues/2026-07-11-deadline-rst-keep-connection-domain-review.md の REVIEW-20260711-007〜009 に追記しています。

High — dead/fatal sessionの全owner terminal化 → 対応

指摘どおり、streaming pullループは connection_usable を確認せずI/Oを再駆動できました。connection_io_allowed()(dead / fd / session を確認、drainingは許可 — GOAWAY admit済みstreamは完走)を追加し、pullループ先頭でterminal化。あわせて send_pending_h2_frames_with_deadline にdead時の早期エラーreturnを入れ、どの経路からもdead sessionへ書けないようにしました。

fault injection について: RST submit失敗・partial flush失敗を現行harnessで決定的に再現する手段がない(socket buffer詰まり依存)ため、fixture :50066(1本目のstreamにmessage送出後保持、2本目のrequestでTCP切断)を追加し、PHPT 034 で同じ不変条件を直接固定しました: 別ownerのcallでdeadになった後、deadlineなしの生存streamの next() がhangせずterminalになり、dead以後の wire I/O イベント(socket/TLS read・write、frame_out)がトレース上に一切ないこと。flush失敗経路の縮退(接続破棄)自体は従来から send_pending_h2_frames_with_deadline 内で担保されています。fault injection hookの導入が必要であれば別issueとして切り出します。

Medium — 64KiB backlog時のreuse保証 → 仕様を限定(選択肢b)

実測どおり、backlogがdrain cap(64KiB / 64 iterations)を超えるとEAGAIN境界に達せずdraining → 新規接続になります。bounded adoption(未読GOAWAY処理後のadmit)は本PRのスコープ(deadline時の接続温存)を超えるため実装せず、SPEC §4.2 に reuseはbest-effort であることとcap fallback挙動を明記しました。PHPT 035(64KiB×200 messages + user cancel → follow-upがSTATUS_OK / persistent_reused=false / connection preface 2本)でfallbackの安全性を固定しています。bounded adoptionを将来入れる場合はこのassertionとSPECを同時に更新します。

Low — survivorをRST後まで生存 → 対応

survivor delayを700msへ拡大し(trailersは並行RSTの約1秒後)、トレースで (1) survivor宛 wire.frame_out RST_STREAMが存在しない、(2) survivorのterminal HEADERS(flags & END_STREAM)が並行RSTより後、(3) 全message受信 + STATUS_OK をassertしました。

検証

  • tools/test/check-phpt.sh: 20/20 PASS(新規034/035含む)、033/034/035は3回連続PASS
  • C unit / PHPUnit 31 tests / 静的解析: すべてpass

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

再レビューしました(HEAD 6795a5a)。前回の SPECのreuse保証過大 (Medium) はbest-effort化で解消、PHPT 033のsurvivorがRST前に完了していた問題 (Low) も700ms化+wire順序assertionで解消しています。normal TCP-dead後のowner再駆動guardも有効です。

ただし、以下 High 2 / Medium 2 が残るため、現時点では REQUEST_CHANGES 相当です。

[High] nghttp2 fatal return後はcleanupでもsessionへ触れないでください

新しいcancel helperで nghttp2_submit_rst_stream()NGHTTP2_ERR_NOMEM、または nghttp2_session_send()NGHTTP2_ERR_CALLBACK_FAILURE を直接返した後も、owner cleanupは unregister_grpc_call_stream() から nghttp2_session_set_stream_user_data() を呼びます(unregister_grpc_call_streamcancel_grpc_call_stream)。nghttp2のerror contractはdirect fatal return後に許す操作を nghttp2_session_del() のみに限定しています。PHPT 034はTCP EOFであり、この分岐を通りません。fatal後のunregisterはlocal bookkeepingだけにし、fault injectionで session_del 以外の後続APIがないことを固定してください。

[High] draining上のadmit済みstreamも解放前にRSTで閉じてください

connection_io_allowed() はdraining上の既存streamにsession I/Oを許しますが、cancel_grpc_call_stream()、streaming cancel/destructorはまだ connection_usable() をgateに使うため、GOAWAYにadmit済みのstreamではRSTをskipします(transport.c:329,358-379server_streaming_call.c:419-423)。明示cancelはno-opとなり、resourceを後で破棄すると、large request DATAがflow-controlでpendingな場合にnghttp2が data_provider.source.ptr を保持したまま grpc_call / requestをfreeします。別のadmit済みownerがdraining sessionをsend駆動すると h2_send_data_callback() が解放済みpointerを参照でき、UAFになります。stream-local close gateを connection_io_allowed() に揃え、RST成功またはconnection deadのどちらかを満たしてからcall stateを解放してください。

[Medium] shared connection deathはsurvivorにもUNAVAILABLEとして記録してください

新guardは completed=true にするだけなので(server_streaming_call.c:260-268)、既に :status 200 と1 messageを受けたsurvivorはstatus fallbackで UNKNOWN(2) になります。fixture :50066 で実測し、detailsは recv failed: Connection reset by peer でした。gRPC Coreのstatus taxonomyではclient側の「data送信後のconnection break」は UNAVAILABLE です。PHPT 034の !== OK は誤分類を見逃します。call-owned transport-failure state/detailをsnapshotし、codeをexact UNAVAILABLE、detailsをnon-emptyかつtransport failure由来としてassertしてください。response-startedなのでtransparent retry不可のままで構いません。

[Medium] PHPT 035でcap超backlogをcancel前に同期してください

最初の64KiB message直後にcancelしても、追加の64KiB超がclient socketへ到着済みとは限りません(035-preflight-drain-cap-fallback.phpt:33-41,72-77)。このHEADでtargeted runは persistent_reused=true によりFAILし、直後の単独runはPASSしました。独立した12回反復でも PASS 11 / FAIL 1 です。backlogがcap未満ならreuseは修正後SPECどおり正常です。first message後に別control connectionから送信開始を指示し、TCP ACK/client pending-byte観測等でclient側到着を証明するbarrierを設け、cap到達のtraceもassertしてからfresh connectionを期待してください。sleepやserver側write完了だけでは不十分です。

検証: PHPT 033/034 targeted PASS、PHPT 035 FAIL↔PASSを独立再現、git diff --check PASS。

dkkoma and others added 2 commits July 11, 2026 20:40
…connection break=UNAVAILABLE / drain capのini化

- REVIEW-20260711-010 (High): unregister_grpc_call_streamでdead connectionのnghttp2_session_set_stream_user_dataをskipし、fatal後のcleanupをlocal bookkeepingのみに (dead接続はsend/recvゲートで二度と駆動されず、nghttp2_session_delはstream callbackを呼ばないため安全)
- REVIEW-20260711-011 (High): cancel_grpc_call_stream / streaming cancel / destroyのgateをconnection_usableからconnection_io_allowedへ統一。draining接続のadmit済みstreamもRST_STREAMでcloseしてからcall stateを解放し、nghttp2のdata provider経由UAFを防止。fixture :50067 + PHPT 036でGOAWAY(MaxInt32)後のcancelがワイヤにRSTを出すことを固定
- REVIEW-20260711-012 (Medium): grpc_call.connection_brokenを追加し、shared connection deathをsurvivorへsnapshotしてUNAVAILABLEに分類 (wire status / deadline / cancel / resetが優先)。PHPT 034はexact UNAVAILABLE + details non-empty、C unitに優先順位テストを追加
- REVIEW-20260711-013 (Medium): PHPT 035を決定的化。kernel TCP receive window(~64KiB)がproduction cap(64KiB)超のbacklog滞留を物理的に許さないため、grpc_lite.preflight_drain_max_bytes ini (default 65536)を導入し、SO_SNDBUF縮小によるACK barrier (fixture :50068/:50069のarm/floodプロトコル) + cap=16KiB指定でcap fallbackを検証。cap到達をトレースでassertしてからfresh connectionを期待

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

第二パスの4件、すべて妥当と判断し b13362b で対応しました。レビュー記録は REVIEW-20260711-010〜013 として追記しています。

[High] nghttp2 fatal後のsession API → 対応

指摘どおりです。unregister_grpc_call_streamconnection->dead の場合に nghttp2_session_set_stream_user_data をskipし、dead後のcleanupをlocal bookkeepingのみにしました。fatal経路はすべて mark_connection_dead を経由するのでdeadゲートで網羅されます。stream user dataが残っても安全な根拠: dead接続は connection_io_allowed ゲート(send/recv両方)で二度と駆動されず、nghttp2_session_del() はstream callbackを呼びません。

fault injectionについて: nghttp2 API呼び出しの失敗を決定的に注入する仕組みが現行harnessになく(NOMEM/CALLBACK_FAILUREはsocket詰まり・メモリ枯渇依存)、今回は「dead ⇒ 以後 session_del 以外のsession API呼び出しに到達する経路がない」ことをコード上の不変条件として固定しました。fault injection hookが必要であれば別issueで導入を検討させてください。

[High] draining上のadmit済みstreamのclose → 対応

指摘どおりUAF経路でした。cancel_grpc_call_stream / streaming cancel / destructor のgateを connection_usable から connection_io_allowed へ統一し、draining接続でもRSTをsubmit+flushしてstream(とnghttp2が保持するdata provider)をcloseしてからcall stateを解放します。flush失敗時はdeadに落ち、以後sessionが駆動されないため解放済みpointerは参照されません。fixture :50067(message + GOAWAY(MaxInt32) + stream保持)を追加し、PHPT 036 でdraining開始後のcancelがワイヤに RST_STREAM(CANCEL) を出すこと(GOAWAYより後のtimestamp)を固定しました。

[Medium] survivorのstatus → UNAVAILABLE

grpc_call.connection_broken flagを追加し、dead-terminal guardでconnectionの last_error_detail / last_io_errno をcallへsnapshotして立てます。taxonomyは wire grpc-status / deadline / cancel / reset の後・UNKNOWN fallbackの前に UNAVAILABLE を挿入(C unit test_connection_broken_mapping で優先順位ごと固定)。PHPT 034はexact UNAVAILABLE + details non-empty をassertするよう強化しました。response_startedなのでtransparent retry不可は不変です。

[Medium] PHPT 035のbarrier → 対応 (調査結果込み)

barrierを実装する過程で根本原因を特定しました: default kernel設定(tcp_rmem initial 131072 / tcp_adv_win_scale=1)ではclientのTCP receive windowが約64KiBで、production capの64KiB超を未読のままclient kernelに滞留させることは物理的に不可能です(ご実測の11/12 flakeはこの境界とdrain/refill raceの現れ)。256KiB floodのACK待ちはwindow一杯でwrite blockして成立しませんでした。

そこで cap を grpc_lite.preflight_drain_max_bytes ini(default 65536 = 従来値、PHP_INI_SYSTEM)へ昇格し、テストは --INI-- で cap=16KiB、backlog=48KiB(window内)として決定的にcap超過を作ります。barrierはご提案どおりcontrol connection方式で、fixture :50068(data) / :50069(control)の arm/flood プロトコル + SO_SNDBUF=4KiBでのwrite完了 = client kernel到着の証明(TCPはACKまでsend bufferにデータを保持するため)です。cap到達(preflight read ≥ 16KiB)をトレースでassertしてから fresh connection を期待します。FAILベースで23回連続実行しflakeなしを確認しました。

検証

  • tools/test/check-phpt.sh: 21/21 PASS(新規036、002-iniにini default assert追加)
  • 対象4テスト(033-036): 23回連続FAILなし
  • C unit(connection_broken taxonomy追加) / PHPUnit 31 tests / 静的解析: すべてpass

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

再レビューしました(HEAD be1b97e / 対応commit b13362b)。前回のdraining時UAFのproduction code fixとPHPT 035の到着barrierは有効です。034/035/036は30回連続PASSし、flakeも再現しませんでした。

ただし、以下 High 1 / Medium 1 / Low 2 が残るため、現時点ではREQUEST_CHANGES相当です。

[High] fatal nghttp2 returnを全call siteでdeadへ遷移させてください

dead後のunregisterをlocal-onlyにした点は直っていますが、「fatal経路はすべて mark_connection_dead() を通る」はまだ成立しません。unary / server-streamingnghttp2_submit_request() fatal returnはconnectionを再利用可能なまま残し、response callback/parser内の nghttp2_submit_rst_stream()戻り値を捨てています。後者が NGHTTP2_ERR_NOMEM を返しても最外callbackからfailureを返さないため、同一mem-recv内の後続callback、またはsuccessful return後のsession getter / want_write / session_sendへ進み得ます。nghttp2のerror contractはfatal return後に許す操作を nghttp2_session_del() のみに限定しています。

nghttp2_is_fatal(rv) を共通判定にし、submit-request fatalはdead化、response-side RST submit fatalはdead化したうえで最外のregistered callbackからfailureを返し、外側 mem_recv を即時unwindしてください。fault injectionで以後 session_del 以外のsession APIがないことも固定してください。

[Medium] same-pullのconnection breakもUNAVAILABLEへ写像してください

connection_broken はpull開始時にすでにdeadだったguardでしか立ちません。同じpull内のsocket/TLS send failureまたはnon-timeout recv EOF/errorは completed=true だけなので、1 message受信後にpeerが同じTCP connectionをcloseするfocused reproで UNKNOWN(2), details connection closed になりました(1 attempt、transparent retryなし)。connection-I/O由来の各dead transitionでもerrorをcallへsnapshotし、exact UNAVAILABLE を固定してください。nghttp2 direct fatalのtaxonomyは上のHighと分けてください。

[Low] preflight_drain_max_bytes を実際のread上限にしてください

loopは合計値を比較しますが、各readは常に64KiBを要求します。INI=16384のPHPT 035 traceでも requested_len=65536, result_len=49179 でした。read長を min(buffer_len, max_bytes - total_read) にし、合計readがcap以下であることをassertしてください。

[Low] draining UAFの元shapeをsanitizer regressionで固定してください

PHPT 036はGOAWAY後のRST送出を固定できていますが、fixtureはrequest END_STREAM後にGOAWAYを送りsmall request / single owner / explicit cancelなのでpending DATA provider + destructor + second admitted owner driveを作りません。small initial window + large request A + admitted B + GOAWAY + A destructor + B/WINDOW_UPDATE driveをASan/UBSanで通し、lifetime commentから「drainingならsession_sendしない」という誤った条件も除いてください。

検証: full PHPT 21/21 PASS、C unit 3/3 PASS、targeted 034/035/036 3/3 PASS、combined 30反復(90実行)PASS。same-pull EOFとstrict-cap overshootは上記のとおりfocused reproで確認しました。

dkkoma and others added 2 commits July 12, 2026 10:29
…break=UNAVAILABLE / drain cap read上限化 / UAF shapeのsanitizer固定

- REVIEW-20260712-001 (High): grpc_protocol_submit_rst_stream_in_callback()を追加し、parser/callback内の全RST submit(14箇所)をfatal→mark_connection_dead+NGHTTP2_ERR_CALLBACK_FAILUREでmem_recv即時unwindする形に統一。unary/streamingのnghttp2_submit_request fatalもdead化。fault injection seam GRPC_LITE_TEST_FAULT (rst-submit-fatal / submit-request-fatal)を導入し、PHPT 038/039でfatal→dead→wire上にframeなし・fresh connectionを固定
- REVIEW-20260712-002 (Medium): grpc_call_note_connection_broken()を共通化し、streaming/unaryのsame-pull send失敗・non-timeout recv EOF/error経路すべてでUNAVAILABLEにsnapshot付きで写像 (mem_recvのnghttp2失敗は別taxonomyで据え置き)
- REVIEW-20260712-003 (Low): preflight drainのread長を残余capでクリップし、PHPT 035にsum<=cap assertを追加
- REVIEW-20260712-004 (Low): fixture :50070 (INITIAL_WINDOW_SIZE=1024 + request未消費のstream A応答 + GOAWAY(Max) + 遅延WINDOW_UPDATE) + PHPT 037でUAF original shape (pending request DATA + draining destructor + 別owner drive)を固定。h2_send_data_callbackのlifetimeコメントからdraining誤条件を除去。GOAWAYはmessage前送出でdraining観測を決定的化 (fixture :50067も同様)。sanitizer (ASan/UBSan)スイート24/24 PASS・報告ゼロ

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

第三パスの4件、すべて妥当と判断し d54a2ba で対応しました。レビュー記録は REVIEW-20260712-001〜004 として追記しています。

[High] fatal nghttp2 returnの全call site dead遷移 → 対応

ご指摘のとおり「fatal経路はすべて mark_connection_dead を通る」は成立していませんでした。対応:

  • callback/parser内のRST submit(14箇所): grpc_protocol_submit_rst_stream_in_callback() に統一。nghttp2_is_fatal(rv) でfatalを判定し、mark_connection_dead + NGHTTP2_ERR_CALLBACK_FAILURE を最外のregistered callback(on_frame_recv / on_data_chunk / on_header経由のhelperすべて)から返して外側 mem_recv を即時unwindします。non-fatal submit失敗は従来どおり無視(policy flagが結果を決めるため)
  • nghttp2_submit_request fatal(unary / streaming): mark_connection_dead を追加
  • fault injection: GRPC_LITE_TEST_FAULT env(GRPC_LITE_TRACE_FILE と同型のprocess単位hook)で rst-submit-fatal / submit-request-fatal を注入するseamを導入しました。PHPT 038(cancel経路 + callback policy経路: fatal後にRSTがwireに出ない・接続はdead・後続はfresh connection・statusは timed_out / RESOURCE_EXHAUSTED のまま)、PHPT 039(submit-request fatal: 再attemptごとにfresh connection、stream frameがwireに出ない)で「fatal後に session_del 以外のsession APIへ到達しない」制御フローを固定しています

[Medium] same-pullのconnection break → UNAVAILABLE

grpc_call_note_connection_broken()(timed_outはno-op、connectionのdetail/errnoをcallへsnapshot)を共通化し、streaming pullループの send失敗 / non-timeout recv EOF・error / post-recv send失敗、およびunaryの同経路すべてに適用しました。ご指摘どおりnghttp2 direct fatal(mem_recv失敗)は別taxonomyとして据え置いています。ご実測のrepro(1 message受信後にpeerがTCP close)は same-pull recv EOF経路 → UNAVAILABLE + snapshot済みdetails になります。

[Low] drain capのread上限化

read長を min(buffer_len, max_bytes - total_read) にクリップし(TLS/socket両経路、traceの requested_len も追随)、PHPT 035 に preflightBytes <= cap を追加して合計readが上下両側からcapに一致することをassertしました。

[Low] UAF original shapeのsanitizer固定

fixture :50070 を追加しました: INITIAL_WINDOW_SIZE=1024(+connection WINDOW_UPDATE)、stream Aへはrequestを消費せずmessage応答(Aのrequest DATAはdeferredのまま)、Bへは GOAWAY(MaxInt32) → message の順で応答(TCP順序でdraining観測を決定的化。:50067も同様に修正)、500ms後にAのwindowを開けてBを完走。PHPT 037 が「A(256KiB request)を1 message受信後にdraining上でdestruct → RST(A)がGOAWAY後にwireへ出る → 遅延WINDOW_UPDATE(A)を跨いでBがOK完走」を固定します。なおstreamingのwire openは最初のrecv batchで遅延実行されるため、Aは1 message読んでstream openしてからdestructする形にしています(request bodyはwindow飢餓でdeferredのまま=ご指摘のpending DATA provider shape)。h2_send_data_callback のlifetimeコメントからdraining誤条件を除去しました。

sanitizer検証: check-c-sanitizer.sh(ASan/UBSan)で全PHPT 24/24 PASS、sanitizer報告ゼロ。

検証

  • tools/test/check-phpt.sh: 24/24 PASS(新規037/038/039)
  • 対象7テスト(033〜039): 15回反復でFAILなし(036のGOAWAY順序race 1/10を検出し修正済み)
  • sanitizerスイート: 24/24 PASS・ASan/UBSan報告ゼロ
  • C unit / PHPUnit 31 tests / 静的解析: すべてpass

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

再レビューしました(HEAD 2af2d58 / 対応commit d54a2ba)。前回指摘したfatal returnのdead遷移、strict drain cap、complete-message受信後のsame-pull connection break、pending DATA provider UAFの回帰fixtureは修正を確認しました。ASan/UBSan buildでfull PHPT 24/24、追加PHPT 037–039の30回反復 90/90、C unit、static analysisもPASSしています。

ただし、以下 High 1 / Medium 2 が残るためREQUEST_CHANGES相当です。

[High] test fault seamをproduction buildから分離してください

grpc_lite_test_fault_enabled()はtest-onlyとされていますが、build guardがなく、通常buildのsubmit/cancel/parser pathにも同じseamが組み込まれます。

さらに、最初のRPCで取得したgetenv()のraw pointerをprocess-staticへ保存するため、PHP putenv()のrequest shutdown後にdanglingになります。ASan buildの長寿命workerで2 requestを跨ぐprobeを行い、2件目のRPCがline 899のstrstr()でheap-use-after-freeになることを再現しました。ZTSの並行first callにも同期なしstatic read/write raceがあります。同じファイルのtrace hookはこの問題を避けるためMINITで値をcopyしています

fault seamはdefault-offのtest build defineでcompile outし、test buildでもMINIT-owned copyとexact token matchを使ってください。request境界を跨ぐASan/ZTS regressionも追加してください。

[Medium] partial message中のconnection breakをserver streamingでもUNAVAILABLEにしてください

same-pullのconnection break marker自体は有効ですが、server streamingはterminal pathでparser途中stateがあると無条件にmalformed_response_frameを立てます。これがstatus priorityconnection_brokenより先に評価されます。

response HEADERS + 3-byte partial gRPC header後にTCPをcloseする既存fixtureでは、unaryはUNAVAILABLE(14)、server streamingはINTERNAL(13)になりました。unaryはclean stream closeの場合だけtruncated frameをmalformed化しています

TCP/TLS connection breakは両call kindともUNAVAILABLE、clean END_STREAM途中のframeだけINTERNALへ分類してください。response-started callをtransparent retryしない点は維持してください。

[Medium] unary submit fatal時にdead connectionをcacheから即時detachしてください

unaryのsubmit fatal branchはconnectionをdeadにして終了しますが、clear_connection_call_owner()はcacheをdetachせずwrapperもFAILUREをそのまま返します

同じkeyなら次回取得時にlazy evictionされますが、異なるauthority 129件でfatalを注入すると、128件のdead entryがcacheを占有し、129件目がpersistent connection cache limit exceededになることを再現しました。PHPT 039は同じkeyを使うため、この保持を検出しません。

fatal cleanup内でdead entryを即時detachし、最後のowner解放後に破棄してください。

dkkoma and others added 2 commits July 12, 2026 16:48
…on break=UNAVAILABLE / fatal時のcache即時evict

- REVIEW-20260712-005 (High): fault injection seamを--enable-grpc-test-fault (default off)でcompile outし、production buildから排除。test buildではGRPC_LITE_TEST_FAULTをMINITで固定バッファへcopy (getenv pointer非保持=putenv後のUAF解消、MINITはthread起動前でZTS安全)し、カンマ区切りexact token matchへ変更。test系スクリプト4本にflag追加。PHPT 038にputenv snapshot regressionとsuperstring decoy tokenによるexact match regressionを追加、038/039はseam未buildでSKIP
- REVIEW-20260712-006 (Medium): streamingのtruncated判定にconnection_broken除外を追加し、TCP/TLS connection break mid-messageを両call kindでUNAVAILABLEに統一 (clean END_STREAM途中のみINTERNAL)。PHPT 024の:50057をexact UNAVAILABLEに強化しstreamingケースを追加
- REVIEW-20260712-007 (Medium): unary/streamingのsubmit fatal branchでdetach_persistent_connection_by_ptrによる即時evictを追加し、distinct keyのdead entry累積によるcache limit枯渇を解消。PHPT 039に130 distinct key sweepを追加

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

第四パスの3件、すべて妥当と判断し 3e128d4 で対応しました。レビュー記録は REVIEW-20260712-005〜007 に追記しています。

[High] fault seamのtest build分離 → 対応

3点とも実在の問題でした(build guardなし / getenv raw pointerのstatic保持によるputenv後UAF / ZTS static race)。対応:

  • compile out: --enable-grpc-test-fault(default off)を config.m4 に追加し、seam全体を PHP_GRPC_LITE_ENABLE_TEST_FAULT でguard。未定義時は grpc_lite_test_fault_enabled() をconstant falseマクロにし、呼び出しbranchごと最適化で消えます。production build(--enable-grpc のみ)がwarningなしでビルドでき、038/039がSKIPすることを確認済み
  • MINIT-owned copy + exact token match: trace hookと同様、GRPC_LITE_TEST_FAULT はMINITで固定バッファへcopyし(getenv pointerを保持しない)、カンマ区切りのexact token matchで判定。MINITはrequest thread起動前に1回だけ走るため、以後は読み取り専用でZTS安全です
  • regression: PHPT 038に (1) putenv(削除+別fault値)後もMINIT snapshotどおりに動作すること、(2) superstring decoy token(submit-request-fatal-decoy)がexact matchで無視されること(substring matchなら全コールがthrowしてテストが壊れる構造)を追加。request跨ぎのFPM再現はharness外ですが、原因のpointer保持自体を排除しています。ZTSスイート(check-zts-phpt.sh、seam有効build)24/24、sanitizerスイート24/24・報告ゼロ
  • test系スクリプト4本(check-phpt / check-zts-phpt / check-c-sanitizer / check-c-coverage)にflagを追加しました。release/bench系のbuildにはflagを渡していません

[Medium] partial message中のconnection break → 両call kindでUNAVAILABLE

ご実測どおり、streamingのtruncated判定が connection_broken を除外しておらず、status priorityでINTERNALが先勝ちしていました。除外条件に !call->connection_broken を追加し、「TCP/TLS breakは UNAVAILABLE、clean END_STREAM途中のframeのみ INTERNAL」に統一。PHPT 024の :50057 をexact UNAVAILABLE に強化し、server streamingケースを追加しました。response-started callのtransparent retry不可は不変です。

[Medium] submit fatal時のcache即時detach → 対応

ご指摘どおりlazy per-key evictionではdistinct keyのdead entryが累積します。unary / streaming両方のsubmit fatal branchで detach_persistent_connection_by_ptr() により即時evictし、最後のowner解放後に破棄されるようにしました(unaryは destroy_detached_connection_if_unowned を追加、streamingはdestroy経路のowner clearが破棄を担います)。PHPT 039grpc.default_authority を変えた130 distinct keyのsweepを追加し、全件が "nghttp2_submit_request failed"(cache exhaustionでない)かつconnection preface 133本(dead entry再利用なし)であることを固定しました。

検証

  • test-fault build: PHPT 24/24 PASS、影響8テスト(024/033〜039)8回反復FAILなし
  • production build(--enable-grpc のみ): クリーンビルド、038/039 SKIP、001 PASS
  • ZTSスイート: 24/24 PASS / sanitizerスイート(ASan/UBSan): 24/24 PASS・報告ゼロ
  • C unit / PHPUnit 31 tests / 静的解析: すべてpass

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

再レビューしました(HEAD 287bc939 / 対応commit 3e128d4)。前回3件について、fault seamのproduction分離、partial-frame TCP breakのcall-kind整合、通常APIのfatal cache evictionは修正を確認しました。standard NTS PHPT 24/24、ZTS PHPT 24/24、C unit、PHPUnit、static analysisもPASSしています。

ただし、今回の修正に起因する High 1 / Medium 1 が残るためREQUEST_CHANGES相当です。

[High] unary coreとdiagnostic callerのconnection lifetime契約を揃えてください

submit fatal時、unary coreはconnectionをdead化・cacheからdetachし、owner clear後に最終destroyしてFAILUREを返します。一方、--enable-grpc-benchのdiagnostic callerはそのFAILURE後も同じraw pointerをconnection_usable()へ渡します

--enable-grpc --enable-grpc-bench --enable-grpc-test-faultのASan/UBSan buildでsubmit-request-fatalを注入しdiagnostic grpc_lite_unary()を1回呼ぶと、free=unary_call.c:171 / transport.c:221、read=diagnostic/bench.c:1879 / transport.c:873としてdeterministicにheap-use-after-freeを再現しました。real nghttp2_submit_request() fatalでも同じbranchへ到達します。

calleeがconnectionを消費して返る契約なら、diagnostic failure branchからconnection_usable() / remove_unusable_persistent_connection()を除き、pointerへ再度触れないでください。あるいはdetach/destroyをkeyを持つcallerへ統一してください。bench + test-fault併用ASan regressionも必要です。

[Medium] RST submit fatalでもdeadlineのstatus detailsを維持してください

server streamingで1 messageを受信後、次のpull前にdeadlineが切れるとtimed_out=trueを確定してRST_STREAM(CANCEL)を送ります。RST submitがfatalになるfocused reproでは、status codeはDEADLINE_EXCEEDED(4)のままですが、detailsがnghttp2 error: Out of memoryになりました。

code resolverはtimed_outを最優先する一方、details resolverはconnection errorをdeadline固有detailsより先に返します。PHPT 038も「timed_outがconnection failureより優先」と説明しながらcodeしかassertしないため、この不一致を見逃します。

deadlineはprimary call outcome、RST送出失敗はsecondary connection-cleanup failureとして扱い、PHP-visible code/detailsをともにdeadlineへ揃えてください。PHPT 038へbetween-pull server-streaming deadlineを追加し、exact code/detailsとfresh connectionを固定してください。

dkkoma and others added 2 commits July 12, 2026 20:23
… details維持

- [High] unary coreのFAILURE契約を「connectionを消費して返る」に統一:
  submit fatalに加えregister失敗・mem_recv fatalでもdetach+destroyし、
  bench diagnostic callerはFAILURE後にconnection pointerへ触れない
  (旧コードはfreed pointerへのconnection_usable()でASan UAF)。
  PHPT 040で固定(旧コードではASan buildで実際にFAILすることを確認済み)
- [Medium] timed_outのcallはstatus codeだけでなくdetailsも
  "HTTP/2 transport deadline exceeded" を維持(fatal RST submit等の
  teardown失敗はsecondaryでありdetailsを上書きしない)。
  PHPT 038にbetween-pull streaming deadline + exact details assertを追加
- check-phpt / check-c-sanitizer に --enable-grpc-bench を追加し、
  001はbench surfaceの露出をビルド構成と両方向で照合するassertへ変更
  (MINFO行 "grpc_lite bench diagnostics" で判定)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

第五パスの2件、すべて妥当と判断し e424689 で対応しました。レビュー記録は REVIEW-20260712-008〜009 に追記しています。

[High] unary coreとdiagnostic callerのconnection lifetime契約 → 統一

ご指摘のとおり、第四パスで私が入れた「callee側でdetach+destroy」がdiagnostic caller(--enable-grpc-benchgrpc_lite_unary)の connection_usable() 参照と矛盾しており、FAILURE後のraw pointer参照がUAFになる構造でした。対応:

  • 契約の統一: 「FAILUREはconnectionを消費して返る(unusable化したbranchはdetach+destroy済み、callerはpointerへ再度触れない)/ SUCCESSはpointer有効でcallerがevict」に統一し、core定義へ明文化。unary coreはsubmit fatalに加え、register失敗(dead化する)と nghttp2_session_mem_recv fatalのFAILURE branchでも detach_persistent_connection_by_ptr + destroy_detached_connection_if_unowned を行います
  • caller側: diagnostic failure branchから connection_usable() / remove_unusable_persistent_connection() を除去。streaming diagnostic(grpc_lite_server_streaming_open)はraw pointerを保持せず、streamingのstate destroy経路は従来からunusable時にdetachするため同型問題がないことを確認済み
  • regression: check-phpt.sh / check-c-sanitizer.sh--enable-grpc-bench を追加し、PHPT 040(grpc_lite_unary × submit-request-fatal、2 attempt)を新設。旧callerコードを一時復元したASan buildで040が実際にheap-use-after-freeでFAILすることを確認したうえで、修正版でPASSすることを確認しました(regressionの検出力を実証)。PHPT 001はbench surfaceの露出をMINFO行 "grpc_lite bench diagnostics" とのiff関係でassertする形に変更し、production buildの非露出保証は維持しています

[Medium] RST submit fatal時のdeadline details → deadlineへ整合

ご実測どおり、code resolverは timed_out 最優先なのにdetails resolverはconnection error detailを先に返す非対称がありました。grpc_lite_status_details_from_call に「DEADLINE_EXCEEDED && timed_out ならdeadline固有details」をI/O error detail評価より前(server供給 grpc-message の直後)へ追加し、deadline = primary outcome / RST送出失敗 = secondary cleanup failure としてcode/detailsを整合させました。PHPT 038に (1) 既存unary deadlineケースのexact details assert、(2) between-pull server-streaming deadlineケース(1 message受信 → deadline → fatal RST submit)を追加し、exact code/details・受信message数1・後続callのfresh connection(preface 3本)を固定しています。server供給 grpc-message の優先は従来どおりです。

検証

  • test-fault+benchビルド: PHPT 25/25 PASS(新規040、038強化)、影響5テスト(024/033/038/039/040)8回反復FAILなし
  • sanitizerスイート(ASan/UBSan、bench併用): 25/25 PASS・報告ゼロ(旧コードでは040がUAFでFAIL)
  • production build(--enable-grpc のみ): warningなしビルド、001 PASS(bench関数非露出)、fault系SKIP
  • ZTSスイート: 24 PASS / 1 SKIP(040はbench無効ビルドのため)
  • C unit 3/3 / PHPUnit 31 tests / cppcheck(production+bench両構成): すべてpass

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

再レビューしました(HEAD 3081608 / 対応commit e424689)。前回の High / Medium は修正を確認しました。bench+fault ASan/UBSan PHPT 25/25、NTS PHPT 25/25、ZTS PHPT 24 PASS / 1 SKIP、C unit、PHPUnit、static analysisもPASSしています。

ただし、今回の対応で検証境界に Medium 1 / Low 2 が残るため、REQUEST_CHANGES相当です。

[Medium] production sanitizer laneをbench laneで置き換えないでください

check-c-sanitizer.sh は常に --enable-grpc-bench を有効にするよう変更されています。しかしbench buildはgrpc_call のlayoutunary coreのsignature/分岐を変え、production buildとは異なるbinaryです。非benchのcoverage/ZTS laneはsanitizerではなく、CIのCrash/UB laneもextension全体ではなくprotocol-core fuzzerです。

今回のUAF regressionをbench sanitizerで固定するのは妥当ですが、既存のproduction相当ASan/UBSan laneを置換せず、production full sanitizerbench+fault sanitizer の2 laneを実行してください。現状ではproduction-onlyのlayout/分岐に入るmemory bugがsanitizer gateから外れます。

[Low] PHPT 001のbench期待値を同じmoduleのMINFOから導かないでください

PHPT 001はMINFOの grpc_lite bench diagnostics 行を読み、その値からbench関数の露出期待値を決めています。これはMINFOと関数表の整合性は検査しますが、「production buildではbench surfaceを露出しない」という外部不変条件にはなりません。build flagの誤りで両方が有効になった場合もPASSします。

runnerから GRPC_LITE_EXPECT_BENCH=0|1 のような期待variantを渡すか、production/bench用の検査を分け、production lane自身が非露出をassertしてください。

[Low] unary mem-recv fatalのownership修正もsanitizer regressionへ固定してください

e424689 はsubmit fatalだけでなく、connection registration failurenghttp2_session_mem_recv() fatalのownership cleanupも変更しています。一方、追加されたPHPT 040はsubmit-request-fatalだけを通します

既存の rst-submit-fatal と小さいreceive limit+大きいresponseを組み合わせるとdiagnostic unaryのmem-recv fatalへ到達できます。focused ASanを2 attemptで実行し、現在のコードが安全なこと自体は確認しましたが、repository regressionとして固定されていません。2 attempt・ASan無報告・壊れたconnectionをcacheへ残さないことを検査するPHPTを追加してください。

dkkoma and others added 2 commits July 13, 2026 07:09
…sion

- [Medium] check-c-sanitizer.sh を production lane(--enable-grpc のみ、
  純production binaryで全PHPT、fault/bench系はSKIP)と bench+fault lane の
  2 laneに分離。bench buildによるlayout/分岐差でproduction-onlyの
  memory bugがsanitizer gateから外れないようにする
- [Low] PHPT 001 のbench surface期待値を同一moduleのMINFOから導出せず、
  runnerが宣言する GRPC_LITE_EXPECT_BENCH=0|1 を外部入力として使用
  (未設定=production期待)。MINFO行も同じ外部期待と照合
- [Low] PHPT 041 を追加: rst-submit-fatal × 小receive limitで
  diagnostic unaryの mem_recv fatal FAILURE branch を通し、
  2 attemptのlifetime + 130 distinct-key sweepでcache非残留を固定
  (detachを一時除去すると実際にFAILすることを確認済み)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

第六パスの3件、すべて妥当と判断し 24abba3 で対応しました。レビュー記録は REVIEW-20260713-001〜003 に追記しています。

[Medium] production sanitizer laneのbench置換 → 2 lane化

ご指摘のとおり、第五パスの変更でsanitizer gateの検証対象がbench binary(grpc_call layout・unary core分岐が異なる)に置き換わり、production-onlyの経路がmemory-safety gateから外れていました。check-c-sanitizer.sh のPHPT実行を run_phpt_lane() に共通化し、2 laneを順に実行する構成へ変更:

  • production lane: --enable-grpc のみ(fault seamも持たない純production binary)で全PHPTを実行。fault/bench系4テストはSKIP。従来laneは --enable-grpc-test-fault 付きだったため、純productionのsanitizer実行はむしろ従来より厳密になっています
  • bench+fault lane: --enable-grpc-test-fault --enable-grpc-bench で全26テスト(UAF regression 040/041含む)を実行

実測: production lane 22 PASS / 4 SKIP、bench-fault lane 26/26 PASS、ASan/UBSan報告ゼロ(halt_on_error下でexit 0)。

[Low] PHPT 001の期待値のMINFO由来 → runner宣言の外部入力へ

ご指摘のとおり、同一moduleのMINFOから期待値を導く形は整合性検査にしかならず、build flag誤りで両方が有効になるとPASSする循環がありました。期待値を GRPC_LITE_EXPECT_BENCH=0|1 の外部入力に変更し、未設定はproduction期待(非露出)にデフォルトさせました。benchをbuildするrunner(check-phpt.sh、sanitizer bench-fault lane)だけが =1 を宣言し、production lane / ZTS / coverage / 素の実行は非露出をassertします(bench buildを宣言なしで走らせると001がFAILする=露出は明示宣言が必要な設計)。MINFO行も同じ外部期待と照合します。

[Low] mem-recv fatal ownershipのregression → PHPT 041で固定

ご指摘のとおり040はsubmit-request-fatalのみで、e424689で変更したmem_recv fatal branchが未固定でした。PHPT 041 を追加: rst-submit-fatal × max_receive_message_length=8 × 1KiB responseで、callback内policy RSTのfatal → nghttp2_session_mem_recv fatal → unary coreのdetach+destroy → diagnostic callerがpointerへ触れない経路を2 attemptで通し、さらに130 distinct-key sweepで全attemptが "nghttp2_session_mem_recv failed"(cache exhaustionでない)かつconnection preface 132本(消費済みconnectionの再利用なし)であることをassertします。mem_recv branchのdetachを一時除去した状態で041が実際にFAILする(sweepがcache limitへ到達する)ことを確認し、regressionの検出力を実証済みです。041はsanitizer bench-fault laneでも実行されます(ASan無報告)。なおregister失敗branchは nghttp2_session_set_stream_user_data の失敗を外部から誘発できないため、ご指摘のスコープどおりmem-recvのみを固定対象としています。

検証

  • sanitizer 2 lane: production 22 PASS / 4 SKIP + bench-fault 26/26 PASS、報告ゼロ
  • NTS(bench+fault)PHPT: 26/26 PASS(新規041)、影響4テスト(001/038/040/041)8回反復FAILなし
  • ZTS: 24 PASS / 2 SKIP(040/041、bench無効ビルド)
  • PHPUnit 31 tests OK。今回Cソース変更なし(テスト+スクリプトのみ)のため、C unit / cppcheckは前回結果が有効です

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

第七パスの再レビューを完了しました(HEAD 199bf01 / 対応commit 24abba3)。

前回のMedium(production sanitizer laneのbench置換)は2 lane化で修正済みです。bench surfaceの期待値外部化もadequateで、PHPT 041はmem-recv fatalへの到達、diagnostic callerのpost-failure UAF、cache detach / non-reuseを固定できています。現HEADのproduction codeに新たなlifetime defectは確認していません。

ただしrequired gateとしては Low 3件 が残るため、REQUEST_CHANGES相当です。

[Low] production laneでtest-fault seam非露出も外部期待へ照合してください

check-c-sanitizer.sh はproduction laneを「no fault seam / no bench surface」と定義していますが、runnerから渡す期待値はbenchだけです。PHPT 001もbench MINFO/functionsだけを外部期待へ照合し、test-fault availabilityは同じmoduleのMINFOから導出しています。

そのためproduction laneへ誤って --enable-grpc-test-fault が入っても、038/039がSKIP解除されてPASSするだけでsuite全体は成功し得ます。現HEADのproduction buildに漏出はありませんが、pure-production variantのregression oracleが片側だけです。

GRPC_LITE_EXPECT_TEST_FAULT=0|1(または単一build-variant値)をrunnerから渡し、grpc_lite test fault seam MINFO rowも外部期待へ照合してください。productionは0、bench+fault / test buildは1が期待です。

[Low] PHPT 041でdetached connectionのdestroyまで観測してください

PHPT 041はcontractを detach + destroy と説明しますが、実際のoracleはexception、cache非残留、fresh preface 132本、RST非送出です。mem-recv fatal branchから最後の destroy_detached_connection_if_unowned() だけを除去しても、entryはdetach済みなので130-key sweepはcache exhaustionせず、各attemptはfresh connectionになってpreface数も変わりません。ASanは detect_leaks=0のため、132個のconnection / fd / nghttp2 session leakも検出しません。

現実装は正しくdestroyしていますが、この1行の退行は現在のgateを通ります。test-only destroy counter / trace eventで全132 connectionのdestroyをassertするか、PHPT内でboundedな /proc/self/fd 差分を検査し、destroy-only mutationでFAILすることを確認してください。

[Low] open issueを現在のdeadline/RST lifecycleへ揃えてください

元issueはdeadline時にconnectionを破棄しRSTを送らない状態を「現状」と記載し、RST後に短時間drainする案をactive Planとして残しています。一方、同文書のProgressでは実装済み、Decision Logではdrain不採用です。またVerificationは第六パスが第五パスより先になっています。

Backgroundを「変更前」と明示し、Planを現行方針へ更新するかsupersededと表示し、第五→第六の順へ直してください。元issueの中心概念であるstream-local deadline cancellationを入口文書が逆に説明しない状態にする必要があります。

検証

  • ASan/UBSan sanitizer: production 22 PASS / 4 SKIP、bench+fault 26/26 PASS、reportなし
  • NTS PHPT: 26/26 PASS
  • ZTS PHPT: 24 PASS / 2 SKIP、0 FAIL
  • C unit: 3/3 PASS
  • PHPUnit: 31 tests / 116 assertions PASS
  • C static analysis / runner bash -n / git diff --check: PASS

- [Low] GRPC_LITE_EXPECT_TEST_FAULT=0|1 をrunnerが宣言し、PHPT 001が
  MINFOの "grpc_lite test fault seam" 行を外部期待と照合(未設定=非露出)。
  production laneへのseam漏出はSKIP解除でなく001のFAILとして検出される
- [Low] destroy_h2_connection に wire.connection_close traceイベントを追加し、
  PHPT 041で全132 connectionのdestroyをassert。detach-only退行
  (leak検出無効下では不可視)でもFAILすることをdestroy一時除去で実証済み
- [Low] issue文書のBackgroundを変更前/現在で書き分け、Planのdrain項を
  superseded化(Decision Log参照)、Verificationを時系列順(第五→第六)へ修正

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

第七パスの3件、すべて妥当と判断し 3ed170f で対応しました。レビュー記録は REVIEW-20260713-004〜006 に追記しています。

[Low] test-fault seam非露出の外部期待照合 → 対応

ご指摘のとおり、外部期待化したのはbenchだけで、seamのavailabilityはSKIPIF経由で同一moduleのMINFO由来のままでした(production laneへの漏出は038/039のSKIP解除で静かに通る)。PHPT 001に GRPC_LITE_EXPECT_TEST_FAULT(未設定=非露出期待)とMINFO "grpc_lite test fault seam" 行の照合を追加し、seamをbuildする全runner(check-phpt.sh / check-zts-phpt.sh / check-c-coverage.sh / sanitizer bench-fault lane)が =1 を、sanitizer production laneが =0 を宣言します。production laneへのseam漏出は001のFAILとして検出されます。helpers.incのSKIPIF用MINFO導出は残置しました(SKIP判定は保護対象の不変条件ではなく、oracleは001が担う整理です)。

[Low] PHPT 041のdestroy観測 → wire.connection_close traceイベントで固定

ご指摘のとおり、destroy_detached_connection_if_unowned() の1行だけを除去してもdetach済みのためsweepはexhaustionせず、detect_leaks=0 のASanでもleakは不可視で、現gateを通る構造でした。destroy_h2_connection()wire.connection_close traceイベント(wire.connection_preface の対、fd/dead付き)を追加し、041で全132 connectionのdestroy(connection_close == 132)をassertします。destroy呼び出しを一時除去した状態で041が実際にFAILすることを確認し、検出力を実証済みです。実装はtest-only counterでなくproduction trace機構への追加としました(接続lifecycleの観測はデバッグでも有用、既存テストはevent名でfilterするため非破壊)。

[Low] open issueの入口文書整合 → 対応

Backgroundを「変更前」(過去形)と「現在の実装」(stream-scoped RST + 接続温存)で書き分け、Planのdrain項をsuperseded化(不採用理由と50ms grace deadlineへの置換をDecision Log参照付きで注記、該当文は取り消し線)、Verificationを第五→第六の時系列順へ修正しました。

検証

  • NTS PHPT: 26/26 PASS、影響4テスト(001/029/040/041)8回反復FAILなし
  • sanitizer 2 lane: production(EXPECT両方=0)22 PASS / 4 SKIP + bench-fault(=1)26/26 PASS、報告ゼロ
  • ZTS: 24 PASS / 2 SKIP(EXPECT_TEST_FAULT=1宣言)
  • C unit 3/3 / cppcheck exit 0(transport.c変更のため再実行)/ PHPUnit 31 tests OK

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

第八パスの再レビューを完了しました(HEAD 1faf80a / 対応commit 3ed170f)。

前回のLow 3件について、test-fault seamの外部期待は全runnerでbuild flagと一致しており、PHPT 041も132 connectionのdestroy invocationまで固定できています。ASan/UBSanを含む再検証でも、新たなC lifetime / runtime defectは確認していません。

ただしrequired gateとしては Low 2件 が残るため、REQUEST_CHANGES相当です。

[Low] wire.connection_closeをlocal destructorのsemanticsへ揃えてください

新eventは destroy_h2_connection() の入口で、SSL_free()close(fd)nghttp2_session_del()より前にemitされます。したがって観測しているのはlocal h2_connection objectのdestroy開始であり、TCP FIN/RST、TLS close、peer close、HTTP/2 GOAWAYではありません。また同destructorはpreface送出前のsetup failureからも呼ばれるため、コメントの「wire.connection_prefaceのcounterpart」は一般には成立しません。

PHPT 041のdestroy-only mutation検出力はadequateです。eventを transport.connection_destroy 等のlocal lifecycle名へrenameして041を追従させ、発火phaseとsetup未完了connectionにも出ることをcode-reading guideへ明記してください。prefaceとのpairをcontractにするならstable connection idで対応付ける必要があります。

[Low] 元issueに残ったstreaming deadlineの旧説明を変更前として直してください

元issueは19行目で現在の実装をstream-scoped RST + connection温存と正しく説明した直後、28行目では「streamingのdeadline経路だけが接続破棄」と現在形で記載しています。これはProgressおよびSPEC §4.2のunary / server streaming共通のstream-local cancellationと正面から矛盾します。

当該paragraphを「変更前」と明示して過去形へ直し、現在のreuseはSPECどおりbest-effort(RST flush失敗やpreflight drain cap超過ではfresh connectionへfallback)であることへ揃えてください。

検証

  • ASan/UBSan: production 22 PASS / 4 SKIP、bench+fault 26/26 PASS、報告なし
  • NTS PHPT: 26/26 PASS(2回)
  • ZTS PHPT: 24 PASS / 2 SKIP
  • C unit: 3/3 PASS
  • PHPUnit: 31 tests / 116 assertions PASS
  • C static analysis、bash -ngit diff --check: PASS
  • pure-production + fault token focused probe: PHPT 001 / 010 が2/2 PASS

一般的なCI matrix拡張とbench-only variantは元issue外として投稿対象から除外しました。

dkkoma and others added 2 commits July 13, 2026 22:14
- [Low] wire.connection_close を transport.connection_destroy へrename。
  発火点はlocal destructorの入口(TLS/fd/session解放前)で、preface前の
  setup failureでも発火するためwire semanticsではない。コメントと
  code-reading guideに発火phaseと非1対1対応を明記、PHPT 041を追従
- [Low] issue文書の「streamingのdeadline経路だけが接続破棄」を変更前として
  過去形へ直し、現在のbest-effort reuse(SPEC §4.2)へ揃えた

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

第八パスの2件、すべて妥当と判断し 1c55f56 で対応しました。レビュー記録は REVIEW-20260713-007〜008 に追記しています。

[Low] connection destroy eventのlocal lifecycle命名 → transport.connection_destroy へrename

ご指摘のとおり、発火点は destroy_h2_connection() の入口(TLS/fd/nghttp2 sessionの解放より前)で、観測しているのはlocal objectのdestroy開始です。preface送出前のsetup failureでも発火するため「wire.connection_preface のcounterpart」も一般には成立しませんでした。eventを transport.connection_destroy へrenameし、コメントを「local lifecycle event(wire.ではなくtransport.)、destructor入口で発火、setup未完了connectionでも発火」へ修正。code-reading guideのtrace節に発火phaseと wire.connection_preface との非1対1対応を明記し、PHPT 041を追従させました。prefaceとのpairはcontractにしない整理のため、stable connection idは導入していません(041のoracleはdestroy invocation数で、第七パスで実証したdestroy-only mutationの検出力はrenameで不変です)。

[Low] issue文書のstreaming deadline旧説明 → 変更前として過去形化

ご指摘のとおり、「公式実装との差異」末尾の「deadline経路だけが接続破棄になっている」が現在形のまま残り、直前のBackground・Progress・SPEC §4.2と矛盾していました。当該paragraphを「変更前」明示+過去形へ直し、現在はunary / server streamingともstream-scoped RST_STREAM(CANCEL)で、reuseはSPEC §4.2どおりbest-effort(RST flushのgrace deadline超過やpreflight drain cap超過ではfresh connectionへフォールバック)であることを追記しました。

検証

  • NTS PHPT: 26/26 PASS、029/041 8回反復FAILなし
  • sanitizer 2 lane(ASan/UBSan): production 22 PASS / 4 SKIP + bench-fault 26/26 PASS、報告ゼロ
  • ZTS: 24 PASS / 2 SKIP
  • C unit 3/3 / cppcheck exit 0 / PHPUnit 31 tests OK

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

第九パスの再レビューを完了しました(HEAD 76d2827 / 対応commit 1c55f56)。

前回のLow 2件のうち、transport.connection_destroy へのrenameと発火phaseの明文化はadequateです。streaming deadlineの旧説明も「変更前」+過去形へ直っています。runtime / C lifetime / dead・draining・active owner・production / diagnostic boundaryに新たな欠陥は確認していません。

ただしrequired gateとしては Low 1件 が残るため、REQUEST_CHANGES相当です。

[Low] best-effort reuseのfallback説明にRSTの即時失敗も含めてください

元issueのcurrent-state文は、fresh connectionへのfallbackを「RST flushがgrace deadlineを超過した場合」と説明しています。しかし実装はtimeoutだけではなく、nghttp2_submit_rst_stream() の即時失敗でもconnectionをdeadにしnghttp2_session_send() / socket / TLS / coalesced-buffer flushの任意の失敗でもreuseを禁止します。SPEC §4.2も「RST書き込み失敗時」としており、「SPEC §4.2のとおり」とする文がfallback集合を狭く記述しています。

例えば「reuseはSPEC §4.2どおりbest-effort(RST submit / flush失敗〔grace deadline超過を含む〕またはpreflight drain cap超過時はfresh connectionへフォールバック)」へ揃えてください。即時失敗後にconnectionを捨てるのは、fatal nghttp2 sessionやpartial wire stateを再駆動しない本issueの安全側lifecycle contractです。

検証

  • ASan/UBSan: production 22 PASS / 4 SKIP、bench+fault 26/26 PASS、報告なし
  • NTS PHPT: 26/26 PASS(2回)
  • ZTS PHPT: 24 PASS / 2 SKIP
  • C unit: 3/3 PASS
  • C static analysis: production / benchともPASS
  • PHPUnit: 31 tests / 116 assertions PASS
  • bash -n / git diff --check: PASS

$closeCountのlocal naming、historical review recordの旧event名、一般的なCI/bench拡張は非指摘としました。

…IEW-20260714-001)

「RST flushのgrace deadline超過」だけでなく、submitの即時失敗と
nghttp2_session_send / socket / TLS / coalesced-buffer flushの任意の失敗
でもfresh connectionへフォールバックすることを明記(SPEC §4.2と整合)。
Planの同型の狭い記述2箇所も揃えた。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

第九パスの1件、妥当と判断し f94d3ba で対応しました。レビュー記録は REVIEW-20260714-001 に追記しています。

[Low] best-effort reuseのfallback説明の拡張 → 対応

ご指摘のとおり、第八パスで私が書いたcurrent-state文はfallbackを「RST flushがgrace deadlineを超過した場合」に限定しており、実装のfallback集合(RST submitの即時失敗によるdead化、nghttp2_session_send / socket / TLS / coalesced-buffer flushの任意の失敗によるreuse禁止)より狭く、SPEC §4.2の「RST書き込み失敗時」とも粒度が合っていませんでした。ご提案の形で「RST submit / flushの失敗〔grace deadline超過のほか、submitの即時失敗や nghttp2_session_send / socket / TLS / coalesced-buffer flushの任意の失敗を含む〕またはpreflight drain cap超過時はfresh connectionへフォールバック」へ拡張し、即時失敗後にconnectionを捨てるのはfatal nghttp2 sessionやpartial wire stateを再駆動しない安全側lifecycle contractである旨も追記しました。

あわせて、Planのsuperseded注記と取り消し線行にも同型の狭い記述(grace deadline超過のみ)が残っていたため、「失敗〔grace deadline超過を含む〕」へ揃えています。

検証

文書のみの変更(コード変更なし)のため、git diff --check クリーンの確認のみ行いました。直近のコード検証(sanitizer 2 lane・NTS 26/26・ZTS・C unit・cppcheck・PHPUnit)は第八パスの状態から不変です。

🤖 Generated with Claude Code

@dkkoma dkkoma left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

第十パスの再レビューを完了しました(HEAD f94d3ba)。第九パスの REVIEW-20260714-001 は適切に修正されています。HTTP/2 / gRPC domain gate は Blocker / High / Medium が none、今回新たに Low 1件です。runtime assertionは有効で、指摘はPHPT 035のtest contractコメント1か所に限定します。

検証: ASan/UBSan production 22 PASS / 4 SKIP、bench+fault 26/26 PASS、NTS PHPT 26/26、ZTS PHPT 24 PASS / 2 SKIP、C unit / C static analysis PASS、PHPUnit 31 tests / 116 assertions PASS。


$call->cancel();

// Connection reuse is best-effort: with more backlog than the 64KiB drain

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] testで設定した16KiB capとして説明してください

このPHPTはgrpc_lite.preflight_drain_max_bytes=16384を指定し、48KiB backlogを設定済み16KiB capに当てています。production defaultの64KiB超過は検証しておらず、上のfixture説明と下の16384-byte assertionにも反します。configured 16KiB drain capへ直し、production 64KiB境界はkernel window制約のため直接跨いでいないことと区別してください。

ini導入前の固定64KiB cap時代のコメント残骸を修正。テストが固定するのは
--INI-- で下げた16KiB capの機構であり、production 64KiB境界は
kernel window制約のため直接跨いでいないことを明記。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dkkoma

dkkoma commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

第十パスの1件、妥当と判断し b7b9fd1 で対応しました。レビュー記録は REVIEW-20260714-002 に追記しています。

[Low] PHPT 035のcapコメント → 設定済み16KiBとして記述

ご指摘のとおり、後続コール直前のコメントの「64KiB drain cap」はini導入前(固定64KiB cap時代)の設計のコメント残骸で、テストが実際に固定しているのは --INI-- で16KiBへ下げたcapの機構です(冒頭のfixture説明・16384-byte assertionと矛盾していました)。コメントを「configured 16KiB cap(--INI--)を超えるbacklogでfallbackする。production 64KiB境界自体はkernel windowが64KiB超の未読backlogを保持できないため直接跨いでおらず、本テストはcap機構を下げた設定値で固定する」へ書き換え、production境界を検証していないことを明示的に区別しました。

検証

コメントのみの変更のため、PHPT 035単体のPASSを確認しました。その他のコード検証は第八パスの状態から不変です。

🤖 Generated with Claude Code

@dkkoma

dkkoma commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

第十一パスの再レビューを完了しました(HEAD / 対応commit b7b9fd1)。

前回のLowはadequateです。PHPT 035のコメントは、48KiB backlogが--INI--で設定した16KiB capを超えること、production defaultの64KiB境界自体はこのtestで直接跨いでいないこと、縮小したeffective capで同じfallback機構を固定していることを明確に区別するようになりました。INI、fixture、16384-byte上下限assertion、SPEC §4.2、中央review recordとも一致しています。

新規指摘はありません。HTTP/2 / gRPC required domain gateは次のとおりall clearです。

  • Blocker: none
  • High: none
  • Medium: none
  • Low: none
  • Design Decision: none

検証も再実行しました。

  • ASan / UBSan production: 22 PASS / 4 SKIP、reportなし
  • ASan / UBSan bench+fault: 26/26 PASS、reportなし
  • NTS PHPT: 26/26 PASS
  • ZTS PHPT: 24 PASS / 2 SKIP
  • C unit: protocol_core / status_core / transport_core PASS
  • C static analysis: production / benchともPASS
  • PHPUnit: 31 tests / 116 assertions PASS
  • bash -n / git diff --check: PASS

このHEADはAPPROVE相当です。

@dkkoma
dkkoma merged commit 255a3cb into main Jul 13, 2026
4 checks passed
@dkkoma
dkkoma deleted the codex/issue-deadline-rst-stream-keep-connection branch July 13, 2026 23:54
dkkoma added a commit that referenced this pull request Jul 14, 2026
- PR #26/#27/#28/#29 でマージ済みの issue(goaway transparent retry / bin metadata unpadded base64 / status taxonomy / encoding flag=0 / deadline RST_STREAM)を Status: Closed にして docs/issues/closed/ へ移動
- PR #29 レビューサイクル(pass 4〜11)の記録 30 ファイルを docs/reviews/issues/ に追加

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants