Skip to content
Open
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
43 changes: 43 additions & 0 deletions Include/moduleobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,49 @@ struct PyModuleDef {
freefunc m_free;
};

#if defined(_PyHack_check_version_on_modinit) && defined(Py_BUILD_CORE)
/* The mechanism for the check has been implemented on Python 3.15+:
* https://github.com/python/cpython/pull/137212.
* In Fedora, we need this in older Pythons too:
* if somebody attempts to import a module compiled for a different Python version,
* instead of segmentation fault a meaningful error is raised.
*/
PyAPI_DATA(const unsigned long) Py_Version;

static inline int
_PyHack_CheckInternalAPIVersion(const char *mod_name)
{
if (PY_VERSION_HEX != Py_Version) {
PyErr_Format(
PyExc_ImportError,
"internal Python C API version mismatch: "
"module %s compiled with %lu.%lu.%lu; "
"runtime version is %lu.%lu.%lu",
mod_name,
(const unsigned long)((PY_VERSION_HEX >> 24) & 0xFF),
(const unsigned long)((PY_VERSION_HEX >> 16) & 0xFF),
(const unsigned long)((PY_VERSION_HEX >> 8) & 0xFF),
(const unsigned long)((Py_Version >> 24) & 0xFF),
(const unsigned long)((Py_Version >> 16) & 0xFF),
(const unsigned long)((Py_Version >> 8) & 0xFF)
);
return -1;
}
return 0;
}

static inline PyObject *
PyModuleDef_Init_with_check(PyModuleDef *def)
{
if (_PyHack_CheckInternalAPIVersion(def->m_name) < 0) {
return NULL;
}
return PyModuleDef_Init(def);
}

#define PyModuleDef_Init PyModuleDef_Init_with_check
#endif

#ifdef __cplusplus
}
#endif
Expand Down
11 changes: 10 additions & 1 deletion Lib/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,13 +972,22 @@ def _wrap_ipv6(self, ip):
return ip

def _tunnel(self):
if _contains_disallowed_url_pchar_re.search(self._tunnel_host):
raise ValueError('Tunnel host can\'t contain control characters %r'
% (self._tunnel_host,))
connect = b"CONNECT %s:%d %s\r\n" % (
self._wrap_ipv6(self._tunnel_host.encode("idna")),
self._tunnel_port,
self._http_vsn_str.encode("ascii"))
headers = [connect]
for header, value in self._tunnel_headers.items():
headers.append(f"{header}: {value}\r\n".encode("latin-1"))
header_bytes = header.encode("latin-1")
value_bytes = value.encode("latin-1")
if not _is_legal_header_name(header_bytes):
raise ValueError('Invalid header name %r' % (header_bytes,))
if _is_illegal_header_value(value_bytes):
raise ValueError('Invalid header value %r' % (value_bytes,))
headers.append(b"%s: %s\r\n" % (header_bytes, value_bytes))
headers.append(b"\r\n")
# Making a single send() call instead of one per line encourages
# the host OS to use a more optimal packet size instead of
Expand Down
4 changes: 3 additions & 1 deletion Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
# We compile these in _mode_xxx.
_Literal = br'.*{(?P<size>\d+)}$'
_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'

_control_chars = re.compile(b'[\x00-\x1F\x7F]')


class IMAP4:
Expand Down Expand Up @@ -1108,6 +1108,8 @@ def _command(self, name, *args):
if arg is None: continue
if isinstance(arg, str):
arg = bytes(arg, self._encoding)
if _control_chars.search(arg):
raise ValueError("Control characters not allowed in commands")
data = data + b' ' + arg

literal = self.literal
Expand Down
2 changes: 2 additions & 0 deletions Lib/poplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ def _putline(self, line):
def _putcmd(self, line):
if self._debugging: print('*cmd*', repr(line))
line = bytes(line, self.encoding)
if re.search(b'[\x00-\x1F\x7F]', line):
raise ValueError('Control characters not allowed in commands')
self._putline(line)


