Files
hakmem/docs/tls_sll_header_corruption_investigation.md
2025-12-03 10:34:39 +09:00

8.3 KiB
Raw Blame History

TLS SLL Header Corruption Investigation

症状とスコープ

  • sh8bench 実行時に [TLS_SLL_HDR_RESET] cls=1 base=0x715210fbf858 got=0x51 expect=0xa1 count=0 などが発生class 6 の got=0x00 expect=0xa6 も報告)。
  • 発生地点: core/box/tls_sll_box.h の TLS SLL pop 境界Header Box による検証失敗 → SLL をリセット)。
  • 再現範囲: sh8bench でほぼ毎回。Larson/cfrac では再現しにくい。

箱マップBox Theory

  • Header Box (core/box/tiny_header_box.h): C1-C6 はヘッダー保持HEADER_MAGIC|class_idxtiny_header_write_if_preserved() / tiny_header_validate() が単一の境界 API。
  • Freelist Box: freelist ↔ TLS SLL の橋渡し。ここでヘッダーを復元しないと TLS SLL pop で Fail-Fast する。
  • TLS SLL Box (core/box/tls_sll_box.h): push でヘッダーを復元・検証、pop でヘッダー検証&リセット。リングイベント TINY_RING_EVENT_TLS_SLL_HDR_CORRUPT あり。
  • Class Map Box: class_idx をヘッダー無しで特定する高速経路。デフォルトで header write をスキップするため、TLS SLL がヘッダーに依存する場合は境界不整合が起きる。

TLS_SLL_HDR_RESET 詳細分析

  • pop 経路は tiny_class_preserves_header(class_idx) が true (C1-C6) の場合に必ず tiny_header_validate() を呼ぶ。失敗時は SLL をリセットして [TLS_SLL_HDR_RESET] を 10k 回に 1 回だけ出力。
  • core/tiny_region_id.h では デフォルト g_write_header=1A/B ガードで HAKMEM_TINY_WRITE_HEADER=0 による切替可)。ヘッダーを書かずに freelist に入ると、Freelist→TLS SLL の移送時に復元漏れがあると pop で 0x00/0x51/0x61 を読み出してリセットする。
  • 既知の漏れ(修正済みの根治ポイント)
    • core/box/carve_push_box.c: box_carve_and_push_with_freelist() で freelist pop→push 前にヘッダー未復元commit 3c6c76cb1 で修正)。
    • core/hakmem_tiny_free.inc: tiny_drain_freelist_to_sll_once() のデッドパスでも同様に未復元commit a94344c1a で修正)。
  • 破損パターン
    • got=0x00: mmap zero 由来の stale data
    • got=0x51 / 0x61: 前世の payload 断片(ヘッダー未書き込み時に漏れる)

3つの仮説

  • a. ヘッダー書き込みの未実行(最有力)
    • デフォルトで header write OFFg_write_header=0 freelist→TLS SLL での復元漏れが重なると、pop 側の検証で Fail-Fast する。上記 2 箇所の修正がこれを潰す。
  • b. TLS SLL と Class Map の境界不整合
    • Class Map は「ヘッダー不要」を前提、一方 TLS SLL pop はヘッダー必須。境界freelist→TLS SLLでヘッダーを必ず再構成するか、Class Map 情報に切り替えるかのどちらかに統一すべき。
  • c. sh8bench 固有パターン
    • 8 スレッドで class 1 の高速 churn により freelist→TLS SLL デッドパスdrain/carveの頻度が高い。ヘッダー復元漏れが顕在化しやすい。

今回の実行結果

  • LD_PRELOAD=./libhakmem.so(リリースビルド)は unified_cache_refill+0x46f で SEGFAULTmov 0x1(%r15),%rdx)。r15 は TLS freelist から読みだしたポインタで、無効値を指していた。→ リリース版で sh8bench を回す前に unified cache 側のクラッシュ修正が必要。
  • LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libasan.so.6 ./libhakmem_asan.so" で代替実行LeakSanitizer のリーク警告付きだが完走):
    • ベースライン + HAKMEM_TINY_SLL_RING=1: [TLS_SLL_HDR_RESET] ゼロ件(sh8bench_baseline.log)。
    • HAKMEM_TINY_WRITE_HEADER=1 A/B5 回)+ Ring: すべて [TLS_SLL_HDR_RESET] ゼロ件(sh8bench_header_on_run*.log)。
    • HAKMEM_TINY_SLL_SAFEHEADER=1 HAKMEM_TINY_SLL_VALIDATE_HDR=1 HAKMEM_DEBUG_LEVEL=3 + Ring: [TLS_SLL_PUSH_BAD_HDR] / [TLS_SLL_REJECT] ゼロ件(sh8bench_safeheader.log)。
  • 現時点では TLS_SLL_HDR_RESET は再現せず。リリース版の unified cache 落ちを直した上で再確認が必要。

