# Box Theory 残り境界の徹底検証レポート ## 調査概要 HAKMEM tiny allocator の Box Theory(箱理論)における 3つの残り境界(Box 3, 2, 4)の詳細検証結果。 検証対象ファイル: - core/hakmem_tiny_free.inc (メイン free ロジック) - core/slab_handle.h (所有権管理) - core/tiny_publish.c (publish 実装) - core/tiny_mailbox.c (mailbox 実装) - core/tiny_remote.c (remote queue 操作) - core/hakmem_tiny_superslab.h (owner/drain 実装) - core/hakmem_tiny.c (publish/adopt 実装) --- ## Box 3: Same-thread Freelist Push 検証 ### 不変条件 **freelist への push は `owner_tid == my_tid` の時のみ** ### 検証結果 #### ✅ 問題なし: slab_handle.h の slab_freelist_push() ```c // core/slab_handle.h:205-236 static inline int slab_freelist_push(SlabHandle* h, void* ptr) { if (!h || !h->valid) { return 0; // Box: No ownership → FAIL } // ... // Ownership guaranteed by valid==1 → safe to modify freelist *(void**)ptr = h->meta->freelist; h->meta->freelist = ptr; // ... return 1; } ``` ✓ 所有権チェック(valid==1)を確認後のみ freelist 操作 ✓ 直接 freelist push の唯一の安全な入口 #### ✅ 問題なし: hakmem_tiny_free.inc の same-thread freelist push ```c // core/hakmem_tiny_free.inc:1044-1076 if (!g_tiny_force_remote && meta->owner_tid != 0 && meta->owner_tid == my_tid) { // Fast path: Direct freelist push (same-thread) // ... if (!tiny_remote_guard_allow_local_push(ss, slab_idx, meta, ptr, "local_free", my_tid)) { // Fall back to remote if guard fails int transitioned = ss_remote_push(ss, slab_idx, ptr); // ... return; } void* prev = meta->freelist; *(void**)ptr = prev; meta->freelist = ptr; // ← Safe freelist push // ... } ``` ✓ owner_tid == my_tid の厳密なチェック ✓ guard check で追加の安全性確保 ✓ owner_tid != my_tid の場合は確実に remote_push へ #### ✅ 問題なし: publish 時の owner_tid リセット ```c // core/hakmem_tiny.c:639-670 (ss_partial_publish) for (int s = 0; s < cap_pub; s++) { uint32_t prev = __atomic_exchange_n(&ss->slabs[s].owner_tid, 0u, __ATOMIC_RELEASE); // ...記録のみ... } ``` ✓ publish 時に明示的に owner_tid=0 をセット ✓ ATOMIC_RELEASE で memory barrier 確保 **Box 3 評価: ✅ PASS - 境界は堅牢。直接 freelist push は所有権ガード完全。** --- ## Box 2: Remote Push の重複(dup_push)検証 ### 不変条件 **同じノードを remote queue に二重 push しない** ### 検証結果 #### ✅ 問題なし: tiny_remote_queue_contains_guard() ```c // core/hakmem_tiny_free.inc:10-30 static inline int tiny_remote_queue_contains_guard(SuperSlab* ss, int slab_idx, void* target) { if (!ss || slab_idx < 0) return 0; uintptr_t cur = atomic_load_explicit(&ss->remote_heads[slab_idx], memory_order_acquire); int limit = 8192; while (cur && limit-- > 0) { if ((void*)cur == target) { return 1; // Found duplicate } uintptr_t next; if (__builtin_expect(g_remote_side_enable, 0)) { next = tiny_remote_side_get(ss, slab_idx, (void*)cur); } else { next = atomic_load_explicit((_Atomic uintptr_t*)cur, memory_order_relaxed); } cur = next; } if (limit <= 0) { return 1; // fail-safe: treat unbounded traversal as duplicate } return 0; } ``` ✓ 8192 ノード上限でループ安全化 ✓ Fail-safe: 上限に達したら dup として扱う(conservative) ✓ remote_side 両対応 #### ✅ 問題なし: free 時の dup_remote チェック ```c // core/hakmem_tiny_free.inc:1183-1197 int dup_remote = tiny_remote_queue_contains_guard(ss, slab_idx, ptr); if (!dup_remote && __builtin_expect(g_remote_side_enable, 0)) { dup_remote = (head_word == TINY_REMOTE_SENTINEL) || tiny_remote_side_contains(ss, slab_idx, ptr); } // ... if (dup_remote) { uintptr_t aux = tiny_remote_pack_diag(0xA214u, ss_base, ss_size, (uintptr_t)ptr); tiny_remote_watch_mark(ptr, "dup_prevent", my_tid); tiny_remote_watch_note("dup_prevent", ss, slab_idx, ptr, 0xA214u, my_tid, 0); tiny_debug_ring_record(TINY_RING_EVENT_REMOTE_INVALID, (uint16_t)ss->size_class, ptr, aux); if (g_tiny_safe_free_strict) { raise(SIGUSR2); return; } return; // ← Prevent double-push } ``` ✓ 二重チェック(queue walk + side table) ✓ A214 コード(dup_prevent)で検出を記録 ✓ Fail-Fast: 検出後は即座に return(push しない) #### ✅ 問題なし: ss_remote_push() の CAS ループ ```c // core/hakmem_tiny_superslab.h:282-376 _Atomic(uintptr_t)* head = &ss->remote_heads[slab_idx]; uintptr_t old; do { old = atomic_load_explicit(head, memory_order_acquire); if (!g_remote_side_enable) { *(void**)ptr = (void*)old; // legacy embedding } } while (!atomic_compare_exchange_weak_explicit(head, &old, (uintptr_t)ptr, memory_order_release, memory_order_relaxed)); ``` ✓ CAS ループで atomic な single-pop-then-push ✓ ptr は new head になるのみ(二重化不可) #### ✅ 問題なし: tiny_remote_side_set() で remote_side への重複登録防止 ```c // core/tiny_remote.c:529-575 uint32_t i = hmix(k) & (REM_SIDE_SIZE - 1); for (uint32_t n=0; nsize_class, node, aux); // ...dump + raise... } return; // ← Prevent duplicate } } ``` ✓ Side table の CAS-or-collision チェック ✓ A212 コード(dup_push)で検出・記録 ✓ 既に key=k の entry があれば即座に return(二重登録防止) **Box 2 評価: ✅ PASS - 二重 push は 3 層で防止。A214/A212 コード検出も有効。** --- ## Box 4: Publish/Fetch は通知のみ検証 ### 不変条件 **publish/fetch 側から drain や owner_tid を触らない** ### 検証結果 #### ✅ 問題なし: tiny_publish_notify() は通知のみ ```c // core/tiny_publish.c:13-34 void tiny_publish_notify(int class_idx, SuperSlab* ss, int slab_idx) { if (__builtin_expect(class_idx < 0 || class_idx >= TINY_NUM_CLASSES, 0)) { tiny_debug_ring_record(TINY_RING_EVENT_SUPERSLAB_ADOPT_FAIL, (uint16_t)0xEEu, ss, (uintptr_t)class_idx); return; } g_pub_notify_calls[class_idx]++; tiny_debug_ring_record(TINY_RING_EVENT_SUPERSLAB_PUBLISH, (uint16_t)class_idx, ss, (uintptr_t)slab_idx); // ...トレース(副作用なし)... tiny_mailbox_publish(class_idx, ss, slab_idx); // ← 単なる通知 } ``` ✓ drain 呼び出しなし ✓ owner_tid 操作なし ✓ mailbox へ (class_idx, ss, slab_idx) の 3-tuple を記録するのみ #### ✅ 問題なし: tiny_mailbox_publish() は記録のみ ```c // core/tiny_mailbox.c:109-119 void tiny_mailbox_publish(int class_idx, SuperSlab* ss, int slab_idx) { tiny_mailbox_register(class_idx); // Encode entry locally uintptr_t ent = ((uintptr_t)ss) | ((uintptr_t)slab_idx & 0x3Fu); uint32_t slot = g_tls_mailbox_slot[class_idx]; tiny_debug_ring_record(TINY_RING_EVENT_MAILBOX_PUBLISH, ...); atomic_store_explicit(&g_pub_mailbox_entries[class_idx][slot], ent, memory_order_release); // ← 単なる記録 } ``` ✓ drain 呼び出しなし ✓ owner_tid 操作なし ✓ メモリへの記録のみ #### ✅ 問題なし: tiny_mailbox_fetch() は読み込みと提示のみ ```c // core/tiny_mailbox.c:130-252 uintptr_t tiny_mailbox_fetch(int class_idx) { // ...スロット走査... uintptr_t ent = atomic_exchange_explicit(mailbox, (uintptr_t)0, memory_order_acq_rel); if (ent) { g_pub_mail_hits[class_idx]++; SuperSlab* ss = (SuperSlab*)(ent & ~((uintptr_t)SUPERSLAB_SIZE_MIN - 1u)); int slab = (int)(ent & 0x3Fu); tiny_debug_ring_record(TINY_RING_EVENT_MAILBOX_FETCH, ...); return ent; // ← ヒントを返すのみ } return (uintptr_t)0; } ``` ✓ drain 呼び出しなし ✓ owner_tid 操作なし ✓ fetch は単なる "ヒント提供"(候補の推薦) #### ✅ 問題なし: ss_partial_publish() は owner リセット + unbind + 通知 ```c // core/hakmem_tiny.c:639-717 void ss_partial_publish(int class_idx, SuperSlab* ss) { if (!ss) return; // ① owner_tid リセット(publish の一部) unsigned prev = atomic_exchange_explicit(&ss->listed, 1u, memory_order_acq_rel); if (prev != 0u) return; // already listed // ② 所有者をリセット(adopt 準備) int cap_pub = ss_slabs_capacity(ss); for (int s = 0; s < cap_pub; s++) { uint32_t prev = __atomic_exchange_n(&ss->slabs[s].owner_tid, 0u, __ATOMIC_RELEASE); // ...記録のみ... } // ③ TLS unbind(publish 側が使わなくするため) extern __thread TinyTLSSlab g_tls_slabs[]; if (g_tls_slabs[class_idx].ss == ss) { g_tls_slabs[class_idx].ss = NULL; g_tls_slabs[class_idx].meta = NULL; g_tls_slabs[class_idx].slab_base = NULL; g_tls_slabs[class_idx].slab_idx = 0; } // ④ hint 計算(提示用) // ...hint を計算して ss->publish_hint セット... // ⑤ ring に登録(通知) for (int i = 0; i < SS_PARTIAL_RING; i++) { // ...ring の empty slot を探して登録... } } ``` ✓ drain 呼び出しなし(重要!) ✓ owner_tid リセットは「publish の責務」の範囲内(adopter 準備) ✓ **NOTE: publish 側から drain を呼ばない** ← Box 4 厳守 ✓ 以下のコメント参照: ```c // NOTE: Do NOT drain here! The old SuperSlab may have slabs owned by other threads // that just adopted from it. Draining without ownership checks causes freelist corruption. // The adopter will drain when needed (with proper ownership checks in tiny_refill.h). ``` #### ✅ 問題なし: ss_partial_adopt() は fetch + リセット+利用のみ ```c // core/hakmem_tiny.c:719-742 SuperSlab* ss_partial_adopt(int class_idx) { for (int i = 0; i < SS_PARTIAL_RING; i++) { SuperSlab* ss = atomic_exchange_explicit(&g_ss_partial_ring[class_idx][i], NULL, memory_order_acq_rel); if (ss) { // Clear listed flag to allow future publish atomic_store_explicit(&ss->listed, 0u, memory_order_release); g_ss_adopt_dbg[class_idx]++; return ss; // ← 利用側へ返却 } } // Fallback: adopt from overflow stack while (1) { SuperSlab* head = atomic_load_explicit(&g_ss_partial_over[class_idx], memory_order_acquire); if (!head) break; SuperSlab* next = head->partial_next; if (atomic_compare_exchange_weak_explicit(&g_ss_partial_over[class_idx], &head, next, memory_order_acq_rel, memory_order_relaxed)) { atomic_store_explicit(&head->listed, 0u, memory_order_release); g_ss_adopt_dbg[class_idx]++; return head; // ← 利用側へ返却 } } return NULL; } ``` ✓ drain 呼び出しなし ✓ owner_tid 操作なし(すでに publish で 0 にされている) ✓ 単なる slab の検索+返却 #### ✅ 問題なし: adopt 側での drain は refill 境界で実施 ```c // core/hakmem_tiny_free.inc:696-740 // (superslab_refill 内) SuperSlab* adopt = ss_partial_adopt(class_idx); if (adopt && adopt->magic == SUPERSLAB_MAGIC) { // ...best slab 探索... if (best >= 0) { uint32_t self = tiny_self_u32(); SlabHandle h = slab_try_acquire(adopt, best, self); // ← Box 3: 所有権取得 if (slab_is_valid(&h)) { slab_drain_remote_full(&h); // ← Box 2: 所有権ガード下で drain if (slab_remote_pending(&h)) { // ...pending check... slab_release(&h); } if (slab_freelist(&h)) { tiny_tls_bind_slab(tls, h.ss, h.slab_idx); // ← Box 3: bind return h.ss; } slab_release(&h); } } } ``` ✓ **drain は採用側の refill 境界で実施** ← Box 4 完全遵守 ✓ 所有権取得 → drain → bind の順序が正確 ✓ slab_handle.h の slab_drain_remote() でガード **Box 4 評価: ✅ PASS - publish/fetch は純粋な通知。drain は adopter 側境界でのみ実施。** --- ## 残り問題の検証: TOCTOU バグ(既知) ### 既知: Box 2→3 の TOCTOU バグ(修正済み) 前述の "drain 後に remote_pending チェック漏れ" は以下で修正済み: ```c // core/hakmem_tiny_free.inc:714-717 SlabHandle h = slab_try_acquire(adopt, best, self); if (slab_is_valid(&h)) { slab_drain_remote_full(&h); if (slab_remote_pending(&h)) { // ← チェック追加(修正) slab_release(&h); // continue to next candidate } } ``` ✓ drain 完了後に remote_pending をチェック ✓ pending がまだあれば acquire を release して次へ ✓ TOCTOU window を最小化 --- ## 追加調査: Remote A213/A202 コードの生成源特定 ### A213: pre_push corruption(TLS guard scribble) ```c // core/hakmem_tiny_free.inc:1187-1207 if (__builtin_expect(head_word == TINY_REMOTE_SENTINEL && !dup_remote && g_debug_remote_guard, 0)) { tiny_remote_watch_note("dup_scan_miss", ss, slab_idx, ptr, 0xA215u, my_tid, 0); } if (dup_remote) { // ...A214... } if (__builtin_expect(g_remote_side_enable && (head_word & 0xFFFFu) == 0x6261u, 0)) { // TLS guard scribble detected on the node's first word uintptr_t aux = tiny_remote_pack_diag(0xA213u, ss_base, ss_size, (uintptr_t)ptr); tiny_debug_ring_record(TINY_RING_EVENT_REMOTE_INVALID, (uint16_t)ss->size_class, ptr, aux); tiny_remote_watch_mark(ptr, "pre_push", my_tid); tiny_remote_watch_note("pre_push", ss, slab_idx, ptr, 0xA231u, my_tid, 0); tiny_remote_report_corruption("pre_push", ptr, head_word); if (g_tiny_safe_free_strict) { raise(SIGUSR2); return; } return; } ``` ✓ A213: 発見元は hakmem_tiny_free.inc:1198-1206 ✓ 原因: node の first word に 0x6261 (ba) scribble が見られた ✓ 意味: 同じ pointer で既に ss_remote_side_set が呼ばれている可能性 ✓ 修正: dup_remote チェックで事前に防止(現実装で動作) ### A202: sentinel corruption(drain 時) ```c // core/hakmem_tiny_superslab.h:409-427 if (__builtin_expect(g_remote_side_enable, 0)) { if (!tiny_remote_sentinel_ok(node)) { uintptr_t aux = tiny_remote_pack_diag(0xA202u, base, ss_size, (uintptr_t)node); tiny_debug_ring_record(TINY_RING_EVENT_REMOTE_INVALID, (uint16_t)ss->size_class, node, aux); // ...corruption report... if (g_tiny_safe_free_strict) { raise(SIGUSR2); return; } } tiny_remote_side_clear(ss, slab_idx, node); } ``` ✓ A202: 発見元は hakmem_tiny_superslab.h:410 ✓ 原因: drain 時に node の sentinel が不正(0xBADA55... ではない) ✓ 意味: node の first word が何らかの理由で上書きされた ✓ 対策: g_remote_side_enable でも sentinel チェック --- ## Box Theory の完全性評価 ### Box 境界チェックリスト | Box | 機能 | 不変条件 | 検証 | 評価 | |-----|------|---------|------|------| | **Box 1** | Atomic Ops | CAS/Exchange の秩序付け(Release/Acquire) | 記載省略(下層) | ✅ | | **Box 2** | Remote Queue | push は freelist/owner に触れない | 二重 push: A214/A212 | ✅ PASS | | **Box 3** | Ownership | acquire/release の正確性 | owner_tid CAS | ✅ PASS | | **Box 4** | Publish/Adopt | publish から drain 呼ばない | 採用境界分離確認 | ✅ PASS | | **Box 3↔2** | Drain boundary | ownership 確保後 drain | slab_handle.h 経由 | ✅ PASS | | **Box 4→3** | Adopt boundary | drain→bind→owner の順序 | refill 1箇所 | ✅ PASS | ### 結論 **✅ Box 境界の不変条件は厳密に守られている。** 1. **Box 3 (Ownership)**: - freelist push は owner_tid==my_tid のみ - publish 時の owner リセットが明確 - slab_handle.h の SlabHandle でガード完全 2. **Box 2 (Remote Queue)**: - 二重 push は 3 層で防止(free 側: A214, side-set: A212, traverse limit: fail-safe) - remote_side の sentinel で追加保護 - drain 時の sentinel チェックで corruption 検出 3. **Box 4 (Publish/Fetch)**: - publish は owner リセット+通知のみ - drain は publish 側では呼ばない - 採用側 refill 境界でのみ drain(ownership ガード下) 4. **remote_invalid の A213/A202 検出**: - A213: dup_remote チェック(1183)で事前防止 - A202: sentinel 検査(410)で drain 時検出 - どちらも fail-fast で即座に報告・停止 --- ## 推奨事項 ### 現在の状態 **Box Theory の実装は健全です。散発的な remote_invalid は以下に起因する可能性:** 1. **Timing window** - publish → unlisted(catalog から外れる)→ adopt の間に - owner=0 のまま別スレッドが allocate する可能性は低いが、エッジケースあり得る 2. **Platform memory ordering** - x86: Acquire/Release は効くが、他の platform では要注意 - memory_order_acq_rel で CAS してるので current は安全 3. **Rare race in ss_partial_adopt()** - overflow stack での LIFO pop と新規登録の タイミング - 概率は低いが、同時並行で複数スレッドが overflow を走査 ### テスト・デバッグ提案 ```bash # 散発的なバグを局所化 HAKMEM_TINY_REMOTE_SIDE=1 # Side table 有効化 HAKMEM_DEBUG_COUNTERS=1 # 統計カウント HAKMEM_TINY_RF_TRACE=1 # publish/fetch の トレース HAKMEM_TINY_SS_ADOPT=1 # SuperSlab adopt 有効化 # 検出時のダンプ HAKMEM_TINY_MAILBOX_SLOWDISC=1 # Slow discovery ``` --- ## まとめ **徹底検証の結果、Box 3, 2, 4 の不変条件は守られている。** - Box 3: freelist push は所有権ガード完全 ✅ - Box 2: 二重 push は 3 層で防止 ✅ - Box 4: publish/fetch は純粋な通知、drain は adopter 側 ✅ remote_invalid (A213/A202) の散発は、Box Theory のバグではなく、 **edge case in timing** である可能性が高い。 TOCTOU window 最小化と memory barrier の強化で、 さらに robust化できる可能性あり。