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
4 changes: 2 additions & 2 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,8 @@ embedding applications, and native libraries.
unwinding for the whole Python process.

(Contributed by Pablo Galindo Salgado and Savannah Ostrowski in
:gh:`149201`; PEP 831 written by Pablo Galindo Salgado, Ken Jin, and
Savannah Ostrowski.)
:gh:`149201`; PEP 831 written by Pablo Galindo Salgado, Ken Jin,
Savannah Ostrowski, and Diego Russo.)

.. seealso:: :pep:`831` for further details.

Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ annotationlib
Use :meth:`annotationlib.ForwardRef.evaluate`
or :func:`typing.evaluate_forward_ref` instead.

functools
---------

* Calling the Python implementation of :func:`functools.reduce` with *function*
or *sequence* as keyword arguments has been deprecated since Python 3.14.

sysconfig
---------

Expand Down
35 changes: 6 additions & 29 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def __ge__(self, other):

_initial_missing = object()

def reduce(function, sequence, initial=_initial_missing):
def reduce(function, sequence, /, initial=_initial_missing):
"""
reduce(function, iterable, /[, initial]) -> value

Expand Down Expand Up @@ -264,6 +264,11 @@ def reduce(function, sequence, initial=_initial_missing):

return value

try:
from _functools import reduce
except ImportError:
pass


################################################################################
### partial() argument application
Expand Down Expand Up @@ -1178,31 +1183,3 @@ def __get__(self, instance, owner=None):
return val

__class_getitem__ = classmethod(GenericAlias)

def _warn_python_reduce_kwargs(py_reduce):
@wraps(py_reduce)
def wrapper(*args, **kwargs):
if 'function' in kwargs or 'sequence' in kwargs:
import os
import warnings
warnings.warn(
'Calling functools.reduce with keyword arguments '
'"function" or "sequence" '
'is deprecated in Python 3.14 and will be '
'forbidden in Python 3.16.',
DeprecationWarning,
skip_file_prefixes=(os.path.dirname(__file__),))
return py_reduce(*args, **kwargs)
return wrapper

reduce = _warn_python_reduce_kwargs(reduce)
del _warn_python_reduce_kwargs

# The import of the C accelerated version of reduce() has been moved
# here due to gh-121676. In Python 3.16, _warn_python_reduce_kwargs()
# should be removed and the import block should be moved back right
# after the definition of reduce().
try:
from _functools import reduce
except ImportError:
pass
16 changes: 11 additions & 5 deletions Lib/tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,16 +830,22 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
if member.islnk() or member.issym():
if os.path.isabs(member.linkname):
raise AbsoluteLinkError(member)
# A link member that resolves to the destination directory itself
# would replace it with a (sym)link, redirecting the destination
# for all subsequent members.
if target_path == dest_path:
raise OutsideDestinationError(member, target_path)
normalized = os.path.normpath(member.linkname)
if normalized != member.linkname:
new_attrs['linkname'] = normalized
if member.issym():
target_path = os.path.join(dest_path,
os.path.dirname(name),
member.linkname)
# The symlink is created at `name` with trailing separators
# stripped, so its target is relative to the directory
# containing that path.
link_dir = os.path.dirname(name.rstrip('/' + os.sep))
target_path = os.path.join(dest_path, link_dir, normalized)
else:
target_path = os.path.join(dest_path,
member.linkname)
target_path = os.path.join(dest_path, normalized)
target_path = os.path.realpath(target_path,
strict=os.path.ALLOW_MISSING)
if os.path.commonpath([target_path, dest_path]) != dest_path:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
"""Test in-process C stack unwinders against Python and JIT frames.

