# FREE_TO_SS=1 SEGV原因調査レポート ## 調査日時 2025-11-06 ## 問題概要 `HAKMEM_TINY_FREE_TO_SS=1` (環境変数) を有効にすると、必ずSEGVが発生する。 ## 調査方法論 1. hakmem.c の FREE_TO_SS 経路を全て特定 2. hak_super_lookup() と hak_tiny_free_superslab() の実装を検証 3. メモリ安全性とTOCTOU競合を分析 4. 配列境界チェックの完全性を確認 --- ## 第1部: FREE_TO_SS経路の全体像 ### 発見:リソース管理に1つ明らかなバグあり(後述) **FREE_TO_SSは2つのエントリポイント:** #### エントリポイント1: `hakmem.c:914-938`(外側ルーティング) ```c // SS-first (A/B): only when FREE_TO_SS=1 { if (s_free_to_ss_env) { // 行921 extern int g_use_superslab; if (g_use_superslab != 0) { // 行923 SuperSlab* ss = hak_super_lookup(ptr); // 行924 if (ss && ss->magic == SUPERSLAB_MAGIC) { int sidx = slab_index_for(ss, ptr); // 行927 int cap = ss_slabs_capacity(ss); // 行928 if (sidx >= 0 && sidx < cap) { // 行929: 範囲ガード hak_tiny_free(ptr); // 行931 return; } } } } } ``` **呼び出し結果:** `hak_tiny_free(ptr)` → hak_tiny_free.inc:1459 --- #### エントリポイント2: `hakmem.c:967-980`(内側ルーティング) ```c // A/B: Force precise Tiny slow free (SS freelist path + publish on first-free) #ifdef HAKMEM_TINY_PHASE6_BOX_REFACTOR // デフォルト有効(=1) { if (s_free_to_ss) { // 行967 SuperSlab* ss = hak_super_lookup(ptr); // 行969 if (ss && ss->magic == SUPERSLAB_MAGIC) { int sidx = slab_index_for(ss, ptr); // 行971 int cap = ss_slabs_capacity(ss); // 行972 if (sidx >= 0 && sidx < cap) { // 行973: 範囲ガード hak_tiny_free(ptr); // 行974 return; } } // Fallback: if SS not resolved or invalid, keep normal tiny path below } } ``` **呼び出し結果:** `hak_tiny_free(ptr)` → hak_tiny_free.inc:1459 --- ### hak_tiny_free() の内部ルーティング **エントリポイント3:** `hak_tiny_free.inc:1469-1487`(BENCH_SLL_ONLY) ```c if (g_use_superslab) { SuperSlab* ss = hak_super_lookup(ptr); // 1471行 if (ss && ss->magic == SUPERSLAB_MAGIC) { class_idx = ss->size_class; } } ``` **エントリポイント4:** `hak_tiny_free.inc:1490-1512`(Ultra) ```c if (g_tiny_ultra) { if (g_use_superslab) { SuperSlab* ss = hak_super_lookup(ptr); // 1494行 if (ss && ss->magic == SUPERSLAB_MAGIC) { class_idx = ss->size_class; } } } ``` **エントリポイント5:** `hak_tiny_free.inc:1517-1524`(メイン) ```c if (g_use_superslab) { fast_ss = hak_super_lookup(ptr); // 1518行 if (fast_ss && fast_ss->magic == SUPERSLAB_MAGIC) { fast_class_idx = fast_ss->size_class; // 1520行 ★★★ BUG1 } else { fast_ss = NULL; } } ``` **最終処理:** `hak_tiny_free.inc:1554-1566` ```c SuperSlab* ss = fast_ss; if (!ss && g_use_superslab) { ss = hak_super_lookup(ptr); if (!(ss && ss->magic == SUPERSLAB_MAGIC)) { ss = NULL; } } if (ss && ss->magic == SUPERSLAB_MAGIC) { hak_tiny_free_superslab(ptr, ss); // 1563行: 最終的な呼び出し HAK_STAT_FREE(ss->size_class); // 1564行 ★★★ BUG2 return; } ``` --- ## 第2部: hak_tiny_free_superslab() 実装分析 **位置:** `hakmem_tiny_free.inc:1160` ### 関数シグネチャ ```c static inline void hak_tiny_free_superslab(void* ptr, SuperSlab* ss) ``` ### 検証ステップ #### ステップ1: slab_idx の導出 (1164行) ```c int slab_idx = slab_index_for(ss, ptr); ``` **slab_index_for() の実装** (`hakmem_tiny_superslab.h:141`): ```c static inline int slab_index_for(const SuperSlab* ss, const void* p) { uintptr_t base = (uintptr_t)ss; uintptr_t addr = (uintptr_t)p; uintptr_t off = addr - base; int idx = (int)(off >> 16); // 64KB単位で除算 int cap = ss_slabs_capacity(ss); // 1MB=16, 2MB=32 return (idx >= 0 && idx < cap) ? idx : -1; } ``` #### ステップ2: slab_idx の範囲ガード (1167-1172行) ```c if (__builtin_expect(slab_idx < 0, 0)) { // ...エラー処理... if (g_tiny_safe_free_strict) { raise(SIGUSR2); return; } return; } ``` **問題:** slab_idx がメモリ管理下の外でオーバーフローしている可能性がある - slab_index_for() は -1 を返す場合を正しく処理しているが、 - 上位ビットのオーバーフローは検出していない。 例: slab_idx が 10000(32超)の場合、以下でバッファオーバーフローが発生: ```c TinySlabMeta* meta = &ss->slabs[slab_idx]; // 1173行 ``` #### ステップ3: メタデータアクセス (1173行) ```c TinySlabMeta* meta = &ss->slabs[slab_idx]; ``` **配列定義** (`hakmem_tiny_superslab.h:90`): ```c TinySlabMeta slabs[SLABS_PER_SUPERSLAB_MAX]; // Max = 32 ``` **危険: slab_idx がこの検証をスキップできる場合:** - slab_index_for() は (`idx >= 0 && idx < cap`) をチェックしているが、 - **下位呼び出しで hak_super_lookup() が不正なSSを返す可能性がある** - **TOCTOU: lookup 後に SS が解放される可能性がある** #### ステップ4: SAFE_FREE チェック (1188-1213行) ```c if (__builtin_expect(g_tiny_safe_free, 0)) { size_t blk = g_tiny_class_sizes[ss->size_class]; // ★★★ BUG3 // ... } ``` **BUG3: ss->size_class の範囲チェックなし!** - `ss->size_class` は 0..7 であるべき (TINY_NUM_CLASSES=8) - しかし検証されていない - 腐ったSSメモリを読むと、任意の値を持つ可能性 - `g_tiny_class_sizes[ss->size_class]` にアクセスすると OOB (Out-Of-Bounds) --- ## 第3部: バグ・脆弱性・TOCTOU分析 ### BUG #1: size_class の範囲チェック欠落 ★★★ CRITICAL **位置:** - `hakmem_tiny_free.inc:1520` (fast_class_idx の導出) - `hakmem_tiny_free.inc:1189` (g_tiny_class_sizes のアクセス) - `hakmem_tiny_free.inc:1564` (HAK_STAT_FREE) **根本原因:** ```c if (fast_ss && fast_ss->magic == SUPERSLAB_MAGIC) { fast_class_idx = fast_ss->size_class; // チェックなし! } // ... if (g_tiny_safe_free, 0)) { size_t blk = g_tiny_class_sizes[ss->size_class]; // OOB! } // ... HAK_STAT_FREE(ss->size_class); // OOB! ``` **問題:** - `size_class` は SuperSlab 初期化時に設定される - しかしメモリ破損やTOCTOUで腐った値を持つ可能性 - チェック: `ss->size_class >= 0 && ss->size_class < TINY_NUM_CLASSES` が不足 **影響:** 1. `g_tiny_class_sizes[bad_size_class]` → OOB read → SEGV 2. `HAK_STAT_FREE(bad_size_class)` → グローバル配列 OOB write → SEGV/無言破損 3. `meta->capacity` で計算時に wrong class size → 無言メモリリーク **修正案:** ```c if (ss && ss->magic == SUPERSLAB_MAGIC) { // ADD: Validate size_class if (ss->size_class >= TINY_NUM_CLASSES) { // Invalid size class tiny_debug_ring_record(TINY_RING_EVENT_REMOTE_INVALID, 0x99, ptr, ss->size_class); if (g_tiny_safe_free_strict) { raise(SIGUSR2); } return; } hak_tiny_free_superslab(ptr, ss); } ``` --- ### BUG #2: hak_super_lookup() の TOCTOU 競合 ★★ HIGH **位置:** `hakmem_super_registry.h:73-106` **実装:** ```c static inline SuperSlab* hak_super_lookup(void* ptr) { if (!g_super_reg_initialized) return NULL; // Try both 1MB and 2MB alignments for (int lg = 20; lg <= 21; lg++) { // ... linear probing ... SuperRegEntry* e = &g_super_reg[(h + i) & SUPER_REG_MASK]; uintptr_t b = atomic_load_explicit((_Atomic uintptr_t*)&e->base, memory_order_acquire); if (b == base && e->lg_size == lg) { SuperSlab* ss = atomic_load_explicit(&e->ss, memory_order_acquire); if (!ss) return NULL; // Entry cleared by unregister if (ss->magic != SUPERSLAB_MAGIC) return NULL; // Being freed return ss; } } return NULL; } ``` **TOCTOU シナリオ:** ``` Thread A: ss = hak_super_lookup(ptr) ← NULL チェック + magic チェック成功 ↓ ↓ (Context switch) ↓ Thread B: hak_super_unregister() 呼び出し ↓ base = 0 を書き込み (release semantics) ↓ munmap() を呼び出し ↓ Thread A: TinySlabMeta* meta = &ss->slabs[slab_idx] ← SEGV! (ss が unmapped memory のため) ``` **根本原因:** - `hak_super_lookup()` は magic チェック時の SS validity をチェックしているが、 - **チェック後、メタデータアクセス時にメモリが unmapped される可能性** - atomic_load で acquire したのに、その後の memory access order が保証されない **修正案:** - `hak_super_unregister()` の前に refcount 検証 - または: `hak_tiny_free_superslab()` 内で再度 magic チェック --- ### BUG #3: ss->lg_size の範囲検証欠落 ★ MEDIUM **位置:** `hakmem_tiny_free.inc:1165` **コード:** ```c size_t ss_size = (size_t)1ULL << ss->lg_size; // lg_size が 20..21 であると仮定 ``` **問題:** - `ss->lg_size` が腐った値 (22+) を持つと、オーバーフロー - 例: `1ULL << 64` → undefined behavior (シフト量 >= 64) - 結果: `ss_size` が 0 または corrupt **修正案:** ```c if (ss->lg_size < 20 || ss->lg_size > 21) { // Invalid SuperSlab size tiny_debug_ring_record(TINY_RING_EVENT_REMOTE_INVALID, 0x9A, ptr, ss->lg_size); if (g_tiny_safe_free_strict) { raise(SIGUSR2); } return; } size_t ss_size = (size_t)1ULL << ss->lg_size; ``` --- ### TOCTOU #1: slab_index_for 後の pointer validity **流れ:** ``` 1. hak_super_lookup() ← lock-free, acquire semantics 2. slab_index_for() ← pointer math, local calculation 3. hak_tiny_free_superslab(ptr, ss) ← ss は古い可能性 ``` **競合シナリオ:** ``` Thread A: ss = hak_super_lookup(ptr) ✓ valid sidx = slab_index_for(ss, ptr) ✓ valid hak_tiny_free_superslab(ptr, ss) ↓ (Context switch) ↓ Thread B: [別プロセス] SuperSlab が MADV_FREE される ↓ pages が reclaim される ↓ Thread A: TinySlabMeta* meta = &ss->slabs[sidx] ← SEGV! ``` --- ## 第4部: 発見したバグの優先度 | ID | 場所 | 種類 | 深刻度 | 原因 | |----|------|------|--------|------| | BUG#1 | hakmem_tiny_free.inc:1520, 1189, 1564 | OOB | CRITICAL | size_class 未検証 | | BUG#2 | hakmem_super_registry.h:73 | TOCTOU | HIGH | lookup 後の mmap/munmap 競合 | | BUG#3 | hakmem_tiny_free.inc:1165 | OOB | MEDIUM | lg_size オーバーフロー | | TOCTOU#1 | hakmem.c:924, 969 | Race | HIGH | pointer invalidation | | Missing | hakmem.c:927-929, 971-973 | Logic | HIGH | cap チェックのみ、size_class 検証なし | --- ## 第5部: SEGV の最も可能性が高い原因 ### 最確と思われる原因チェーン ``` 1. HAKMEM_TINY_FREE_TO_SS=1 を有効化 ↓ 2. Free call → hakmem.c:967-980 (内側ルーティング) ↓ 3. hak_super_lookup(ptr) で SS を取得 ↓ 4. slab_index_for(ss, ptr) で sidx チェック ← OK (範囲内) ↓ 5. hak_tiny_free(ptr) → hak_tiny_free.inc:1554-1564 ↓ 6. ss->magic == SUPERSLAB_MAGIC ← OK ↓ 7. hak_tiny_free_superslab(ptr, ss) を呼び出し ↓ 8. TinySlabMeta* meta = &ss->slabs[slab_idx] ← ✓ ↓ 9. if (g_tiny_safe_free, 0) { size_t blk = g_tiny_class_sizes[ss->size_class]; ↑↑↑ ss->size_class が [0, 8) 外の値 ↓ SEGV! (OOB read または OOB write) } ``` ### または (別シナリオ): ``` 1. HAKMEM_TINY_FREE_TO_SS=1 ↓ 2. hak_super_lookup() で SS を取得して magic チェック ← OK ↓ 3. Context switch → 別スレッドが hak_super_unregister() 呼び出し ↓ 4. SuperSlab が munmap される ↓ 5. TinySlabMeta* meta = &ss->slabs[slab_idx] ↓ SEGV! (unmapped memory access) ``` --- ## 推奨される修正順序 ### 優先度 1 (即座に修正): ```c // hakmem_tiny_free.inc:1553-1566 に追加 if (ss && ss->magic == SUPERSLAB_MAGIC) { // CRITICAL FIX: Validate size_class if (ss->size_class >= TINY_NUM_CLASSES) { tiny_debug_ring_record(TINY_RING_EVENT_REMOTE_INVALID, (uint16_t)0xBAD_SIZE_CLASS, ptr, ss->size_class); if (g_tiny_safe_free_strict) { raise(SIGUSR2); } return; } // CRITICAL FIX: Validate lg_size if (ss->lg_size < 20 || ss->lg_size > 21) { tiny_debug_ring_record(TINY_RING_EVENT_REMOTE_INVALID, (uint16_t)0xBAD_LG_SIZE, ptr, ss->lg_size); if (g_tiny_safe_free_strict) { raise(SIGUSR2); } return; } hak_tiny_free_superslab(ptr, ss); HAK_STAT_FREE(ss->size_class); return; } ``` ### 優先度 2 (TOCTOU対策): ```c // hakmem_tiny_free_superslab() 内冒頭に追加 if (ss->magic != SUPERSLAB_MAGIC) { // Re-check magic in case of TOCTOU tiny_debug_ring_record(TINY_RING_EVENT_REMOTE_INVALID, (uint16_t)0xTOCTOU_MAGIC, ptr, 0); if (g_tiny_safe_free_strict) { raise(SIGUSR2); } return; } ``` ### 優先度 3 (防御的プログラミング): ```c // hakmem.c:924-932, 969-976 の両方で、size_class も検証 if (sidx >= 0 && sidx < cap && ss->size_class < TINY_NUM_CLASSES) { hak_tiny_free(ptr); return; } ``` --- ## 結論 FREE_TO_SS=1 で SEGV が発生する最主要な理由は、**size_class の範囲チェック欠落**である。 腐った SuperSlab メモリ (corruption, TOCTOU) を指す場合でも、 proper validation の欠落が root cause。 修正後は厳格なメモリ検証 (magic + size_class + lg_size) で安全性を確保できる。