mirror of
https://github.com/python/cpython.git
synced 2026-06-27 19:36:07 +00:00
[3.15] gh-151029: Fix sys.remote_exec() unable to find writable memory when libpython replaced on disk (GH-151032) (#152443)
gh-151029: Fix sys.remote_exec() unable to find writable memory when libpython replaced on disk (GH-151032)
(cherry picked from commit a69d0fc41e)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Co-authored-by: Pablo Galindo <pablogsal@gmail.com>
This commit is contained in:
parent
3206fc74fb
commit
e88d41685b
3 changed files with 218 additions and 7 deletions
|
|
@ -6,6 +6,7 @@
|
|||
import operator
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
|
|
@ -2013,7 +2014,8 @@ def tearDown(self):
|
|||
test.support.reap_children()
|
||||
|
||||
def _run_remote_exec_test(self, script_code, python_args=None, env=None,
|
||||
prologue='',
|
||||
python_executable=None, prologue='',
|
||||
after_ready=None,
|
||||
script_path=os_helper.TESTFN + '_remote.py'):
|
||||
# Create the script that will be remotely executed
|
||||
self.addCleanup(os_helper.unlink, script_path)
|
||||
|
|
@ -2061,7 +2063,10 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None,
|
|||
''')
|
||||
|
||||
# Start the target process and capture its output
|
||||
cmd = [sys.executable]
|
||||
if python_executable is None:
|
||||
python_executable = sys.executable
|
||||
|
||||
cmd = [python_executable]
|
||||
if python_args:
|
||||
cmd.extend(python_args)
|
||||
cmd.append(target)
|
||||
|
|
@ -2086,6 +2091,9 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None,
|
|||
response = client_socket.recv(1024)
|
||||
self.assertEqual(response, b"ready")
|
||||
|
||||
if after_ready is not None:
|
||||
after_ready(proc)
|
||||
|
||||
# Try remote exec on the target process
|
||||
sys.remote_exec(proc.pid, script_path)
|
||||
|
||||
|
|
@ -2108,6 +2116,19 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None,
|
|||
proc.terminate()
|
||||
proc.wait(timeout=SHORT_TIMEOUT)
|
||||
|
||||
def _run_remote_exec_with_deleted_mapping(self, deleted_path, **kwargs):
|
||||
def delete_loaded_mapping(proc):
|
||||
os_helper.unlink(deleted_path)
|
||||
with open(f'/proc/{proc.pid}/maps', encoding='utf-8') as maps:
|
||||
self.assertIn(f'{deleted_path} (deleted)', maps.read())
|
||||
|
||||
script = 'print("Remote script executed successfully!")'
|
||||
returncode, stdout, stderr = self._run_remote_exec_test(
|
||||
script, after_ready=delete_loaded_mapping, **kwargs)
|
||||
self.assertEqual(returncode, 0)
|
||||
self.assertIn(b"Remote script executed successfully!", stdout)
|
||||
self.assertEqual(stderr, b"")
|
||||
|
||||
def test_remote_exec(self):
|
||||
"""Test basic remote exec functionality"""
|
||||
script = 'print("Remote script executed successfully!")'
|
||||
|
|
@ -2234,6 +2255,75 @@ def test_remote_exec_invalid_script_path(self):
|
|||
with self.assertRaises(OSError):
|
||||
sys.remote_exec(os.getpid(), "invalid_script_path")
|
||||
|
||||
@unittest.skipUnless(sys.platform == 'linux', 'Linux-only regression test')
|
||||
@unittest.skipUnless(
|
||||
sysconfig.get_config_var('Py_ENABLE_SHARED') == 1,
|
||||
'requires a shared libpython build')
|
||||
def test_remote_exec_deleted_libpython(self):
|
||||
"""Test remote exec when the target libpython was deleted."""
|
||||
build_dir = sysconfig.get_config_var('abs_builddir')
|
||||
ldlibrary = sysconfig.get_config_var('LDLIBRARY')
|
||||
instsoname = sysconfig.get_config_var('INSTSONAME')
|
||||
if not build_dir or not ldlibrary or not instsoname:
|
||||
self.skipTest('cannot determine shared libpython location')
|
||||
|
||||
source_libpython = os.path.join(build_dir, instsoname)
|
||||
if not os.path.exists(source_libpython):
|
||||
self.skipTest(f'{source_libpython!r} does not exist')
|
||||
|
||||
with os_helper.temp_dir() as lib_dir:
|
||||
copied_libpython = os.path.join(lib_dir, instsoname)
|
||||
shutil.copy2(source_libpython, copied_libpython)
|
||||
if ldlibrary != instsoname:
|
||||
os.symlink(instsoname, os.path.join(lib_dir, ldlibrary))
|
||||
|
||||
env = os.environ.copy()
|
||||
ld_library_path = env.get('LD_LIBRARY_PATH')
|
||||
env['LD_LIBRARY_PATH'] = lib_dir if not ld_library_path else (
|
||||
lib_dir + os.pathsep + ld_library_path)
|
||||
|
||||
self._run_remote_exec_with_deleted_mapping(copied_libpython,
|
||||
env=env)
|
||||
|
||||
@unittest.skipUnless(sys.platform == 'linux', 'Linux-only regression test')
|
||||
@unittest.skipUnless(
|
||||
sysconfig.get_config_var('Py_ENABLE_SHARED') == 0,
|
||||
'requires a static Python build')
|
||||
def test_remote_exec_deleted_static_executable(self):
|
||||
"""Test remote exec when the target static executable was deleted."""
|
||||
build_dir = sysconfig.get_config_var('abs_builddir')
|
||||
srcdir = sysconfig.get_config_var('srcdir')
|
||||
if not build_dir or not srcdir:
|
||||
self.skipTest('cannot determine build-tree locations')
|
||||
|
||||
pybuilddir_txt = os.path.join(build_dir, 'pybuilddir.txt')
|
||||
if not os.path.exists(pybuilddir_txt):
|
||||
self.skipTest(f'{pybuilddir_txt!r} does not exist')
|
||||
|
||||
with open(pybuilddir_txt, encoding='utf-8') as pybuilddir_file:
|
||||
pybuilddir = pybuilddir_file.read().strip()
|
||||
source_ext_dir = os.path.join(build_dir, pybuilddir)
|
||||
if not os.path.isdir(source_ext_dir):
|
||||
self.skipTest(f'{source_ext_dir!r} does not exist')
|
||||
|
||||
with os_helper.temp_dir() as copied_root:
|
||||
copied_build_dir = os.path.join(copied_root, 'build')
|
||||
copied_pybuilddir = os.path.join(copied_build_dir, pybuilddir)
|
||||
os.makedirs(os.path.dirname(copied_pybuilddir))
|
||||
os.symlink(os.path.join(srcdir, 'Lib'),
|
||||
os.path.join(copied_root, 'Lib'))
|
||||
os.symlink(source_ext_dir, copied_pybuilddir)
|
||||
shutil.copy2(pybuilddir_txt,
|
||||
os.path.join(copied_build_dir, 'pybuilddir.txt'))
|
||||
|
||||
copied_python = os.path.join(copied_build_dir,
|
||||
os.path.basename(sys.executable))
|
||||
shutil.copy2(sys.executable, copied_python)
|
||||
|
||||
self._run_remote_exec_with_deleted_mapping(
|
||||
copied_python, python_args=['-S'],
|
||||
python_executable=copied_python)
|
||||
|
||||
def test_remote_exec_in_process_without_debug_fails_envvar(self):
|
||||
"""Test remote exec in a process without remote debugging enabled"""
|
||||
script = os_helper.TESTFN + '_remote.py'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
On Linux, fix :func:`sys.remote_exec` unable to find remote writable memory
|
||||
when ``libpython`` replaced on disk.
|
||||
|
|
@ -781,6 +781,106 @@ search_elf_file_for_section(
|
|||
return result;
|
||||
}
|
||||
|
||||
static const char *
|
||||
find_debug_cookie(const char *buffer, size_t len)
|
||||
{
|
||||
const char *cookie = _Py_Debug_Cookie;
|
||||
const size_t cookie_len = sizeof(_Py_Debug_Cookie) - 1;
|
||||
if (len < cookie_len) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t pos = 0;
|
||||
size_t last = len - cookie_len;
|
||||
while (pos <= last) {
|
||||
const char *candidate = memchr(
|
||||
buffer + pos, cookie[0], last - pos + 1);
|
||||
if (candidate == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
pos = (size_t)(candidate - buffer);
|
||||
if (memcmp(candidate, cookie, cookie_len) == 0) {
|
||||
return candidate;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int
|
||||
linux_map_path_is_deleted(const char *path)
|
||||
{
|
||||
static const char deleted_suffix[] = " (deleted)";
|
||||
size_t path_len = strlen(path);
|
||||
size_t suffix_len = sizeof(deleted_suffix) - 1;
|
||||
return path_len >= suffix_len
|
||||
&& strcmp(path + path_len - suffix_len, deleted_suffix) == 0;
|
||||
}
|
||||
|
||||
static int
|
||||
linux_map_perms_are_readwrite(const char *perms)
|
||||
{
|
||||
return perms[0] == 'r' && perms[1] == 'w';
|
||||
}
|
||||
|
||||
static uintptr_t
|
||||
scan_linux_mapping_for_pyruntime_cookie(
|
||||
proc_handle_t *handle,
|
||||
uintptr_t start,
|
||||
uintptr_t end)
|
||||
{
|
||||
if (end <= start) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size_t cookie_len = sizeof(_Py_Debug_Cookie) - 1;
|
||||
const size_t overlap = cookie_len - 1;
|
||||
const size_t chunk_size = 1024 * 1024;
|
||||
char *buffer = PyMem_Malloc(chunk_size);
|
||||
if (buffer == NULL) {
|
||||
PyErr_NoMemory();
|
||||
_set_debug_exception_cause(PyExc_MemoryError,
|
||||
"Cannot allocate memory while scanning PID %d for PyRuntime cookie",
|
||||
handle->pid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uintptr_t retval = 0;
|
||||
uintptr_t mapping_size = end - start;
|
||||
uintptr_t offset = 0;
|
||||
while (offset < mapping_size) {
|
||||
uintptr_t remaining = mapping_size - offset;
|
||||
size_t wanted = remaining > chunk_size
|
||||
? chunk_size : (size_t)remaining;
|
||||
if (_Py_RemoteDebug_ReadRemoteMemory(
|
||||
handle, start + offset, wanted, buffer) < 0) {
|
||||
if (_Py_RemoteDebug_HasPermissionError()) {
|
||||
goto exit;
|
||||
}
|
||||
// A candidate mapping can disappear or contain unreadable holes while
|
||||
// the target process keeps running. Treat those as non-matches and
|
||||
// keep scanning other candidate mappings.
|
||||
PyErr_Clear();
|
||||
}
|
||||
else {
|
||||
const char *hit = find_debug_cookie(buffer, wanted);
|
||||
if (hit != NULL) {
|
||||
retval = start + offset + (uintptr_t)(hit - buffer);
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (wanted <= overlap) {
|
||||
break;
|
||||
}
|
||||
offset += wanted - overlap;
|
||||
}
|
||||
|
||||
exit:
|
||||
PyMem_Free(buffer);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static uintptr_t
|
||||
search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
|
||||
section_validator_t validator)
|
||||
|
|
@ -835,16 +935,22 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
|
|||
linelen = 0;
|
||||
|
||||
unsigned long start = 0;
|
||||
unsigned long path_pos = 0;
|
||||
sscanf(line, "%lx-%*x %*s %*s %*s %*s %ln", &start, &path_pos);
|
||||
unsigned long end = 0;
|
||||
int path_pos = 0;
|
||||
char perms[5] = "";
|
||||
int fields = sscanf(line, "%lx-%lx %4s %*s %*s %*s %n",
|
||||
&start, &end, perms, &path_pos);
|
||||
|
||||
if (!path_pos) {
|
||||
if (fields < 3 || !path_pos) {
|
||||
// Line didn't match our format string. This shouldn't be
|
||||
// possible, but let's be defensive and skip the line.
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *path = line + path_pos;
|
||||
if (path[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (path[0] == '[' && path[strlen(path)-1] == ']') {
|
||||
// Skip [heap], [stack], [anon:cpython:pymalloc], etc.
|
||||
continue;
|
||||
|
|
@ -858,8 +964,21 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
|
|||
}
|
||||
|
||||
if (strstr(filename, substr)) {
|
||||
PyErr_Clear();
|
||||
retval = search_elf_file_for_section(handle, secname, start, path);
|
||||
int deleted_pyruntime_mapping =
|
||||
strcmp(secname, "PyRuntime") == 0
|
||||
&& linux_map_path_is_deleted(path);
|
||||
if (deleted_pyruntime_mapping
|
||||
&& linux_map_perms_are_readwrite(perms)) {
|
||||
PyErr_Clear();
|
||||
retval = scan_linux_mapping_for_pyruntime_cookie(
|
||||
handle, (uintptr_t)start, (uintptr_t)end);
|
||||
}
|
||||
if (!deleted_pyruntime_mapping
|
||||
&& retval == 0 && !PyErr_Occurred()) {
|
||||
PyErr_Clear();
|
||||
retval = search_elf_file_for_section(
|
||||
handle, secname, start, path);
|
||||
}
|
||||
if (retval) {
|
||||
if (validator == NULL || validator(handle, retval)) {
|
||||
break;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue