Skip to content

CSProperties._instances never evicted thus stale wrapper of the wrong Python class returned for a live property #83

Description

@MohamoudAli12

Assisted-by: DeepSeek V4 Flash Free

Hi @thliebig I was running a test for my project and the test was failing intermittently with AttributeError.
I asked AI to investigate and it found a bug in CSXCAD. below is investigation form AI

Summary

CSProperties.fromPtr() is keyed on the raw C++ address and returns whatever wrapper is cached in the never-evicted, process-global _instances dict. When a property's C++ object is freed (its owning structure is destroyed) and the allocator reuses that address for a property of a different type, GetProperty() returns the old wrapper: GetTypeString()/GetName() still work (virtual dispatch), but the Python class is wrong, so any type-specific method raises AttributeError.

Environment

  • csxcad 0.7.0rc1.post1.dev8+gcb931b747
  • Python 3.11, Linux

Reproducer

import gc
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory

from CSXCAD import ContinuousStructure
from openEMS.openEMS import openEMS

ACCESSOR = {"Material": "GetEpsilon", "Excitation": "GetEnabled",
            "ProbeBox": "GetProbeType", "LumpedElement": "GetResistance"}

def build_model():
    csx = ContinuousStructure()
    csx.AddMaterial("substrate")
    csx.AddMetal("ground").AddBox([0, 0, 0], [20, 20, 0.1])
    csx.AddMetal("trace").AddBox([0, 0, 0.1], [1, 1, 0.2])
    for n in (1, 2):
        le = csx.AddLumpedElement(f"port_resist_{n}", ny="z", caps=True, R=50)
        le.AddBox([0, 0, 0], [1, 1, 1])
        csx.AddExcitation(f"port_excite_{n}", exc_type=0, exc_val=(1.0, 0.0, 0.0))
        csx.AddProbe(f"port_ut_{n}", 0)
        csx.AddProbe(f"port_it_{n}", 1)
    grid = csx.GetGrid()
    grid.SetDeltaUnit(1e-3)
    for axis in "xyz":
        grid.AddLine(axis, [-5.0, 0.0, 5.0, 10.0, 15.0])
    return csx

def reload_roundtrip(model_xml):
    """Load a saved model into an openEMS object, wrap its CSX properties, write back."""
    loader = openEMS()
    loader.ReadFromXML(str(model_xml))
    csx = loader.GetCSX()
    for j in range(csx.GetQtyProperties()):
        csx.GetProperty(j)
    with NamedTemporaryFile(suffix=".xml") as tmp:
        csx.Write2XML(tmp.name)
        loader.Write2XML(tmp.name)

with TemporaryDirectory() as d:
    d = Path(d)
    xml = d / "structure.xml"
    model_xml = d / "model.xml"
    for i in range(50):
        csx = build_model()
        csx.Write2XML(str(xml))
        fdtd = openEMS(NrTS=300, EndCriteria=1e-3)
        fdtd.SetCSX(csx)                 # the FDTD owns the CSX
        fdtd.SetBoundaryCond(["PML_8"] * 6)
        fdtd.SetGaussExcite(2.5e9, 0.5e9)
        fdtd.Write2XML(str(model_xml))
        reload_roundtrip(model_xml)
        del fdtd                         # frees the CSX and all its properties
        gc.collect()

        csx2 = ContinuousStructure()
        csx2.ReadFromXML(str(xml))
        for j in range(csx2.GetQtyProperties()):
            prop = csx2.GetProperty(j)
            type_str = prop.GetTypeString()
            if type_str not in ACCESSOR:
                continue
            expected = {"Material": "CSPropMaterial", "Excitation": "CSPropExcitation",
                        "ProbeBox": "CSPropProbeBox", "LumpedElement": "CSPropLumpedElement"}[type_str]
            if type(prop).__name__ != expected:
                print(f"iteration {i}: property {j} ({prop.GetName()!r}) is a live "
                      f"{type_str} but was returned as a {type(prop).__name__}")
                getattr(prop, ACCESSOR[type_str])()   # <- crashes here
        del csx2
    else:
        print("no stale wrapper in 50 iterations")

Output: iteration 2: property 8 ('port_excite_2') is a live Excitation but was returned as a CSPropMaterial → AttributeError: 'CSXCAD.CSProperties.CSPropMaterial' object has no attribute 'GetEnabled' (reverse direction also observed: CSPropExcitation for a Material).

Root cause

  • python/CSXCAD/CSProperties.pyx:143-145 — fromPtr returns the cached wrapper on an address hit with no type check; entries are added in _SetPtr (:182) and never removed (_instances = {} at :54).
  • A ContinuousStructure has no dealloc; its C++ properties are only freed when the owning openEMS FDTD is destroyed (openEMS.pyx:113-116).
  • Same pattern in python/CSXCAD/CSPrimitives.pyx:51,109,137.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions