Files
hakmem/docs/analysis/SLAB_INDEX_FOR_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

slab_index_for/SS範囲チェック実装調査 - 詳細分析報告書

Executive Summary

CRITICAL BUG FOUND: Buffer overflow vulnerability in multiple code paths when slab_index_for() returns -1 (invalid range).

The slab_index_for() function correctly returns -1 when ptr is outside SuperSlab bounds, but calling code does NOT check for -1 before using it as an array index. This causes out-of-bounds memory access to SuperSlab's internal structure.


1. slab_index_for() 実装確認

Location: core/hakmem_tiny_superslab.h (Line 141-148)

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 per slab (2^16)
    int cap = ss_slabs_capacity(ss);
    return (idx >= 0 && idx < cap) ? idx : -1;
    // ^^^^^^^^^^ Returns -1 when:
    //            1. ptr < ss (negative offset)
    //            2. ptr >= ss + (cap * 64KB) (outside capacity)
}

Implementation Analysis

正の部分:

  • Offset calculation: (addr - base) は正確
  • Capacity check: ss_slabs_capacity(ss) で 1MB/2MB どちらにも対応
  • Return value: -1 で明示的に「無効」を示す

問題のある部分:

  • Call site で -1 をチェックしていない箇所が複数存在

ss_slabs_capacity() Implementation (Line 135-138)

static inline int ss_slabs_capacity(const SuperSlab* ss) {
    size_t ss_size = (size_t)1 << ss->lg_size;  // 1MB (20) or 2MB (21)
    return (int)(ss_size / SLAB_SIZE);           // 16 or 32
}

This correctly computes 16 slabs for 1MB or 32 slabs for 2MB.


2. 問題1: tiny_free_fast_ss() での範囲チェック欠落

Location: core/tiny_free_fast.inc.h (Line 91-92)

