Executor-Tyrant-Framework Claude Opus 4.6 (1M context) commited on
Commit
c85191b
Β·
1 Parent(s): 96f3213

Fix LD_PRELOAD deadlock: static .bss ring buffer, zero heap allocation

Browse files

LazyLock heap-allocated the ring buffer via malloc β€” which is hooked β€”
causing infinite recursion deadlock on startup.

Fix: pack events into AtomicU64 (8 bytes each), store in a static array
that lives in .bss (zero-initialized by the OS loader, no malloc needed).
8K slots Γ— 8 bytes = 64KB ring, entirely in static memory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. rust_core/src/membrane.rs +40 -52
rust_core/src/membrane.rs CHANGED
@@ -410,37 +410,35 @@ const EVENT_ALLOC: u8 = 1;
410
  const EVENT_FREE: u8 = 2;
411
  const EVENT_EMPTY: u8 = 0;
412
 
413
- /// Single event in the lock-free ring buffer.
414
- /// Sized for cache-line alignment (32 bytes).
415
- #[repr(C)]
416
- struct RingEvent {
417
- tag: std::sync::atomic::AtomicU8, // EVENT_EMPTY, EVENT_ALLOC, EVENT_FREE
418
- _pad: u8,
419
- size_kb: u16, // size in KB (enough for up to 64MB)
420
- address: u32, // lower 32 bits of address (unique enough for tracking)
421
- timestamp_ns: u64, // monotonic time
422
- _reserved: [u8; 16], // pad to 32 bytes
 
 
 
 
 
 
 
 
 
423
  }
424
 
425
- /// Lock-free ring buffer capacity β€” must be power of 2
426
- const RING_SIZE: usize = 65536; // 64K slots Γ— 32 bytes = 2MB ring
427
-
428
- /// The ring buffer β€” malloc/free push events here with no locks.
429
- /// The background drain thread reads them.
430
- static RING: std::sync::LazyLock<Box<[RingEvent]>> = std::sync::LazyLock::new(|| {
431
- let mut v = Vec::with_capacity(RING_SIZE);
432
- for _ in 0..RING_SIZE {
433
- v.push(RingEvent {
434
- tag: std::sync::atomic::AtomicU8::new(EVENT_EMPTY),
435
- _pad: 0,
436
- size_kb: 0,
437
- address: 0,
438
- timestamp_ns: 0,
439
- _reserved: [0; 16],
440
- });
441
- }
442
- v.into_boxed_slice()
443
- });
444
 
445
  /// Write cursor β€” atomically incremented by malloc/free hooks
446
  static WRITE_POS: AtomicUsize = AtomicUsize::new(0);
