{"record":{"id":"40c6a3c6e7ecf5e1","repo":"BoundaryML/baml","slug":"dynamic-impl-table-lock-poisoned","errorCode":null,"errorMessage":"dynamic-impl table lock poisoned","messagePattern":"dynamic-impl table lock poisoned","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"baml_language/crates/bex_vm/src/package_load.rs","lineNumber":83,"sourceCode":"}\n\n/// Engine-local side tables for runtime-created nominal definitions.\n///\n/// Anonymous typebuilder classes have no owning package to hold their impl\n/// rules, so this is where their witnesses live — keyed by the interface's\n/// `Object::Interface` pointer, the same key every package's `impl_rules` map\n/// uses. It is a *findability* index only: nothing here is a GC root, and every\n/// entry is dropped the moment its class is collected.\n#[derive(Default, Debug)]\npub struct DynDispatchTables {\n    impl_rules: RwLock<IndexMap<HeapPtr, Vec<DynRuleEntry>>>,\n}\n\nimpl DynDispatchTables {\n    pub fn register_rule(&self, interface: HeapPtr, entry: DynRuleEntry) {\n        self.impl_rules\n            .write()\n            .expect(\"dynamic-impl table lock poisoned\")\n            .entry(interface)\n            .or_default()\n            .push(entry);\n    }\n\n    /// The witness rules registered for `interface`, as pointers to their heap\n    /// `Object::ImplRule`s. Callers borrow the rule through the VM exactly as\n    /// they borrow a package-owned one.\n    pub fn rules_of(&self, interface: HeapPtr) -> Vec<HeapPtr> {\n        self.impl_rules\n            .read()\n            .expect(\"dynamic-impl table lock poisoned\")\n            .get(&interface)\n            .into_iter()\n            .flatten()\n            .map(|entry| entry.rule)\n            .collect()\n    }","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/BoundaryML/baml/blob/bd85ce9dee1463ff04d27efd20531013a4ff46c1/baml_language/crates/bex_vm/src/package_load.rs#L65-L101","documentation":"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.","triggerScenarios":"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.","commonSituations":"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'.","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."],"exampleFix":"// before\nself.impl_rules\n    .write()\n    .expect(\"dynamic-impl table lock poisoned\")\n    .entry(interface)\n    .or_default()\n    .push(entry);\n// after\n// don't panic while holding the lock; fail the operation instead\nif let Err(e) = self.impl_rules.write() {\n    return Err(format!(\"impl table lock poisoned: {e}\"));\n}\nOk(self.impl_rules.write().unwrap().entry(interface).or_default().push(entry))","handlingStrategy":"retry","validationCode":"// before registering, confirm the table is not poisoned\nfn lock_ok<T>(l: &std::sync::RwLock<T>) -> bool { l.try_read().is_ok() }\nif !lock_ok(&tables.impl_rules) { /* rebuild VM or fix root panic first */ }","typeGuard":null,"tryCatchPattern":"// recover at the thread boundary\nlet result = std::panic::catch_unwind(|| vm.load_package(pkg));\nmatch result {\n    Ok(v) => v,\n    Err(_) => restart_vm_and_reload_packages(), // poisoned locks cannot be reused safely\n}","preventionTips":["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."],"tags":["rust","concurrency","mutex-poisoned","vm"],"backgroundTag":"mutex-lock-poisoned","analyzedSha":"bd85ce9dee1463ff04d27efd20531013a4ff46c1","analyzedAt":"2026-09-12T03:38:25.718Z","contentChangedAt":"2026-09-12T03:38:25.718Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}