static inline int tiny_free_fast_ss(SuperSlab* ss, int slab_idx, void* ptr, uint32_t my_tid) {
    TinySlabMeta* meta = &ss->slabs[slab_idx];  // <-- CRITICAL BUG
    // If slab_idx == -1, this accesses ss->slabs[-1]!

Vulnerability Details

When slab_index_for() returns -1:

  • slab_idx = -1 (from tiny_free_fast.inc.h:205)
  • &ss->slabs[-1] points to memory BEFORE the slabs array

Memory layout of SuperSlab:

ss+0000: SuperSlab header (64B)
  - magic (8B)
  - size_class (1B)
  - active_slabs (1B)
  - lg_size (1B)
  - _pad0 (1B)
  - slab_bitmap (4B)
  - freelist_mask (4B)
  - nonempty_mask (4B)
  - total_active_blocks (4B)
  - refcount (4B)
  - listed (4B)
  - partial_epoch (4B)
  - publish_hint (1B)
  - _pad1 (3B)

ss+0040: remote_heads[SLABS_PER_SUPERSLAB_MAX] (128B = 32*8B)
ss+00C0: remote_counts[SLABS_PER_SUPERSLAB_MAX] (128B = 32*4B)
ss+0140: slab_listed[SLABS_PER_SUPERSLAB_MAX] (128B = 32*4B)
ss+01C0: partial_next (8B)

ss+01C8: *** VULNERABILITY ZONE ***
         &ss->slabs[-1] points here (16B before valid slabs[0])
         This overlaps with partial_next and padding!

ss+01D0: ss->slabs[0] (first valid TinySlabMeta, 16B)
         - freelist (8B)
         - used (2B)
         - capacity (2B)
         - owner_tid (4B)

ss+01E0: ss->slabs[1] ...

Impact

When slab_idx = -1:

  1. meta = &ss->slabs[-1] reads/writes 16 bytes at offset 0x1C8
  2. This corrupts partial_next pointer (bytes 8-15 of the buffer)
  3. Subsequent access to meta->owner_tid reads garbage or partially-valid data
  4. tiny_free_is_same_thread_ss() performs ownership check on corrupted data

Root Cause Path

tiny_free_fast()  [tiny_free_fast.inc.h:209]
  ↓
slab_index_for(ss, ptr)  [returns -1 if ptr out of range]
  ↓
tiny_free_fast_ss(ss, slab_idx=-1, ...)  [NO bounds check]
  ↓
&ss->slabs[-1]  [OUT-OF-BOUNDS ACCESS]

3. 問題2: hak_tiny_free_with_slab() での範囲チェック

Location: core/hakmem_tiny_free.inc (Line 96-101)

int slab_idx = slab_index_for(ss, ptr);
int ss_cap = ss_slabs_capacity(ss);
if (__builtin_expect(slab_idx < 0 || slab_idx >= ss_cap, 0)) {
    tiny_debug_ring_record(TINY_RING_EVENT_SUPERSLAB_ADOPT_FAIL, ...);
    return;
}

Status: CORRECT

  • Bounds check present: slab_idx < 0 || slab_idx >= ss_cap
  • Early return prevents OOB access

4. 問題3: hak_tiny_free_superslab() での範囲チェック

Location: core/hakmem_tiny_free.inc (Line 1164-1172)

int slab_idx = slab_index_for(ss, ptr);
size_t ss_size = (size_t)1ULL << ss->lg_size;
uintptr_t ss_base = (uintptr_t)ss;
if (__builtin_expect(slab_idx < 0, 0)) {
    uintptr_t aux = tiny_remote_pack_diag(0xBAD1u, ss_base, ss_size, (uintptr_t)ptr);
    tiny_debug_ring_record(...);
    return;
}

Status: PARTIAL

  • Checks slab_idx < 0
  • ⚠️ Missing check: slab_idx >= ss_cap
    • If slab_idx >= capacity, next line accesses out-of-bounds:
      TinySlabMeta* meta = &ss->slabs[slab_idx];  // Can OOB if idx >= 32
      

Vulnerability Scenario

For 1MB SuperSlab (cap=16):

  • If ptr is at offset 1088KB (0x110000), off >> 16 = 0x11 = 17
  • slab_index_for() returns -1 (not >= cap=16)
  • Line 1167 check passes: -1 < 0? YES → returns
  • OK (caught by < 0 check)

For 2MB SuperSlab (cap=32):

  • If ptr is at offset 2112KB (0x210000), off >> 16 = 0x21 = 33
  • slab_index_for() returns -1 (not >= cap=32)
  • Line 1167 check passes: -1 < 0? YES → returns
  • OK (caught by < 0 check)

Actually, since slab_index_for() returns -1 when idx >= cap, the < 0 check is sufficient!


5. 問題4: Magazine spill 経路での範囲チェック

Location: core/hakmem_tiny_free.inc (Line 305-316)

SuperSlab* owner_ss = hak_super_lookup(it.ptr);
if (owner_ss && owner_ss->magic == SUPERSLAB_MAGIC) {
    int slab_idx = slab_index_for(owner_ss, it.ptr);
    TinySlabMeta* meta = &owner_ss->slabs[slab_idx];  // <-- NO CHECK!
    *(void**)it.ptr = meta->freelist;
    meta->freelist = it.ptr;
    meta->used--;

Status: CRITICAL BUG

  • No bounds check for slab_idx
  • slab_idx = -1 → &owner_ss->slabs[-1] out-of-bounds access

Similar Issue at Line 464

int slab_idx = slab_index_for(ss_owner, it.ptr);
TinySlabMeta* meta = &ss_owner->slabs[slab_idx];  // <-- NO CHECK!

6. 問題5: tiny_free_fast.inc.h:205 での範囲チェック

Location: core/tiny_free_fast.inc.h (Line 205-209)

int slab_idx = slab_index_for(ss, ptr);
uint32_t self_tid = tiny_self_u32();

// Box 6 Boundary: Try same-thread fast path
if (tiny_free_fast_ss(ss, slab_idx, ptr, self_tid)) {  // <-- PASSES slab_idx=-1

Status: CRITICAL BUG

  • No bounds check before calling tiny_free_fast_ss()
  • tiny_free_fast_ss() immediately accesses ss->slabs[slab_idx]

7. SS範囲チェック全体サマリー

Code Path File:Line Check Status Severity
hak_tiny_free_with_slab() hakmem_tiny_free.inc:96-101 OK (both < and >=) None
hak_tiny_free_superslab() hakmem_tiny_free.inc:1164-1172 OK (checks < 0, -1 means invalid) None
magazine spill path 1 hakmem_tiny_free.inc:305-316 NO CHECK CRITICAL
magazine spill path 2 hakmem_tiny_free.inc:464-468 NO CHECK CRITICAL
tiny_free_fast_ss() tiny_free_fast.inc.h:91-92 NO CHECK on entry CRITICAL
tiny_free_fast() call site tiny_free_fast.inc.h:205-209 NO CHECK before call CRITICAL

8. 所有権/範囲ガード詳細

Box 3: Ownership Encapsulation (slab_handle.h)

slab_try_acquire() (Line 32-78):

static inline SlabHandle slab_try_acquire(SuperSlab* ss, int idx, uint32_t tid) {
    if (!ss || ss->magic != SUPERSLAB_MAGIC) return {0};
    
    int cap = ss_slabs_capacity(ss);
    if (idx < 0 || idx >= cap) {  // <-- CORRECT: Range check
        return {0};
    }
    
    TinySlabMeta* m = &ss->slabs[idx];
    if (!ss_owner_try_acquire(m, tid)) {
        return {0};
    }
    
    h.valid = 1;
    return h;
}

Status: CORRECT

  • Range validation present before array access
  • owner_tid check done safely

9. TOCTOU 問題の可能性

Check-Then-Use Pattern Analysis

In tiny_free_fast_ss():

  1. Time T0: slab_idx = slab_index_for(ss, ptr) (no check)
  2. Time T1: meta = &ss->slabs[slab_idx] (use)
  3. Time T2: tiny_free_is_same_thread_ss() reads meta->owner_tid

TOCTOU Race Scenario:

  • Thread A: slab_idx = slab_index_for(ss, ptr) → slab_idx = 0 (valid)
  • Thread B: [simultaneously] SuperSlab ss is unmapped and remapped elsewhere
  • Thread A: &ss->slabs[0] now points to wrong memory
  • Thread A: Reads/writes garbage data

Status: UNLIKELY but POSSIBLE

  • Most likely attack: freeing to already-freed SuperSlab
  • Mitigated by: hak_super_lookup() validation (SUPERSLAB_MAGIC check)
  • But: If magic still valid, race exists

10. 発見したバグ一覧

Bug #1: tiny_free_fast_ss() - No bounds check on slab_idx

File: core/tiny_free_fast.inc.h Line: 91-92 Severity: CRITICAL Impact: Buffer overflow when slab_index_for() returns -1

static inline int tiny_free_fast_ss(SuperSlab* ss, int slab_idx, void* ptr, uint32_t my_tid) {
    TinySlabMeta* meta = &ss->slabs[slab_idx];  // BUG: No check if slab_idx < 0 or >= capacity

Fix:

if (slab_idx < 0 || slab_idx >= ss_slabs_capacity(ss)) return 0;
TinySlabMeta* meta = &ss->slabs[slab_idx];

Bug #2: Magazine spill path (first occurrence) - No bounds check

File: core/hakmem_tiny_free.inc Line: 305-308 Severity: CRITICAL Impact: Buffer overflow in magazine recycling

int slab_idx = slab_index_for(owner_ss, it.ptr);
TinySlabMeta* meta = &owner_ss->slabs[slab_idx];  // BUG: No bounds check
*(void**)it.ptr = meta->freelist;

Fix:

int slab_idx = slab_index_for(owner_ss, it.ptr);
if (slab_idx < 0 || slab_idx >= ss_slabs_capacity(owner_ss)) continue;
TinySlabMeta* meta = &owner_ss->slabs[slab_idx];

Bug #3: Magazine spill path (second occurrence) - No bounds check

File: core/hakmem_tiny_free.inc Line: 464-467 Severity: CRITICAL Impact: Same as Bug #2

int slab_idx = slab_index_for(ss_owner, it.ptr);
TinySlabMeta* meta = &ss_owner->slabs[slab_idx];  // BUG: No bounds check

Fix: Same as Bug #2

Bug #4: tiny_free_fast() call site - No bounds check before tiny_free_fast_ss()

File: core/tiny_free_fast.inc.h Line: 205-209 Severity: HIGH (depends on function implementation) Impact: Passes invalid slab_idx to tiny_free_fast_ss()

int slab_idx = slab_index_for(ss, ptr);
uint32_t self_tid = tiny_self_u32();

// Box 6 Boundary: Try same-thread fast path
if (tiny_free_fast_ss(ss, slab_idx, ptr, self_tid)) {  // Passes slab_idx without checking

Fix:

int slab_idx = slab_index_for(ss, ptr);
if (slab_idx < 0 || slab_idx >= ss_slabs_capacity(ss)) {
    hak_tiny_free(ptr);  // Fallback to slow path
    return;
}
uint32_t self_tid = tiny_self_u32();
if (tiny_free_fast_ss(ss, slab_idx, ptr, self_tid)) {

11. 修正提案

Priority 1: Fix tiny_free_fast_ss() entry point

File: core/tiny_free_fast.inc.h (Line 91)

static inline int tiny_free_fast_ss(SuperSlab* ss, int slab_idx, void* ptr, uint32_t my_tid) {
    // ADD: Range validation
    if (__builtin_expect(slab_idx < 0 || slab_idx >= ss_slabs_capacity(ss), 0)) {
        return 0;  // Invalid index → delegate to slow path
    }

    TinySlabMeta* meta = &ss->slabs[slab_idx];
    // ... rest of function

Rationale: This is the fastest fix (5 bytes code addition) that prevents the OOB access.

Priority 2: Fix magazine spill paths

File: core/hakmem_tiny_free.inc (Line 305 and 464)

At both locations, add bounds check:

int slab_idx = slab_index_for(owner_ss, it.ptr);
if (slab_idx < 0 || slab_idx >= ss_slabs_capacity(owner_ss)) {
    continue;  // Skip if invalid
}
TinySlabMeta* meta = &owner_ss->slabs[slab_idx];

Rationale: Magazine spill is not a fast path, so small overhead acceptable.

Priority 3: Add bounds check at tiny_free_fast() call site

File: core/tiny_free_fast.inc.h (Line 205)

Add validation before calling tiny_free_fast_ss():

int slab_idx = slab_index_for(ss, ptr);
if (__builtin_expect(slab_idx < 0 || slab_idx >= ss_slabs_capacity(ss), 0)) {
    hak_tiny_free(ptr);  // Fallback
    return;
}
uint32_t self_tid = tiny_self_u32();

if (tiny_free_fast_ss(ss, slab_idx, ptr, self_tid)) {
    return;
}

Rationale: Defense in depth - validate at call site AND in callee.


12. Test Case to Trigger Bugs

void test_slab_index_for_oob() {
    SuperSlab* ss = allocate_1mb_superslab();
    
    // Case 1: Pointer before SuperSlab
    void* ptr_before = (void*)((uintptr_t)ss - 1024);
    int idx = slab_index_for(ss, ptr_before);
    assert(idx == -1);  // Should return -1
    
    // Case 2: Pointer at SS end (just beyond capacity)
    void* ptr_after = (void*)((uintptr_t)ss + (1024*1024));
    idx = slab_index_for(ss, ptr_after);
    assert(idx == -1);  // Should return -1
    
    // Case 3: tiny_free_fast() with OOB pointer
    tiny_free_fast(ptr_after);  // BUG: Calls tiny_free_fast_ss(ss, -1, ptr, tid)
    // Without fix: Accesses ss->slabs[-1] → buffer overflow
}

Summary

Issue Location Severity Status
slab_index_for() implementation hakmem_tiny_superslab.h:141 Info Correct
tiny_free_fast_ss() bounds check tiny_free_fast.inc.h:91 CRITICAL Bug
Magazine spill #1 bounds check hakmem_tiny_free.inc:305 CRITICAL Bug
Magazine spill #2 bounds check hakmem_tiny_free.inc:464 CRITICAL Bug
tiny_free_fast() call site tiny_free_fast.inc.h:205 HIGH Bug
slab_try_acquire() bounds check slab_handle.h:32 Info Correct
hak_tiny_free_superslab() bounds check hakmem_tiny_free.inc:1164 Info Correct