pydantic/monty · critical

Heap::boundary_uuid: entry already freed

Error message

Heap::boundary_uuid: entry already freed

What it means

`Heap::boundary_uuid` panics if `entries.entry(id)` returns `None` (the entry was already freed) or, one line later, if the entry is not an `Instance` or `Class`. It is used to obtain the boundary uuid identifying a class/instance for the host boundary (snapshot/pool identity). A panic means a caller passed a stale id or a non-boundary type — an internal invariant violation.

Source

Thrown at crates/monty/src/heap/mod.rs:1295

            }
            Some(WeakIndexKey::HostType(uuid)) if self.host_type_index.get(&uuid) == Some(&id) => {
                self.host_type_index.remove(&uuid);
            }
            Some(WeakIndexKey::Boundary(_) | WeakIndexKey::HostType(_)) | None => {}
        }
    }

    /// Boundary uuid of the sandbox class or instance at `id`, generated and
    /// indexed on its first crossing to the host so the host can hand the
    /// object back by id (see [`Heap::resolve_boundary_uuid`]).
    ///
    /// # Panics
    /// If `id` is not a live `Instance` or `Class` entry.
    pub(crate) fn boundary_uuid(&mut self, id: HeapId) -> MontyUuid {
        let mut entry = self
            .entries
            .entry(id)
            .expect("Heap::boundary_uuid: entry already freed");
        let uuid = match entry.get_mut().data.0.get_mut() {
            HeapData::Instance(instance) => instance.boundary_uuid(),
            HeapData::Class(class) => class.boundary_uuid(),
            _ => unreachable!("Heap::boundary_uuid: only classes and instances carry a boundary uuid"),
        };
        self.boundary_index.insert(uuid, id);
        uuid
    }

    /// The live sandbox class or instance that crossed to the host as `uuid`,
    /// if it still exists; the index holds no reference, so the returned id is
    /// borrowed.
    #[must_use]
    pub(crate) fn resolve_boundary_uuid(&self, uuid: &MontyUuid) -> Option<HeapId> {
        self.boundary_index.get(uuid).copied()
    }

    /// The live type object for the host class `uuid`, if the sandbox holds

View on GitHub (pinned to adc986b362)

Solutions

  1. Check liveness before calling: use `try_entry`-style lookup or verify the ref is still reachable.
  2. Verify the value's type is Instance/Class before requesting a boundary uuid.
  3. Fix the upstream drop ordering that frees the object before boundary resolution completes (audit `drop_with`/GC paths).

Example fix

// before
let uuid = vm.heap.boundary_uuid(id); // id may be freed or non-class
// after
if let Some(kind) = reader.heap.try_entry_kind(id) {
    assert!(matches!(kind, Instance | Class));
    let uuid = vm.heap.boundary_uuid(id);
}
Defensive patterns

Strategy: type-guard

Validate before calling

// confirm type before requesting a boundary uuid
assert!(matches!(reader.heap.entries.entry(id).map(|e| e.data_kind()), Some(Instance | Class)), 'id must be a live Instance or Class');

Type guard

fn is_boundary_object(vm: &VM, value: &Value) -> bool { matches!(value, Value::Ref(_)) && matches!(value.py_type(vm), Type::Instance(_) | Type::Class(_)) }

Prevention

When it happens

Trigger: Calling `boundary_uuid` with a `HeapId` of an object already freed by refcount drop or cycle collection; passing the id of a List/Dict/String instead of an Instance/Class.

Common situations: Snapshotting or resolving boundary identity after the referenced object was dropped; type confusion where a value's type was assumed to be class/instance without checking.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13). Data as JSON: /api/errors/28dd0bc6e78e892e. Report an issue: GitHub.