Skip to content

Commit 891c91d

Browse files
ncoghlanlarryhastings
authored andcommitted
[3.5] bpo-32551: Consistently configure sys.path[0] (#5197)
Directory and zipfile execution previously added the parent directory of the directory or zipfile as sys.path[0] and then subsequently overwrote it with the directory or zipfile itself. This caused problems in isolated mode, as it overwrote the "stdlib as a zip archive" entry in sys.path, as the parent directory was never added. The attempted fix to that issue in bpo-29319 created the opposite problem in *non*-isolated mode, by potentially leaving the parent directory on sys.path instead of overwriting it. This change fixes the root cause of the problem by removing the whole "add-and-overwrite" dance for sys.path[0], and instead simply never adds the parent directory to sys.path in the first place. (cherry picked from commit d2977a3)
1 parent 57fa0ab commit 891c91d

File tree

3 files changed

+124
-28
lines changed

3 files changed

+124
-28
lines changed

Lib/test/test_cmd_line_script.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,73 @@ def test_syntaxerror_indented_caret_position(self):
573573
self.assertNotIn("\f", text)
574574
self.assertIn("\n 1 + 1 = 2\n ^", text)
575575

576+
def test_consistent_sys_path_for_direct_execution(self):
577+
# This test case ensures that the following all give the same
578+
# sys.path configuration:
579+
#
580+
# ./python -s script_dir/__main__.py
581+
# ./python -s script_dir
582+
# ./python -I script_dir
583+
script = textwrap.dedent("""\
584+
import sys
585+
for entry in sys.path:
586+
print(entry)
587+
""")
588+
# Always show full path diffs on errors
589+
self.maxDiff = None
590+
with support.temp_dir() as work_dir, support.temp_dir() as script_dir:
591+
script_name = _make_test_script(script_dir, '__main__', script)
592+
# Reference output comes from directly executing __main__.py
593+
# We omit PYTHONPATH and user site to align with isolated mode
594+
p = spawn_python("-Es", script_name, cwd=work_dir)
595+
out_by_name = kill_python(p).decode().splitlines()
596+
self.assertEqual(out_by_name[0], script_dir)
597+
self.assertNotIn(work_dir, out_by_name)
598+
# Directory execution should give the same output
599+
p = spawn_python("-Es", script_dir, cwd=work_dir)
600+
out_by_dir = kill_python(p).decode().splitlines()
601+
self.assertEqual(out_by_dir, out_by_name)
602+
# As should directory execution in isolated mode
603+
p = spawn_python("-I", script_dir, cwd=work_dir)
604+
out_by_dir_isolated = kill_python(p).decode().splitlines()
605+
self.assertEqual(out_by_dir_isolated, out_by_dir, out_by_name)
606+
607+
def test_consistent_sys_path_for_module_execution(self):
608+
# This test case ensures that the following both give the same
609+
# sys.path configuration:
610+
# ./python -sm script_pkg.__main__
611+
# ./python -sm script_pkg
612+
#
613+
# And that this fails as unable to find the package:
614+
# ./python -Im script_pkg
615+
script = textwrap.dedent("""\
616+
import sys
617+
for entry in sys.path:
618+
print(entry)
619+
""")
620+
# Always show full path diffs on errors
621+
self.maxDiff = None
622+
with support.temp_dir() as work_dir:
623+
script_dir = os.path.join(work_dir, "script_pkg")
624+
os.mkdir(script_dir)
625+
script_name = _make_test_script(script_dir, '__main__', script)
626+
# Reference output comes from `-m script_pkg.__main__`
627+
# We omit PYTHONPATH and user site to better align with the
628+
# direct execution test cases
629+
p = spawn_python("-sm", "script_pkg.__main__", cwd=work_dir)
630+
out_by_module = kill_python(p).decode().splitlines()
631+
self.assertEqual(out_by_module[0], '')
632+
self.assertNotIn(script_dir, out_by_module)
633+
# Package execution should give the same output
634+
p = spawn_python("-sm", "script_pkg", cwd=work_dir)
635+
out_by_package = kill_python(p).decode().splitlines()
636+
self.assertEqual(out_by_package, out_by_module)
637+
# Isolated mode should fail with an import error
638+
exitcode, stdout, stderr = assert_python_failure(
639+
"-Im", "script_pkg", cwd=work_dir
640+
)
641+
traceback_lines = stderr.decode().splitlines()
642+
self.assertIn("No module named script_pkg", traceback_lines[-1])
576643

577644
def test_main():
578645
support.run_unittest(CmdLineTest)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
The ``sys.path[0]`` initialization change for bpo-29139 caused a regression
2+
by revealing an inconsistency in how sys.path is initialized when executing
3+
``__main__`` from a zipfile, directory, or other import location. This is
4+
considered a potential security issue, as it may lead to privileged
5+
processes unexpectedly loading code from user controlled directories in
6+
situations where that was not previously the case.
7+
8+
The interpreter now consistently avoids ever adding the import location's
9+
parent directory to ``sys.path``, and ensures no other ``sys.path`` entries
10+
are inadvertently modified when inserting the import location named on the
11+
command line. (Originally reported as bpo-29723 against Python 3.6rc1, but
12+
it was missed at the time that the then upcoming Python 3.5.4 release would
13+
also be affected)

Modules/main.c

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -220,55 +220,60 @@ static int RunModule(wchar_t *modname, int set_argv0)
220220
return 0;
221221
}
222222

