From 2f8c03024172d88570f842d7ec763fa2d7e8571e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= Date: Sun, 26 Oct 2025 13:16:13 +0100 Subject: [PATCH 1/2] fix --- msgspec/_core.c | 7 ++++++- tests/test_struct_meta.py | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/msgspec/_core.c b/msgspec/_core.c index 6b6ad1c4..72957290 100644 --- a/msgspec/_core.c +++ b/msgspec/_core.c @@ -1886,6 +1886,7 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { Meta *out = (Meta *)Meta_Type.tp_alloc(&Meta_Type, 0); if (out == NULL) return NULL; +// set fields on Meta and increase their refcount #define SET_FIELD(x) do { Py_XINCREF(x); out->x = x; } while(0) SET_FIELD(gt); SET_FIELD(ge); @@ -1893,7 +1894,6 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { SET_FIELD(le); SET_FIELD(multiple_of); SET_FIELD(pattern); - SET_FIELD(regex); SET_FIELD(min_length); SET_FIELD(max_length); SET_FIELD(tz); @@ -1903,6 +1903,11 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { SET_FIELD(extra_json_schema); SET_FIELD(extra); #undef SET_FIELD + + // set fields on Meta without increasing their refcount + // regex was created by a PyObject_CallOneArg call, so refcount started out as 1; no need to increase + out->regex = regex; + return (PyObject *)out; } diff --git a/tests/test_struct_meta.py b/tests/test_struct_meta.py index ebd9b0b1..6e32469e 100644 --- a/tests/test_struct_meta.py +++ b/tests/test_struct_meta.py @@ -1,4 +1,8 @@ """Tests for the exposed StructMeta metaclass.""" +import gc +import re +import secrets +import uuid import pytest import msgspec @@ -362,3 +366,22 @@ class Container(metaclass=EncoderMeta): assert decoded.count == 1 assert decoded.item.id == 123 assert decoded.item.name == "test" + + +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 should be no re.Pattern any more with our pattern anymore. 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) From f3c3acc5d8a5df961dbd9c7691e33b2f4ef94921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= Date: Sun, 26 Oct 2025 19:24:50 +0100 Subject: [PATCH 2/2] formatting --- tests/test_struct_meta.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_struct_meta.py b/tests/test_struct_meta.py index 6e32469e..ed0ab740 100644 --- a/tests/test_struct_meta.py +++ b/tests/test_struct_meta.py @@ -1,8 +1,8 @@ """Tests for the exposed StructMeta metaclass.""" + import gc import re import secrets -import uuid import pytest import msgspec @@ -384,4 +384,8 @@ def test_struct_meta_pattern_ref_leak(): gc.collect() # there should be no re.Pattern any more with our pattern anymore. 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) + assert not any( + o + for o in gc.get_objects() + if isinstance(o, re.Pattern) and o.pattern == pattern_string + )