Skip to content

Commit c09cec5

Browse files
gh-133886: Fix sys.remote_exec() for non-UTF-8 paths (GH-133887)
It now supports non-ASCII paths in non-UTF-8 locales and non-UTF-8 paths in UTF-8 locales.
1 parent 8cf4947 commit c09cec5

File tree

4 files changed

+97
-71
lines changed

4 files changed

+97
-71
lines changed

Lib/test/test_sys.py

+27-8
Original file line numberDiff line numberDiff line change
@@ -1976,12 +1976,13 @@ 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,
1980+
prologue='',
1981+
script_path=os_helper.TESTFN + '_remote.py'):
19801982
# Create the script that will be remotely executed
1981-
script = os_helper.TESTFN + '_remote.py'
1982-
self.addCleanup(os_helper.unlink, script)
1983+
self.addCleanup(os_helper.unlink, script_path)
19831984

1984-
with open(script, 'w') as f:
1985+
with open(script_path, 'w') as f:
19851986
f.write(script_code)
19861987

19871988
# Create and run the target process
@@ -2050,7 +2051,7 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None, prologu
20502051
self.assertEqual(response, b"ready")
20512052

20522053
# Try remote exec on the target process
2053-
sys.remote_exec(proc.pid, script)
2054+
sys.remote_exec(proc.pid, script_path)
20542055

20552056
# Signal script to continue
20562057
client_socket.sendall(b"continue")
@@ -2073,14 +2074,32 @@ def _run_remote_exec_test(self, script_code, python_args=None, env=None, prologu
20732074

20742075
def test_remote_exec(self):
20752076
"""Test basic remote exec functionality"""
2076-
script = '''
2077-
print("Remote script executed successfully!")
2078-
'''
2077+
script = 'print("Remote script executed successfully!")'
20792078
returncode, stdout, stderr = self._run_remote_exec_test(script)
20802079
# self.assertEqual(returncode, 0)
20812080
self.assertIn(b"Remote script executed successfully!", stdout)
20822081
self.assertEqual(stderr, b"")
20832082

2083+
def test_remote_exec_bytes(self):
2084+
script = 'print("Remote script executed successfully!")'
2085+
script_path = os.fsencode(os_helper.TESTFN) + b'_bytes_remote.py'
2086+
returncode, stdout, stderr = self._run_remote_exec_test(script,
2087+
script_path=script_path)
2088+
self.assertIn(b"Remote script executed successfully!", stdout)
2089+
self.assertEqual(stderr, b"")
2090+
2091+
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE, 'requires undecodable path')
2092+
@unittest.skipIf(sys.platform == 'darwin',
2093+
'undecodable paths are not supported on macOS')
2094+
def test_remote_exec_undecodable(self):
2095+
script = 'print("Remote script executed successfully!")'
2096+
script_path = os_helper.TESTFN_UNDECODABLE + b'_undecodable_remote.py'
2097+
for script_path in [script_path, os.fsdecode(script_path)]:
2098+
returncode, stdout, stderr = self._run_remote_exec_test(script,
2099+
script_path=script_path)
2100+
self.assertIn(b"Remote script executed successfully!", stdout)
2101+
self.assertEqual(stderr, b"")
2102+
20842103
def test_remote_exec_with_self_process(self):
20852104
"""Test remote exec with the target process being the same as the test process"""
20862105

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

+16-9
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", "O", 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

+52-54
Original file line numberDiff line numberDiff line change
@@ -2451,38 +2451,71 @@ 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);
2458-
if (debugger_script_path == NULL) {
2484+
PyObject *path;
2485+
const char *debugger_script_path;
2486+
2487+
if (PyUnicode_FSConverter(script, &path) < 0) {
24592488
return NULL;
24602489
}
2461-
2490+
debugger_script_path = PyBytes_AS_STRING(path);
24622491
#ifdef MS_WINDOWS
2492+
PyObject *unicode_path;
2493+
if (PyUnicode_FSDecoder(path, &unicode_path) < 0) {
2494+
goto error;
2495+
}
24632496
// Use UTF-16 (wide char) version of the path for permission checks
2464-
wchar_t *debugger_script_path_w = PyUnicode_AsWideCharString(script, NULL);
2497+
wchar_t *debugger_script_path_w = PyUnicode_AsWideCharString(unicode_path, NULL);
2498+
Py_DECREF(unicode_path);
24652499
if (debugger_script_path_w == NULL) {
2466-
return NULL;
2500+
goto error;
24672501
}
2468-
2469-
// Check file attributes using wide character version (W) instead of ANSI (A)
24702502
DWORD attr = GetFileAttributesW(debugger_script_path_w);
2471-
PyMem_Free(debugger_script_path_w);
24722503
if (attr == INVALID_FILE_ATTRIBUTES) {
24732504
DWORD err = GetLastError();
2505+
PyMem_Free(debugger_script_path_w);
24742506
if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
24752507
PyErr_SetString(PyExc_FileNotFoundError, "Script file does not exist");
24762508
}
24772509
else if (err == ERROR_ACCESS_DENIED) {
24782510
PyErr_SetString(PyExc_PermissionError, "Script file cannot be read");
24792511
}
24802512
else {
2481-
PyErr_SetFromWindowsErr(0);
2513+
PyErr_SetFromWindowsErr(err);
24822514
}
2483-
return NULL;
2515+
goto error;
24842516
}
2485-
#else
2517+
PyMem_Free(debugger_script_path_w);
2518+
#else // MS_WINDOWS
24862519
if (access(debugger_script_path, F_OK | R_OK) != 0) {
24872520
switch (errno) {
24882521
case ENOENT:
@@ -2494,54 +2527,19 @@ sys_remote_exec_unicode_path(PyObject *module, int pid, PyObject *script)
24942527
default:
24952528
PyErr_SetFromErrno(PyExc_OSError);
24962529
}
2497-
return NULL;
2530+
goto error;
24982531
}
2499-
#endif
2500-
2532+
#endif // MS_WINDOWS
25012533
if (_PySysRemoteDebug_SendExec(pid, 0, debugger_script_path) < 0) {
2502-
return NULL;
2534+
goto error;
25032535
}
25042536

2537+
Py_DECREF(path);
25052538
Py_RETURN_NONE;
2506-
}
2507-
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.
25222539

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;
2540+
error:
2541+
Py_DECREF(path);
2542+
return NULL;
25452543
}
25462544

25472545

0 commit comments

Comments
 (0)