From 1144d6b573ff68cf6f7309355f245ab51e9bb76b Mon Sep 17 00:00:00 2001 From: Gleb Kolobkov Date: Fri, 26 Jun 2026 16:35:55 -0700 Subject: [PATCH 1/4] fix: avoid stage1 bootstrap stdlib shadowing Remove the launcher directory from sys.path before the Python stage1 bootstrap imports stdlib modules. Generated target outputs can otherwise shadow stdlib modules such as shutil.py before the bootstrap re-execs into the configured runtime. Preserve sys.path when Python is already running in safe-path mode. Add a regression test that places generated shutil.py and types.py outputs beside a system_python bootstrap launcher. --- python/private/python_bootstrap_template.txt | 14 +++++++++- tests/bootstrap_impls/BUILD.bazel | 26 +++++++++++++++++++ .../bootstrap_impls/stdlib_shadowing_test.py | 20 ++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/bootstrap_impls/stdlib_shadowing_test.py diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 38c93ec5b5..69d5b0ed27 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -5,13 +5,25 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import sys + +# By default, Python prepends the directory containing this script to +# sys.path. The stage 1 bootstrap only needs stdlib modules before it +# re-execs into the configured runtime, so avoid resolving imports from the +# target's output or runfiles package directory. This matters when that +# directory contains files with stdlib names, such as shutil.py or types.py. +# +# Python 3.11 introduced PYTHONSAFEPATH (-P), which disables the unsafe prepend. +# In that case, sys.path[0] is not the script directory and should be preserved. +if not getattr(sys.flags, "safe_path", False) and sys.path: + del sys.path[0] + # Generated file from @rules_python//python/private:python_bootstrap_template.txt from os.path import abspath, dirname, join, basename, normpath import os import shutil import subprocess -import sys # NOTE: The sentinel strings are split (e.g., "%stage2" + "_bootstrap%") so that # the substitution logic won't replace them. This allows runtime detection of diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index ab3148db00..89cd682a6a 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -136,6 +136,32 @@ py_reconfig_test( main = "sys_path_order_test.py", ) +genrule( + name = "stdlib_shadowing_outputs", + outs = [ + "shutil.py", + "types.py", + ], + cmd = """ +cat > $(@D)/shutil.py <<'PY' +raise RuntimeError("target output shutil.py shadowed the stdlib shutil module") +PY +cat > $(@D)/types.py <<'PY' +raise RuntimeError("target output types.py shadowed the stdlib types module") +PY +""", +) + +py_reconfig_test( + name = "stdlib_shadowing_system_python_test", + srcs = [ + "stdlib_shadowing_test.py", + ":stdlib_shadowing_outputs", + ], + bootstrap_impl = "system_python", + main = "stdlib_shadowing_test.py", +) + py_reconfig_test( name = "main_module_test", srcs = ["main_module.py"], diff --git a/tests/bootstrap_impls/stdlib_shadowing_test.py b/tests/bootstrap_impls/stdlib_shadowing_test.py new file mode 100644 index 0000000000..fc913857db --- /dev/null +++ b/tests/bootstrap_impls/stdlib_shadowing_test.py @@ -0,0 +1,20 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Verifies stage 1 bootstrap stdlib imports cannot be shadowed.""" + + +def test_bootstrap_reached_main(): + # If stage 1 imports target outputs such as shutil.py instead of stdlib + # modules, the process fails before this test module is executed. + pass From f5ed6b5f9ea23d513d68494969caa772d01a1dab Mon Sep 17 00:00:00 2001 From: Gleb Kolobkov Date: Fri, 26 Jun 2026 16:47:02 -0700 Subject: [PATCH 2/4] fix: pin venv bat template line endings --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index fafafd001b..3b40ce7226 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ python/features.bzl export-subst tools/publish/*.txt linguist-generated=true tests/uv/lock/testdata/requirements.txt text eol=lf +python/private/pypi/venv_entry_point_template.bat text eol=lf python/private/runtimes_manifest_workspace.bzl text eol=lf python/private/runtimes_manifest.txt text eol=lf From 05dbf1bd594341195ec63d0a366e1941a8583ea4 Mon Sep 17 00:00:00 2001 From: Gleb Kolobkov Date: Fri, 26 Jun 2026 16:51:02 -0700 Subject: [PATCH 3/4] fix: respect isolated bootstrap sys path --- python/private/python_bootstrap_template.txt | 10 ++++++++-- python/private/stage2_bootstrap_template.py | 16 ++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 69d5b0ed27..482918c038 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -14,8 +14,14 @@ import sys # directory contains files with stdlib names, such as shutil.py or types.py. # # Python 3.11 introduced PYTHONSAFEPATH (-P), which disables the unsafe prepend. -# In that case, sys.path[0] is not the script directory and should be preserved. -if not getattr(sys.flags, "safe_path", False) and sys.path: +# Isolated mode (-I) also disables it, including on older interpreters without +# safe_path. In either case, sys.path[0] is not the script directory and should +# be preserved. +if ( + not getattr(sys.flags, "safe_path", False) and + not getattr(sys.flags, "isolated", False) and + sys.path +): del sys.path[0] # Generated file from @rules_python//python/private:python_bootstrap_template.txt diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index b11fc76093..ac3dcdb336 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -9,11 +9,16 @@ # and is a special case of #7091. # # Python 3.11 introduced an PYTHONSAFEPATH (-P) option that disables this -# behaviour, which we set in the stage 1 bootstrap. +# behaviour, which we set in the stage 1 bootstrap. Isolated mode (-I) also +# disables it, including on older interpreters without safe_path. # So the prepended entry needs to be removed only if the above option is either # unset or not supported by the interpreter. # NOTE: This can be removed when Python 3.10 and below is no longer supported -if not getattr(sys.flags, "safe_path", False): +if ( + not getattr(sys.flags, "safe_path", False) + and not getattr(sys.flags, "isolated", False) + and sys.path +): del sys.path[0] import contextlib @@ -539,8 +544,11 @@ def main(): # means only other generated files are importable (not source files). # # To replicate this behavior, we add main's directory within the runfiles - # when safe path isn't enabled. - if not getattr(sys.flags, "safe_path", False): + # when safe path or isolated mode isn't enabled. + if ( + not getattr(sys.flags, "safe_path", False) + and not getattr(sys.flags, "isolated", False) + ): prepend_path_entries = [ os.path.join(runfiles_root, os.path.dirname(main_rel_path)) ] From bcf59612c91db8a846c5a9d162db8e67531ba818 Mon Sep 17 00:00:00 2001 From: Gleb Kolobkov Date: Fri, 26 Jun 2026 16:55:32 -0700 Subject: [PATCH 4/4] style: format bootstrap template --- python/private/stage2_bootstrap_template.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index ac3dcdb336..f445ad2b6a 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -545,9 +545,8 @@ def main(): # # To replicate this behavior, we add main's directory within the runfiles # when safe path or isolated mode isn't enabled. - if ( - not getattr(sys.flags, "safe_path", False) - and not getattr(sys.flags, "isolated", False) + if not getattr(sys.flags, "safe_path", False) and not getattr( + sys.flags, "isolated", False ): prepend_path_entries = [ os.path.join(runfiles_root, os.path.dirname(main_rel_path))