デバッグ手順4ステップ

  1. 現状再現とリング取得

    cd /mnt/workdisk/public_share/hakmem
    HAKMEM_TINY_SLL_RING=1 LD_PRELOAD=./libhakmem.so \
      ./mimalloc-bench/out/bench/sh8bench 2>&1 | \
      tee sh8bench_baseline.log | grep -E "TLS_SLL_HDR_RESET|TLS_SLL_HDR_CORRUPT|Total elapsed"
    
    • 目的: [TLS_SLL_HDR_RESET] 発生位置と ring payload (got/expect) を確定。
  2. ヘッダー強制書き込みで A/B(仮説 a の確認)

    for i in {1..5}; do
      echo "=== header_on run $i ==="
      HAKMEM_TINY_WRITE_HEADER=1 HAKMEM_TINY_SLL_RING=1 LD_PRELOAD=./libhakmem.so \
        ./mimalloc-bench/out/bench/sh8bench 2>&1 | \
        grep -E "TLS_SLL_HDR_RESET|Total elapsed"
    done
    
    • 消えれば「ヘッダー未復元」が原因と確定。
  3. push 側の境界検証を強化

    HAKMEM_TINY_SLL_SAFEHEADER=1 HAKMEM_TINY_SLL_VALIDATE_HDR=1 HAKMEM_DEBUG_LEVEL=3 \
      LD_PRELOAD=./libhakmem.so ./mimalloc-bench/out/bench/sh8bench \
      2>&1 | tee sh8bench_safeheader.log
    
    • TLS_SLL_PUSH_BAD_HDR / TLS_SLL_REJECT が出れば freelist→TLS SLL 境界で復元漏れが残っている。ring で callsite を特定。
  4. 修正後のリグレッション確認

    # sh8bench 10 回
    for i in {1..10}; do LD_PRELOAD=./libhakmem.so ./mimalloc-bench/out/bench/sh8bench \
      2>&1 | grep -E "TLS_SLL_HDR_RESET|Total elapsed"; done
    
    # Larson / cfrac で広めに確認
    LD_PRELOAD=./libhakmem.so ./mimalloc-bench/out/bench/larson 10 7 8 100 10000
    LD_PRELOAD=./libhakmem.so ./mimalloc-bench/out/bench/cfrac 17545186520507317056371138836327483792789528
    
    • 目的: sh8bench だけでなく他ワークロードでも Fail-Fast が出ないことを確認。

修正パターン(根治案)

  • パターンA: 境界一箇所でヘッダー復元を徹底(推奨)

    • Freelist→TLS SLL を 1 つの Box 境界として tiny_header_write_if_preserved() を必ず実行(現行の box_carve_and_push_with_freelist() / tiny_drain_freelist_to_sll_once() の形を維持)。
    • 追加ガード: HAKMEM_TINY_SLL_SAFEHEADER=1 を運用フラグとして残し、違反時は push 拒否+リング記録。
  • パターンB: ヘッダー常時書き込みに戻す

    • core/tiny_region_id.hg_write_header をデフォルト 1 にして Class Map 依存を減らす。
    • 実装済み: デフォルト ON に変更(HAKMEM_TINY_WRITE_HEADER=0 で旧挙動に戻せる)。
    • A/B 切替: 環境変数 HAKMEM_TINY_WRITE_HEADER=0 でオフにできるよう保持(切り戻し容易)。
  • パターンC: TLS SLL を Class Map 準拠にリファクタ

    • tls_sll_pop() でヘッダー検証を tiny_header_validate()class_map_lookup() の優先順に変更(ヘッダーが無い場合は Class Map で検証)。
    • tiny_header_write_if_preserved() を「Best Effort」扱いにしつつ、リングで Class Map vs header の不一致を Fail-Fast ログする。

環境変数 / ビルド / テスト

  • デバッグフラグ
    • HAKMEM_TINY_WRITE_HEADER=1 : ヘッダー強制書き込み(仮説 a 検証)
    • HAKMEM_TINY_SLL_RING=1 : TLS SLL イベントをリング出力
    • HAKMEM_TINY_SLL_VALIDATE_HDR=1 または HAKMEM_DEBUG_LEVEL=3 : push でのヘッダー検証を常時 ON
    • HAKMEM_TINY_SLL_SAFEHEADER=1 : ヘッダー不一致時は push 拒否Fail-Fast
    • HAKMEM_WRAP_DIAG=1 : Wrapper トレース
  • ビルド
    cd /mnt/workdisk/public_share/hakmem
    rm -f *.o libhakmem.so
    make shared -j8                           # Release
    make shared -j8 EXTRA_CFLAGS="-g -O0 -UHAKMEM_BUILD_RELEASE"  # Debug
    
  • テスト(再掲)
    • LD_PRELOAD=./libhakmem.so ./mimalloc-bench/out/bench/sh8bench
    • LD_PRELOAD=./libhakmem.so ./mimalloc-bench/out/bench/larson 10 7 8 100 10000
    • いずれも grep -E "TLS_SLL_HDR_RESET|TLS_SLL_HDR_CORRUPT" でゼロ件を確認。

根治の原則: 「ヘッダーを書かないなら検証しない」「検証するなら境界で必ず書く」を Box 境界 1 箇所に閉じ込め、Fail-Fast とリング可視化で sh8bench パターンでも再発しないことを確認する。