Skip to content

Commit a2ac2f8

Browse files
gh-133886: Fix sys.remote_exec() for non-UTF-8 paths
It now supports non-ASCII paths in non-UTF-8 locales and non-UTF-8 paths in UTF-8 locales.
1 parent 70f9b3d commit a2ac2f8

File tree

4 files changed

+95
-64
lines changed

4 files changed

+95
-64
lines changed

Lib/test/test_sys.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1976,12 +1976,11 @@ class TestRemoteExec(unittest.TestCase):
19761976
def tearDown(self):
19771977
test.support.reap_children()
19781978

1979-
def _run_remote_exec_test(self, script_code, python_args=None, env=None, prologue=''):
1979+
def _run_remote_exec_test(self, script_code, python_args=None, env=None, prologue='', script_path=os_helper.TESTFN + '_remote.py'):
19801980
# Create the script that will be remotely executed
1981-
script = os_helper.TESTFN + '_remote.py'
1982-
self.addCleanup(os_helper.unlink, script)
1981+
self.addCleanup(os_helper.unlink, script_path)
19831982

1984-
with open(script, 'w') as f:
1983+
with open(script_path, 'w') as f:
19851984
f.write(script_code)
19861985

19871986
# Create and run the target process
@@ -2050,7 +2049,7 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None, prologu
20502049
self.assertEqual(response, b"ready")
20512050

20522051
# Try remote exec on the target process
2053-
sys.remote_exec(proc.pid, script)
2052+
sys.remote_exec(proc.pid, script_path)
20542053

20552054
# Signal script to continue
20562055
client_socket.sendall(b"continue")
@@ -2081,6 +2080,27 @@ def test_remote_exec(self):
20812080
self.assertIn(b"Remote script executed successfully!", stdout)
20822081
self.assertEqual(stderr, b"")
20832082

2083+
def test_remote_exec_bytes(self):
2084+
script = '''
2085+
print("Remote script executed successfully!")
2086+
'''
2087+
script_path = os.fsencode(os_helper.TESTFN) + b'_bytes_remote.py'
2088+
returncode, stdout, stderr = self._run_remote_exec_test(script,
2089+
script_path=script_path)
2090+
self.assertIn(b"Remote script executed successfully!", stdout)
2091+
self.assertEqual(stderr, b"")
2092+
2093+
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE, 'requires undecodable path')
2094+
def test_remote_exec_undecodable(self):
2095+
script = '''
2096+
print("Remote script executed successfully!")
2097+
'''
2098+
script_path = os_helper.TESTFN_UNDECODABLE + b'_undecodable_remote.py'
2099+
returncode, stdout, stderr = self._run_remote_exec_test(script,
2100+
script_path=script_path)
2101+
self.assertIn(b"Remote script executed successfully!", stdout)
2102+
self.assertEqual(stderr, b"")
2103+
20842104
def test_remote_exec_with_self_process(self):
20852105
"""Test remote exec with the target process being the same as the test process"""
20862106

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`sys.remote_exec` for non-ASCII paths in non-UTF-8 locales and
2+
non-UTF-8 paths in UTF-8 locales.

Python/ceval_gil.c

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,38 +1218,38 @@ static inline int run_remote_debugger_source(PyObject *source)
12181218

