diff --git a/src/msgspec/_core.c b/src/msgspec/_core.c index 4ccdbdb7..d4151cd8 100644 --- a/src/msgspec/_core.c +++ b/src/msgspec/_core.c @@ -1907,16 +1907,22 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { } Meta *out = (Meta *)Meta_Type.tp_alloc(&Meta_Type, 0); - if (out == NULL) return NULL; + if (out == NULL) { + Py_XDECREF(regex); + return NULL; + } +/* SET_FIELD handles borrowed values that need an extra INCREF. + * SET_FIELD_OWNED passes through references we already own. */ #define SET_FIELD(x) do { Py_XINCREF(x); out->x = x; } while(0) +#define SET_FIELD_OWNED(x) do { out->x = x; } while(0) SET_FIELD(gt); SET_FIELD(ge); SET_FIELD(lt); SET_FIELD(le); SET_FIELD(multiple_of); SET_FIELD(pattern); - SET_FIELD(regex); + SET_FIELD_OWNED(regex); SET_FIELD(min_length); SET_FIELD(max_length); SET_FIELD(tz); @@ -1926,6 +1932,8 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { SET_FIELD(extra_json_schema); SET_FIELD(extra); #undef SET_FIELD +#undef SET_FIELD_OWNED + return (PyObject *)out; } diff --git a/tests/unit/test_struct_meta.py b/tests/unit/test_struct_meta.py index 1904d31b..3f85c051 100644 --- a/tests/unit/test_struct_meta.py +++ b/tests/unit/test_struct_meta.py @@ -1,5 +1,8 @@ """Tests for the exposed StructMeta metaclass.""" +import gc +import re +import secrets from abc import ABCMeta, _abc_init, abstractmethod import pytest @@ -624,3 +627,26 @@ def foo(self) -> int: c = Concrete(5) assert c.foo() == 5 + + +def test_struct_meta_pattern_ref_leak(): + # ensure that we're not keeping around references to re.Pattern longer than necessary + # see https://github.com/jcrist/msgspec/pull/899 for details + + # clear cache to get a baseline + re.purge() + + # use a random string to create a pattern, to ensure there can never be an overlap + # with any cached pattern + pattern_string = secrets.token_hex() + msgspec.Meta(pattern=pattern_string) + # purge cache and gc again + re.purge() + gc.collect() + # there shouldn't be an re.Pattern with our pattern any more. if there is, it's + # being kept alive by some reference + assert not any( + o + for o in gc.get_objects() + if isinstance(o, re.Pattern) and o.pattern == pattern_string + )