[Python-checkins] gh-57684: Add -P cmdline option and PYTHONSAFEPATH env var (#31542)

vstinner webhook-mailer at python.org
Thu May 5 19:34:22 EDT 2022


https://github.com/python/cpython/commit/ada8b6d1b1b02ae7c38f161c2a0ad866559fe18b
commit: ada8b6d1b1b02ae7c38f161c2a0ad866559fe18b
branch: main
author: Victor Stinner <vstinner at python.org>
committer: vstinner <vstinner at python.org>
date: 2022-05-06T01:34:11+02:00
summary:

gh-57684: Add -P cmdline option and PYTHONSAFEPATH env var (#31542)

Add the -P command line option and the PYTHONSAFEPATH environment
variable to not prepend a potentially unsafe path to sys.path.

* Add sys.flags.safe_path flag.
* Add PyConfig.safe_path member.
* Programs/_bootstrap_python.c uses config.safe_path=0.
* Update subprocess._optim_args_from_interpreter_flags() to handle
  the -P command line option.
* Modules/getpath.py sets safe_path to 1 if a "._pth" file is
  present.

files:
A Misc/NEWS.d/next/Security/2022-05-04-14-32-24.gh-issue-57684.HrlDrM.rst
M Doc/c-api/init_config.rst
M Doc/library/sys.rst
M Doc/using/cmdline.rst
M Doc/whatsnew/3.11.rst
M Include/cpython/initconfig.h
M Lib/subprocess.py
M Lib/test/test_cmd_line.py
M Lib/test/test_embed.py
M Lib/test/test_support.py
M Lib/test/test_sys.py
M Misc/python.man
M Modules/getpath.py
M Modules/main.c
M Programs/_bootstrap_python.c
M Programs/_testembed.c
M Python/dynload_win.c
M Python/getopt.c
M Python/initconfig.c
M Python/sysmodule.c

diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst
index bab5313dd9ec7..728df2100336e 100644
--- a/Doc/c-api/init_config.rst
+++ b/Doc/c-api/init_config.rst
@@ -543,6 +543,25 @@ PyConfig
 
       See also the :c:member:`~PyConfig.orig_argv` member.
 
+   .. c:member:: int safe_path
+
+      If equals to zero, ``Py_RunMain()`` prepends a potentially unsafe path to
+      :data:`sys.path` at startup:
+
+      * If :c:member:`argv[0] <PyConfig.argv>` is equal to ``L"-m"``
+        (``python -m module``), prepend the current working directory.
+      * If running a script (``python script.py``), prepend the script's
+        directory.  If it's a symbolic link, resolve symbolic links.
+      * Otherwise (``python -c code`` and ``python``), prepend an empty string,
+        which means the current working directory.
+
+      Set to 1 by the :option:`-P` command line option and the
+      :envvar:`PYTHONSAFEPATH` environment variable.
+
+      Default: ``0`` in Python config, ``1`` in isolated config.
+
+      .. versionadded:: 3.11
+
    .. c:member:: wchar_t* base_exec_prefix
 
       :data:`sys.base_exec_prefix`.
@@ -809,13 +828,14 @@ PyConfig
 
       If greater than 0, enable isolated mode:
 
-      * :data:`sys.path` contains neither the script's directory (computed from
-        ``argv[0]`` or the current directory) nor the user's site-packages
-        directory.
+      * Set :c:member:`~PyConfig.safe_path` to 1:
+        don't prepend a potentially unsafe path to :data:`sys.path` at Python
+        startup.
+      * Set :c:member:`~PyConfig.use_environment` to 0.
+      * Set :c:member:`~PyConfig.user_site_directory` to 0: don't add the user
+        site directory to :data:`sys.path`.
       * Python REPL doesn't import :mod:`readline` nor enable default readline
         configuration on interactive prompts.
-      * Set :c:member:`~PyConfig.use_environment` and
-        :c:member:`~PyConfig.user_site_directory` to 0.
 
       Default: ``0`` in Python mode, ``1`` in isolated mode.
 
@@ -1029,12 +1049,13 @@ PyConfig
    .. c:member:: wchar_t* run_filename
 
       Filename passed on the command line: trailing command line argument
-      without :option:`-c` or :option:`-m`.
+      without :option:`-c` or :option:`-m`. It is used by the
+      :c:func:`Py_RunMain` function.
 
       For example, it is set to ``script.py`` by the ``python3 script.py arg``
-      command.
+      command line.
 
-      Used by :c:func:`Py_RunMain`.
+      See also the :c:member:`PyConfig.skip_source_first_line` option.
 
       Default: ``NULL``.
 
@@ -1419,9 +1440,16 @@ site-package directory to :data:`sys.path`.
 The following configuration files are used by the path configuration:
 
 * ``pyvenv.cfg``
-* ``python._pth`` (Windows only)
+* ``._pth`` file (ex: ``python._pth``)
 * ``pybuilddir.txt`` (Unix only)
 
+If a ``._pth`` file is present:
+
+* Set :c:member:`~PyConfig.isolated` to 1.
+* Set :c:member:`~PyConfig.use_environment` to 0.
+* Set :c:member:`~PyConfig.site_import` to 0.
+* Set :c:member:`~PyConfig.safe_path` to 1.
+
 The ``__PYVENV_LAUNCHER__`` environment variable is used to set
 :c:member:`PyConfig.base_executable`
 
diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst
index f43344887e421..5f3b9b5776cb8 100644
--- a/Doc/library/sys.rst
+++ b/Doc/library/sys.rst
@@ -520,6 +520,7 @@ always available.
    :const:`hash_randomization`   :option:`-R`
    :const:`dev_mode`             :option:`-X dev <-X>` (:ref:`Python Development Mode <devmode>`)
    :const:`utf8_mode`            :option:`-X utf8 <-X>`
+   :const:`safe_path`            :option:`-P`
    ============================= ================================================================
 
    .. versionchanged:: 3.2
@@ -539,6 +540,9 @@ always available.
       Mode <devmode>` and the ``utf8_mode`` attribute for the new  :option:`-X`
       ``utf8`` flag.
 
+   .. versionchanged:: 3.11
+      Added the ``safe_path`` attribute for :option:`-P` option.
+
 
 .. data:: float_info
 
@@ -1138,15 +1142,19 @@ always available.
    the environment variable :envvar:`PYTHONPATH`, plus an installation-dependent
    default.
 
-   As initialized upon program startup, the first item of this list, ``path[0]``,
-   is the directory containing the script that was used to invoke the Python
-   interpreter.  If the script directory is not available (e.g.  if the interpreter
-   is invoked interactively or if the script is read from standard input),
-   ``path[0]`` is the empty string, which directs Python to search modules in the
-   current directory first.  Notice that the script directory is inserted *before*
-   the entries inserted as a result of :envvar:`PYTHONPATH`.
+   By default, as initialized upon program startup, a potentially unsafe path
+   is prepended to :data:`sys.path` (*before* the entries inserted as a result
+   of :envvar:`PYTHONPATH`):
+
+   * ``python -m module`` command line: prepend the current working
+     directory.
+   * ``python script.py`` command line: prepend the script's directory.
+     If it's a symbolic link, resolve symbolic links.
+   * ``python -c code`` and ``python`` (REPL) command lines: prepend an empty
+     string, which means the current working directory.
 
-   The initialization of :data:`sys.path` is documented at :ref:`sys-path-init`.
+   To not prepend this potentially unsafe path, use the :option:`-P` command
+   line option or the :envvar:`PYTHONSAFEPATH` environment variable?
 
    A program is free to modify this list for its own purposes.  Only strings
    and bytes should be added to :data:`sys.path`; all other data types are
diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst
index d341ea8bb43c8..668459f352008 100644
--- a/Doc/using/cmdline.rst
+++ b/Doc/using/cmdline.rst
@@ -257,6 +257,8 @@ Miscellaneous options
    Ignore all :envvar:`PYTHON*` environment variables, e.g.
    :envvar:`PYTHONPATH` and :envvar:`PYTHONHOME`, that might be set.
 
+   See also the :option:`-P` and :option:`-I` (isolated) options.
+
 
 .. cmdoption:: -i
 
@@ -271,7 +273,9 @@ Miscellaneous options
 
 .. cmdoption:: -I
 
-   Run Python in isolated mode. This also implies -E and -s.
+   Run Python in isolated mode. This also implies :option:`-E`, :option:`-P`
+   and :option:`-s` options.
+
    In isolated mode :data:`sys.path` contains neither the script's directory nor
    the user's site-packages directory. All :envvar:`PYTHON*` environment
    variables are ignored, too. Further restrictions may be imposed to prevent
@@ -301,6 +305,23 @@ Miscellaneous options
       Modify ``.pyc`` filenames according to :pep:`488`.
 
 
+.. cmdoption:: -P
+
+   Don't prepend a potentially unsafe path to :data:`sys.path`:
+
+   * ``python -m module`` command line: Don't prepend the current working
+     directory.
+   * ``python script.py`` command line: Don't prepend the script's directory.
+     If it's a symbolic link, resolve symbolic links.
+   * ``python -c code`` and ``python`` (REPL) command lines: Don't prepend an
+     empty string, which means the current working directory.
+
+   See also the :envvar:`PYTHONSAFEPATH` environment variable, and :option:`-E`
+   and :option:`-I` (isolated) options.
+
+   .. versionadded:: 3.11
+
+
 .. cmdoption:: -q
 
    Don't display the copyright and version messages even in interactive mode.
@@ -583,6 +604,14 @@ conflict.
    within a Python program as the variable :data:`sys.path`.
 
 
+.. envvar:: PYTHONSAFEPATH
+
+   If this is set to a non-empty string, don't prepend a potentially unsafe
+   path to :data:`sys.path`: see the :option:`-P` option for details.
+
+   .. versionadded:: 3.11
+
+
 .. envvar:: PYTHONPLATLIBDIR
 
    If this is set to a non-empty string, it overrides the :data:`sys.platlibdir`
diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst
index 2f32b56423de7..87dc5ddb4e611 100644
--- a/Doc/whatsnew/3.11.rst
+++ b/Doc/whatsnew/3.11.rst
@@ -362,6 +362,11 @@ Other Language Changes
   pickles instance attributes implemented as :term:`slots <__slots__>`.
   (Contributed by Serhiy Storchaka in :issue:`26579`.)
 
+* Add :option:`-P` command line option and :envvar:`PYTHONSAFEPATH` environment
+  variable to not prepend a potentially unsafe path to :data:`sys.path` such as
+  the current directory, the script's directory or an empty string.
+  (Contributed by Victor Stinner in :gh:`57684`.)
+
 
 Other CPython Implementation Changes
 ====================================
@@ -636,6 +641,9 @@ sys
   (equivalent to ``sys.exc_info()[1]``).
   (Contributed by Irit Katriel in :issue:`46328`.)
 
+* Add the :data:`sys.flags.safe_path <sys.flags>` flag.
+  (Contributed by Victor Stinner in :gh:`57684`.)
+
 
 sysconfig
 ---------
@@ -1480,6 +1488,8 @@ New Features
   representation of exceptions.
   (Contributed by Irit Katriel in :issue:`46343`.)
 
+* Added the :c:member:`PyConfig.safe_path` member.
+  (Contributed by Victor Stinner in :gh:`57684`.)
 
 Porting to Python 3.11
 ----------------------
diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h
index 2ba1224d9b0b0..3b6d59389f26b 100644
--- a/Include/cpython/initconfig.h
+++ b/Include/cpython/initconfig.h
@@ -176,6 +176,7 @@ typedef struct PyConfig {
 #endif
     wchar_t *check_hash_pycs_mode;
     int use_frozen_modules;
+    int safe_path;
 
     /* --- Path configuration inputs ------------ */
     int pathconfig_warnings;
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 9099822d0b5b1..6e61cc2e5e7b0 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -313,12 +313,14 @@ def _args_from_interpreter_flags():
             args.append('-E')
         if sys.flags.no_user_site:
             args.append('-s')
+        if sys.flags.safe_path:
+            args.append('-P')
 
     # -W options
     warnopts = sys.warnoptions[:]
-    bytes_warning = sys.flags.bytes_warning
     xoptions = getattr(sys, '_xoptions', {})
-    dev_mode = ('dev' in xoptions)
+    bytes_warning = sys.flags.bytes_warning
+    dev_mode = sys.flags.dev_mode
 
     if bytes_warning > 1:
         warnopts.remove("error::BytesWarning")
diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py
index e8f1964c2a40d..26506312185cf 100644
--- a/Lib/test/test_cmd_line.py
+++ b/Lib/test/test_cmd_line.py
@@ -579,13 +579,13 @@ def test_unknown_options(self):
                      'Cannot run -I tests when PYTHON env vars are required.')
     def test_isolatedmode(self):
         self.verify_valid_flag('-I')
-        self.verify_valid_flag('-IEs')
+        self.verify_valid_flag('-IEPs')
         rc, out, err = assert_python_ok('-I', '-c',
             'from sys import flags as f; '
-            'print(f.no_user_site, f.ignore_environment, f.isolated)',
+            'print(f.no_user_site, f.ignore_environment, f.isolated, f.safe_path)',
             # dummyvar to prevent extraneous -E
             dummyvar="")
-        self.assertEqual(out.strip(), b'1 1 1')
+        self.assertEqual(out.strip(), b'1 1 1 True')
         with os_helper.temp_cwd() as tmpdir:
             fake = os.path.join(tmpdir, "uuid.py")
             main = os.path.join(tmpdir, "main.py")
@@ -880,7 +880,8 @@ def test_sys_flags_not_set(self):
         # Issue 31845: a startup refactoring broke reading flags from env vars
         expected_outcome = """
             (sys.flags.debug == sys.flags.optimize ==
-             sys.flags.dont_write_bytecode == sys.flags.verbose == 0)
+             sys.flags.dont_write_bytecode ==
+             sys.flags.verbose == sys.flags.safe_path == 0)
         """
         self.run_ignoring_vars(
             expected_outcome,
@@ -888,6 +889,7 @@ def test_sys_flags_not_set(self):
             PYTHONOPTIMIZE="1",
             PYTHONDONTWRITEBYTECODE="1",
             PYTHONVERBOSE="1",
+            PYTHONSAFEPATH="1",
         )
 
 class SyntaxErrorTests(unittest.TestCase):
diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py
index e25541820748b..169ae5cb0a06e 100644
--- a/Lib/test/test_embed.py
+++ b/Lib/test/test_embed.py
@@ -479,6 +479,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         '_init_main': 1,
         '_isolated_interpreter': 0,
         'use_frozen_modules': not Py_DEBUG,
+        'safe_path': 0,
         '_is_python_build': IGNORE_CONFIG,
     }
     if MS_WINDOWS:
@@ -496,6 +497,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
         isolated=1,
         use_environment=0,
         user_site_directory=0,
+        safe_path=1,
         dev_mode=0,
         install_signal_handlers=0,
         use_hash_seed=0,
@@ -855,6 +857,7 @@ def test_init_from_config(self):
             'faulthandler': 1,
             'platlibdir': 'my_platlibdir',
             'module_search_paths': self.IGNORE_CONFIG,
+            'safe_path': 1,
 
             'check_hash_pycs_mode': 'always',
             'pathconfig_warnings': 0,
@@ -889,6 +892,7 @@ def test_init_compat_env(self):
             'warnoptions': ['EnvVar'],
             'platlibdir': 'env_platlibdir',
             'module_search_paths': self.IGNORE_CONFIG,
+            'safe_path': 1,
         }
         self.check_all_configs("test_init_compat_env", config, preconfig,
                                api=API_COMPAT)
@@ -919,6 +923,7 @@ def test_init_python_env(self):
             'warnoptions': ['EnvVar'],
             'platlibdir': 'env_platlibdir',
             'module_search_paths': self.IGNORE_CONFIG,
+            'safe_path': 1,
         }
         self.check_all_configs("test_init_python_env", config, preconfig,
                                api=API_PYTHON)
@@ -959,12 +964,13 @@ def test_preinit_parse_argv(self):
         }
         config = {
             'argv': ['script.py'],
-            'orig_argv': ['python3', '-X', 'dev', 'script.py'],
+            'orig_argv': ['python3', '-X', 'dev', '-P', 'script.py'],
             'run_filename': os.path.abspath('script.py'),
             'dev_mode': 1,
             'faulthandler': 1,
             'warnoptions': ['default'],
             'xoptions': ['dev'],
+            'safe_path': 1,
         }
         self.check_all_configs("test_preinit_parse_argv", config, preconfig,
                                api=API_PYTHON)
@@ -975,7 +981,7 @@ def test_preinit_dont_parse_argv(self):
             'isolated': 0,
         }
         argv = ["python3",
-               "-E", "-I",
+               "-E", "-I", "-P",
                "-X", "dev",
                "-X", "utf8",
                "script.py"]
@@ -990,6 +996,7 @@ def test_preinit_dont_parse_argv(self):
     def test_init_isolated_flag(self):
         config = {
             'isolated': 1,
+            'safe_path': 1,
             'use_environment': 0,
             'user_site_directory': 0,
         }
@@ -999,6 +1006,7 @@ def test_preinit_isolated1(self):
         # _PyPreConfig.isolated=1, _PyCoreConfig.isolated not set
         config = {
             'isolated': 1,
+            'safe_path': 1,
             'use_environment': 0,
             'user_site_directory': 0,
         }
@@ -1008,6 +1016,7 @@ def test_preinit_isolated2(self):
         # _PyPreConfig.isolated=0, _PyCoreConfig.isolated=1
         config = {
             'isolated': 1,
+            'safe_path': 1,
             'use_environment': 0,
             'user_site_directory': 0,
         }
diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py
index 90e971deb397f..dce49809385c6 100644
--- a/Lib/test/test_support.py
+++ b/Lib/test/test_support.py
@@ -522,6 +522,7 @@ def test_args_from_interpreter_flags(self):
             ['-E'],
             ['-v'],
             ['-b'],
+            ['-P'],
             ['-q'],
             ['-I'],
             # same option multiple times
@@ -541,7 +542,8 @@ def test_args_from_interpreter_flags(self):
             with self.subTest(opts=opts):
                 self.check_options(opts, 'args_from_interpreter_flags')
 
-        self.check_options(['-I', '-E', '-s'], 'args_from_interpreter_flags',
+        self.check_options(['-I', '-E', '-s', '-P'],
+                           'args_from_interpreter_flags',
                            ['-I'])
 
     def test_optim_args_from_interpreter_flags(self):
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index bbf01b6a39011..8aaf23272607b 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -669,10 +669,10 @@ def test_sys_flags(self):
                  "dont_write_bytecode", "no_user_site", "no_site",
                  "ignore_environment", "verbose", "bytes_warning", "quiet",
                  "hash_randomization", "isolated", "dev_mode", "utf8_mode",
-                 "warn_default_encoding")
+                 "warn_default_encoding", "safe_path")
         for attr in attrs:
             self.assertTrue(hasattr(sys.flags, attr), attr)
-            attr_type = bool if attr == "dev_mode" else int
+            attr_type = bool if attr in ("dev_mode", "safe_path") else int
             self.assertEqual(type(getattr(sys.flags, attr)), attr_type, attr)
         self.assertTrue(repr(sys.flags))
         self.assertEqual(len(sys.flags), len(attrs))
diff --git a/Misc/NEWS.d/next/Security/2022-05-04-14-32-24.gh-issue-57684.HrlDrM.rst b/Misc/NEWS.d/next/Security/2022-05-04-14-32-24.gh-issue-57684.HrlDrM.rst
new file mode 100644
index 0000000000000..f7cddccdaa782
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2022-05-04-14-32-24.gh-issue-57684.HrlDrM.rst
@@ -0,0 +1,3 @@
+Add the :option:`-P` command line option and the :envvar:`PYTHONSAFEPATH`
+environment variable to not prepend a potentially unsafe path to
+:data:`sys.path`. Patch by Victor Stinner.
diff --git a/Misc/python.man b/Misc/python.man
index 45a49271d4dfe..c2e7e507e2fd6 100644
--- a/Misc/python.man
+++ b/Misc/python.man
@@ -43,6 +43,9 @@ python \- an interpreted, interactive, object-oriented programming language
 .B \-OO
 ]
 [
+.B \-P
+]
+[
 .B \-s
 ]
 [
@@ -154,7 +157,7 @@ useful to inspect global variables or a stack trace when a script
 raises an exception.
 .TP
 .B \-I
-Run Python in isolated mode. This also implies \fB\-E\fP and \fB\-s\fP. In
+Run Python in isolated mode. This also implies \fB\-E\fP, \fB\-P\fP and \fB\-s\fP. In
 isolated mode sys.path contains neither the script's directory nor the user's
 site-packages directory. All PYTHON* environment variables are ignored, too.
 Further restrictions may be imposed to prevent the user from injecting
@@ -177,6 +180,11 @@ adding .opt-1 before the .pyc extension.
 Do \fB-O\fP and also discard docstrings; change the filename for
 compiled (bytecode) files by adding .opt-2 before the .pyc extension.
 .TP
+.B \-P
+Don't automatically prepend a potentially unsafe path to \fBsys.path\fP such
+as the current directory, the script's directory or an empty string. See also the
+\fBPYTHONSAFEPATH\fP environment variable.
+.TP
 .B \-q
 Do not print the version and copyright messages. These messages are
 also suppressed in non-interactive mode.
@@ -398,6 +406,10 @@ needed for developing Python extensions and embedding the
 interpreter.
 .RE
 .SH ENVIRONMENT VARIABLES
+.IP PYTHONSAFEPATH
+If this is set to a non-empty string, don't automatically prepend a potentially
+unsafe path to \fBsys.path\fP such as the current directory, the script's
+directory or an empty string. See also the \fB\-P\fP option.
 .IP PYTHONHOME
 Change the location of the standard Python libraries.  By default, the
 libraries are searched in ${prefix}/lib/python<version> and
diff --git a/Modules/getpath.py b/Modules/getpath.py
index 26465c88aaea5..9aff19c0af7ed 100644
--- a/Modules/getpath.py
+++ b/Modules/getpath.py
@@ -717,6 +717,7 @@ def search_up(prefix, *landmarks, test=isfile):
     config['isolated'] = 1
     config['use_environment'] = 0
     config['site_import'] = 0
+    config['safe_path'] = 1
     pythonpath = []
     for line in pth:
         line = line.partition('#')[0].strip()
diff --git a/Modules/main.c b/Modules/main.c
index 624c0f3946085..cca669bdbf4b8 100644
--- a/Modules/main.c
+++ b/Modules/main.c
@@ -570,7 +570,7 @@ pymain_run_python(int *exitcode)
             goto error;
         }
     }
-    else if (!config->isolated) {
+    else if (!config->safe_path) {
         PyObject *path0 = NULL;
         int res = _PyPathConfig_ComputeSysPath0(&config->argv, &path0);
         if (res < 0) {
diff --git a/Programs/_bootstrap_python.c b/Programs/_bootstrap_python.c
index f6b49c8c806a1..6ecbf0c72b5ff 100644
--- a/Programs/_bootstrap_python.c
+++ b/Programs/_bootstrap_python.c
@@ -71,6 +71,7 @@ main(int argc, char **argv)
     config.parse_argv = 1;
     // add current script dir to sys.path
     config.isolated = 0;
+    config.safe_path = 0;
 
 #ifdef MS_WINDOWS
     status = PyConfig_SetArgv(&config, argc, argv);
diff --git a/Programs/_testembed.c b/Programs/_testembed.c
index 3830dc3f8b6ec..9d3d0cbddf0e5 100644
--- a/Programs/_testembed.c
+++ b/Programs/_testembed.c
@@ -676,6 +676,8 @@ static int test_init_from_config(void)
     Py_FrozenFlag = 0;
     config.pathconfig_warnings = 0;
 
+    config.safe_path = 1;
+
     config._isolated_interpreter = 1;
 
     init_from_config_clear(&config);
@@ -742,6 +744,7 @@ static void set_most_env_vars(void)
     putenv("PYTHONFAULTHANDLER=1");
     putenv("PYTHONIOENCODING=iso8859-1:replace");
     putenv("PYTHONPLATLIBDIR=env_platlibdir");
+    putenv("PYTHONSAFEPATH=1");
 }
 
 
@@ -823,6 +826,10 @@ static int test_init_isolated_flag(void)
 
     Py_IsolatedFlag = 0;
     config.isolated = 1;
+    // These options are set to 1 by isolated=1
+    config.safe_path = 0;
+    config.use_environment = 1;
+    config.user_site_directory = 1;
 
     config_set_program_name(&config);
     set_all_env_vars();
@@ -901,6 +908,7 @@ static int test_preinit_dont_parse_argv(void)
     wchar_t *argv[] = {L"python3",
                        L"-E",
                        L"-I",
+                       L"-P",
                        L"-X", L"dev",
                        L"-X", L"utf8",
                        L"script.py"};
@@ -934,7 +942,7 @@ static int test_preinit_parse_argv(void)
 
     /* Pre-initialize implicitly using argv: make sure that -X dev
        is used to configure the allocation in preinitialization */
-    wchar_t *argv[] = {L"python3", L"-X", L"dev", L"script.py"};
+    wchar_t *argv[] = {L"python3", L"-X", L"dev", L"-P", L"script.py"};
     config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv);
     config_set_program_name(&config);
     init_from_config_clear(&config);
diff --git a/Python/dynload_win.c b/Python/dynload_win.c
index 854b1e64c15fa..b43e9fc26f614 100644
--- a/Python/dynload_win.c
+++ b/Python/dynload_win.c
@@ -3,7 +3,6 @@
 
 #include "Python.h"
 #include "pycore_fileutils.h"     // _Py_add_relfile()
-#include "pycore_pathconfig.h"    // _PyPathConfig_ComputeSysPath0()
 #include "pycore_pystate.h"       // _PyInterpreterState_GET()
 
 #ifdef HAVE_DIRECT_H
diff --git a/Python/getopt.c b/Python/getopt.c
index 2e3891aae2d16..fcea60759d12c 100644
--- a/Python/getopt.c
+++ b/Python/getopt.c
@@ -41,7 +41,7 @@ static const wchar_t *opt_ptr = L"";
 
 /* Python command line short and long options */
 
-#define SHORT_OPTS L"bBc:dEhiIJm:OqRsStuvVW:xX:?"
+#define SHORT_OPTS L"bBc:dEhiIJm:OPqRsStuvVW:xX:?"
 
 static const _PyOS_LongOption longopts[] = {
     {L"check-hash-based-pycs", 1, 0},
diff --git a/Python/initconfig.c b/Python/initconfig.c
index d928ebe88553a..265c7ca8e8f76 100644
--- a/Python/initconfig.c
+++ b/Python/initconfig.c
@@ -49,6 +49,7 @@ static const char usage_2[] = "\
          .pyc extension; also PYTHONOPTIMIZE=x\n\
 -OO    : do -O changes and also discard docstrings; add .opt-2 before\n\
          .pyc extension\n\
+-P     : don't add sys.path[0]\n\
 -q     : don't print version and copyright messages on interactive startup\n\
 -s     : don't add user site directory to sys.path; also PYTHONNOUSERSITE\n\
 -S     : don't imply 'import site' on initialization\n\
@@ -113,6 +114,7 @@ PYTHONPATH   : '%lc'-separated list of directories prefixed to the\n\
                default module search path.  The result is sys.path.\n\
 ";
 static const char usage_5[] =
+"PYTHONSAFEPATH: don't prepend a potentially unsafe path to sys.path.\n"
 "PYTHONHOME   : alternate <prefix> directory (or <prefix>%lc<exec_prefix>).\n"
 "               The default module search path uses %s.\n"
 "PYTHONPLATLIBDIR : override sys.platlibdir.\n"
@@ -647,6 +649,10 @@ config_check_consistency(const PyConfig *config)
     assert(config->check_hash_pycs_mode != NULL);
     assert(config->_install_importlib >= 0);
     assert(config->pathconfig_warnings >= 0);
+    assert(config->_is_python_build >= 0);
+    assert(config->safe_path >= 0);
+    // config->use_frozen_modules is initialized later
+    // by _PyConfig_InitImportConfig().
     return 1;
 }
 #endif
@@ -737,6 +743,7 @@ _PyConfig_InitCompatConfig(PyConfig *config)
 #else
     config->use_frozen_modules = 1;
 #endif
+    config->safe_path = 0;
     config->_is_python_build = 0;
     config->code_debug_ranges = 1;
 }
@@ -792,6 +799,7 @@ PyConfig_InitIsolatedConfig(PyConfig *config)
     config->use_hash_seed = 0;
     config->faulthandler = 0;
     config->tracemalloc = 0;
+    config->safe_path = 1;
     config->pathconfig_warnings = 0;
 #ifdef MS_WINDOWS
     config->legacy_windows_stdio = 0;
@@ -959,6 +967,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2)
     COPY_ATTR(_init_main);
     COPY_ATTR(_isolated_interpreter);
     COPY_ATTR(use_frozen_modules);
+    COPY_ATTR(safe_path);
     COPY_WSTRLIST(orig_argv);
     COPY_ATTR(_is_python_build);
 
@@ -1065,6 +1074,7 @@ _PyConfig_AsDict(const PyConfig *config)
     SET_ITEM_INT(_isolated_interpreter);
     SET_ITEM_WSTRLIST(orig_argv);
     SET_ITEM_INT(use_frozen_modules);
+    SET_ITEM_INT(safe_path);
     SET_ITEM_INT(_is_python_build);
 
     return dict;
@@ -1350,6 +1360,7 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
     GET_UINT(_init_main);
     GET_UINT(_isolated_interpreter);
     GET_UINT(use_frozen_modules);
+    GET_UINT(safe_path);
     GET_UINT(_is_python_build);
 
 #undef CHECK_VALUE
@@ -1633,6 +1644,10 @@ config_read_env_vars(PyConfig *config)
         }
     }
 
+    if (config_get_env(config, "PYTHONSAFEPATH")) {
+        config->safe_path = 1;
+    }
+
     return _PyStatus_OK();
 }
 
@@ -2000,6 +2015,7 @@ config_init_import(PyConfig *config, int compute_path_config)
                               "(expected \"on\" or \"off\")");
     }
 
+    assert(config->use_frozen_modules >= 0);
     return _PyStatus_OK();
 }
 
@@ -2327,6 +2343,10 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions,
             config->optimization_level++;
             break;
 
+        case 'P':
+            config->safe_path = 1;
+            break;
+
         case 'B':
             config->write_bytecode = 0;
             break;
@@ -2849,6 +2869,7 @@ _PyConfig_Read(PyConfig *config, int compute_path_config)
 
     assert(config->isolated >= 0);
     if (config->isolated) {
+        config->safe_path = 1;
         config->use_environment = 0;
         config->user_site_directory = 0;
     }
@@ -2994,6 +3015,7 @@ _Py_DumpPathConfig(PyThreadState *tstate)
     PySys_WriteStderr("  isolated = %i\n", config->isolated);
     PySys_WriteStderr("  environment = %i\n", config->use_environment);
     PySys_WriteStderr("  user site = %i\n", config->user_site_directory);
+    PySys_WriteStderr("  safe_path = %i\n", config->safe_path);
     PySys_WriteStderr("  import site = %i\n", config->site_import);
     PySys_WriteStderr("  is in build tree = %i\n", config->_is_python_build);
     DUMP_CONFIG("stdlib dir", stdlib_dir);
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index d5a62fc12b668..edd1d1f23fdea 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -2479,6 +2479,7 @@ static PyStructSequence_Field flags_fields[] = {
     {"dev_mode",                "-X dev"},
     {"utf8_mode",               "-X utf8"},
     {"warn_default_encoding",   "-X warn_default_encoding"},
+    {"safe_path", "-P"},
     {0}
 };
 
@@ -2486,7 +2487,7 @@ static PyStructSequence_Desc flags_desc = {
     "sys.flags",        /* name */
     flags__doc__,       /* doc */
     flags_fields,       /* fields */
-    16
+    17
 };
 
 static int
@@ -2526,6 +2527,7 @@ set_flags_from_config(PyInterpreterState *interp, PyObject *flags)
     SetFlagObj(PyBool_FromLong(config->dev_mode));
     SetFlag(preconfig->utf8_mode);
     SetFlag(config->warn_default_encoding);
+    SetFlagObj(PyBool_FromLong(config->safe_path));
 #undef SetFlagObj
 #undef SetFlag
     return 0;



More information about the Python-checkins mailing list