From ecad092ffa97ac236fb9a6b33ff7f5af4af80eb6 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 19 Jul 2024 09:01:02 -0700 Subject: [PATCH] feat: inherit PYTHONSAFEPATH env var from outer process (#2076) By default, PYTHONSAFEPATH is enabled to help prevent imports being found where they shouldn't be. However, this behavior can't be disabled, which makes it harder to use a py_binary when the non-safe path behavior is explicitly desired. To fix, the bootstrap now respects the caller environment's PYTHONSAFEPATH environment variable, if set. This allows the callers to set `PYTHONSAFEPATH=` (empty string) to override the default behavior that enables it. Fixes https://github.com/bazelbuild/rules_python/issues/2060 --- CHANGELOG.md | 3 ++ python/private/stage1_bootstrap_template.sh | 8 ++- tests/base_rules/BUILD.bazel | 8 +++ tests/base_rules/bin.py | 5 +- .../inherit_pythonsafepath_env_test.sh | 51 +++++++++++++++++++ 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100755 tests/base_rules/inherit_pythonsafepath_env_test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 560c04ea8a..9ccff79aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ A brief description of the categories of changes: containing ">" sign ### Added +* (rules) `PYTHONSAFEPATH` is inherited from the calling environment to allow + disabling it (Requires {obj}`--bootstrap_impl=script`) + ([#2060](https://github.com/bazelbuild/rules_python/issues/2060)). * (gazelle) Added `python_generation_mode_per_package_require_test_entry_point` in order to better accommodate users who use a custom macro, [`pytest-bazel`][pytest_bazel], [rules_python_pytest] or `rules_py` diff --git a/python/private/stage1_bootstrap_template.sh b/python/private/stage1_bootstrap_template.sh index 46e33b4837..895793b41f 100644 --- a/python/private/stage1_bootstrap_template.sh +++ b/python/private/stage1_bootstrap_template.sh @@ -105,7 +105,13 @@ declare -a interpreter_args # Don't prepend a potentially unsafe path to sys.path # See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH # NOTE: Only works for 3.11+ -interpreter_env+=("PYTHONSAFEPATH=1") +# We inherit the value from the outer environment in case the user wants to +# opt-out of using PYTHONSAFEPATH. +# Because empty means false and non-empty means true, we have to distinguish +# between "defined and empty" and "not defined at all". +if [[ -z "${PYTHONSAFEPATH+x}" ]]; then + interpreter_env+=("PYTHONSAFEPATH=${PYTHONSAFEPATH+1}") +fi if [[ "$IS_ZIPFILE" == "1" ]]; then interpreter_args+=("-XRULES_PYTHON_ZIP_DIR=$zip_dir") diff --git a/tests/base_rules/BUILD.bazel b/tests/base_rules/BUILD.bazel index 62d73ac88f..e04d3147fd 100644 --- a/tests/base_rules/BUILD.bazel +++ b/tests/base_rules/BUILD.bazel @@ -51,3 +51,11 @@ sh_py_run_test( sh_src = "run_binary_zip_no_test.sh", target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT, ) + +sh_py_run_test( + name = "inherit_pythonsafepath_env_test", + bootstrap_impl = "script", + py_src = "bin.py", + sh_src = "inherit_pythonsafepath_env_test.sh", + target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT, +) diff --git a/tests/base_rules/bin.py b/tests/base_rules/bin.py index cffb79ba19..c46e43adc8 100644 --- a/tests/base_rules/bin.py +++ b/tests/base_rules/bin.py @@ -11,11 +11,14 @@ # 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. -# + +import os import sys print("Hello") print( "RULES_PYTHON_ZIP_DIR:{}".format(sys._xoptions.get("RULES_PYTHON_ZIP_DIR", "UNSET")) ) +print("PYTHONSAFEPATH:", os.environ.get("PYTHONSAFEPATH", "UNSET") or "EMPTY") +print("sys.flags.safe_path:", sys.flags.safe_path) print("file:", __file__) diff --git a/tests/base_rules/inherit_pythonsafepath_env_test.sh b/tests/base_rules/inherit_pythonsafepath_env_test.sh new file mode 100755 index 0000000000..bf85d26ad2 --- /dev/null +++ b/tests/base_rules/inherit_pythonsafepath_env_test.sh @@ -0,0 +1,51 @@ +# Copyright 2024 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. + +# --- begin runfiles.bash initialization v3 --- +# Copy-pasted from the Bazel Bash runfiles library v3. +set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v3 --- +set +e + +bin=$(rlocation $BIN_RLOCATION) +if [[ -z "$bin" ]]; then + echo "Unable to locate test binary: $BIN_RLOCATION" + exit 1 +fi + + +function expect_match() { + local expected_pattern=$1 + local actual=$2 + if ! (echo "$actual" | grep "$expected_pattern" ) >/dev/null; then + echo "expected output to match: $expected_pattern" + echo "but got:\n$actual" + return 1 + fi +} + + +actual=$(PYTHONSAFEPATH= $bin 2>&1) +expect_match "sys.flags.safe_path: False" "$actual" +expect_match "PYTHONSAFEPATH: EMPTY" "$actual" + +actual=$(PYTHONSAFEPATH=OUTER $bin 2>&1) +expect_match "sys.flags.safe_path: True" "$actual" +expect_match "PYTHONSAFEPATH: OUTER" "$actual"