12191219
// Note that this function is inline to avoid creating a PLT entry
12201220
// that would be an easy target for a ROP gadget.
1221-
static inline void run_remote_debugger_script(const char *path)
1221+
static inline void run_remote_debugger_script(PyObject *path)
12221222
{
1223-
if (0 != PySys_Audit("remote_debugger_script", "s", path)) {
1223+
if (0 != PySys_Audit("remote_debugger_script", "U", path)) {
12241224
PyErr_FormatUnraisable(
1225-
"Audit hook failed for remote debugger script %s", path);
1225+
"Audit hook failed for remote debugger script %U", path);
12261226
return;
12271227
}
12281228

12291229
// Open the debugger script with the open code hook, and reopen the
12301230
// resulting file object to get a C FILE* object.
1231-
PyObject* fileobj = PyFile_OpenCode(path);
1231+
PyObject* fileobj = PyFile_OpenCodeObject(path);
12321232
if (!fileobj) {
1233-
PyErr_FormatUnraisable("Can't open debugger script %s", path);
1233+
PyErr_FormatUnraisable("Can't open debugger script %U", path);
12341234
return;
12351235
}
12361236

12371237
PyObject* source = PyObject_CallMethodNoArgs(fileobj, &_Py_ID(read));
12381238
if (!source) {
1239-
PyErr_FormatUnraisable("Error reading debugger script %s", path);
1239+
PyErr_FormatUnraisable("Error reading debugger script %U", path);
12401240
}
12411241

12421242
PyObject* res = PyObject_CallMethodNoArgs(fileobj, &_Py_ID(close));
12431243
if (!res) {
1244-
PyErr_FormatUnraisable("Error closing debugger script %s", path);
1244+
PyErr_FormatUnraisable("Error closing debugger script %U", path);
12451245
} else {
12461246
Py_DECREF(res);
12471247
}
12481248
Py_DECREF(fileobj);
12491249

12501250
if (source) {
12511251
if (0 != run_remote_debugger_source(source)) {
1252-
PyErr_FormatUnraisable("Error executing debugger script %s", path);
1252+
PyErr_FormatUnraisable("Error executing debugger script %U", path);
12531253
}
12541254
Py_DECREF(source);
12551255
}
@@ -1278,7 +1278,14 @@ int _PyRunRemoteDebugger(PyThreadState *tstate)
12781278
pathsz);
12791279
path[pathsz - 1] = '\0';
12801280
if (*path) {
1281-
run_remote_debugger_script(path);
1281+
PyObject *path_obj = PyUnicode_DecodeFSDefault(path);
1282+
if (path_obj == NULL) {
1283+
PyErr_FormatUnraisable("Can't decode debugger script");
1284+
}
1285+
else {
1286+
run_remote_debugger_script(path_obj);
1287+
Py_DECREF(path_obj);
1288+
}
12821289
}
12831290
PyMem_Free(path);
12841291
}

Python/sysmodule.c

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2451,23 +2451,56 @@ sys_is_remote_debug_enabled_impl(PyObject *module)
24512451
#endif
24522452
}
24532453