Expand Down
9 changes: 8 additions & 1 deletion Lib/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,15 @@ def getsitepackages(prefixes=None):
return sitepackages

def addsitepackages(known_paths, prefixes=None):
"""Add site-packages to sys.path"""
"""Add site-packages to sys.path

'/usr/local' is included in PREFIXES if RPM build is not detected
to make packages installed into this location visible.

"""
_trace("Processing global site-packages")
if ENABLE_USER_SITE and 'RPM_BUILD_ROOT' not in os.environ:
PREFIXES.insert(0, "/usr/local")
for sitedir in getsitepackages(prefixes):
if os.path.isdir(sitedir):
addsitedir(sitedir, known_paths)
Expand Down
50 changes: 49 additions & 1 deletion Lib/sysconfig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@
else:
_INSTALL_SCHEMES['venv'] = _INSTALL_SCHEMES['posix_venv']

# For a brief period of time in the Fedora 36 life cycle,
# this installation scheme existed and was documented in the release notes.
# For backwards compatibility, we keep it here (at least on 3.10 and 3.11).
_INSTALL_SCHEMES['rpm_prefix'] = _INSTALL_SCHEMES['posix_prefix']


def _get_implementation():
return 'Python'

Expand Down Expand Up @@ -169,6 +175,19 @@ def joinuser(*args):
},
}

# This is used by distutils.command.install in the stdlib
# as well as pypa/distutils (e.g. bundled in setuptools).
# The self.prefix value is set to sys.prefix + /local/
# if neither RPM build nor virtual environment is
# detected to make distutils install packages
# into the separate location.
# https://fedoraproject.org/wiki/Changes/Making_sudo_pip_safe
if (not (hasattr(sys, 'real_prefix') or
sys.prefix != sys.base_prefix) and
'RPM_BUILD_ROOT' not in os.environ):
_prefix_addition = '/local'


_SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include',
'scripts', 'data')

Expand Down Expand Up @@ -268,11 +287,40 @@ def _extend_dict(target_dict, other_dict):
target_dict[key] = value


_CONFIG_VARS_LOCAL = None


def _config_vars_local():
# This function returns the config vars with prefixes amended to /usr/local
# https://fedoraproject.org/wiki/Changes/Making_sudo_pip_safe
global _CONFIG_VARS_LOCAL
if _CONFIG_VARS_LOCAL is None:
_CONFIG_VARS_LOCAL = dict(get_config_vars())
_CONFIG_VARS_LOCAL['base'] = '/usr/local'
_CONFIG_VARS_LOCAL['platbase'] = '/usr/local'
return _CONFIG_VARS_LOCAL


def _expand_vars(scheme, vars):
res = {}
if vars is None:
vars = {}
_extend_dict(vars, get_config_vars())

# when we are not in a virtual environment or an RPM build
# we change '/usr' to '/usr/local'
# to avoid surprises, we explicitly check for the /usr/ prefix
# Python virtual environments have different prefixes
# we only do this for posix_prefix, not to mangle the venv scheme
# posix_prefix is used by sudo pip install
# we only change the defaults here, so explicit --prefix will take precedence
# https://fedoraproject.org/wiki/Changes/Making_sudo_pip_safe
if (scheme == 'posix_prefix' and
sys.prefix == '/usr' and
'RPM_BUILD_ROOT' not in os.environ):
_extend_dict(vars, _config_vars_local())
else:
_extend_dict(vars, get_config_vars())

if os.name == 'nt':
# On Windows we want to substitute 'lib' for schemes rather
# than the native value (without modifying vars, in case it
Expand Down
45 changes: 45 additions & 0 deletions Lib/test/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,51 @@ def test_invalid_headers(self):
with self.assertRaisesRegex(ValueError, 'Invalid header'):
conn.putheader(name, value)

