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
11 changes: 10 additions & 1 deletion Lib/ftplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,16 @@ def ftpcp(source, sourcename, target, targetname = '', type = 'I'):
type = 'TYPE ' + type
source.voidcmd(type)
target.voidcmd(type)
sourcehost, sourceport = parse227(source.sendcmd('PASV'))
# Don't trust the IPv4 address the source server advertises in its PASV
# reply: a malicious source could otherwise point the target's data
# connection at an arbitrary host (SSRF). A caller that needs the old
# behavior can set trust_server_pasv_ipv4_address on the source FTP
# object. See FTP.makepasv(), which applies the same rule.
untrusted_host, sourceport = parse227(source.sendcmd('PASV'))
if source.trust_server_pasv_ipv4_address:
sourcehost = untrusted_host
else:
sourcehost = source.sock.getpeername()[0]
target.sendport(sourcehost, sourceport)
# RFC 959: the user must "listen" [...] BEFORE sending the
# transfer request.
Expand Down
36 changes: 35 additions & 1 deletion Lib/test/test_ftplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
except ImportError:
ssl = None

from unittest import TestCase, skipUnless
from unittest import mock, TestCase, skipUnless
from test import support
from test.support import requires_subprocess
from test.support import threading_helper
Expand Down Expand Up @@ -1145,6 +1145,40 @@ def testTimeoutDirectAccess(self):
ftp.close()


class TestFtpcpSecurity(TestCase):
"""ftpcp() must not trust the host a source server advertises in PASV.

A malicious source server can otherwise redirect the target server's
data connection to an arbitrary host:port (SSRF), so ftpcp() uses the
source server's actual peer address instead, the same as FTP.makepasv().
"""

def _make_pair(self, *, advertised_host, real_host, trust=False):
source = mock.Mock(spec=ftplib.FTP)
source.trust_server_pasv_ipv4_address = trust
source.sock.getpeername.return_value = (real_host, 21)
# PASV replies give the host as comma-separated octets, not dotted.
advertised = advertised_host.replace('.', ',')
source.sendcmd.side_effect = lambda cmd: (
f'227 Entering Passive Mode ({advertised},1,2).'
if cmd == 'PASV' else '150 ok')
target = mock.Mock(spec=ftplib.FTP)
target.sendcmd.return_value = '150 ok'
return source, target

def test_ftpcp_ignores_untrusted_pasv_host(self):
source, target = self._make_pair(advertised_host='10.0.0.5',
real_host='198.51.100.7')
ftplib.ftpcp(source, 'a', target, 'b')
target.sendport.assert_called_once_with('198.51.100.7', 258)

def test_ftpcp_trust_server_pasv_ipv4_address(self):
source, target = self._make_pair(advertised_host='10.0.0.5',
real_host='198.51.100.7', trust=True)
ftplib.ftpcp(source, 'a', target, 'b')
target.sendport.assert_called_once_with('10.0.0.5', 258)


class MiscTestCase(TestCase):
def test__all__(self):
not_exported = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
The :mod:`ftplib` module's undocumented ``ftpcp`` function no longer trusts
the IPv4 address value returned from the source server in response to the
``PASV`` command by default, completing the fix for CVE-2021-4189. As with
:class:`ftplib.FTP`, the former behavior can be re-enabled by setting the
``trust_server_pasv_ipv4_address`` attribute on the source :class:`ftplib.FTP`
instance to ``True``. Thanks to Qi Deng at Aurascape AI for the report.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update bundled `libexpat <https://libexpat.github.io/>`_ to version 2.8.1
for the fix for :cve:`2026-45186`.
16 changes: 8 additions & 8 deletions Misc/sbom.spdx.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Modules/_posixsubprocess.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
# endif
#endif

#if defined(__FreeBSD__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__DragonFly__)
#if defined(__CYGWIN__) || defined(__FreeBSD__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__DragonFly__)
# define FD_DIR "/dev/fd"
#else
# define FD_DIR "/proc/self/fd"
Expand Down
2 changes: 1 addition & 1 deletion Modules/expat/expat.h
Original file line number Diff line number Diff line change
Expand Up @@ -1094,7 +1094,7 @@ XML_SetReparseDeferralEnabled(XML_Parser parser, XML_Bool enabled);
*/
# define XML_MAJOR_VERSION 2
# define XML_MINOR_VERSION 8
# define XML_MICRO_VERSION 0
# define XML_MICRO_VERSION 1

# ifdef __cplusplus
}
Expand Down
6 changes: 3 additions & 3 deletions Modules/expat/refresh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ fi

# Update this when updating to a new version after verifying that the changes
# the update brings in are good. These values are used for verifying the SBOM, too.
expected_libexpat_tag="R_2_8_0"
expected_libexpat_version="2.8.0"
expected_libexpat_sha256="c7cec5f60ea3a42e7780781c6745255c19aa3dbfeeae58646b7132f88dc24780"
expected_libexpat_tag="R_2_8_1"
expected_libexpat_version="2.8.1"
expected_libexpat_sha256="a52eb72108be160e190b5cafa5bba8663f1313f2013e26060d1c18e26e31067b"

