Commit 6693f730 authored by Eric Snow's avatar Eric Snow Committed by GitHub

bpo-38187: Fix a refleak in Tools/c-analyzer. (gh-16304)

The "Slot" helper (descriptor) is leaking references due to its caching mechanism. The change includes a partial fix to Slot, but also adds Variable.storage to replace the problematic use of Slot.

https://bugs.python.org/issue38187
parent 90558158
import os.path
from test.support import load_package_tests
def load_tests(*args):
return load_package_tests(os.path.dirname(__file__), *args)
...@@ -15,9 +15,6 @@ class FromFileTests(unittest.TestCase): ...@@ -15,9 +15,6 @@ class FromFileTests(unittest.TestCase):
_return_read_tsv = () _return_read_tsv = ()
def tearDown(self):
Variable._isglobal.instances.clear()
@property @property
def calls(self): def calls(self):
try: try:
......
import os.path
from test.support import load_package_tests
def load_tests(*args):
return load_package_tests(os.path.dirname(__file__), *args)
...@@ -64,7 +64,9 @@ class StaticsFromBinaryTests(_Base): ...@@ -64,7 +64,9 @@ class StaticsFromBinaryTests(_Base):
**self.kwargs)) **self.kwargs))
self.assertEqual(found, [ self.assertEqual(found, [
info.Variable.from_parts('dir1/spam.c', None, 'var1', 'int'),
info.Variable.from_parts('dir1/spam.c', None, 'var2', 'static int'), info.Variable.from_parts('dir1/spam.c', None, 'var2', 'static int'),
info.Variable.from_parts('dir1/spam.c', None, 'var3', 'char *'),
info.Variable.from_parts('dir1/eggs.c', None, 'var1', 'static int'), info.Variable.from_parts('dir1/eggs.c', None, 'var1', 'static int'),
info.Variable.from_parts('dir1/eggs.c', 'func1', 'var2', 'static char *'), info.Variable.from_parts('dir1/eggs.c', 'func1', 'var2', 'static char *'),
]) ])
...@@ -299,7 +301,7 @@ class StaticsTest(_Base): ...@@ -299,7 +301,7 @@ class StaticsTest(_Base):
info.Variable.from_parts('src1/spam.c', None, 'var1', 'static const char *'), info.Variable.from_parts('src1/spam.c', None, 'var1', 'static const char *'),
info.Variable.from_parts('src1/spam.c', None, 'var1b', 'const char *'), info.Variable.from_parts('src1/spam.c', None, 'var1b', 'const char *'),
info.Variable.from_parts('src1/spam.c', 'ham', 'initialized', 'static int'), info.Variable.from_parts('src1/spam.c', 'ham', 'initialized', 'static int'),
info.Variable.from_parts('src1/spam.c', 'ham', 'result', 'int'), info.Variable.from_parts('src1/spam.c', 'ham', 'result', 'int'), # skipped
info.Variable.from_parts('src1/spam.c', None, 'var2', 'static PyObject *'), info.Variable.from_parts('src1/spam.c', None, 'var2', 'static PyObject *'),
info.Variable.from_parts('src1/eggs.c', 'tofu', 'ready', 'static int'), info.Variable.from_parts('src1/eggs.c', 'tofu', 'ready', 'static int'),
info.Variable.from_parts('src1/spam.c', None, 'freelist', 'static (PyTupleObject *)[10]'), info.Variable.from_parts('src1/spam.c', None, 'freelist', 'static (PyTupleObject *)[10]'),
...@@ -318,6 +320,7 @@ class StaticsTest(_Base): ...@@ -318,6 +320,7 @@ class StaticsTest(_Base):
self.assertEqual(found, [ self.assertEqual(found, [
info.Variable.from_parts('src1/spam.c', None, 'var1', 'static const char *'), info.Variable.from_parts('src1/spam.c', None, 'var1', 'static const char *'),
info.Variable.from_parts('src1/spam.c', None, 'var1b', 'const char *'),
info.Variable.from_parts('src1/spam.c', 'ham', 'initialized', 'static int'), info.Variable.from_parts('src1/spam.c', 'ham', 'initialized', 'static int'),
info.Variable.from_parts('src1/spam.c', None, 'var2', 'static PyObject *'), info.Variable.from_parts('src1/spam.c', None, 'var2', 'static PyObject *'),
info.Variable.from_parts('src1/eggs.c', 'tofu', 'ready', 'static int'), info.Variable.from_parts('src1/eggs.c', 'tofu', 'ready', 'static int'),
......
import os.path
from test.support import load_package_tests
def load_tests(*args):
return load_package_tests(os.path.dirname(__file__), *args)
...@@ -4,7 +4,7 @@ import unittest ...@@ -4,7 +4,7 @@ import unittest
from ..util import PseudoStr, StrProxy, Object from ..util import PseudoStr, StrProxy, Object
from .. import tool_imports_for_tests from .. import tool_imports_for_tests
with tool_imports_for_tests(): with tool_imports_for_tests():
from c_analyzer_common.info import ID from c_analyzer_common.info import ID, UNKNOWN
from c_parser.info import ( from c_parser.info import (
normalize_vartype, Variable, normalize_vartype, Variable,
) )
...@@ -31,38 +31,47 @@ class VariableTests(unittest.TestCase): ...@@ -31,38 +31,47 @@ class VariableTests(unittest.TestCase):
VALID_ARGS = ( VALID_ARGS = (
('x/y/z/spam.c', 'func', 'eggs'), ('x/y/z/spam.c', 'func', 'eggs'),
'static',
'int', 'int',
) )
VALID_KWARGS = dict(zip(Variable._fields, VALID_ARGS)) VALID_KWARGS = dict(zip(Variable._fields, VALID_ARGS))
VALID_EXPECTED = VALID_ARGS VALID_EXPECTED = VALID_ARGS
def test_init_typical_global(self): def test_init_typical_global(self):
static = Variable( for storage in ('static', 'extern', 'implicit'):
id=ID( with self.subTest(storage):
filename='x/y/z/spam.c', static = Variable(
funcname=None, id=ID(
name='eggs', filename='x/y/z/spam.c',
), funcname=None,
vartype='int', name='eggs',
) ),
storage=storage,
vartype='int',
)
self.assertEqual(static, ( self.assertEqual(static, (
('x/y/z/spam.c', None, 'eggs'), ('x/y/z/spam.c', None, 'eggs'),
'int', storage,
)) 'int',
))
def test_init_typical_local(self): def test_init_typical_local(self):
static = Variable( for storage in ('static', 'local'):
id=ID( with self.subTest(storage):
filename='x/y/z/spam.c', static = Variable(
funcname='func', id=ID(
name='eggs', filename='x/y/z/spam.c',
), funcname='func',
vartype='int', name='eggs',
) ),
storage=storage,
vartype='int',
)
self.assertEqual(static, ( self.assertEqual(static, (
('x/y/z/spam.c', 'func', 'eggs'), ('x/y/z/spam.c', 'func', 'eggs'),
storage,
'int', 'int',
)) ))
...@@ -71,10 +80,12 @@ class VariableTests(unittest.TestCase): ...@@ -71,10 +80,12 @@ class VariableTests(unittest.TestCase):
with self.subTest(repr(value)): with self.subTest(repr(value)):
static = Variable( static = Variable(
id=value, id=value,
storage=value,
vartype=value, vartype=value,
) )
self.assertEqual(static, ( self.assertEqual(static, (
None,
None, None,
None, None,
)) ))
...@@ -89,34 +100,42 @@ class VariableTests(unittest.TestCase): ...@@ -89,34 +100,42 @@ class VariableTests(unittest.TestCase):
PseudoStr('func'), PseudoStr('func'),
PseudoStr('spam'), PseudoStr('spam'),
), ),
storage=PseudoStr('static'),
vartype=PseudoStr('int'), vartype=PseudoStr('int'),
), ),
(id, (id,
'static',
'int', 'int',
)), )),
('non-str 1', ('non-str 1',
dict( dict(
id=id, id=id,
storage=Object(),
vartype=Object(), vartype=Object(),
), ),
(id, (id,
'<object>',
'<object>', '<object>',
)), )),
('non-str 2', ('non-str 2',
dict( dict(
id=id, id=id,
storage=StrProxy('static'),
vartype=StrProxy('variable'), vartype=StrProxy('variable'),
), ),
(id, (id,
'static',
'variable', 'variable',
)), )),
('non-str', ('non-str',
dict( dict(
id=id, id=id,
vartype=('a', 'b', 'c'), storage=('a', 'b', 'c'),
vartype=('x', 'y', 'z'),
), ),
(id, (id,
"('a', 'b', 'c')", "('a', 'b', 'c')",
"('x', 'y', 'z')",
)), )),
] ]
for summary, kwargs, expected in tests: for summary, kwargs, expected in tests:
...@@ -134,36 +153,43 @@ class VariableTests(unittest.TestCase): ...@@ -134,36 +153,43 @@ class VariableTests(unittest.TestCase):
def test_iterable(self): def test_iterable(self):
static = Variable(**self.VALID_KWARGS) static = Variable(**self.VALID_KWARGS)
id, vartype = static id, storage, vartype = static
values = (id, vartype) values = (id, storage, vartype)
for value, expected in zip(values, self.VALID_EXPECTED): for value, expected in zip(values, self.VALID_EXPECTED):
self.assertEqual(value, expected) self.assertEqual(value, expected)
def test_fields(self): def test_fields(self):
static = Variable(('a', 'b', 'z'), 'x') static = Variable(('a', 'b', 'z'), 'x', 'y')
self.assertEqual(static.id, ('a', 'b', 'z')) self.assertEqual(static.id, ('a', 'b', 'z'))
self.assertEqual(static.vartype, 'x') self.assertEqual(static.storage, 'x')
self.assertEqual(static.vartype, 'y')
def test___getattr__(self): def test___getattr__(self):
static = Variable(('a', 'b', 'z'), 'x') static = Variable(('a', 'b', 'z'), 'x', 'y')
self.assertEqual(static.filename, 'a') self.assertEqual(static.filename, 'a')
self.assertEqual(static.funcname, 'b') self.assertEqual(static.funcname, 'b')
self.assertEqual(static.name, 'z') self.assertEqual(static.name, 'z')
def test_validate_typical(self): def test_validate_typical(self):
static = Variable( validstorage = ('static', 'extern', 'implicit', 'local')
id=ID( self.assertEqual(set(validstorage), set(Variable.STORAGE))
filename='x/y/z/spam.c',
funcname='func', for storage in validstorage:
name='eggs', with self.subTest(storage):
), static = Variable(
vartype='int', id=ID(
) filename='x/y/z/spam.c',
funcname='func',
name='eggs',
),
storage=storage,
vartype='int',
)
static.validate() # This does not fail. static.validate() # This does not fail.
def test_validate_missing_field(self): def test_validate_missing_field(self):
for field in Variable._fields: for field in Variable._fields:
...@@ -171,6 +197,13 @@ class VariableTests(unittest.TestCase): ...@@ -171,6 +197,13 @@ class VariableTests(unittest.TestCase):
static = Variable(**self.VALID_KWARGS) static = Variable(**self.VALID_KWARGS)
static = static._replace(**{field: None}) static = static._replace(**{field: None})
with self.assertRaises(TypeError):
static.validate()
for field in ('storage', 'vartype'):
with self.subTest(field):
static = Variable(**self.VALID_KWARGS)
static = static._replace(**{field: UNKNOWN})
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
static.validate() static.validate()
...@@ -185,6 +218,7 @@ class VariableTests(unittest.TestCase): ...@@ -185,6 +218,7 @@ class VariableTests(unittest.TestCase):
) + badch ) + badch
tests = [ tests = [
('id', ()), # Any non-empty str is okay. ('id', ()), # Any non-empty str is okay.
('storage', ('external', 'global') + notnames),
('vartype', ()), # Any non-empty str is okay. ('vartype', ()), # Any non-empty str is okay.
] ]
seen = set() seen = set()
...@@ -199,6 +233,8 @@ class VariableTests(unittest.TestCase): ...@@ -199,6 +233,8 @@ class VariableTests(unittest.TestCase):
static.validate() static.validate()
for field, invalid in tests: for field, invalid in tests:
if field == 'id':
continue
valid = seen - set(invalid) valid = seen - set(invalid)
for value in valid: for value in valid:
with self.subTest(f'{field}={value!r}'): with self.subTest(f'{field}={value!r}'):
......
import os.path
from test.support import load_package_tests
def load_tests(*args):
return load_package_tests(os.path.dirname(__file__), *args)
...@@ -262,7 +262,7 @@ def _known(symbol): ...@@ -262,7 +262,7 @@ def _known(symbol):
raise raise
if symbol.name not in decl: if symbol.name not in decl:
decl = decl + symbol.name decl = decl + symbol.name
return Variable(varid, decl) return Variable(varid, 'static', decl)
def known_row(varid, decl): def known_row(varid, decl):
...@@ -291,7 +291,7 @@ def known_rows(symbols, *, ...@@ -291,7 +291,7 @@ def known_rows(symbols, *,
except KeyError: except KeyError:
found = _find_match(symbol, cache, filenames) found = _find_match(symbol, cache, filenames)
if found is None: if found is None:
found = Variable(symbol.id, UNKNOWN) found = Variable(symbol.id, UNKNOWN, UNKNOWN)
yield _as_known(found.id, found.vartype) yield _as_known(found.id, found.vartype)
else: else:
raise NotImplementedError # XXX incorporate KNOWN raise NotImplementedError # XXX incorporate KNOWN
......
...@@ -34,34 +34,41 @@ def from_file(infile, *, ...@@ -34,34 +34,41 @@ def from_file(infile, *,
id = ID(filename, funcname, name) id = ID(filename, funcname, name)
if kind == 'variable': if kind == 'variable':
values = known['variables'] values = known['variables']
value = Variable(id, declaration) if funcname:
value._isglobal = _is_global(declaration) or id.funcname is None storage = _get_storage(declaration) or 'local'
else:
storage = _get_storage(declaration) or 'implicit'
value = Variable(id, storage, declaration)
else: else:
raise ValueError(f'unsupported kind in row {row}') raise ValueError(f'unsupported kind in row {row}')
if value.name == 'id' and declaration == UNKNOWN: value.validate()
# None of these are variables. # if value.name == 'id' and declaration == UNKNOWN:
declaration = 'int id'; # # None of these are variables.
else: # declaration = 'int id';
value.validate() # else:
# value.validate()
values[id] = value values[id] = value
return known return known
def _is_global(vartype): def _get_storage(decl):
# statics # statics
if vartype.startswith('static '): if decl.startswith('static '):
return True return 'static'
if vartype.startswith(('Py_LOCAL(', 'Py_LOCAL_INLINE(')): if decl.startswith(('Py_LOCAL(', 'Py_LOCAL_INLINE(')):
return True return 'static'
if vartype.startswith(('_Py_IDENTIFIER(', '_Py_static_string(')): if decl.startswith(('_Py_IDENTIFIER(', '_Py_static_string(')):
return True return 'static'
if vartype.startswith('PyDoc_VAR('): if decl.startswith('PyDoc_VAR('):
return True return 'static'
if vartype.startswith(('SLOT1BINFULL(', 'SLOT1BIN(')): if decl.startswith(('SLOT1BINFULL(', 'SLOT1BIN(')):
return True return 'static'
if vartype.startswith('WRAP_METHOD('): if decl.startswith('WRAP_METHOD('):
return True return 'static'
# public extern # public extern
if vartype.startswith('PyAPI_DATA('): if decl.startswith('extern '):
return True return 'extern'
return False if decl.startswith('PyAPI_DATA('):
return 'extern'
# implicit or local
return None
...@@ -82,6 +82,13 @@ class Slot: ...@@ -82,6 +82,13 @@ class Slot:
self.default = default self.default = default
self.readonly = readonly self.readonly = readonly
# The instance cache is not inherently tied to the normal
# lifetime of the instances. So must do something in order to
# avoid keeping the instances alive by holding a reference here.
# Ideally we would use weakref.WeakValueDictionary to do this.
# However, most builtin types do not support weakrefs. So
# instead we monkey-patch __del__ on the attached class to clear
# the instance.
self.instances = {} self.instances = {}
self.name = None self.name = None
...@@ -89,6 +96,12 @@ class Slot: ...@@ -89,6 +96,12 @@ class Slot:
if self.name is not None: if self.name is not None:
raise TypeError('already used') raise TypeError('already used')
self.name = name self.name = name
try:
slotnames = cls.__slot_names__
except AttributeError:
slotnames = cls.__slot_names__ = []
slotnames.append(name)
self._ensure___del__(cls, slotnames)
def __get__(self, obj, cls): def __get__(self, obj, cls):
if obj is None: # called on the class if obj is None: # called on the class
...@@ -115,7 +128,23 @@ class Slot: ...@@ -115,7 +128,23 @@ class Slot:
def __delete__(self, obj): def __delete__(self, obj):
if self.readonly: if self.readonly:
raise AttributeError(f'{self.name} is readonly') raise AttributeError(f'{self.name} is readonly')
self.instances[id(obj)] = self.default self.instances[id(obj)] = self.default # XXX refleak?
def _ensure___del__(self, cls, slotnames): # See the comment in __init__().
try:
old___del__ = cls.__del__
except AttributeError:
old___del__ = (lambda s: None)
else:
if getattr(old___del__, '_slotted', False):
return
def __del__(_self):
for name in slotnames:
delattr(_self, name)
old___del__(_self)
__del__._slotted = True
cls.__del__ = __del__
def set(self, obj, value): def set(self, obj, value):
"""Update the cached value for an object. """Update the cached value for an object.
......
from collections import namedtuple from collections import namedtuple
import re
from c_analyzer_common import info, util from c_analyzer_common import info, util
from c_analyzer_common.util import classonly, _NTBase from c_analyzer_common.util import classonly, _NTBase
...@@ -15,28 +16,53 @@ def normalize_vartype(vartype): ...@@ -15,28 +16,53 @@ def normalize_vartype(vartype):
return str(vartype) return str(vartype)
def extract_storage(decl, *, isfunc=False):
"""Return (storage, vartype) based on the given declaration.
The default storage is "implicit" or "local".
"""
if decl == info.UNKNOWN:
return decl, decl
if decl.startswith('static '):
return 'static', decl
#return 'static', decl.partition(' ')[2].strip()
elif decl.startswith('extern '):
return 'extern', decl
#return 'extern', decl.partition(' ')[2].strip()
elif re.match('.*\b(static|extern)\b', decl):
raise NotImplementedError
elif isfunc:
return 'local', decl
else:
return 'implicit', decl
class Variable(_NTBase, class Variable(_NTBase,
namedtuple('Variable', 'id vartype')): namedtuple('Variable', 'id storage vartype')):
"""Information about a single variable declaration.""" """Information about a single variable declaration."""
__slots__ = () __slots__ = ()
_isglobal = util.Slot()
def __del__(self): STORAGE = (
del self._isglobal 'static',
'extern',
'implicit',
'local',
)
@classonly @classonly
def from_parts(cls, filename, funcname, name, vartype, isglobal=False): def from_parts(cls, filename, funcname, name, decl, storage=None):
if storage is None:
storage, decl = extract_storage(decl, isfunc=funcname)
id = info.ID(filename, funcname, name) id = info.ID(filename, funcname, name)
self = cls(id, vartype) self = cls(id, storage, decl)
if isglobal:
self._isglobal = True
return self return self
def __new__(cls, id, vartype): def __new__(cls, id, storage, vartype):
self = super().__new__( self = super().__new__(
cls, cls,
id=info.ID.from_raw(id), id=info.ID.from_raw(id),
storage=str(storage) if storage else None,
vartype=normalize_vartype(vartype) if vartype else None, vartype=normalize_vartype(vartype) if vartype else None,
) )
return self return self
...@@ -63,18 +89,17 @@ class Variable(_NTBase, ...@@ -63,18 +89,17 @@ class Variable(_NTBase,
"""Fail if the object is invalid (i.e. init with bad data).""" """Fail if the object is invalid (i.e. init with bad data)."""
self._validate_id() self._validate_id()
if self.storage is None or self.storage == info.UNKNOWN:
raise TypeError('missing storage')
elif self.storage not in self.STORAGE:
raise ValueError(f'unsupported storage {self.storage:r}')
if self.vartype is None or self.vartype == info.UNKNOWN: if self.vartype is None or self.vartype == info.UNKNOWN:
raise TypeError('missing vartype') raise TypeError('missing vartype')
@property @property
def isglobal(self): def isglobal(self):
try: return self.storage != 'local'
return self._isglobal
except AttributeError:
# XXX Include extern variables.
# XXX Ignore functions.
self._isglobal = ('static' in self.vartype.split())
return self._isglobal
@property @property
def isconst(self): def isconst(self):
......
...@@ -163,7 +163,7 @@ def find_variables(varids, filenames=None, *, ...@@ -163,7 +163,7 @@ def find_variables(varids, filenames=None, *,
srcfiles = [varid.filename] srcfiles = [varid.filename]
else: else:
if not filenames: if not filenames:
yield Variable(varid, UNKNOWN) yield Variable(varid, UNKNOWN, UNKNOWN)
continue continue
srcfiles = filenames srcfiles = filenames
for filename in srcfiles: for filename in srcfiles:
...@@ -177,4 +177,4 @@ def find_variables(varids, filenames=None, *, ...@@ -177,4 +177,4 @@ def find_variables(varids, filenames=None, *,
used.add(found) used.add(found)
break break
else: else:
yield Variable(varid, UNKNOWN) yield Variable(varid, UNKNOWN, UNKNOWN)
...@@ -68,14 +68,11 @@ def find_in_source(symbol, dirnames, *, ...@@ -68,14 +68,11 @@ def find_in_source(symbol, dirnames, *,
if symbol.funcname and symbol.funcname != UNKNOWN: if symbol.funcname and symbol.funcname != UNKNOWN:
raise NotImplementedError raise NotImplementedError
(filename, funcname, vartype (filename, funcname, decl
) = _find_symbol(symbol.name, filenames, _perfilecache) ) = _find_symbol(symbol.name, filenames, _perfilecache)
if filename == UNKNOWN: if filename == UNKNOWN:
return None return None
return info.Variable( return info.Variable.from_parts(filename, funcname, symbol.name, decl)
id=(filename, funcname, symbol.name),
vartype=vartype,
)
def get_resolver(knownvars=None, dirnames=None, *, def get_resolver(knownvars=None, dirnames=None, *,
...@@ -144,6 +141,7 @@ def symbols_to_variables(symbols, *, ...@@ -144,6 +141,7 @@ def symbols_to_variables(symbols, *,
#raise NotImplementedError(symbol) #raise NotImplementedError(symbol)
resolved = info.Variable( resolved = info.Variable(
id=symbol.id, id=symbol.id,
storage=UNKNOWN,
vartype=UNKNOWN, vartype=UNKNOWN,
) )
yield resolved yield resolved
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment