BoundaryML/baml · error

dynamic-impl table lock poisoned

Error message

dynamic-impl table lock poisoned

What it means

This panic fires when the RwLock guarding the VM's dynamic-impl table (`impl_rules`) is poisoned — i.e. a previous thread panicked while holding the write lock. `DynDispatchTables::register_rule` needs a write lock to append a new rule entry, and once the lock is poisoned the stored map may be in an inconsistent state, so the library deliberately aborts instead of mutating possibly-corrupt dispatch data.

Source

Thrown at baml_language/crates/bex_vm/src/package_load.rs:83

}

/// Engine-local side tables for runtime-created nominal definitions.
///
/// Anonymous typebuilder classes have no owning package to hold their impl
/// rules, so this is where their witnesses live — keyed by the interface's
/// `Object::Interface` pointer, the same key every package's `impl_rules` map
/// uses. It is a *findability* index only: nothing here is a GC root, and every
/// entry is dropped the moment its class is collected.
#[derive(Default, Debug)]
pub struct DynDispatchTables {
    impl_rules: RwLock<IndexMap<HeapPtr, Vec<DynRuleEntry>>>,
}

impl DynDispatchTables {
    pub fn register_rule(&self, interface: HeapPtr, entry: DynRuleEntry) {
        self.impl_rules
            .write()
            .expect("dynamic-impl table lock poisoned")
            .entry(interface)
            .or_default()
            .push(entry);
    }

    /// The witness rules registered for `interface`, as pointers to their heap
    /// `Object::ImplRule`s. Callers borrow the rule through the VM exactly as
    /// they borrow a package-owned one.
    pub fn rules_of(&self, interface: HeapPtr) -> Vec<HeapPtr> {
        self.impl_rules
            .read()
            .expect("dynamic-impl table lock poisoned")
            .get(&interface)
            .into_iter()
            .flatten()
            .map(|entry| entry.rule)
            .collect()
    }

View on GitHub (pinned to bd85ce9dee)

Solutions

  1. Find and fix the original panic that occurred while the `impl_rules` lock was held — the poisoned-lock panic is always secondary; look for the first panic message in logs.
  2. Avoid panicking while holding the lock: make lock-held code non-panicking (return Result instead of expect/unreachable! inside the critical section).
  3. Ensure only one thread registers rules for a package during load, or serialize registration behind an outer mutex.
  4. If corruption is expected after poisoning, replace the shared `RwLock` with a redesign (e.g. ownership transfer or `parking_lot` with explicit recovery) rather than unwrapping.
  5. Wrap VM package loading in catch_unwind at the thread boundary and restart the VM instead of reusing poisoned state.

Example fix

// before
self.impl_rules
    .write()
    .expect("dynamic-impl table lock poisoned")
    .entry(interface)
    .or_default()
    .push(entry);
// after
// don't panic while holding the lock; fail the operation instead
if let Err(e) = self.impl_rules.write() {
    return Err(format!("impl table lock poisoned: {e}"));
}
Ok(self.impl_rules.write().unwrap().entry(interface).or_default().push(entry))
Defensive patterns

Strategy: retry

Validate before calling

// before registering, confirm the table is not poisoned
fn lock_ok<T>(l: &std::sync::RwLock<T>) -> bool { l.try_read().is_ok() }
if !lock_ok(&tables.impl_rules) { /* rebuild VM or fix root panic first */ }

Try / catch

// recover at the thread boundary
let result = std::panic::catch_unwind(|| vm.load_package(pkg));
match result {
    Ok(v) => v,
    Err(_) => restart_vm_and_reload_packages(), // poisoned locks cannot be reused safely
}

Prevention

When it happens

Trigger: Calling `DynDispatchTables::register_rule(interface, entry)` after any thread panicked while holding the `impl_rules` lock. In practice this means a panic inside another registration path (or a sweep/registration interleaving) that left a `RwLockWriteGuard` dropped via unwind while the VM was loading a package's witnesses.

Common situations: Multithreaded VM setups where a package-loading thread panics mid-registration; a prior `rules_for_class`/`sweep_and_forward` panic poisoning the shared lock; running the VM behind a thread pool that swallows the original panic so the next caller only sees 'lock poisoned'.

Related errors


AI-assisted analysis of BoundaryML/baml@bd85ce9dee (2026-09-12). Data as JSON: /api/errors/40c6a3c6e7ecf5e1. Report an issue: GitHub.