Skip to content

Commit 8956eab

Browse files
authored
Merge branch 'main' into wasi-no-tracebacks
2 parents d3ebc46 + 20e6c2f commit 8956eab

28 files changed

Lines changed: 1151 additions & 341 deletions

Doc/library/tarfile.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,10 @@ reused in custom filters:
11121112
paths (in case the name is absolute
11131113
even after stripping slashes, e.g. ``C:/foo`` on Windows).
11141114
This raises :class:`~tarfile.AbsolutePathError`.
1115+
- Normalize filenames (:attr:`TarInfo.name`) that contain ``..`` components
1116+
using :func:`os.path.normpath`.
1117+
Note that this removes internal ``..`` components, which may change the
1118+
meaning of the name if it traverses symbolic links.
11151119
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
11161120
path (after following symlinks) would end up outside the destination.
11171121
This raises :class:`~tarfile.OutsideDestinationError`.
@@ -1120,6 +1124,10 @@ reused in custom filters:
11201124

11211125
Return the modified ``TarInfo`` member.
11221126

1127+
.. versionchanged:: next
1128+
1129+
Filenames containing ``..`` components are now normalized.
1130+
11231131
.. function:: data_filter(member, path)
11241132

11251133
Implements the ``'data'`` filter.

Include/internal/pycore_interpframe_structs.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ struct _PyInterpreterFrame {
6666
PyObject *prefix##_qualname; \
6767
_PyErr_StackItem prefix##_exc_state; \
6868
PyObject *prefix##_origin_or_finalizer; \
69-
char prefix##_hooks_inited; \
70-
char prefix##_closed; \
71-
char prefix##_running_async; \
69+
int8_t prefix##_hooks_inited; \
70+
int8_t prefix##_closed; \
71+
int8_t prefix##_running_async; \
7272
/* The frame */ \
7373
int8_t prefix##_frame_state; \
7474
_PyInterpreterFrame prefix##_iframe; \

Lib/tarfile.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
833833
# For example, 'C:/foo' on Windows.
834834
raise AbsolutePathError(member)
835835
# Ensure we stay in the destination
836+
if '..' in name.replace(os.sep, '/').split('/'):
837+
# Directories are created from the name as given, so a name that
838+
# leaves the destination part-way through would create them
839+
# outside it even if the resolved path stays inside.
840+
normalized = os.path.normpath(name)
841+
if normalized != name:
842+
name = new_attrs['name'] = normalized
836843
target_path = os.path.realpath(os.path.join(dest_path, name),
837844
strict=os.path.ALLOW_MISSING)
838845
if os.path.commonpath([target_path, dest_path]) != dest_path:

Lib/test/support/__init__.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,15 +1271,20 @@ def set_memlimit(limit: str) -> None:
12711271

12721272

12731273
def _memory_watchdog(pid):
1274-
"""Return a function printing the memory usage of process *pid*."""
1274+
"""Return a function printing the memory usage of process *pid*.
1275+
1276+
The largest value it saw is kept in its ``peak`` attribute.
1277+
"""
12751278
# Imported here: test.support does not depend on test.libregrtest.
12761279
from test.libregrtest.utils import get_process_memory_usage
12771280

12781281
def watch():
12791282
mem = get_process_memory_usage(pid)
12801283
if mem is not None:
1284+
watch.peak = max(watch.peak, mem)
12811285
print(f" ... process data size: {mem / (1024 ** 3):.1f} GiB",
12821286
flush=True)
1287+
watch.peak = 0
12831288
return watch
12841289

12851290

@@ -1333,7 +1338,23 @@ def wrapper(self):
13331338
qualname = f'{cls.__qualname__}.{f.__name__}'
13341339
proc = isolation._start_test(cls.__module__, qualname)
13351340
watchdog = _memory_watchdog(proc.pid) if verbose else None
1336-
isolation._replay_test(self, *proc.wait(tick=watchdog))
1341+
payload, output, returncode = proc.wait(tick=watchdog)
1342+
if watchdog:
1343+
# The subprocess measures its own peak exactly. What the
1344+
# parent sampled is only a lower bound.
1345+
maxrss = payload and payload.get('maxrss')
1346+
peak = maxrss or watchdog.peak
1347+
if peak:
1348+
print(f" ... peak memory use: "
1349+
f"{peak / (1024 ** 3):.1f} GiB"
1350+
f"{'' if maxrss else ' or more'}", flush=True)
1351+
majflt = payload and payload.get('majflt')
1352+
if majflt:
1353+
# The test did not fit in memory, so its timing means
1354+
# little.
1355+
print(f" ... {majflt} major page faults: the test "
1356+
f"waited for the disk", flush=True)
1357+
isolation._replay_test(self, payload, output, returncode)
13371358
return
13381359

13391360
return f(self, maxsize)

Lib/test/support/subprocess_runner.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,45 @@ def _outcome(kind, test, detail):
7272
for t, tb in result.expectedFailures]
7373
outcomes += [_outcome('skipped', t, reason) for t, reason in result.skipped]
7474

