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>
18 KiB
18 KiB
ポインタライフサイクル追跡システムと根本原因の分析
実施日
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: ポインタ状態追跡システムの実装
設計概要
追跡イベント
- CARVE: Linear carve で新規生成
- ALLOC_FREELIST: Freelist から割り当て
- ALLOC_TLS_POP: TLS SLL から pop して割り当て
- FREE_TLS_PUSH: Free 時に TLS SLL へ push
- DRAIN_TO_FREELIST: Drain で TLS SLL → Freelist 移動
- SLAB_REUSE: Slab 再利用(ポインタ無効化)
- 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 - 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: 短期修正(即座に適用可能)
- Freelist allocation の header 書き換えタイミング変更
- ファイル:
core/tiny_superslab_alloc.inc.h:149-175 - 変更: header 書き換えを freelist 更新の前に移動
- テスト: Larson ベンチマーク 1000 回実行でクラッシュ率を確認
- 期待: クラッシュ率 50% → 5% 以下
- ファイル:
Phase 2: 中期改善(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ヶ月以内、オプション)
-
Pointer Trace System の本格運用
- 環境変数で特定クラスまたはポインタを追跡
- クラッシュ時の完全なライフサイクル分析
- 期待: 将来の double-free バグを即座に診断
-
アーキテクチャ検討: 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 テスト(短期修正)
- Unit Test: Freelist allocation の header タイミング確認
- 期待: Header が freelist 更新前に書き換えられる
- Integration Test: Larson 1000 回実行
- 期待: クラッシュ率 < 5%
- Stress Test: 並列 Larson (threads=8, iterations=1M)
- 期待: 0 クラッシュ
Phase 2 テスト(中期改善)
- Unit Test: TLS SLL push/pop の header 状態確認
- 期待: Pop 時に header が正しく復元される
- Integration Test: TLS SLL duplicate check
- 期待: Duplicate が正しく検出される
- Stress Test: Larson 10000 回実行
- 期待: 0 クラッシュ
Phase 3 テスト(追跡システム)
- Trace Test: 特定ポインタのライフサイクル追跡
- 環境変数:
HAKMEM_PTR_TRACE=0x7c3ff7a40430 - 期待: CARVE → ALLOC → FREE → TLS_PUSH の完全な記録
- 環境変数:
- 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 クラッシュ
推奨修正
- 即座に適用: Freelist allocation の header タイミング変更(10行)
- 1週間以内: TLS SLL の Lazy Header Restore(30行)
- 追跡システム: 将来のバグ診断のため、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)