# ポインタライフサイクル追跡システムと根本原因の分析 ## 実施日 2025-11-28 ## 目的 Larson ベンチマークで発生している double-free クラッシュの根本原因を特定し、修正案を提示する。 ## 背景 ### 問題の症状 - **現象**: 同じポインタ `0x7c3ff7a40430` が 6 回 allocate される - **クラッシュタイミング**: Slab refill **前** (最初の 2000 操作内) - **検出箇所**: TLS SLL の duplicate check (position 11 に同じポインタ) - **疑惑**: Freelist と TLS SLL の同期が壊れている ### 期待される動作 ``` alloc → [freelist] → user → free → [TLS SLL push] alloc → [TLS SLL pop] → user → free → ... ``` ### 実際の動作(推測) ``` alloc → [freelist] → user → free → [TLS SLL push] alloc → [freelist!?] → 同じポインタが再度割り当て → TLS SLL にまだ残っている → free 時に重複検出 ``` --- ## Part 1: ポインタ状態追跡システムの実装 ### 設計概要 #### 追跡イベント 1. **CARVE**: Linear carve で新規生成 2. **ALLOC_FREELIST**: Freelist から割り当て 3. **ALLOC_TLS_POP**: TLS SLL から pop して割り当て 4. **FREE_TLS_PUSH**: Free 時に TLS SLL へ push 5. **DRAIN_TO_FREELIST**: Drain で TLS SLL → Freelist 移動 6. **SLAB_REUSE**: Slab 再利用(ポインタ無効化) 7. **REFILL**: Slab refill #### 記録情報 - ポインタアドレス (BASE) - グローバル操作番号 (atomic counter) - イベント種類 - クラス - 補助情報(TLS count, freelist head, slab index) - 呼び出し元 (__FILE__, __LINE__) #### 環境変数制御 - `HAKMEM_PTR_TRACE_ALL=1`: 全ポインタ追跡(高負荷) - `HAKMEM_PTR_TRACE=0x...`: 特定ポインタのみ - `HAKMEM_PTR_TRACE_CLASS=N`: 特定クラスのみ - `HAKMEM_PTR_TRACE_VERBOSE=1`: リアルタイム出力 ### 実装 #### 新規ファイル - **`core/box/ptr_trace_box.h`**: 完全なライフサイクル追跡システム - リングバッファ (4096 エントリ/スレッド) - デバッグビルドのみ有効 (`!HAKMEM_BUILD_RELEASE`) - ゼロオーバーヘッド (リリースビルドは no-op) #### 統合ポイント ##### Allocation パス (`core/tiny_superslab_alloc.inc.h`) ```c // Linear carve (2箇所) PTR_TRACE_CARVE(block, class_idx, slab_idx); // Freelist allocation void* next = tiny_next_read(meta->class_idx, block); PTR_TRACE_ALLOC_FREELIST(block, meta->class_idx, meta->freelist); meta->freelist = next; // Refill PTR_TRACE_REFILL(class_idx, ss, slab_idx); ``` ##### TLS SLL パス (`core/box/tls_sll_box.h`) ```c // Push (in tls_sll_push_impl) ptr_trace_record_impl(PTR_EVENT_FREE_TLS_PUSH, ptr, class_idx, op_num, ...); // Pop (in tls_sll_pop_impl) ptr_trace_record_impl(PTR_EVENT_ALLOC_TLS_POP, base, class_idx, op_num, ...); ``` ##### Drain パス (`core/box/tls_sll_drain_box.h`) ```c // Drain each block ptr_trace_record_impl(PTR_EVENT_DRAIN_TO_FREELIST, base, class_idx, op_num, ...); ``` --- ## Part 2: 根本原因の推定 ### コード分析結果 #### 発見 1: Freelist 割り当ての header 書き換えタイミング **`tiny_superslab_alloc.inc.h:149-151` (修正後)**: ```c void* next = tiny_next_read(meta->class_idx, block); PTR_TRACE_ALLOC_FREELIST(block, meta->class_idx, meta->freelist); meta->freelist = next; ``` **問題点**: - `tiny_next_read()` は **header 位置から next ポインタを読む** - その直後に `meta->freelist = next` で更新 - **まだ header は書き換えられていない**(line 166 で初めて書き換え) - この間に別スレッドが同じポインタを見ると、古い header を読む可能性がある #### 発見 2: TLS SLL push の header 復元タイミング **`tls_sll_box.h:361-363`**: ```c PTR_TRACK_TLS_PUSH(ptr, class_idx); PTR_TRACK_HEADER_WRITE(ptr, expected); *b = expected; // Header 復元 ``` **問題点**: - TLS SLL push 時に header を復元 (`0xA0 | class_idx`) - しかし、この header は **next ポインタの格納領域と重複** (class 1-6) - Header 復元が next ポインタを破壊する可能性がある #### 発見 3: Linear carve と freelist の header 書き込みタイミングの違い **Linear carve (line 106-108)**: ```c void* user = tiny_region_id_write_header(block_base, meta->class_idx); ``` → **即座に header を書く** **Freelist allocation (line 166-169)**: ```c void* user = tiny_region_id_write_header(block, meta->class_idx); ``` → **freelist 更新後に header を書く** **リスクシナリオ**: ``` 1. Freelist allocation: block を取得、next を読む 2. meta->freelist = next を更新 ← この時点で freelist は既に次へ進んでいる 3. まだ header は書き換えていない 4. 別スレッドが同じ slab の freelist から allocate → 同じ block を取得? 5. Header 書き換え競合 ``` ### 疑わしい競合パターン #### パターン A: Freelist/TLS SLL の二重存在 ``` Thread 1: 1. Alloc from freelist → ptr A (header 未書き換え) 2. meta->freelist = next (freelist は進んだ) 3. User が使用 4. Free → TLS SLL に push Thread 2 (または後の Thread 1): 5. Alloc from freelist → なぜか ptr A を再度取得 (理由: header が未書き換えで、next ポインタが壊れていた?) Result: ptr A が TLS SLL と user の両方に存在 → double-free ``` #### パターン B: Header 書き換えによる next ポインタ破壊 ``` 状況: ptr A が freelist にある (next = ptr B) Thread 1: 1. Alloc from freelist → ptr A を読む 2. next_ptr = tiny_next_read(cls, A) → B を読む 3. meta->freelist = B (freelist 更新) Thread 2 (極めて短い時間窓): 4. TLS SLL push(A, cls=1) → header を 0xA1 に復元 → header 位置は next ポインタと同じ (offset=0 for cls 1-6) → next ポインタ破壊! Thread 1 (続き): 5. tiny_region_id_write_header(A, cls) → header を再度書き換え 6. User に返す Result: Freelist の integrity が壊れ、次の allocation で同じポインタを返す可能性 ``` ### 最有力仮説: **Header と Next ポインタの競合** #### 構造的な問題 ``` Class 1-6 の場合: BASE[0]: Header (1 byte) と Next ポインタ (8 bytes) が重複 Freelist 状態: BASE[0..7]: Next ポインタ (8 bytes) TLS SLL 状態: BASE[0]: Header (0xA0 | class_idx) BASE[0..7]: Next ポインタ (TLS SLL リンク) ``` #### 競合タイミング ``` Time Thread 1 (Alloc from freelist) Thread 2 (Free → TLS push) ---- --------------------------------- --------------------------- T1 Read freelist head = A T2 Read next = A[0..7] = B T3 meta->freelist = B (freelist更新) T4 TLS SLL push(A) T5 → Write A[0] = 0xA1 (header) T6 → CORRUPTS A[0..7] ! T7 Write header A[0] = 0xA1 (遅い) T8 Return A to user ---- Result: Freelist は B を指すが、B の next ポインタが破壊されている → 次の alloc で A または B が再度返される可能性 ``` --- ## Part 3: 設計改善の提案 ### 短期修正 (Priority 1): **Atomic Header+Freelist 更新** #### 目的 Header 書き換えと freelist 更新の間の競合窓を閉じる。 #### 実装 ```c // In superslab_alloc_from_slab() - Freelist mode // BEFORE (競合あり): void* next = tiny_next_read(meta->class_idx, block); meta->freelist = next; meta->used++; // ... (遅延 header 書き換え) void* user = tiny_region_id_write_header(block, meta->class_idx); return user; // AFTER (競合なし): void* next = tiny_next_read(meta->class_idx, block); void* user = tiny_region_id_write_header(block, meta->class_idx); // 即座に header 書き換え meta->freelist = next; // その後 freelist 更新 meta->used++; return user; ``` #### 効果 - Header 書き換え後に freelist を更新することで、freelist から取得したポインタは常に有効な header を持つ - TLS SLL push が header を復元しても、既に freelist からは外れているため影響なし #### リスク - 軽微: header 書き換えのタイミングが数命令早まるだけ(互換性問題なし) --- ### 中期改善 (Priority 2): **TLS SLL の Header 復元を遅延** #### 目的 TLS SLL push 時の header 復元を、次の pop まで遅延することで、next ポインタ破壊を防ぐ。 #### 現状の問題 ```c // tls_sll_push_impl (line 361-363) *b = expected; // Header を即座に復元 → next ポインタ破壊リスク PTR_NEXT_WRITE("tls_push", class_idx, ptr, 0, g_tls_sll[class_idx].head); ``` #### 提案: Lazy Header Restore ```c // TLS SLL push: header 復元を **スキップ** // (next ポインタのみ書き換え) PTR_NEXT_WRITE("tls_push", class_idx, ptr, 0, g_tls_sll[class_idx].head); g_tls_sll[class_idx].head = ptr; // 注意: header は壊れたまま (0xA1 のまま、または任意のデータ) // TLS SLL pop: header を復元してから返す void* base = g_tls_sll[class_idx].head; void* next = tiny_next_read(class_idx, base); g_tls_sll[class_idx].head = next; // ここで初めて header を復元 uint8_t* b = (uint8_t*)base; *b = (uint8_t)(HEADER_MAGIC | (class_idx & HEADER_CLASS_MASK)); *out = base; return true; ``` #### 効果 - TLS SLL に格納されている間は header が壊れていても問題なし(next ポインタのみ使用) - Pop 時に header を復元するため、user に返す時は正しい header - Freelist との競合窓が消滅 #### リスク - 中程度: TLS SLL の integrity check が header に依存している場合は修正が必要 - テスト: Duplicate check が header を読まないことを確認 --- ### 長期設計 (Priority 3): **Header と Next ポインタの分離** #### 目的 根本的に header と next ポインタを別の場所に格納することで、競合を完全に排除。 #### アプローチ A: Header をブロック末尾に移動 ``` 現状 (Class 1, stride=16): [0]: Header (1 byte) [1..15]: User data (15 bytes) 提案: [0..14]: User data (15 bytes) [15]: Header (1 byte) Next ポインタ (freelist/TLS): [0..7]: Next (8 bytes) ← Header と重複しない ``` **利点**: - Header と next ポインタの競合が完全に解消 - User data は引き続き [1..15] または [0..14] で連続 **欠点**: - Header 読み取り位置が変わる(`ptr - 1` → `ptr + stride - 1`) - 全コードで header アクセスを変更する必要がある(大規模リファクタリング) #### アプローチ B: Next ポインタを別オフセットに格納 ``` Class 1-6 の場合: Header: [0] (1 byte) Next (freelist): [8..15] (8 bytes) ← Header と重複しない Next (TLS SLL): [8..15] (8 bytes) ``` **利点**: - Header は変更不要 - Next ポインタのみ移動(局所的な変更) **欠点**: - Stride が 16 未満のクラス (C1: 16 bytes) では [8..15] が使えない - C0 (8 bytes) では不可能 #### アプローチ C: Class 0 と 7 以外は header を廃止、metadata のみで管理 ``` 現状: Class 1-6: Header で class 識別 提案: Class 1-6: Header 廃止、SuperSlab metadata のみで class 管理 → Header と next ポインタの競合が存在しない ``` **利点**: - Header 書き換え不要 → 競合窓が消滅 - Free 時の class 判定は SuperSlab lookup のみ(既存の仕組み) **欠点**: - Header ベースの高速 class 判定ができなくなる(パフォーマンス低下) - 現在の Phase 7 最適化(header ベース free)が無効化 --- ### 推奨実装順序 #### Phase 1: 短期修正(即座に適用可能) 1. **Freelist allocation の header 書き換えタイミング変更** - ファイル: `core/tiny_superslab_alloc.inc.h:149-175` - 変更: header 書き換えを freelist 更新の前に移動 - テスト: Larson ベンチマーク 1000 回実行でクラッシュ率を確認 - 期待: クラッシュ率 50% → 5% 以下 #### Phase 2: 中期改善(1週間以内) 2. **TLS SLL の Lazy Header Restore** - ファイル: `core/box/tls_sll_box.h:361-363, 516-554` - 変更: push 時の header 復元を削除、pop 時に復元 - テスト: TLS SLL の integrity check、duplicate check が動作することを確認 - 期待: クラッシュ率 5% → 0% #### Phase 3: 長期設計(1ヶ月以内、オプション) 3. **Pointer Trace System の本格運用** - 環境変数で特定クラスまたはポインタを追跡 - クラッシュ時の完全なライフサイクル分析 - 期待: 将来の double-free バグを即座に診断 4. **アーキテクチャ検討: Header 位置の再設計** - アプローチ A/B/C の詳細設計とプロトタイプ - ベンチマークでパフォーマンス影響を評価 - 期待: 根本的な競合排除、保守性向上 --- ## 影響範囲の分析 ### 短期修正の影響 - **変更箇所**: 1ファイル, 10行以内 - **パフォーマンス**: 影響なし(命令順序の変更のみ) - **互換性**: 完全互換(external API 不変) - **リスク**: 極めて低い ### 中期改善の影響 - **変更箇所**: 1ファイル, 30行以内 - **パフォーマンス**: 影響なし(header 書き換えタイミングのみ) - **互換性**: TLS SLL 内部実装のみ(external API 不変) - **リスク**: 低い(TLS SLL の integrity check 要確認) ### 長期設計の影響 - **変更箇所**: 全 header アクセス箇所(100+ ファイル) - **パフォーマンス**: アプローチ次第(-5% ~ +2%) - **互換性**: Internal API 変更(大規模リファクタリング) - **リスク**: 高い(段階的移行が必要) --- ## テスト計画 ### Phase 1 テスト(短期修正) 1. **Unit Test**: Freelist allocation の header タイミング確認 - 期待: Header が freelist 更新前に書き換えられる 2. **Integration Test**: Larson 1000 回実行 - 期待: クラッシュ率 < 5% 3. **Stress Test**: 並列 Larson (threads=8, iterations=1M) - 期待: 0 クラッシュ ### Phase 2 テスト(中期改善) 1. **Unit Test**: TLS SLL push/pop の header 状態確認 - 期待: Pop 時に header が正しく復元される 2. **Integration Test**: TLS SLL duplicate check - 期待: Duplicate が正しく検出される 3. **Stress Test**: Larson 10000 回実行 - 期待: 0 クラッシュ ### Phase 3 テスト(追跡システム) 1. **Trace Test**: 特定ポインタのライフサイクル追跡 - 環境変数: `HAKMEM_PTR_TRACE=0x7c3ff7a40430` - 期待: CARVE → ALLOC → FREE → TLS_PUSH の完全な記録 2. **Class Trace Test**: Class 1 全体の追跡 - 環境変数: `HAKMEM_PTR_TRACE_CLASS=1` - 期待: クラッシュ時に duplicate の発生経路が特定できる --- ## 結論 ### 根本原因(最有力仮説) **Header と Next ポインタの格納位置重複による競合** - Class 1-6 では header (BASE[0]) と next ポインタ (BASE[0..7]) が重複 - Freelist allocation 時の遅延 header 書き換えにより、競合窓が発生 - TLS SLL push 時の header 復元が next ポインタを破壊 - → 同じポインタが freelist と TLS SLL の両方に存在 - → Double-free クラッシュ ### 推奨修正 1. **即座に適用**: Freelist allocation の header タイミング変更(10行) 2. **1週間以内**: TLS SLL の Lazy Header Restore(30行) 3. **追跡システム**: 将来のバグ診断のため、ptr_trace_box.h を運用 ### 期待効果 - **短期修正**: クラッシュ率 90% 削減 - **中期改善**: クラッシュ完全解消 - **長期設計**: アーキテクチャの根本的改善(保守性・拡張性向上) --- ## 実装ファイル ### 新規作成 - `/mnt/workdisk/public_share/hakmem/core/box/ptr_trace_box.h` - 完全なポインタライフサイクル追跡システム - デバッグビルドのみ有効 - リングバッファ 4096 エントリ - 環境変数制御 ### 修正済み - `/mnt/workdisk/public_share/hakmem/core/tiny_superslab_alloc.inc.h` - 追跡フック追加: CARVE, ALLOC_FREELIST, REFILL - `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_box.h` - 追跡フック追加: FREE_TLS_PUSH, ALLOC_TLS_POP - `/mnt/workdisk/public_share/hakmem/core/box/tls_sll_drain_box.h` - 追跡フック追加: DRAIN_TO_FREELIST ### 次のステップで修正予定 - `/mnt/workdisk/public_share/hakmem/core/tiny_superslab_alloc.inc.h:149-175` - Header 書き換えタイミング変更(短期修正) --- ## 補足資料 ### 関連ドキュメント - `docs/analysis/TLS_SLL_ARCHITECTURE_INVESTIGATION.md` - TLS SLL の既知の問題と Phase 1 修正 - `docs/analysis/PHASE9_LRU_ARCHITECTURE_ISSUE.md` - LRU と drain の関係 ### デバッグコマンド ```bash # ポインタ追跡システムの使用例 # 1. 特定クラスのみ追跡(低負荷) HAKMEM_PTR_TRACE_CLASS=1 ./larson_hakmem 2 10 10 10000 # 2. 特定ポインタのみ追跡(最低負荷) HAKMEM_PTR_TRACE=0x7c3ff7a40430 ./larson_hakmem 2 10 10 10000 # 3. 全ポインタ追跡(高負荷、短時間テストのみ) HAKMEM_PTR_TRACE_ALL=1 ./larson_hakmem 2 10 10 1000 # 4. リアルタイム出力(診断用) HAKMEM_PTR_TRACE_CLASS=1 HAKMEM_PTR_TRACE_VERBOSE=1 ./larson_hakmem 2 10 10 100 # 5. クラッシュ時の自動ダンプ(終了時に出力) HAKMEM_PTR_TRACE_CLASS=1 ./larson_hakmem 2 10 10 10000 2>&1 | tee trace.log ``` ### ビルド方法 ```bash # デバッグビルド(追跡システム有効) make clean make BUILD_FLAVOR=debug # リリースビルド(追跡システム無効、ゼロオーバーヘッド) make clean make BUILD_FLAVOR=release ``` --- **作成者**: Claude (Anthropic) **レビュー**: 要レビュー **ステータス**: 実装完了(追跡システム)、修正提案済み(Phase 1-3)