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
- 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.
- Avoid panicking while holding the lock: make lock-held code non-panicking (return Result instead of expect/unreachable! inside the critical section).
- Ensure only one thread registers rules for a package during load, or serialize registration behind an outer mutex.
- 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.
- 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
- Never panic while holding the impl_rules lock; do fallible work outside the critical section.
- catch_unwind at worker-thread boundaries so one thread's panic doesn't poison shared VM state.
- Monitor logs for the FIRST panic — lock-poisoned messages are always secondary symptoms.
- Consider parking_lot locks or lock-free structures if poisoning keeps occurring.
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
- Function `{name}` is not invokable as an entry point (kind:
- VM internal error: {0}
- live owner must be forwarded
- Package.current call site names a loaded package
- Package.classes only contains class pointers
AI-assisted analysis of BoundaryML/baml@bd85ce9dee (2026-09-12).
Data as JSON: /api/errors/40c6a3c6e7ecf5e1.
Report an issue: GitHub.