expat_dir="$(realpath "$(dirname -- "${BASH_SOURCE[0]}")")"
cd ${expat_dir}
Expand Down
36 changes: 30 additions & 6 deletions Modules/expat/xmlparse.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* a5d18f6a50f536615ac1c70304f87d94f99cc85a86b502188952440610ccf0f8 (2.8.0+)
/* 75ef4224f81c052e9e5aeea2ac7de75357d2169ff9908e39edc08b9dc3052513 (2.8.1+)
__ __ _
___\ \/ /_ __ __ _| |_
/ _ \\ /| '_ \ / _` | __|
Expand Down Expand Up @@ -387,6 +387,7 @@ typedef struct {
int nDefaultAtts;
int allocDefaultAtts;
DEFAULT_ATTRIBUTE *defaultAtts;
HASH_TABLE defaultAttsNames;
} ELEMENT_TYPE;

typedef struct {
Expand Down Expand Up @@ -3769,6 +3770,8 @@ storeAtts(XML_Parser parser, const ENCODING *enc, const char *attStr,
sizeof(ELEMENT_TYPE));
if (! elementType)
return XML_ERROR_NO_MEMORY;
if (! elementType->defaultAttsNames.parser)
hashTableInit(&(elementType->defaultAttsNames), parser);
if (parser->m_ns && ! setElementTypePrefix(parser, elementType))
return XML_ERROR_NO_MEMORY;
}
Expand Down Expand Up @@ -7102,10 +7105,10 @@ defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata,
if (value || isId) {
/* The handling of default attributes gets messed up if we have
a default which duplicates a non-default. */
int i;
for (i = 0; i < type->nDefaultAtts; i++)
if (attId == type->defaultAtts[i].id)
return 1;
NAMED *const nameFound
= (NAMED *)lookup(parser, &(type->defaultAttsNames), attId->name, 0);
if (nameFound)
return 1;
if (isId && ! type->idAtt && ! attId->xmlns)
type->idAtt = attId;
}
Expand Down Expand Up @@ -7152,6 +7155,12 @@ defineAttribute(ELEMENT_TYPE *type, ATTRIBUTE_ID *attId, XML_Bool isCdata,
att->isCdata = isCdata;
if (! isCdata)
attId->maybeTokenized = XML_TRUE;

NAMED *const nameAddedOrFound = (NAMED *)lookup(
parser, &(type->defaultAttsNames), attId->name, sizeof(NAMED));
if (! nameAddedOrFound)
return 0;

type->nDefaultAtts += 1;
return 1;
}
Expand Down Expand Up @@ -7477,6 +7486,7 @@ dtdReset(DTD *p, XML_Parser parser) {
ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter);
if (! e)
break;
hashTableDestroy(&(e->defaultAttsNames));
if (e->allocDefaultAtts != 0)
FREE(parser, e->defaultAtts);
}
Expand Down Expand Up @@ -7518,6 +7528,7 @@ dtdDestroy(DTD *p, XML_Bool isDocEntity, XML_Parser parser) {
ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter);
if (! e)
break;
hashTableDestroy(&(e->defaultAttsNames));
if (e->allocDefaultAtts != 0)
FREE(parser, e->defaultAtts);
}
Expand Down Expand Up @@ -7611,6 +7622,10 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
sizeof(ELEMENT_TYPE));
if (! newE)
return 0;

if (! newE->defaultAttsNames.parser)
hashTableInit(&(newE->defaultAttsNames), parser);