def test_invalid_tunnel_headers(self):
cases = (
('Invalid\r\nName', 'ValidValue'),
('Invalid\rName', 'ValidValue'),
('Invalid\nName', 'ValidValue'),
('\r\nInvalidName', 'ValidValue'),
('\rInvalidName', 'ValidValue'),
('\nInvalidName', 'ValidValue'),
(' InvalidName', 'ValidValue'),
('\tInvalidName', 'ValidValue'),
('Invalid:Name', 'ValidValue'),
(':InvalidName', 'ValidValue'),
('ValidName', 'Invalid\r\nValue'),
('ValidName', 'Invalid\rValue'),
('ValidName', 'Invalid\nValue'),
('ValidName', 'InvalidValue\r\n'),
('ValidName', 'InvalidValue\r'),
('ValidName', 'InvalidValue\n'),
)
for name, value in cases:
with self.subTest((name, value)):
conn = client.HTTPConnection('example.com')
conn.set_tunnel('tunnel', headers={
name: value
})
conn.sock = FakeSocket('')
with self.assertRaisesRegex(ValueError, 'Invalid header'):
conn._tunnel() # Called in .connect()

def test_invalid_tunnel_host(self):
cases = (
'invalid\r.host',
'\ninvalid.host',
'invalid.host\r\n',
'invalid.host\x00',
'invalid host',
)
for tunnel_host in cases:
with self.subTest(tunnel_host):
conn = client.HTTPConnection('example.com')
conn.set_tunnel(tunnel_host)
conn.sock = FakeSocket('')
with self.assertRaisesRegex(ValueError, 'Tunnel host can\'t contain control characters'):
conn._tunnel() # Called in .connect()