The tests build a recursive Python call stack, ask each _testinternalcapi
unwinder for return addresses, and classify those addresses as Python, JIT, or
other frames. The backends include CPython's manual stack-chain unwinder and
GNU backtrace(), so this module is about in-process C stack unwinding rather
than a single unwind mechanism. GDB integration tests live in test_gdb.
"""

import json
import os
import platform
Expand All @@ -20,7 +29,7 @@
STACK_DEPTH = 10


def _frame_pointers_expected(machine):
def _manual_unwind_expected(machine):
_Py_WITH_FRAME_POINTERS = getattr(
_testinternalcapi,
"_Py_WITH_FRAME_POINTERS",
Expand Down Expand Up @@ -89,6 +98,21 @@ def _frame_pointers_expected(machine):
return None


def _is_arm32_build():
if sys.maxsize >= 2**32:
return False

abi = " ".join(
value for value in (
sysconfig.get_config_var("MULTIARCH"),
sysconfig.get_config_var("HOST_GNU_TYPE"),
sysconfig.get_config_var("SOABI"),
)
if value
).lower()
return "arm" in abi


def _build_stack_and_unwind(unwinder):
import operator

Expand Down Expand Up @@ -180,7 +204,7 @@ def _annotate_unwind_after_executor_free(unwinder_name="gnu_backtrace_unwind"):

def _run_unwind_helper(helper_name, unwinder_name, **env):
code = (
f"from test.test_frame_pointer_unwind import {helper_name}; "
f"from test.test_c_stack_unwind import {helper_name}; "
f"print({helper_name}({unwinder_name!r}));"
)
run_env = os.environ.copy()
Expand Down Expand Up @@ -220,28 +244,32 @@ def _unwind_after_executor_free_result(unwinder_name, **env):

@support.requires_gil_enabled("test requires the GIL enabled")
@unittest.skipIf(support.is_wasi, "test not supported on WASI")
class FramePointerUnwindTests(unittest.TestCase):
class ManualStackUnwindTests(unittest.TestCase):

def setUp(self):
super().setUp()

machine = platform.machine().lower()
expected = _frame_pointers_expected(machine)
expected = _manual_unwind_expected(machine)
if expected is None:
self.skipTest(f"unsupported architecture for frame pointer check: {machine}")
self.skipTest(
f"unsupported architecture for manual stack unwind check: {machine}"
)
if expected == "crash":
self.skipTest(f"test does crash on {machine}")

try:
_testinternalcapi.manual_frame_pointer_unwind()
except RuntimeError as exc:
if "not supported" in str(exc):
self.skipTest("manual frame pointer unwinding not supported on this platform")
self.skipTest(
"manual stack unwinding not supported on this platform"
)
raise
self.machine = machine
self.frame_pointers_expected = expected
self.manual_unwind_expected = expected

def test_manual_unwind_respects_frame_pointers(self):
def test_manual_unwind_finds_expected_frames(self):
jit_available = hasattr(sys, "_jit") and sys._jit.is_available()
envs = [({"PYTHON_JIT": "0"}, False)]
if jit_available:
Expand All @@ -253,7 +281,7 @@ def test_manual_unwind_respects_frame_pointers(self):
jit_frames = result["jit_frames"]
python_frames = result.get("python_frames", 0)
jit_backend = result.get("jit_backend")
if self.frame_pointers_expected:
if self.manual_unwind_expected:
self.assertGreaterEqual(
python_frames,
STACK_DEPTH,
Expand Down Expand Up @@ -295,6 +323,10 @@ def test_manual_unwind_respects_frame_pointers(self):
@support.requires_gil_enabled("test requires the GIL enabled")
@unittest.skipIf(support.is_wasi, "test not supported on WASI")
@unittest.skipUnless(sys.platform == "linux", "GNU backtrace unwinding test requires Linux")
@unittest.skipIf(
_is_arm32_build(),
"GNU backtrace unwinding skipped on Arm 32-bit",
)
class GnuBacktraceUnwindTests(unittest.TestCase):

def setUp(self):
Expand Down
14 changes: 8 additions & 6 deletions Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,14 @@ def add(x, y):
self.assertRaises(TypeError, self.reduce, add, [0, 1], initial="")
self.assertEqual(self.reduce(42, "", initial="1"), "1") # func is never called with one item

def test_reduce_with_kwargs(self):
with self.assertRaises(TypeError):
self.reduce(function=lambda x, y: (x or 1) + y, sequence=[1, 2, 3, 4, 5])
with self.assertRaises(TypeError):
self.reduce(function=lambda x, y: x + y, sequence=[1, 2, 3, 4, 5], initial=1)
with self.assertRaises(TypeError):
self.reduce(lambda x, y: x + y, sequence=[1, 2, 3, 4, 5], initial=1)


@unittest.skipUnless(c_functools, 'requires the C _functools module')
class TestReduceC(TestReduce, unittest.TestCase):
Expand All @@ -1144,12 +1152,6 @@ class TestReduceC(TestReduce, unittest.TestCase):
class TestReducePy(TestReduce, unittest.TestCase):
reduce = staticmethod(py_functools.reduce)

def test_reduce_with_kwargs(self):
with self.assertWarns(DeprecationWarning):
self.reduce(function=lambda x, y: x + y, sequence=[1, 2, 3, 4, 5], initial=1)
with self.assertWarns(DeprecationWarning):
self.reduce(lambda x, y: x + y, sequence=[1, 2, 3, 4, 5], initial=1)


class TestCmpToKey:

Expand Down
87 changes: 83 additions & 4 deletions Lib/test/test_tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3911,10 +3911,19 @@ def test_parent_symlink(self):
+ "which is outside the destination")

with self.check_context(arc.open(), 'data'):
self.expect_exception(
tarfile.LinkOutsideDestinationError,
"""'parent' would link to ['"].*outerdir['"], """
+ "which is outside the destination")
if self.dotdot_resolves_early:
# 'current/../..' normalises to '..', which is rejected.
self.expect_exception(
tarfile.LinkOutsideDestinationError,
"""'parent' would link to ['"].*outerdir['"], """
+ "which is outside the destination")
else:
# 'current/..' normalises to '.'; the rewritten link is
# created and 'parent/evil' lands harmlessly inside the
# destination.
self.expect_file('current', symlink_to='.')
self.expect_file('parent', symlink_to='.')
self.expect_file('evil')

