Skip to content

Commit fbb2ccf

Browse files
authored
fix: avoid stage1 bootstrap stdlib shadowing (bazel-contrib#3854)
The stage 1 system-python bootstrap imports standard-library modules before it re-execs into the configured runtime. If the target output directory contains files named like standard-library modules, such as `shutil.py` or `types.py`, the bootstrap can import those files instead and fail before user code starts. This change removes Python's unsafe script-directory prepend before those early imports, while preserving `sys.path[0]` when Python has already suppressed that prepend through `-P`/`PYTHONSAFEPATH` or isolated mode. Stage 2 now uses the same isolated-mode guard when removing or recreating the main-directory path. It also pins the Windows venv entry point template to LF line endings so the compile-pip CI dirty-worktree check is stable across checkout settings. Before this change, a generated target output could shadow bootstrap stdlib imports, and isolated-mode runs on older Python versions could lose a required stdlib path. After this change, bootstrap stdlib imports resolve from the interpreter's standard library, isolated mode preserves the interpreter-provided path, and the line-ending-sensitive CI check stays clean. Tests: ```shell bazel test --config=fast-tests \ //tests/bootstrap_impls:stdlib_shadowing_system_python_test bazel test --config=fast-tests \ //tests/bootstrap_impls:stdlib_shadowing_system_python_test \ //tests/bootstrap_impls:interpreter_args_test \ //tests/bootstrap_impls:sys_path_order_bootstrap_script_test ```
1 parent 098379f commit fbb2ccf

6 files changed

Lines changed: 82 additions & 5 deletions

File tree

.bazelci/presubmit.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ buildifier:
8484
coverage_targets: ["..."]
8585
.coverage_targets_example_bzlmod_build_file_generation: &coverage_targets_example_bzlmod_build_file_generation
8686
coverage_targets: ["//:bzlmod_build_file_generation_test"]
87+
.coverage_targets_bootstrap: &coverage_targets_bootstrap
88+
coverage_targets:
89+
- //tests/bootstrap_impls:stdlib_shadowing_system_python_test
8790
.coverage_targets_example_multi_python: &coverage_targets_example_multi_python
8891
coverage_targets:
8992
- //tests:my_lib_3_10_test
@@ -216,6 +219,7 @@ tasks:
216219
bazel: 7.x
217220
ubuntu:
218221
<<: *reusable_config
222+
<<: *coverage_targets_bootstrap
219223
name: "Default: Ubuntu, Bazel {bazel}"
220224
platform: ubuntu2204
221225
bazel: ${{ bazel }}

news/3854.fixed.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
(bootstrap) Fixed stage 1 bootstrap imports when target outputs shadow standard
2+
library modules.

python/private/python_bootstrap_template.txt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,31 @@ from __future__ import absolute_import
55
from __future__ import division
66
from __future__ import print_function
77

8+
import sys
9+
10+
# By default, Python prepends the directory containing this script to
11+
# sys.path. The stage 1 bootstrap only needs stdlib modules before it
12+
# re-execs into the configured runtime, so avoid resolving imports from the
13+
# target's output or runfiles package directory. This matters when that
14+
# directory contains files with stdlib names, such as shutil.py or types.py.
15+
#
16+
# Python 3.11 introduced PYTHONSAFEPATH (-P), which disables the unsafe prepend.
17+
# Isolated mode (-I) also disables it, including on older interpreters without
18+
# safe_path. In either case, sys.path[0] is not the script directory and should
19+
# be preserved.
20+
if (
21+
not getattr(sys.flags, "safe_path", False) and
22+
not getattr(sys.flags, "isolated", False) and
23+
sys.path
24+
):
25+
del sys.path[0]
26+
827
# Generated file from @rules_python//python/private:python_bootstrap_template.txt
928

1029
from os.path import abspath, dirname, join, basename, normpath
1130
import os
1231
import shutil
1332
import subprocess
14-
import sys
1533

1634
# NOTE: The sentinel strings are split (e.g., "%stage2" + "_bootstrap%") so that
1735
# the substitution logic won't replace them. This allows runtime detection of

python/private/stage2_bootstrap_template.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
# and is a special case of #7091.
1010
#
1111
# Python 3.11 introduced an PYTHONSAFEPATH (-P) option that disables this
12-
# behaviour, which we set in the stage 1 bootstrap.
12+
# behaviour, which we set in the stage 1 bootstrap. Isolated mode (-I) also
13+
# disables it, including on older interpreters without safe_path.
1314
# So the prepended entry needs to be removed only if the above option is either
1415
# unset or not supported by the interpreter.
1516
# NOTE: This can be removed when Python 3.10 and below is no longer supported
16-
if not getattr(sys.flags, "safe_path", False):
17+
if (
18+
not getattr(sys.flags, "safe_path", False)
19+
and not getattr(sys.flags, "isolated", False)
20+
and sys.path
21+
):
1722
del sys.path[0]
1823

1924
import contextlib
@@ -539,8 +544,10 @@ def main():
539544
# means only other generated files are importable (not source files).
540545
#
541546
# To replicate this behavior, we add main's directory within the runfiles
542-
# when safe path isn't enabled.
543-
if not getattr(sys.flags, "safe_path", False):
547+
# when safe path or isolated mode isn't enabled.
548+
if not getattr(sys.flags, "safe_path", False) and not getattr(
549+
sys.flags, "isolated", False
550+
):
544551
prepend_path_entries = [
545552
os.path.join(runfiles_root, os.path.dirname(main_rel_path))
546553
]

tests/bootstrap_impls/BUILD.bazel

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,32 @@ py_reconfig_test(
136136
main = "sys_path_order_test.py",
137137
)
138138

139+
genrule(
140+
name = "stdlib_shadowing_outputs",
141+
outs = [
142+
"shutil.py",
143+
"types.py",
144+
],
145+
cmd = """
146+
cat > $(@D)/shutil.py <<'PY'
147+
raise RuntimeError("target output shutil.py shadowed the stdlib shutil module")
148+
PY
149+
cat > $(@D)/types.py <<'PY'
150+
raise RuntimeError("target output types.py shadowed the stdlib types module")
151+
PY
152+
""",
153+
)
154+
155+
py_reconfig_test(
156+
name = "stdlib_shadowing_system_python_test",
157+
srcs = [
158+
"stdlib_shadowing_test.py",
159+
":stdlib_shadowing_outputs",
160+
],
161+
bootstrap_impl = "system_python",
162+
main = "stdlib_shadowing_test.py",
163+
)
164+
139165
py_reconfig_test(
140166
name = "main_module_test",
141167
srcs = ["main_module.py"],
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2026 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Verifies stage 1 bootstrap stdlib imports cannot be shadowed."""
15+
16+
17+
def test_bootstrap_reached_main():
18+
# If stage 1 imports target outputs such as shutil.py instead of stdlib
19+
# modules, the process fails before this test module is executed.
20+
pass

0 commit comments

Comments
 (0)