def test_headers_debuglevel(self):
body = (
b'HTTP/1.1 200 OK\r\n'
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,12 @@ def test_unselect(self):
self.assertEqual(data[0], b'Returned to authenticated state. (Success)')
self.assertEqual(client.state, 'AUTH')

def test_control_characters(self):
client, _ = self._setup(SimpleIMAPHandler)
for c0 in support.control_characters_c0():
with self.assertRaises(ValueError):
client.login(f'user{c0}', 'pass')

# property tests

def test_file_property_should_not_be_accessed(self):
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_poplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from test.support import threading_helper
from test.support import asynchat
from test.support import asyncore
from test.support import control_characters_c0


test_support.requires_working_socket(module=True)
Expand Down Expand Up @@ -395,6 +396,13 @@ def test_quit(self):
self.assertIsNone(self.client.sock)
self.assertIsNone(self.client.file)

def test_control_characters(self):
for c0 in control_characters_c0():
with self.assertRaises(ValueError):
self.client.user(f'user{c0}')
with self.assertRaises(ValueError):
self.client.pass_(f'{c0}pass')

@requires_ssl
def test_stls_capa(self):
capa = self.client.capa()
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_pyexpat.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,8 @@ def start_element(name, _):

self.assertEqual(started, ['doc'])

@unittest.skipIf(expat.version_info < (2, 7, 1),
f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_reparse_deferral_disabled(self):
started = []

Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_sax.py
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,8 @@ def test_flush_reparse_deferral_enabled(self):

self.assertEqual(result.getvalue(), start + b"<doc></doc>")

@unittest.skipIf(pyexpat.version_info < (2, 7, 1),
f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_flush_reparse_deferral_disabled(self):
result = BytesIO()
xmlgen = XMLGenerator(result)
Expand Down
17 changes: 15 additions & 2 deletions Lib/test/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,19 @@ def test_get_path(self):
for scheme in _INSTALL_SCHEMES:
for name in _INSTALL_SCHEMES[scheme]:
expected = _INSTALL_SCHEMES[scheme][name].format(**config_vars)
tested = get_path(name, scheme)
# https://fedoraproject.org/wiki/Changes/Making_sudo_pip_safe
if tested.startswith('/usr/local'):
# /usr/local should only be used in posix_prefix
self.assertEqual(scheme, 'posix_prefix')
# Fedora CI runs tests for venv and virtualenv that check for other prefixes
self.assertEqual(sys.prefix, '/usr')
# When building the RPM of Python, %check runs this with RPM_BUILD_ROOT set
# Fedora CI runs this with RPM_BUILD_ROOT unset
self.assertNotIn('RPM_BUILD_ROOT', os.environ)
tested = tested.replace('/usr/local', '/usr')
self.assertEqual(
os.path.normpath(get_path(name, scheme)),
os.path.normpath(tested),
os.path.normpath(expected),
)

Expand Down Expand Up @@ -397,7 +408,7 @@ def test_get_config_h_filename(self):
self.assertTrue(os.path.isfile(config_h), config_h)

def test_get_scheme_names(self):
wanted = ['nt', 'posix_home', 'posix_prefix', 'posix_venv', 'nt_venv', 'venv']
wanted = ['nt', 'posix_home', 'posix_prefix', 'posix_venv', 'nt_venv', 'venv', 'rpm_prefix']
if HAS_USER_BASE:
wanted.extend(['nt_user', 'osx_framework_user', 'posix_user'])
self.assertEqual(get_scheme_names(), tuple(sorted(wanted)))
Expand All @@ -409,6 +420,8 @@ def test_symlink(self): # Issue 7880
cmd = "-c", "import sysconfig; print(sysconfig.get_platform())"
self.assertEqual(py.call_real(*cmd), py.call_link(*cmd))

@unittest.skipIf('RPM_BUILD_ROOT' not in os.environ,
"Test doesn't expect Fedora's paths")
def test_user_similar(self):
# Issue #8759: make sure the posix scheme for the users
# is similar to the global posix_prefix one
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/test_webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ def test_open_bad_new_parameter(self):
arguments=[URL],
kw=dict(new=999))

def test_reject_action_dash_prefixes(self):
browser = self.browser_class(name=CMD_NAME)
with self.assertRaises(ValueError):
browser.open('%action--incognito')
# new=1: action is "--new-window", so "%action" itself expands to
# a dash-prefixed flag even with no dash in the original URL.
with self.assertRaises(ValueError):
browser.open('%action', new=1)


class EdgeCommandTest(CommandTestMixin, unittest.TestCase):

Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_xml_etree.py
Original file line number Diff line number Diff line change
Expand Up @@ -1573,9 +1573,13 @@ def test_simple_xml(self, chunk_size=None, flush=False):
self.assert_event_tags(parser, [('end', 'root')])
self.assertIsNone(parser.close())

@unittest.skipIf(pyexpat.version_info < (2, 7, 1),
f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_simple_xml_chunk_1(self):
self.test_simple_xml(chunk_size=1, flush=True)

@unittest.skipIf(pyexpat.version_info < (2, 7, 1),
f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_simple_xml_chunk_5(self):
self.test_simple_xml(chunk_size=5, flush=True)

Expand Down Expand Up @@ -1802,6 +1806,8 @@ def test_flush_reparse_deferral_enabled(self):

self.assert_event_tags(parser, [('end', 'doc')])

@unittest.skipIf(pyexpat.version_info < (2, 7, 1),
f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_flush_reparse_deferral_disabled(self):
parser = ET.XMLPullParser(events=('start', 'end'))

Expand Down
5 changes: 3 additions & 2 deletions Lib/webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ def _invoke(self, args, remote, autoraise, url=None):

def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
self._check_url(url)
if new == 0:
action = self.remote_action
elif new == 1:
Expand All @@ -288,7 +287,9 @@ def open(self, url, new=0, autoraise=True):
raise Error("Bad 'new' parameter to open(); "
f"expected 0, 1, or 2, got {new}")

args = [arg.replace("%s", url).replace("%action", action)
self._check_url(url.replace("%action", action))

args = [arg.replace("%action", action).replace("%s", url)
for arg in self.remote_args]
args = [arg for arg in args if arg]
success = self._invoke(args, True, autoraise, url)
Expand Down
Loading