Files
hakmem/docs/analysis/FREE_TO_SS_SEGV_INVESTIGATION.md

474 lines
14 KiB
Markdown
Raw Normal View History

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
# 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 が 1000032超の場合、以下でバッファオーバーフローが発生
```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) で安全性を確保できる。