2454+
/*[clinic input]
2455+
sys.remote_exec
2456+
2457+
pid: int
2458+
script: object
2459+
2460+
Executes a file containing Python code in a given remote Python process.
2461+
2462+
This function returns immediately, and the code will be executed by the
2463+
target process's main thread at the next available opportunity, similarly
2464+
to how signals are handled. There is no interface to determine when the
2465+
code has been executed. The caller is responsible for making sure that
2466+
the file still exists whenever the remote process tries to read it and that
2467+
it hasn't been overwritten.
2468+
2469+
The remote process must be running a CPython interpreter of the same major
2470+
and minor version as the local process. If either the local or remote
2471+
interpreter is pre-release (alpha, beta, or release candidate) then the
2472+
local and remote interpreters must be the same exact version.
2473+
2474+
Args:
2475+
pid (int): The process ID of the target Python process.
2476+
script (str|bytes): The path to a file containing
2477+
the Python code to be executed.
2478+
[clinic start generated code]*/
2479+
24542480
static PyObject *
2455-
sys_remote_exec_unicode_path(PyObject *module, int pid, PyObject *script)
2481+
sys_remote_exec_impl(PyObject *module, int pid, PyObject *script)
2482+
/*[clinic end generated code: output=7d94c56afe4a52c0 input=39908ca2c5fe1eb0]*/
24562483
{
2457-
const char *debugger_script_path = PyUnicode_AsUTF8(script);
2484+
PyObject *path;
2485+
const char *debugger_script_path;
2486+
#ifdef MS_WINDOWS
2487+
if (PyUnicode_FSDecoder(script, &path) < 0) {
2488+
return NULL;
2489+
}
2490+
debugger_script_path = PyUnicode_AsUTF8(path);
24582491
if (debugger_script_path == NULL) {
2492+
Py_DECREF(path);
24592493
return NULL;
24602494
}
2461-
2462-
#ifdef MS_WINDOWS
24632495
// Use UTF-16 (wide char) version of the path for permission checks
2464-
wchar_t *debugger_script_path_w = PyUnicode_AsWideCharString(script, NULL);
2496+
wchar_t *debugger_script_path_w = PyUnicode_AsWideCharString(path, NULL);
24652497
if (debugger_script_path_w == NULL) {
2498+
Py_DECREF(path);
24662499
return NULL;
24672500
}
2468-
2469-
// Check file attributes using wide character version (W) instead of ANSI (A)
2501+
Py_BEGIN_ALLOW_THREADS
24702502
DWORD attr = GetFileAttributesW(debugger_script_path_w);
2503+
Py_END_ALLOW_THREADS
24712504
PyMem_Free(debugger_script_path_w);
24722505
if (attr == INVALID_FILE_ATTRIBUTES) {
24732506
DWORD err = GetLastError();
@@ -2478,11 +2511,17 @@ sys_remote_exec_unicode_path(PyObject *module, int pid, PyObject *script)
24782511
PyErr_SetString(PyExc_PermissionError, "Script file cannot be read");
24792512
}
24802513
else {
2481-
PyErr_SetFromWindowsErr(0);
2514+
PyErr_SetFromWindowsErr(err);
24822515
}
2516+
Py_DECREF(path);
24832517
return NULL;
24842518
}
2485-
#else
2519+
#else // MS_WINDOWS
2520+
if (PyUnicode_FSConverter(script, &path) < 0) {
2521+
return NULL;
2522+
}
2523+
debugger_script_path = PyBytes_AS_STRING(path);
2524+
24862525
if (access(debugger_script_path, F_OK | R_OK) != 0) {
24872526
switch (errno) {
24882527
case ENOENT:
@@ -2494,56 +2533,19 @@ sys_remote_exec_unicode_path(PyObject *module, int pid, PyObject *script)
24942533
default:
24952534
PyErr_SetFromErrno(PyExc_OSError);
24962535
}
2536+
Py_DECREF(path);
24972537
return NULL;
24982538
}
2499-
#endif
2500-
2539+
#endif // MS_WINDOWS
25012540
if (_PySysRemoteDebug_SendExec(pid, 0, debugger_script_path) < 0) {
2541+
Py_DECREF(path);
25022542
return NULL;
25032543
}
25042544

2545+
Py_DECREF(path);
25052546
Py_RETURN_NONE;
25062547
}
25072548

2508-
/*[clinic input]
2509-
sys.remote_exec
2510-
2511-
pid: int
2512-
script: object
2513-
2514-
Executes a file containing Python code in a given remote Python process.
2515-
2516-
This function returns immediately, and the code will be executed by the
2517-
target process's main thread at the next available opportunity, similarly
2518-
to how signals are handled. There is no interface to determine when the
2519-
code has been executed. The caller is responsible for making sure that
2520-
the file still exists whenever the remote process tries to read it and that
2521-
it hasn't been overwritten.
2522-
2523-
The remote process must be running a CPython interpreter of the same major
2524-
and minor version as the local process. If either the local or remote
2525-
interpreter is pre-release (alpha, beta, or release candidate) then the
2526-
local and remote interpreters must be the same exact version.
2527-
2528-
Args:
2529-
pid (int): The process ID of the target Python process.
2530-
script (str|bytes): The path to a file containing
2531-
the Python code to be executed.
2532-
[clinic start generated code]*/
2533-
2534-
static PyObject *
2535-
sys_remote_exec_impl(PyObject *module, int pid, PyObject *script)
2536-
/*[clinic end generated code: output=7d94c56afe4a52c0 input=39908ca2c5fe1eb0]*/
2537-
{
2538-
PyObject *ret = NULL;
2539-
PyObject *path;
2540-
if (PyUnicode_FSDecoder(script, &path)) {
2541-
ret = sys_remote_exec_unicode_path(module, pid, path);
2542-
Py_DECREF(path);
2543-
}
2544-
return ret;
2545-
}
2546-
25472549

25482550

25492551
/*[clinic input]

0 commit comments

Comments
 (0)