bazel-contrib / bazel-contrib/rules_python

py_binary no longer runs on Windows with rules_python 2.0.0+ using default Windows settings

オープン
#3,792 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
Starlark
スター
688
フォーク
721
平均マージ
15時間 7分
マージ済み PR(30日)
76

説明

# 🐞 bug report

### Affected Rule

The issue is caused by the rule: py_binary

### Is this a regression?

Yes, the previous version in which this bug was not present was: 1.9.0

### Description

Trying to run a py_binary (either directly or as a dep of a rule) no longer works on Windows in rules_python 2.0.0 or later as it now requires symlink permissions.

## 🔬 Minimal Reproduction

simply `bazel run ` on Windows.

From scratch that would be making a new repo with:

.bazelversion
```
8.7.0
```

MODULE.bazel
```
module(
name = "bazel_demo",
version = "1.0.0",
)

bazel_dep(name = "rules_python", version = "2.0.0")
```

hello.py
```
if __name__ == "__main__":
print("hello!")
```

BUILD.bazel
```
load("@rules_python//python:defs.bzl", "py_binary")

py_binary(
name = "hello",
srcs = ["hello.py"],
)
```

and then running `bazel run hello`, which I did in powershell.

## 🔥 Exception or Error



PS C:\git\bazel_demo> bazel run hello
INFO: Analyzed target //:hello (79 packages loaded, 4467 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello
bazel-bin/hello.exe
INFO: Elapsed time: 13.164s, Critical Path: 4.66s
INFO: 6 processes: 10 action cache hit, 5 internal, 1 local.
INFO: Build completed successfully, 6 total actions
INFO: Running command line: bazel-bin/hello.exe
Traceback (most recent call last):
File "C:\bzl\ufith4hp\execroot\_main\bazel-out\x64_windows-fastbuild-ST-591ff087943d\bin\hello", line 643, in
main()
File "C:\bzl\ufith4hp\execroot\_main\bazel-out\x64_windows-fastbuild-ST-591ff087943d\bin\hello", line 597, in main
python_program = _create_venv(runfiles_root, delete_dirs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\bzl\ufith4hp\execroot\_main\bazel-out\x64_windows-fastbuild-ST-591ff087943d\bin\hello", line 366, in _create_venv
_symlink_exist_ok(from_=venv_python_exe, to=python_exe_actual)
File "C:\bzl\ufith4hp\execroot\_main\bazel-out\x64_windows-fastbuild-ST-591ff087943d\bin\hello", line 527, in _symlink_exist_ok
os.symlink(to, from_)
OSError: [WinError 1314] A required privilege is not held by the client: 'c:\\bzl\\ufith4hp\\execroot\\_main\\bazel-out\\x64_windows-fastbuild-st-591ff087943d\\bin\\hello.exe.runfiles\\rules_python++python+python_3_11_x86_64-pc-windows-msvc\\python.exe' -> 'C:\\Users\\SBARRE~3\\AppData\\Local\\Temp\\bazel._hello.venv.llbvzwkl\\Scripts\\python.exe'

## 🌍 Your Environment

**Operating System:**

  

Windows 11 Enterprise
24H2
26100.8246

**Output of `bazel version`:**

  

bazel 8.7.0

**Rules_python version:**

  

2.0.0

Although same problem is in 2.0.1

**Anything else relevant?**

This was somewhat discussed here: https://github.com/bazel-contrib/rules_python/issues/2586#issuecomment-4442041246
But I moved the discussion into this issue.

I work in a large enterprise and enabling symlinks for users is not an option due to security concerns. This is what grok had to say about symlinks on Windows:

> Windows has a very different security model than Linux/macOS. Many system services, installers, updaters, and even parts of the OS itself run with high privileges (SYSTEM, LocalService, etc.). These processes often follow symbolic links when they resolve file paths.
> If any user could freely create symlinks, they could abuse this in several ways

| Attack Type | How It Works | Potential Impact |
|------------------------------------|------------------------------------------------------------------------------|-------------------------------------------|
| **DLL Hijacking / Binary Planting** | Create a symlink from a user-writable folder (e.g. `C:\Users\victim\AppData\...`) pointing to a malicious DLL, then get a privileged service to load it | Code execution as SYSTEM |
| **Privilege Escalation via Symlink** | Point a symlink at a sensitive file (like a config file, registry hive, or executable) and trick an elevated process into writing to it | Overwrite protected files |
| **TOCTOU Attacks** | Create a symlink between the moment a privileged process checks permissions and when it actually accesses the file | Bypass file access checks |
| **Escaping Restricted Environments** | In sandboxes, containers, or low-privilege contexts, use symlinks to reach files outside the allowed scope | Break out of the sandbox |
| **Path Traversal / Redirection** | Redirect a privileged process to read/write files in protected locations (e.g. `C:\Windows\System32`) | Data theft or system modification |

I created this patch (with help from Claude) which resolved the issue. Note the commit message and comments give more context.

```diff
Subject: [PATCH] Re-enable zipapp mode for Windows

rules_python commit bd7696af broke Windows support when the user does not
have symlink permissions (the default) by forcing Windows to use venv mode
which requires symlinks.

This patch restores the pre-bd7696af behavior by:
1. Re-enabling zipapp mode (build_python_zip) for Windows
2. Handling symlink failures gracefully in zipapp bootstrap
3. Removing the transition that sets enable_runfiles=true for Windows
(added in c805941d for venv mode, not needed for zipapp)
4. Setting PYTHONPATH in zipapp mode for subprocesses

PYTHONPATH handling:
After 0.40.0, commit 547521ed added granular site-packages imports for
Windows venv support. In projects with many dependencies, these import
paths can combine with the runfiles root prefix to exceed Windows' 32767
character environment variable limit.

The solution: attempt to set the full PYTHONPATH with all imports. If this
exceeds the limit (raising ValueError), fall back to setting PYTHONPATH to
just the runfiles root. This fallback works for most cases since sys.path
is already correctly configured by _bazel_site_init for the current process.

Non-Windows platforms are unaffected - they still use venv mode by default.
---
python/private/py_executable.bzl | 26 +--------
python/private/stage2_bootstrap_template.py | 64 +++++++++++++++++++++
python/private/zipapp/zip_main_template.py | 11 +++-
3 files changed, 74 insertions(+), 27 deletions(-)

diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index 6c65bf8f..c2a4a8f8 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -398,10 +398,8 @@ def _create_executable(
extra_default_outputs = []

# NOTE: --build_python_zip defaults to true on Windows
- build_zip_enabled = read_possibly_native_flag(ctx, "build_python_zip") and not is_windows
- if is_windows:
- # The legacy build_python_zip codepath isn't compatible with full venvs on Windows.
- build_zip_enabled = False
+ # Re-enable zipapp for Windows when symlinks aren't available (restores pre-bd7696af behavior)
+ build_zip_enabled = read_possibly_native_flag(ctx, "build_python_zip")

# When --build_python_zip is enabled, then the zip file becomes
# one of the default outputs.
@@ -1956,28 +1954,8 @@ def _create_run_environment_info(ctx, inherited_environment):
inherited_environment = inherited_environment,
)

-def _add_config_setting_defaults(kwargs):
- config_settings = kwargs.get("config_settings", None)
- if config_settings == None:
- config_settings = {}
-
- # NOTE: This code runs in loading phase within the context of the caller.
- # Label() must be used to resolve repo names within rules_python's
- # context to avoid unknown repo name errors.
- default = select({
- labels.PLATFORMS_OS_WINDOWS: {
- labels.ENABLE_RUNFILES: "true",
- },
- "//conditions:default": {},
- })
-
- # Let user-provided settings have precedence
- config_settings = default | config_settings
- kwargs["config_settings"] = config_settings
-
def common_executable_macro_kwargs_setup(kwargs):
convert_legacy_create_init_to_int(kwargs)
- _add_config_setting_defaults(kwargs)

def _transition_executable_impl(settings, attr):
settings = dict(settings)
diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py
index dec356d3..acc980d3 100644
--- a/python/private/stage2_bootstrap_template.py
+++ b/python/private/stage2_bootstrap_template.py
@@ -51,6 +51,10 @@ COVERAGE_INSTRUMENTED = "%coverage_instrumented%" == "1"
# It uses forward slashes, so must be converted for proper usage on Windows.
BUILD_DATA_FILE = "%build_data_file%"

+# Colon-separated list of runfiles-root-relative import paths
+# Used to construct PYTHONPATH for subprocesses in zipapp mode
+IMPORTS = "%imports%"
+
# ===== Template substitutions end =====

@@ -522,6 +526,66 @@ def main():
else:
main_filename = None

+ # Set PYTHONPATH in zipapp mode so subprocesses spawned with sys.executable
+ # can find modules. In zipapp mode, sys.executable points to the raw Python
+ # interpreter, not a venv wrapper, so subprocesses need PYTHONPATH to locate
+ # modules. In venv mode, this isn't needed because the venv's Python already
+ # knows where its site-packages are.
+ #
+ # On Windows, the detailed site-packages imports (added after 0.40.0) can
+ # cause PYTHONPATH to exceed the 32767 character limit. We try to set the
+ # full path, but fall back to just the runfiles root if it's too long.
+ if sys._xoptions.get("RULES_PYTHON_ZIP_DIR"):
+ print_verbose("zipapp mode: IMPORTS has %d parts" % (len(IMPORTS.split(':')) if IMPORTS else 0))
+
+ pythonpath_entries = []
+
+ # Start with runfiles root
+ pythonpath_entries.append(runfiles_root)
+
+ # Add imports from the build-time configuration
+ if IMPORTS:
+ import_parts = IMPORTS.split(':')
+ for part in import_parts:
+ if part: # Skip empty strings
+ import_path = os.path.join(runfiles_root, part)
+ if IS_WINDOWS:
+ import_path = import_path.replace('/', os.sep)
+ pythonpath_entries.append(import_path)
+
+ # Preserve any existing PYTHONPATH
+ old_pythonpath = os.environ.get("PYTHONPATH")
+ if old_pythonpath:
+ print_verbose("zipapp mode: inheriting PYTHONPATH of length %d" % len(old_pythonpath))
+ pythonpath_entries.extend(old_pythonpath.split(os.pathsep))
+
+ # Deduplicate while preserving order (like 0.40.0)
+ seen = set()
+ deduped = []
+ for p in pythonpath_entries:
+ if p and p not in seen:
+ seen.add(p)
+ deduped.append(p)
+
+ pythonpath = os.pathsep.join(deduped)
+ if IS_WINDOWS:
+ pythonpath = pythonpath.replace('/', os.sep)
+
+ print_verbose("zipapp mode: PYTHONPATH length=%d paths=%d" % (len(pythonpath), len(deduped)))
+
+ # On Windows, environment variables have a 32767 character limit.
+ # If we exceed it, Python's os.environ will raise ValueError.
+ # In that case, set PYTHONPATH to just the runfiles root and let
+ # sys.path handle the rest.
+ try:
+ os.environ["PYTHONPATH"] = pythonpath
+ except ValueError as e:
+ if IS_WINDOWS and len(pythonpath) > 32767:
+ print_verbose("zipapp mode: PYTHONPATH too long (%d chars), using runfiles root only" % len(pythonpath))
+ os.environ["PYTHONPATH"] = runfiles_root
+ else:
+ raise
+
if os.environ.get("COVERAGE_DIR"):
import _bazel_site_init

diff --git a/python/private/zipapp/zip_main_template.py b/python/private/zipapp/zip_main_template.py
index 6d12bc9c..a45b6a06 100644
--- a/python/private/zipapp/zip_main_template.py
+++ b/python/private/zipapp/zip_main_template.py
@@ -294,9 +294,14 @@ def finish_venv_setup(runfiles_root):
try:
os.symlink(symlink_to, python_program)
except OSError as e:
- raise Exception(
- f"Unable to create venv python interpreter symlink: {python_program} -> {symlink_to}"
- ) from e
+ # On Windows without --windows_enable_symlinks, just use the actual interpreter directly
+ if IS_WINDOWS:
+ print_verbose(f"Windows symlink failed, using interpreter directly: {e}")
+ python_program = symlink_to
+ else:
+ raise Exception(
+ f"Unable to create venv python interpreter symlink: {python_program} -> {symlink_to}"
+ ) from e
venv_root = dirname(dirname(python_program))
pyvenv_cfg = join(venv_root, "pyvenv.cfg")
if not os.path.exists(pyvenv_cfg):
--
2.52.0
```

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

Windows で rules_python 2.0.0 とデフォルトの symlink 権限を使用して `bazel run hello` の失敗を再現します。python/private/py_executable.bzl、python/private/stage2_bootstrap_template.py、python/private/zipapp/zip_main_template.py を、提案された変更とその周辺のロジックを含めて読みます。py_binary が symlink 権限なしで実行され、Windows 以外での動作に影響がなければ完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
build-system
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
静か
明瞭さ
おおむね明確
初心者へのやさしさ
45/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。