# 🎯 最速診断法: Canary Sandwich 検査 (2025-12-04) **目的**: 180秒クラッシュの**破壊パターン**を検出し、根本原因を特定 **戦略**: TLS SLL の周囲に "Canary" (検査値) を配置し、いつ/どこで破壊されるかを追跡 --- ## 📊 Canary Sandwich の層構造 ``` === Before TLS SLL === g_tls_canary_before_sll = 0xDEADBEEFDEADBEEF ↓ === TLS SLL Array (8 classes × sizeof(TinyTLSSLL)) === g_tls_sll[0..7] { head: hak_base_ptr_t count: uint32_t } ↓ === After TLS SLL === g_tls_canary_after_sll = 0xDEADBEEFDEADBEEF ↓ === Canary in each class === g_tls_sll_canary[0..7] = 0xBADC0FFEEBADC0FFE ``` --- ## 🔍 検査ポイント(5箇所) ### Point 1: TLS SLL 配列のメモリレイアウト確認 ```bash # gdb スクリプト print &g_tls_canary_before_sll print &g_tls_sll[0] print &g_tls_sll[7] print &g_tls_canary_after_sll print sizeof(g_tls_sll) print sizeof(TinyTLSSLL) ``` **期待値**: - `before < &sll[0]` (before は sll の前) - `&sll[7] < after` (after は sll の後) - レイアウトが連続 **崩れたら**: - コンパイラが変わった可能性 - 構造体パディングが変わった --- ### Point 2: Canary の破壊チェック(180秒時点) 修正: `core/hakmem_tiny.c` に定期検査を追加 ```c // hakmem_tiny.c に追加 static void check_tls_sll_canaries(void) { // 5秒ごとに実行(background thread または refill時に) // Check 1: before/after canary const uint64_t EXPECTED = 0xDEADBEEFDEADBEEFULL; extern __thread uint64_t g_tls_canary_before_sll; extern __thread uint64_t g_tls_canary_after_sll; if (g_tls_canary_before_sll != EXPECTED) { fprintf(stderr, "[CANARY_BROKEN_BEFORE] expected=%#llx got=%#llx\n", EXPECTED, g_tls_canary_before_sll); // フルダンプ dump_tls_sll_state(); abort(); } if (g_tls_canary_after_sll != EXPECTED) { fprintf(stderr, "[CANARY_BROKEN_AFTER] expected=%#llx got=%#llx\n", EXPECTED, g_tls_canary_after_sll); dump_tls_sll_state(); abort(); } // Check 2: 各 class の count overflow for (int i = 0; i < TINY_NUM_CLASSES; i++) { if (g_tls_sll[i].count > 10000) { // 異常値 fprintf(stderr, "[SLL_COUNT_OVERFLOW] cls=%d count=%u\n", i, g_tls_sll[i].count); dump_tls_sll_state(); abort(); } } } ``` --- ### Point 3: Head ポインタの破壊パターン 180秒時点での head 値をキャプチャ: ```c // tls_sll_push_impl 内に追加 (line 737 近辺) static __thread int push_count = 0; push_count++; // 180秒ぐらい = 60 million operations なら、50 million+ で検査 if (push_count > 50000000) { fprintf(stderr, "[PUSH_COUNT_CRITICAL] cls=%d count=%d head=%p sll_count=%u\n", class_idx, push_count, HAK_BASE_TO_RAW(g_tls_sll[class_idx].head), g_tls_sll[class_idx].count); // Head が valid range か? uintptr_t head_addr = (uintptr_t)HAK_BASE_TO_RAW(g_tls_sll[class_idx].head); if (head_addr < 4096 || head_addr > 0x00007fffffffffffULL) { fprintf(stderr, "[HEAD_OUT_OF_RANGE] cls=%d head=%p\n", class_idx, HAK_BASE_TO_RAW(g_tls_sll[class_idx].head)); dump_tls_sll_state(); abort(); } } ``` --- ### Point 4: Freelist Chain の整合性 `sll_refill_small_from_ss` line 334 に挿入: ```c // freelist から p を取得した直後 if (meta->freelist) { p = meta->freelist; void* next_raw; PTR_NEXT_READ("refill_check", class_idx, p, 0, next_raw); // next が妥当なメモリアドレス か? uintptr_t next_addr = (uintptr_t)next_raw; // Check 4a: NULL が妥当 (freelist の終端) if (next_raw == NULL) { // OK } // Check 4b: 有効なアドレス範囲 else if (next_addr >= 4096 && next_addr <= 0x00007fffffffffffULL) { // OK - 有効そう } // Check 4c: SuperSlab に属しているか確認 else { SuperSlab* ss_check = hak_super_lookup(next_raw); if (!ss_check || ss_check->magic != SUPERSLAB_MAGIC) { fprintf(stderr, "[FREELIST_NEXT_INVALID] cls=%d p=%p next=%p from_ss=%p\n", class_idx, p, next_raw, ss_check); dump_tls_sll_state(); abort(); } } meta->freelist = next_raw; // ... } ``` --- ### Point 5: Stride 計算の確認 `sll_refill_small_from_ss` line 355 に挿入: ```c // Carve path での addr 計算 else if (meta->carved < meta->capacity) { uint8_t* base = tls->slab_base ? tls->slab_base : tiny_slab_base_for_geometry(tls->ss, tls->slab_idx); if (!base) break; uint16_t idx = meta->carved; if (idx >= meta->capacity) break; const size_t stride = tiny_stride_for_class(class_idx); // Check 5: stride が 0 でないか、overflow していないか if (stride == 0 || stride > 100000) { fprintf(stderr, "[STRIDE_INVALID] cls=%d stride=%zu idx=%u cap=%u\n", class_idx, stride, idx, meta->capacity); dump_tls_sll_state(); abort(); } uint8_t* addr = base + ((size_t)idx * stride); // Check 5b: addr がベースのメモリ範囲内か // (簡易チェック: base より後ろ、かつ reasonable offset) uintptr_t base_addr = (uintptr_t)base; uintptr_t addr_addr = (uintptr_t)addr; size_t max_offset = (size_t)meta->capacity * stride; if (addr_addr < base_addr || (addr_addr - base_addr) > max_offset) { fprintf(stderr, "[ADDR_OUT_OF_BOUNDS] cls=%d base=%p addr=%p offset=%zu max=%zu\n", class_idx, base, addr, (addr_addr - base_addr), max_offset); dump_tls_sll_state(); abort(); } meta->carved++; meta->used++; // ... p = addr; } ``` --- ## 🛠️ 実装手順 ### ステップ 1: Canary 初期化(hakmem_tiny.c) 既にあるはずだが、確認: ```bash grep -n "g_tls_canary_before_sll\|g_tls_canary_after_sll" /mnt/workdisk/public_share/hakmem/core/hakmem_tiny.c | head -20 ``` **存在なら**: Point 4-5 の挿入に進む **存在なし**: Canary の初期化を追加 --- ### ステップ 2: Point 4-5 検査の挿入 ファイル: `core/hakmem_tiny_refill.inc.h` - Line 334 (freelist 処理) に Point 4 挿入 - Line 355 (carve 処理) に Point 5 挿入 --- ### ステップ 3: ビルド & テスト ```bash cd /mnt/workdisk/public_share/hakmem make clean && make RELEASE=0 # デバッグモード有効 # テスト timeout 190 env LD_PRELOAD=./libhakmem.so \ ./mimalloc-bench/out/bench/sh8bench 2>&1 | tail -50 ``` **期待される出力**: - 180秒前後で **制御されたabort()** が発生 - `[CANARY_*]`, `[FREELIST_*]`, `[ADDR_*]` のいずれかのログが表示 - そのログから破壊パターンが明確になる --- ## 📋 Canary Sandwich の利点 1. **最速診断**: クラッシュの 1 秒前に検知 → 詳細ログ 2. **破壊パターン特定**: どの構造体が壊れたか明白 3. **根本原因推定**: パターンから原因が逆算可能 4. **非侵襲的**: 既存コードを大きく変更しない --- ## 🎯 予想される結果 | 破壊パターン | 原因 | 修正 | |-----------|------|------| | `CANARY_BROKEN_BEFORE` | g_tls_sll の手前がバッファオーバーフロー | Stack corruption / グローバル領域破壊 | | `CANARY_BROKEN_AFTER` | g_tls_sll の後ろが上書き | グローバル領域破壊 | | `FREELIST_NEXT_INVALID` | freelist の next が破壊 | Double-free / heap corruption | | `ADDR_OUT_OF_BOUNDS` | carve 計算がオーバーフロー | Integer overflow / stride 計算エラー | | `HEAD_OUT_OF_RANGE` | TLS head が破壊 | TLS SLL push 側のバグ | --- ## ✅ 実装チェックリスト - [ ] Point 1: メモリレイアウト確認(gdb) - [ ] Point 2: Canary 破壊チェック実装 - [ ] Point 3: Head ポインタ破壊検査実装 - [ ] Point 4: Freelist chain 整合性検査実装 - [ ] Point 5: Stride 計算検査実装 - [ ] ビルド成功 - [ ] 180秒テスト実行 → ログ解析 --- **作成日**: 2025-12-04 **方法**: Canary Sandwich - 5層の防御で破壊を検出