if (oldE->nDefaultAtts) {
/* Detect and prevent integer overflow.
* The preprocessor guard addresses the "always false" warning
Expand All @@ -7635,8 +7650,9 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
newE->prefix = (PREFIX *)lookup(oldParser, &(newDtd->prefixes),
oldE->prefix->name, 0);
for (i = 0; i < newE->nDefaultAtts; i++) {
const XML_Char *const attributeName = oldE->defaultAtts[i].id->name;
newE->defaultAtts[i].id = (ATTRIBUTE_ID *)lookup(
oldParser, &(newDtd->attributeIds), oldE->defaultAtts[i].id->name, 0);
oldParser, &(newDtd->attributeIds), attributeName, 0);
newE->defaultAtts[i].isCdata = oldE->defaultAtts[i].isCdata;
if (oldE->defaultAtts[i].value) {
newE->defaultAtts[i].value
Expand All @@ -7645,6 +7661,12 @@ dtdCopy(XML_Parser oldParser, DTD *newDtd, const DTD *oldDtd,
return 0;
} else
newE->defaultAtts[i].value = NULL;

NAMED *const nameAddedOrFound = (NAMED *)lookup(
parser, &(newE->defaultAttsNames), attributeName, sizeof(NAMED));
if (! nameAddedOrFound) {
return 0;
}
}
}

Expand Down Expand Up @@ -8391,6 +8413,8 @@ getElementType(XML_Parser parser, const ENCODING *enc, const char *ptr,
sizeof(ELEMENT_TYPE));
if (! ret)
return NULL;
if (! ret->defaultAttsNames.parser)
hashTableInit(&(ret->defaultAttsNames), getRootParserOf(parser, NULL));
if (ret->name != name)
poolDiscard(&dtd->pool);
else {
Expand Down
2 changes: 1 addition & 1 deletion Objects/mimalloc/prim/unix/prim.c
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ bool _mi_prim_random_buf(void* buf, size_t buf_len) {

#elif defined(__ANDROID__) || defined(__DragonFly__) || \
defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \
defined(__sun)
defined(__sun) || defined(__CYGWIN__)

#include <stdlib.h>
bool _mi_prim_random_buf(void* buf, size_t buf_len) {
Expand Down
36 changes: 25 additions & 11 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -4841,6 +4841,18 @@ type_new_set_attrs(const type_new_ctx *ctx, PyTypeObject *type)
if (type_new_set_classdictcell(dict) < 0) {
return -1;
}

#ifdef Py_GIL_DISABLED
// enable deferred reference counting on functions and descriptors
Py_ssize_t pos = 0;
PyObject *key, *value;
while (PyDict_Next(dict, &pos, &key, &value)) {
if (PyFunction_Check(value) || Py_TYPE(value)->tp_descr_get != NULL) {
PyUnstable_Object_EnableDeferredRefcount(value);
}
}
#endif

return 0;
}

Expand Down Expand Up @@ -6746,12 +6758,11 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value)
assert(!_PyType_HasFeature(metatype, Py_TPFLAGS_MANAGED_DICT));

#ifdef Py_GIL_DISABLED
// gh-139103: Enable deferred refcounting for functions assigned
// to type objects. This is important for `dataclass.__init__`,
// which is generated dynamically.
if (value != NULL &&
PyFunction_Check(value) &&
!_PyObject_HasDeferredRefcount(value))
// gh-139103: Enable deferred refcounting for functions and descriptors
// assigned to type objects. This is important for `dataclass.__init__`,
// which is generated dynamically, and for descriptor scaling on
// free-threaded builds.
if (value != NULL && (PyFunction_Check(value) || Py_TYPE(value)->tp_descr_get != NULL))
{
PyUnstable_Object_EnableDeferredRefcount(value);
}
Expand Down Expand Up @@ -11089,10 +11100,12 @@ static PyObject *
slot_tp_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
PyTypeObject *tp = Py_TYPE(self);
PyObject *get;

get = _PyType_LookupRef(tp, &_Py_ID(__get__));
if (get == NULL) {
PyThreadState *tstate = _PyThreadState_GET();
_PyCStackRef cref;
_PyThreadState_PushCStackRef(tstate, &cref);
_PyType_LookupStackRefAndVersion(tp, &_Py_ID(__get__), &cref.ref);
if (PyStackRef_IsNull(cref.ref)) {
_PyThreadState_PopCStackRef(tstate, &cref);
#ifndef Py_GIL_DISABLED
/* Avoid further slowdowns */
if (tp->tp_descr_get == slot_tp_descr_get)
Expand All @@ -11104,9 +11117,10 @@ slot_tp_descr_get(PyObject *self, PyObject *obj, PyObject *type)
obj = Py_None;
if (type == NULL)
type = Py_None;
PyObject *get = PyStackRef_AsPyObjectBorrow(cref.ref);
PyObject *stack[3] = {self, obj, type};
PyObject *res = PyObject_Vectorcall(get, stack, 3, NULL);
Py_DECREF(get);
_PyThreadState_PopCStackRef(tstate, &cref);
return res;
}

Expand Down
1 change: 1 addition & 0 deletions Python/dynload_shlib.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

const char *_PyImport_DynLoadFiletab[] = {
#ifdef __CYGWIN__
"." SOABI ".dll",
".dll",
#else /* !__CYGWIN__ */
"." SOABI ".so",
Expand Down
17 changes: 17 additions & 0 deletions Tools/ftscalingbench/ftscalingbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,23 @@ def staticmethod_call():
for _ in range(1000 * WORK_SCALE):
obj.my_staticmethod()


class MyDescriptor:
def __get__(self, obj, objtype=None):
return 42

def __set__(self, obj, value):
pass

class MyClassWithDescriptor:
attr = MyDescriptor()

@register_benchmark
def descriptor():
obj = MyClassWithDescriptor()
for _ in range(1000 * WORK_SCALE):
obj.attr

@register_benchmark
def deepcopy():
x = {'list': [1, 2], 'tuple': (1, None)}
Expand Down
Loading