@@ -475,13 +473,12 @@ fn start_drain_thread() {
475
  // Drain up to 1024 events per batch
476
  for _ in 0..1024 {
477
  let slot = &RING[read_pos & (RING_SIZE - 1)];
478
- let tag = slot.tag.load(Ordering::Acquire);
479
- if tag == EVENT_EMPTY {
480
- break;
481
  }
482
 
483
- let address = slot.address as usize;
484
- let size = slot.size_kb as usize * 1024;
485
 
486
  match tag {
487
  EVENT_ALLOC => {
@@ -509,8 +506,8 @@ fn start_drain_thread() {
509
  _ => {}
510
  }
511
 
512
- // Mark slot as consumed
513
- slot.tag.store(EVENT_EMPTY, Ordering::Release);
514
  read_pos += 1;
515
  drained += 1;
516
  }
@@ -524,27 +521,20 @@ fn start_drain_thread() {
524
  .expect("Failed to spawn condensate drain thread");
525
  }
526
 
527
- /// Push an event to the ring buffer β€” lock-free, ~10ns
528
  #[inline(always)]
529
  fn push_event(tag: u8, address: usize, size: usize) {
530
  let pos = WRITE_POS.fetch_add(1, Ordering::Relaxed);
531
  let slot = &RING[pos & (RING_SIZE - 1)];
532
 
533
- // If slot isn't empty, drain thread is behind β€” drop this event.
534
  // Better to lose an event than to stall malloc.
535
- if slot.tag.load(Ordering::Relaxed) != EVENT_EMPTY {
536
  return;
537
  }
538
 
539
- // Write payload before tag (tag is the release fence)
540
- // SAFETY: We're the only writer to this slot (unique pos from atomic increment).
541
- // The drain thread won't read until tag is set.
542
- let slot_ptr = slot as *const RingEvent as *mut RingEvent;
543
- unsafe {
544
- (*slot_ptr).address = address as u32;
545
- (*slot_ptr).size_kb = (size / 1024).min(u16::MAX as usize) as u16;
546
- }
547
- slot.tag.store(tag, Ordering::Release);
548
  }
549
 
550
  /// Get the original malloc function
@@ -638,10 +628,8 @@ static INIT: extern "C" fn() = {
638
  extern "C" fn init() {
639
  INITIALIZED.store(true, Ordering::SeqCst);
640
 
641
- // Force ring buffer allocation before any malloc hooks fire
642
- let _ = &*RING;
643
-
644
- // Start the background drain thread
645
  start_drain_thread();
646
 
647
  unsafe { libc::atexit(condensate_summary) };
 
410
  const EVENT_FREE: u8 = 2;
411
  const EVENT_EMPTY: u8 = 0;
412
 
413
+ /// Lock-free ring buffer capacity β€” must be power of 2.
414
+ /// 8K slots Γ— 8 bytes = 64KB. Lives in .bss, zero heap allocation.
415
+ const RING_SIZE: usize = 8192;
416
+
417
+ /// Compact ring event β€” 8 bytes, packed into a single AtomicU64.
418
+ /// Layout: [tag:8][size_kb:16][address_low:32][_pad:8]
419
+ /// No heap allocation, no struct, no AtomicU8 issues.
420
+ /// The entire ring is a static array of AtomicU64 β€” lives in .bss.
421
+ static RING: [AtomicU64; RING_SIZE] = {
422
+ const ZERO: AtomicU64 = AtomicU64::new(0);
423
+ [ZERO; RING_SIZE]
424
+ };
425
+
426
+ /// Pack an event into a u64: tag in low byte, size_kb in bytes 1-2, address_low in bytes 3-6
427
+ #[inline(always)]
428
+ fn pack_event(tag: u8, address: usize, size: usize) -> u64 {
429
+ let size_kb = (size / 1024).min(0xFFFF) as u64;
430
+ let addr_low = (address as u32) as u64;
431
+ (tag as u64) | (size_kb << 8) | (addr_low << 24)
432
  }
433
 
434
+ /// Unpack: returns (tag, address_low, size_kb)
435
+ #[inline(always)]
436
+ fn unpack_event(packed: u64) -> (u8, usize, usize) {
437
+ let tag = (packed & 0xFF) as u8;
438
+ let size_kb = ((packed >> 8) & 0xFFFF) as usize;
439
+ let addr_low = ((packed >> 24) & 0xFFFFFFFF) as usize;
440
+ (tag, addr_low, size_kb * 1024)
441
+ }
 
 
 
 
 
 
 
 
 
 
 
442
 
443
  /// Write cursor β€” atomically incremented by malloc/free hooks
444
  static WRITE_POS: AtomicUsize = AtomicUsize::new(0);
 
473
  // Drain up to 1024 events per batch
474
  for _ in 0..1024 {
475
  let slot = &RING[read_pos & (RING_SIZE - 1)];
476
+ let packed = slot.load(Ordering::Acquire);
477
+ if packed == 0 {
478
+ break; // empty slot
479
  }
480
 
481
+ let (tag, address, size) = unpack_event(packed);
 
482
 
483
  match tag {
484
  EVENT_ALLOC => {
 
506
  _ => {}
507
  }
508
 
509
+ // Mark slot as consumed (zero = empty)
510
+ slot.store(0, Ordering::Release);
511
  read_pos += 1;
512
  drained += 1;
513
  }
 
521
  .expect("Failed to spawn condensate drain thread");
522
  }
523
 
524
+ /// Push an event to the ring buffer β€” lock-free, ~10ns, zero heap allocation
525
  #[inline(always)]
526
  fn push_event(tag: u8, address: usize, size: usize) {
527
  let pos = WRITE_POS.fetch_add(1, Ordering::Relaxed);
528
  let slot = &RING[pos & (RING_SIZE - 1)];
529
 
530
+ // If slot isn't empty (non-zero), drain thread is behind β€” drop this event.
531
  // Better to lose an event than to stall malloc.
532
+ if slot.load(Ordering::Relaxed) != 0 {
533
  return;
534
  }
535
 
536
+ // Single atomic store β€” the packed value IS the fence
537
+ slot.store(pack_event(tag, address, size), Ordering::Release);
 
 
 
 
 
 
 
538
  }
539
 
540
  /// Get the original malloc function
 
628
  extern "C" fn init() {
629
  INITIALIZED.store(true, Ordering::SeqCst);
630
 
631
+ // Ring buffer is static .bss β€” no initialization needed.
632
+ // Start the background drain thread.
 
 
633
  start_drain_thread();
634
 
635
  unsafe { libc::atexit(condensate_summary) };