Skip to content

Commit 233d916

Browse files
provinzkrautofek
andauthored
Fix refcount leak to re.Pattern when using Meta(pattern=...) (#899)
Co-authored-by: Ofek Lev <ofekmeister@gmail.com>
1 parent 2eafc17 commit 233d916

File tree

2 files changed

+36
-2
lines changed

2 files changed

+36
-2
lines changed

src/msgspec/_core.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1907,16 +1907,22 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) {
19071907
}
19081908

19091909
Meta *out = (Meta *)Meta_Type.tp_alloc(&Meta_Type, 0);
1910-
if (out == NULL) return NULL;
1910+
if (out == NULL) {
1911+
Py_XDECREF(regex);
1912+
return NULL;
1913+
}
19111914

1915+
/* SET_FIELD handles borrowed values that need an extra INCREF.
1916+
* SET_FIELD_OWNED passes through references we already own. */
19121917
#define SET_FIELD(x) do { Py_XINCREF(x); out->x = x; } while(0)
1918+
#define SET_FIELD_OWNED(x) do { out->x = x; } while(0)
19131919
SET_FIELD(gt);
19141920
SET_FIELD(ge);
19151921
SET_FIELD(lt);
19161922
SET_FIELD(le);
19171923
SET_FIELD(multiple_of);
19181924
SET_FIELD(pattern);
1919-
SET_FIELD(regex);
1925+
SET_FIELD_OWNED(regex);
19201926
SET_FIELD(min_length);
19211927
SET_FIELD(max_length);
19221928
SET_FIELD(tz);
@@ -1926,6 +1932,8 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) {
19261932
SET_FIELD(extra_json_schema);
19271933
SET_FIELD(extra);
19281934
#undef SET_FIELD
1935+
#undef SET_FIELD_OWNED
1936+
19291937
return (PyObject *)out;
19301938
}
19311939

tests/unit/test_struct_meta.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Tests for the exposed StructMeta metaclass."""
22

3+
import gc
4+
import re
5+
import secrets
36
from abc import ABCMeta, _abc_init, abstractmethod
47

58
import pytest
@@ -624,3 +627,26 @@ def foo(self) -> int:
624627

625628
c = Concrete(5)
626629
assert c.foo() == 5
630+
631+
632+
def test_struct_meta_pattern_ref_leak():
633+
# ensure that we're not keeping around references to re.Pattern longer than necessary
634+
# see https://github.com/jcrist/msgspec/pull/899 for details
635+
636+
# clear cache to get a baseline
637+
re.purge()
638+
639+
# use a random string to create a pattern, to ensure there can never be an overlap
640+
# with any cached pattern
641+
pattern_string = secrets.token_hex()
642+
msgspec.Meta(pattern=pattern_string)
643+
# purge cache and gc again
644+
re.purge()
645+
gc.collect()
646+
# there shouldn't be an re.Pattern with our pattern any more. if there is, it's
647+
# being kept alive by some reference
648+
assert not any(
649+
o
650+
for o in gc.get_objects()
651+
if isinstance(o, re.Pattern) and o.pattern == pattern_string
652+
)

0 commit comments

Comments
 (0)