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

474 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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