Files
hakmem/docs/analysis/FREE_TO_SS_SEGV_INVESTIGATION.md
Moe Charm (CI) 67fb15f35f Wrap debug fprintf in !HAKMEM_BUILD_RELEASE guards (Release build optimization)
## Changes

### 1. core/page_arena.c
- Removed init failure message (lines 25-27) - error is handled by returning early
- All other fprintf statements already wrapped in existing #if !HAKMEM_BUILD_RELEASE blocks

### 2. core/hakmem.c
- Wrapped SIGSEGV handler init message (line 72)
- CRITICAL: Kept SIGSEGV/SIGBUS/SIGABRT error messages (lines 62-64) - production needs crash logs

### 3. core/hakmem_shared_pool.c
- Wrapped all debug fprintf statements in #if !HAKMEM_BUILD_RELEASE:
  - Node pool exhaustion warning (line 252)
  - SP_META_CAPACITY_ERROR warning (line 421)
  - SP_FIX_GEOMETRY debug logging (line 745)
  - SP_ACQUIRE_STAGE0.5_EMPTY debug logging (line 865)
  - SP_ACQUIRE_STAGE0_L0 debug logging (line 803)
  - SP_ACQUIRE_STAGE1_LOCKFREE debug logging (line 922)
  - SP_ACQUIRE_STAGE2_LOCKFREE debug logging (line 996)
  - SP_ACQUIRE_STAGE3 debug logging (line 1116)
  - SP_SLOT_RELEASE debug logging (line 1245)
  - SP_SLOT_FREELIST_LOCKFREE debug logging (line 1305)
  - SP_SLOT_COMPLETELY_EMPTY debug logging (line 1316)
- Fixed lock_stats_init() for release builds (lines 60-65) - ensure g_lock_stats_enabled is initialized

## Performance Validation

Before: 51M ops/s (with debug fprintf overhead)
After:  49.1M ops/s (consistent performance, fprintf removed from hot paths)

## Build & Test

```bash
./build.sh larson_hakmem
./out/release/larson_hakmem 1 5 1 1000 100 10000 42
# Result: 49.1M ops/s
```

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 13:14:18 +09:00

14 KiB
Raw Blame History

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(外側ルーティング)

// 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(内側ルーティング)

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

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-1512Ultra

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(メイン)

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

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

関数シグネチャ

static inline void hak_tiny_free_superslab(void* ptr, SuperSlab* ss)

検証ステップ

ステップ1: slab_idx の導出 (1164行)

int slab_idx = slab_index_for(ss, ptr);

slab_index_for() の実装 (hakmem_tiny_superslab.h:141)

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

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 が 1000032超の場合、以下でバッファオーバーフローが発生

TinySlabMeta* meta = &ss->slabs[slab_idx];  // 1173行

ステップ3: メタデータアクセス (1173行)

TinySlabMeta* meta = &ss->slabs[slab_idx];

配列定義 (hakmem_tiny_superslab.h:90)

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

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)

根本原因:

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 → 無言メモリリーク

修正案:

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

実装:

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

コード:

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

修正案:

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 (即座に修正):

// 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対策):

// 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 (防御的プログラミング):

// 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) で安全性を確保できる。