Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/msgspec/_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}

Expand Down
26 changes: 26 additions & 0 deletions tests/unit/test_struct_meta.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
)