223-
static int
224-
RunMainFromImporter(wchar_t *filename)
223+
static PyObject *
224+
AsImportPathEntry(wchar_t *filename)
225225
{
226-
PyObject *argv0 = NULL, *importer, *sys_path, *sys_path0;
227-
int sts;
226+
PyObject *sys_path0 = NULL, *importer;
228227

229-
argv0 = PyUnicode_FromWideChar(filename, wcslen(filename));
230-
if (argv0 == NULL)
228+
sys_path0 = PyUnicode_FromWideChar(filename, wcslen(filename));
229+
if (sys_path0 == NULL)
231230
goto error;
232231

233-
importer = PyImport_GetImporter(argv0);
232+
importer = PyImport_GetImporter(sys_path0);
234233
if (importer == NULL)
235234
goto error;
236235

237236
if (importer == Py_None) {
238-
Py_DECREF(argv0);
237+
Py_DECREF(sys_path0);
239238
Py_DECREF(importer);
240-
return -1;
239+
return NULL;
241240
}
242241
Py_DECREF(importer);
242+
return sys_path0;
243+
244+
error:
245+
Py_XDECREF(sys_path0);
246+
PySys_WriteStderr("Failed checking if argv[0] is an import path entry\n");
247+
PyErr_Print();
248+
PyErr_Clear();
249+
return NULL;
250+
}
251+
252+
253+
static int
254+
RunMainFromImporter(PyObject *sys_path0)
255+
{
256+
PyObject *sys_path;
257+
int sts;
243258

244-
/* argv0 is usable as an import source, so put it in sys.path[0]
245-
and import __main__ */
259+
/* Assume sys_path0 has already been checked by AsImportPathEntry,
260+
* so put it in sys.path[0] and import __main__ */
246261
sys_path = PySys_GetObject("path");
247262
if (sys_path == NULL) {
248263
PyErr_SetString(PyExc_RuntimeError, "unable to get sys.path");
249264
goto error;
250265
}
251-
sys_path0 = PyList_GetItem(sys_path, 0);
252-
sts = 0;
253-
if (!sys_path0) {
254-
PyErr_Clear();
255-
sts = PyList_Append(sys_path, argv0);
256-
} else if (PyObject_IsTrue(sys_path0)) {
257-
sts = PyList_Insert(sys_path, 0, argv0);
258-
} else {
259-
sts = PyList_SetItem(sys_path, 0, argv0);
260-
}
266+
sts = PyList_Insert(sys_path, 0, sys_path0);
261267
if (sts) {
262-
argv0 = NULL;
268+
sys_path0 = NULL;
263269
goto error;
264270
}
265-
Py_INCREF(argv0);
266271

267272
sts = RunModule(L"__main__", 0);
268273
return sts != 0;
269274

270275
error:
271-
Py_XDECREF(argv0);
276+
Py_XDECREF(sys_path0);
272277
PyErr_Print();
273278
return 1;
274279
}
@@ -352,6 +357,7 @@ Py_Main(int argc, wchar_t **argv)
352357
int version = 0;
353358
int saw_unbuffered_flag = 0;
354359
PyCompilerFlags cf;
360+
PyObject *main_importer_path = NULL;
355361
PyObject *warning_option = NULL;
356362
PyObject *warning_options = NULL;
357363

@@ -700,7 +706,17 @@ Py_Main(int argc, wchar_t **argv)
700706
argv[_PyOS_optind] = L"-m";
701707
}
702708

703-
PySys_SetArgv(argc-_PyOS_optind, argv+_PyOS_optind);
709+
if (filename != NULL) {
710+
main_importer_path = AsImportPathEntry(filename);
711+
}
712+
713+
if (main_importer_path != NULL) {
714+
/* Let RunMainFromImporter adjust sys.path[0] later */
715+
PySys_SetArgvEx(argc-_PyOS_optind, argv+_PyOS_optind, 0);
716+
} else {
717+
/* Use config settings to decide whether or not to update sys.path[0] */
718+
PySys_SetArgv(argc-_PyOS_optind, argv+_PyOS_optind);
719+
}
704720

705721
if ((Py_InspectFlag || (command == NULL && filename == NULL && module == NULL)) &&
706722
isatty(fileno(stdin))) {
@@ -729,11 +745,11 @@ Py_Main(int argc, wchar_t **argv)
729745

730746
sts = -1; /* keep track of whether we've already run __main__ */
731747

732-
if (filename != NULL) {
733-
sts = RunMainFromImporter(filename);
748+
if (main_importer_path != NULL) {
749+
sts = RunMainFromImporter(main_importer_path);
734750
}
735751

736-
if (sts==-1 && filename!=NULL) {
752+
if (sts==-1 && filename != NULL) {
737753
fp = _Py_wfopen(filename, L"r");
738754
if (fp == NULL) {
739755
char *cfilename_buffer;

0 commit comments

Comments
 (0)