Files
hakmem/docs/analysis/PTR_LIFECYCLE_TRACE_AND_ROOT_CAUSE_ANALYSIS.md
Moe Charm (CI) dc9e650db3 Tiny Pool redesign: P0.1, P0.3, P1.1, P1.2 - Out-of-band class_idx lookup
This commit implements the first phase of Tiny Pool redesign based on
ChatGPT architecture review. The goal is to eliminate Header/Next pointer
conflicts by moving class_idx lookup out-of-band (to SuperSlab metadata).

## P0.1: C0(8B) class upgraded to 16B
- Size table changed: {16,32,64,128,256,512,1024,2048} (8 classes)
- LUT updated: 1..16 → class 0, 17..32 → class 1, etc.
- tiny_next_off: C0 now uses offset 1 (header preserved)
- Eliminates edge cases for 8B allocations

## P0.3: Slab reuse guard Box (tls_slab_reuse_guard_box.h)
- New Box for draining TLS SLL before slab reuse
- ENV gate: HAKMEM_TINY_SLAB_REUSE_GUARD=1
- Prevents stale pointers when slabs are recycled
- Follows Box theory: single responsibility, minimal API

## P1.1: SuperSlab class_map addition
- Added uint8_t class_map[SLABS_PER_SUPERSLAB_MAX] to SuperSlab
- Maps slab_idx → class_idx for out-of-band lookup
- Initialized to 255 (UNASSIGNED) on SuperSlab creation
- Set correctly on slab initialization in all backends

## P1.2: Free fast path uses class_map
- ENV gate: HAKMEM_TINY_USE_CLASS_MAP=1
- Free path can now get class_idx from class_map instead of Header
- Falls back to Header read if class_map returns invalid value
- Fixed Legacy Backend dynamic slab initialization bug

## Documentation added
- HAKMEM_ARCHITECTURE_OVERVIEW.md: 4-layer architecture analysis
- TLS_SLL_ARCHITECTURE_INVESTIGATION.md: Root cause analysis
- PTR_LIFECYCLE_TRACE_AND_ROOT_CAUSE_ANALYSIS.md: Pointer tracking
- TINY_REDESIGN_CHECKLIST.md: Implementation roadmap (P0-P3)

## Test results
- Baseline: 70% success rate (30% crash - pre-existing issue)
- class_map enabled: 70% success rate (same as baseline)
- Performance: ~30.5M ops/s (unchanged)

## Next steps (P1.3, P2, P3)
- P1.3: Add meta->active for accurate TLS/freelist sync
- P2: TLS SLL redesign with Box-based counting
- P3: Complete Header out-of-band migration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 13:42:39 +09:00

18 KiB
Raw Blame History

ポインタライフサイクル追跡システムと根本原因の分析

実施日

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)
// 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)
// 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)
// 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 (修正後):

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:

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):

void* user = tiny_region_id_write_header(block_base, meta->class_idx);

即座に header を書く

Freelist allocation (line 166-169):

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 更新の間の競合窓を閉じる。

実装

// 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 ポインタ破壊を防ぐ。

現状の問題

// 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

// 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 - 1ptr + 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週間以内

  1. 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ヶ月以内、オプション

  1. Pointer Trace System の本格運用

    • 環境変数で特定クラスまたはポインタを追跡
    • クラッシュ時の完全なライフサイクル分析
    • 期待: 将来の double-free バグを即座に診断
  2. アーキテクチャ検討: 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 Restore30行
  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 の関係

デバッグコマンド

# ポインタ追跡システムの使用例

# 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

ビルド方法

# デバッグビルド(追跡システム有効)
make clean
make BUILD_FLAVOR=debug

# リリースビルド(追跡システム無効、ゼロオーバーヘッド)
make clean
make BUILD_FLAVOR=release

作成者: Claude (Anthropic) レビュー: 要レビュー ステータス: 実装完了追跡システム、修正提案済みPhase 1-3