75-
payload = {'outcomes': outcomes, 'durations': result.id_durations}
75+
def _usage():
76+
"""What this process used: peak resident set size in bytes, and the
77+
number of major page faults it took, either of which can be None.
78+
79+
A major page fault is served from disk, so a non-zero count means swapping.
80+
81+
The modules are imported here, after the test has run, so that the test
82+
does not see them.
83+
"""
84+
try:
85+
import resource
86+
except ImportError:
87+
pass
88+
else:
89+
usage = resource.getrusage(resource.RUSAGE_SELF)
90+
# Solaris and illumos leave these fields at 0, which no live process
91+
# has, so treat it as "not supported".
92+
if not usage.ru_maxrss:
93+
return {'maxrss': None, 'majflt': None}
94+
# ru_maxrss is in bytes on macOS, in kilobytes on Linux and the BSDs.
95+
maxrss = usage.ru_maxrss
96+
return {'maxrss': maxrss if sys.platform == 'darwin' else maxrss * 1024,
97+
'majflt': usage.ru_majflt}
98+
try:
99+
import os
100+
import _winapi
101+
handle = _winapi.OpenProcess(
102+
_winapi.PROCESS_QUERY_LIMITED_INFORMATION, False, os.getpid())
103+
except (ImportError, OSError):
104+
return {'maxrss': None, 'majflt': None}
105+
try:
106+
info = _winapi.GetProcessMemoryInfo(handle)
107+
finally:
108+
_winapi.CloseHandle(handle)
109+
# PageFaultCount counts all faults, not only the ones served from disk.
110+
return {'maxrss': info['PeakWorkingSetSize'], 'majflt': None}
111+
112+
113+
payload = {'outcomes': outcomes, 'durations': result.id_durations, **_usage()}
76114
with open(outfile, 'wb') as f:
77115
marshal.dump(payload, f)
78116

Lib/test/test_asyncgen.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,33 @@ async def agenfn():
619619
with self.assertRaisesRegex(RuntimeError, "coroutine ignored GeneratorExit"):
620620
gen.close()
621621

622+
def test_async_gen_athrow_send_non_none(self):
623+
# gh-120321: sending a non-None value to a just-started athrow()
624+
# awaitable must not claim the generator, so the generator stays
625+
# usable and the awaitable can still be awaited afterwards.
626+
class MyExc(Exception):
627+
pass
628+
629+
async def agenfn():
630+
try:
631+
yield 1
632+
except MyExc:
633+
yield 2
634+
635+
agen = agenfn()
636+
with self.assertRaises(StopIteration):
637+
agen.asend(None).send(None)
638+
639+
gen = agen.athrow(MyExc)
640+
with self.assertRaisesRegex(RuntimeError, "non-None value"):
641+
gen.send(42)
642+
self.assertFalse(agen.ag_running)
643+
644+
# The awaitable is still in its initial state and works normally.
645+
with self.assertRaises(StopIteration) as cm:
646+
gen.send(None)
647+
self.assertEqual(cm.exception.value, 2)
648+
622649

623650
class AsyncGenAsyncioTest(unittest.TestCase):
624651

@@ -1950,6 +1977,41 @@ class MyException(Exception):
19501977
):
19511978
nxt.throw(MyException)
19521979

1980+
def test_async_gen_send_same_athrow_coro_after_completion(self):
1981+
# gh-120321: an athrow() awaitable that needs more than one send()
1982+
# to complete must be closed on completion; sending to it again
1983+
# must raise instead of resuming the generator.
1984+
class YieldOnce:
1985+
def __await__(self):
1986+
yield
1987+
1988+
async def async_iterate():
1989+
try:
1990+
yield 1
1991+
except ValueError:
1992+
await YieldOnce()
1993+
yield 2
1994+
1995+
it = async_iterate()
1996+
with self.assertRaises(StopIteration):
1997+
it.__anext__().send(None)
1998+
1999+
nxt = it.athrow(ValueError)
2000+
# The exception handler suspends before the operation completes.
2001+
nxt.send(None)
2002+
with self.assertRaises(StopIteration) as cm:
2003+
nxt.send(None)
2004+
self.assertEqual(cm.exception.value, 2)
2005+
2006+
with self.assertRaisesRegex(
2007+
RuntimeError,
2008+
r"cannot reuse already awaited aclose\(\)/athrow\(\)"
2009+
):
2010+
nxt.send(None)
2011+
2012+
with self.assertRaises(StopIteration):
2013+
it.aclose().send(None)
2014+
19532015
def test_async_gen_aclose_twice_with_different_coros(self):
19542016
# Regression test for https://bugs.python.org/issue39606
19552017
async def async_iterate():

0 commit comments

Comments
 (0)