Skip to content

Commit 874c66a

Browse files
gh-75876: Kill a bigmem test which uses more memory than it declares
The address space cannot be limited everywhere: not under AddressSanitizer, which reserves terabytes for its shadow memory, and not on macOS, which reserves much more than it uses. The parent process already watches the memory of the subprocess running the test, so let it kill the test which uses more than it declares plus the same margin. This also catches a test which fills the memory without reserving that much address space.
1 parent 78b6dea commit 874c66a

2 files changed

Lines changed: 43 additions & 11 deletions

File tree

Lib/test/support/__init__.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,8 +1270,16 @@ def set_memlimit(limit: str) -> None:
12701270
max_memuse = memlimit
12711271

12721272

1273+
def _memory_limit(nbytes):
1274+
"""How much memory a test declaring *nbytes* may use.
1275+
1276+
The interpreter itself reserves about 250 MiB, whatever the test asks for.
1277+
"""
1278+
return int(nbytes) + 512 * _1M
1279+
1280+
12731281
def _limit_address_space(nbytes):
1274-
"""Limit the address space of this process to *nbytes* plus a margin.
1282+
"""Limit the address space of this process to what a test may use.
12751283
12761284
A test which uses much more memory than it declares then fails with a
12771285
MemoryError instead of making the machine swap.
@@ -1280,32 +1288,43 @@ def _limit_address_space(nbytes):
12801288
# AddressSanitizer reserves terabytes of address space for its shadow
12811289
# memory, so any limit stops the interpreter from starting.
12821290
return
1291+
if sys.platform == 'darwin':
1292+
# macOS reserves much more address space than it uses.
1293+
return
12831294
try:
12841295
import resource
12851296
rlimit = resource.RLIMIT_AS
12861297
except (ImportError, AttributeError):
12871298
return
1288-
# The margin does not grow with the declared size: the interpreter itself
1289-
# reserves about 250 MiB, whatever the test asks for. macOS reserves more.
1290-
margin = _1G if sys.platform == 'darwin' else 512 * _1M
1291-
limit = int(nbytes) + margin
1299+
limit = _memory_limit(nbytes)
12921300
soft, hard = resource.getrlimit(rlimit)
12931301
for current in soft, hard:
12941302
if current != resource.RLIM_INFINITY:
12951303
limit = min(limit, current)
12961304
resource.setrlimit(rlimit, (limit, hard))
12971305

12981306

1299-
def _memory_watchdog(pid):
1300-
"""Return a function printing the memory usage of process *pid*."""
1307+
def _memory_watchdog(proc, limit):
1308+
"""Return a function watching the memory used by the test in *proc*.
1309+
1310+
It reports the usage in verbose mode, and kills the test if it uses more
1311+
than *limit* bytes. This is the only limit where the address space cannot
1312+
be limited.
1313+
"""
13011314
# Imported here: test.support does not depend on test.libregrtest.
13021315
from test.libregrtest.utils import get_process_memory_usage
13031316

13041317
def watch():
1305-
mem = get_process_memory_usage(pid)
1306-
if mem is not None:
1318+
mem = get_process_memory_usage(proc.pid)
1319+
if mem is None:
1320+
return
1321+
if verbose:
13071322
print(f" ... process data size: {mem / (1024 ** 3):.1f} GiB",
13081323
flush=True)
1324+
if limit is not None and mem > limit:
1325+
watch.exceeded = mem
1326+
proc.kill()
1327+
watch.exceeded = None
13091328
return watch
13101329

13111330

@@ -1364,8 +1383,17 @@ def wrapper(self):
13641383
cls = type(self)
13651384
qualname = f'{cls.__qualname__}.{f.__name__}'
13661385
proc = isolation._start_test(cls.__module__, qualname)
1367-
watchdog = _memory_watchdog(proc.pid) if verbose else None
1368-
isolation._replay_test(self, *proc.wait(tick=watchdog))
1386+
# Watched even if the address space is not limited: this
1387+
# counts the memory really used.
1388+
watchdog = _memory_watchdog(proc,
1389+
_memory_limit(size * memuse))
1390+
payload, output, returncode = proc.wait(tick=watchdog)
1391+
if watchdog.exceeded:
1392+
raise AssertionError(
1393+
f'the test used {watchdog.exceeded / _1G:.1f} GiB, '
1394+
f'more than the {size * memuse / _1G:.1f} GiB '
1395+
f'it declares')
1396+
isolation._replay_test(self, payload, output, returncode)
13691397
return
13701398

13711399
if (real_max_memuse and limit_address_space

Lib/test/support/isolation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ def __init__(self, proc, result_path):
123123
def pid(self):
124124
return self._proc.pid
125125

126+
def kill(self):
127+
"""Kill the test, for example when it uses too much memory."""
128+
self._proc.kill()
129+
126130
def wait(self, timeout=None, tick=None, interval=1.0):
127131
"""Wait for the test to finish, calling *tick* every *interval* seconds.
128132

0 commit comments

Comments
 (0)