else:
# No symlink support. The symlinks are ignored.
Expand Down Expand Up @@ -4174,6 +4183,76 @@ def test_sly_relative2(self):
+ """['"].*moo['"], which is outside the """
+ "destination")

@symlink_test
@os_helper.skip_unless_symlink
def test_normpath_realpath_mismatch(self):
# The link-target check must validate the value that will actually
# be written to disk (the normalised linkname), not the original.
# Here 'a' is a symlink to a deep nonexistent path, so realpath()
# of 'a/../../...' stays inside the destination while normpath()
# collapses 'a/..' lexically and escapes.
depth = len(self.destdir.parts) + 5
deep = '/'.join(f'p{i}' for i in range(depth))
sneaky = 'a/' + '../' * depth + 'flag'
for kind in 'symlink_to', 'hardlink_to':
with self.subTest(kind):
with ArchiveMaker() as arc:
arc.add('a', symlink_to=deep)
arc.add('escape', **{kind: sneaky})
with self.check_context(arc.open(), 'data'):
self.expect_exception(
tarfile.LinkOutsideDestinationError)

@symlink_test
@os_helper.skip_unless_symlink
def test_symlink_trailing_slash(self):
# A trailing slash on a symlink member's name must not cause the
# link target to be resolved relative to the wrong directory.
with ArchiveMaker() as arc:
t = tarfile.TarInfo('x/')
t.type = tarfile.SYMTYPE
t.linkname = '..'
arc.tar_w.addfile(t)
arc.add('x/escaped', content='hi')

with self.check_context(arc.open(), 'data'):
self.expect_exception(tarfile.LinkOutsideDestinationError)

@symlink_test
@os_helper.skip_unless_symlink
def test_link_at_destination(self):
# A link member whose name resolves to the destination directory
# itself must be rejected: otherwise the destination is replaced
# by a symlink and later members can be redirected through it.
for name in '', '.', './':
with ArchiveMaker() as arc:
t = tarfile.TarInfo(name)
t.type = tarfile.SYMTYPE
t.linkname = '.'
arc.tar_w.addfile(t)

with self.check_context(arc.open(), 'data'):
self.expect_exception(tarfile.OutsideDestinationError)

@symlink_test
@os_helper.skip_unless_symlink
def test_empty_name_symlink_chain(self):
# Regression test for a chain of empty-named symlinks that
# incrementally redirects the destination outwards.
with ArchiveMaker() as arc:
for name, target in [('', ''), ('a/', '..'),
('', 'dummy'), ('', 'a'),
('b/', '..'),
('', 'dummy'), ('', 'a/b')]:
t = tarfile.TarInfo(name)
t.type = tarfile.SYMTYPE
t.linkname = target
arc.tar_w.addfile(t)
arc.add('escaped', content='hi')

with self.check_context(arc.open(), 'data'):
self.expect_exception(tarfile.FilterError)

@symlink_test
def test_deep_symlink(self):
# Test that symlinks and hardlinks inside a directory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Remove kw parameters from python version of :func:`functools.reduce`
function.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:func:`tarfile.data_filter` now validates link targets using the same
normalised value that is written to disk, strips trailing separators from
the member name when resolving a symlink's directory, and rejects link
members that would replace the destination directory itself. This closes
several path-traversal bypasses of the ``data`` extraction filter.
Loading