Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

py_binary rules default to importing non-sandboxed code #7091

Open
FuegoFro opened this issue Jan 11, 2019 · 20 comments
Open

py_binary rules default to importing non-sandboxed code #7091

FuegoFro opened this issue Jan 11, 2019 · 20 comments
Assignees
Labels
P3 We're not considering working on this, but happy to review a PR. (No assignee) team-Rules-Python Native rules for Python type: bug

Comments

@FuegoFro
Copy link

FuegoFro commented Jan 11, 2019

Description of the problem / feature request:

Python (2.7.15 in this case) populates the first entry of the sys.path by resolving the first argument it is given to its real path (including resolving symlinks) and then using the directory containing that real path. This interferes with Bazel's sandboxing of Python files via symlinks and the runfiles in general, since the actual directory from the original source code will be the first entry in the sys.path.

More info on the method of populating sys.path[0] can be found in these discussions:
https://bugs.python.org/issue6386
https://bugs.python.org/issue17639

Bugs: what's the simplest, easiest way to reproduce this bug? Please provide a minimal example if possible.

Init a fresh git repo, git apply this patch, and run bazel run py:test

diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..e69de29
diff --git a/py/BUILD b/py/BUILD
new file mode 100644
index 0000000..ec84ab9
--- /dev/null
+++ b/py/BUILD
@@ -0,0 +1,6 @@
+py_binary(
+        name = 'test',
+        srcs = glob(['**/*.py'], exclude=['foo/baz.py']),
+        imports = [''],
+        main = 'main.py',
+)
diff --git a/py/foo/__init__.py b/py/foo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/py/foo/bar.py b/py/foo/bar.py
new file mode 100644
index 0000000..e69de29
diff --git a/py/foo/baz.py b/py/foo/baz.py
new file mode 100644
index 0000000..e69de29
diff --git a/py/main.py b/py/main.py
new file mode 100644
index 0000000..5c21a2d
--- /dev/null
+++ b/py/main.py
@@ -0,0 +1,10 @@
+import sys
+# sys.path = sys.path[1:]  # Uncommenting this line gives us the expected behavior
+print sys.path
+
+import foo.bar
+print foo.bar.__file__
+
+# This shouldn't work!
+import foo.baz
+print foo.baz.__file__

Note that when sys.path is printed out, the first entry is in the original source code, and not the bazel output directory. Also note that the py/foo/baz.py file isn't included in the target but can be imported. Manually trimming sys.path at the beginning of the main.py file yields the expected behavior.

On solution to this is to not symlink, but rather actually copy, the main file. There are likely other solutions (maybe something involving a .pth file?) but I wasn't able to determine if it was possible to turn off this symlink-following behavior in Python (it seems like not, given the conversations I linked to earlier).

What operating system are you running Bazel on?

macOS 10.14.2

What's the output of bazel info release?

release 0.20.0-homebrew
(though I also repro'd this with non-Brew 0.21.0 and Brew 0.17.2)

Have you found anything relevant by searching the web?

Python issues listed above, regarding sys.path[0]:
https://bugs.python.org/issue6386
https://bugs.python.org/issue17639
Bazel issue regarding symlinking to a runfiles:
#4022
Commit handling runfiles symlinks on Windows:
#6036

@iirina iirina added untriaged team-Rules-Python Native rules for Python labels Jan 14, 2019
@brandjon brandjon added type: bug P3 We're not considering working on this, but happy to review a PR. (No assignee) and removed untriaged labels Mar 14, 2019
@brandjon
Copy link
Member

Thanks for the summary. Another consequence of this issue is #793, where code that relies on importing unqualified names from the same directory as the script will fail depending on whether the script is generated.

My guess is that the easiest solution is for us to hack around this in the stub file somehow.

@brandjon
Copy link
Member

brandjon commented Apr 5, 2019

It looks like sys.argv[0] initialization happens after site.py processing is done. This means that injecting a custom sitecustomize.py into the runfiles (#7959) could not by itself prevent the source dir of the entry point from being added to sys.path.

Looking at the CPython source, apparently sys.argv[0] is not set if the interpreter is launched with a module name (-m) or command (-c) rather than a script. So if we launched the entry point with -c it might work around this, but at that point we may as well just invoke a wrapper module that completely hacks up sys.path to our liking.

@Globegitter
Copy link

Globegitter commented May 16, 2019

It seems we are running into a similar issue when trying to auto-generate py_binary targets for console_script entry points of pip packages (see apt-itude/rules_pip#13). It seems to be quite common practice to have the file of the binary target to be the same as the package name. E.g. luigi has a bin/luigi.py, or pytest a bin/pytest.py. When trying to execute these targets you get an error such as module luigi not found or similar. See a minimal repro case: https://github.com/Globegitter/py_repro

Right now we have to patch the packages to rename these files, e.g. to bin/luigi-bin.py, or instead of depending directly on the binary in the external repository, we can create a py_binary target in our own workspace basically recreating what the binary would do.

It would be nice not having to do this workaround and just work out of the box with these pip packages.

tensorflow-copybara pushed a commit to tensorflow/probability that referenced this issue Sep 9, 2019
Quoting http://python-notes.curiousefficiency.org/en/latest/python_concepts/import_traps.html#the-double-import-trap:

"""
There?s a reason the general ?no package directories on sys.path? guideline
exists, and the fact that the interpreter itself doesn?t follow it when
determining sys.path[0] is the root cause of all sorts of grief.
"""

Also relevant: bazelbuild/bazel#7091

This is addressed toward issue #539

PiperOrigin-RevId: 268058051
brianwa84 added a commit to brianwa84/probability that referenced this issue Sep 30, 2019
Quoting http://python-notes.curiousefficiency.org/en/latest/python_concepts/import_traps.html#the-double-import-trap:

"""
There?s a reason the general ?no package directories on sys.path? guideline
exists, and the fact that the interpreter itself doesn?t follow it when
determining sys.path[0] is the root cause of all sorts of grief.
"""

Also relevant: bazelbuild/bazel#7091

This is addressed toward issue tensorflow#539

PiperOrigin-RevId: 268058051
@thekyz
Copy link

thekyz commented Oct 8, 2019

Is there any plan to merge proposed fixes for that issue? This is really bad as it completely breaks sandboxing for python binaries and libs.

@karantza
Copy link

karantza commented Oct 9, 2019

Just wanted to add, for anyone who comes across this like me, that this bug also affects py_test. We have code that we've always compiled into .pars, which works fine, but we're now adding tests and noticed this issue when running bazel test. I was able to work around it for now by explicitly popping the first element of sys.path before importing my modules, like the commits above that referenced this issue, but that's really ugly.

@FuegoFro
Copy link
Author

FuegoFro commented Dec 2, 2019

Looks like this was fixed by #9250, which I also confirmed locally on Bazel 1.2.1! Thank you for that @alex-xnor and @brandjon! 🎉

@FuegoFro FuegoFro closed this as completed Dec 2, 2019
@FuegoFro
Copy link
Author

FuegoFro commented Dec 5, 2019

Oh no, looks like I spoke too soon! I was bamboozled since I came back to this after not having looked at it for a while. The test I was running was importing code from a different module and hence not seeing these issues.

The wrapper script that was updated does indeed prune sys.path[0], however we then do a subprocess.call/os.execv (depending on the OS) which re-invokes Python which in turn re-does the logic to populate the path and adds our directory to sys.path[0], so that does still remain on the path and by-default break us out of the sandbox 🙁

@FuegoFro FuegoFro reopened this Dec 5, 2019
copybara-service bot pushed a commit to google-deepmind/dm-haiku that referenced this issue Feb 18, 2020
We cannot use bazel until bazelbuild/bazel#7091 is resolved since bazel invokes
Python tests via `python path/to/test.py` and this causes python to add
`path/to` as the first entry in sys.path. This means for any test in
`haiku/_src` we have a collision between `typing` in the standard library and
`haiku/_src/typing.py` causing tests to fail.

Running Haiku tests by passing modules to python (e.g. `python -m path.to.test`)
works, as does running via `pytest`. We prefer `pytest` since it discovers tests
and has support for running tests in parallel.

PiperOrigin-RevId: 291146369
Change-Id: I01165f0d0dc20f1c520c02ae9b1da155dbbf0eb6
@thekyz
Copy link

thekyz commented Feb 1, 2021

How can this be P3? This is a major issue when assessing rebuildability of a py_test or py_binary target. Is there any plan to actually fix this?

@moyamo
Copy link

moyamo commented May 13, 2021

Passing --build_python_zip to bazel seems to work around this issue

@limdor
Copy link
Contributor

limdor commented Nov 3, 2021

What is the status of this issue? Like @thekyz I would also like to challange this bug being P3. One of the basic principles of Bazel is hermeticity and correctness. With this bug sandoboxing in Python is simply broken.

@alexeagle
Copy link
Contributor

Note that we solved this for NodeJS by patching the stdlib to make bazel's sandbox appear as regular files rather than symlinks.

https://github.com/bazelbuild/rules_nodejs/tree/stable/packages/node-patches

@jablin
Copy link
Contributor

jablin commented Feb 14, 2022

Same problem in Ruby.

diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..8fcdabd
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,6 @@
+genrule(
+  name = "rb",
+  srcs = ["demo"],
+  outs = ["dummy"],
+  cmd = "$(location demo) > $@",
+)
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..e69de29
diff --git a/demo b/demo
new file mode 100755
index 0000000..3bb59d3
--- /dev/null
+++ b/demo
@@ -0,0 +1,4 @@
+#!/usr/bin/ruby
+
+require_relative 'lib'
+puts(fn1)
diff --git a/lib.rb b/lib.rb
new file mode 100644
index 0000000..fb69544
--- /dev/null
+++ b/lib.rb
@@ -0,0 +1,3 @@
+def fn1
+  'lib'
+end

This rule should not work because lib.rb is not a declared dependency. Still, the rule works with processwrapper, linux-sandbox and sandboxfs.

I bet it's the same problem with Perl if you use FindBin.

This problem is unsettling.

@jablin
Copy link
Contributor

jablin commented Feb 14, 2022

I think that assigning this problem team-Rules-Python is not a good choice. Isn't this problem about all local execution and sandboxing?

@larsrc-google
Copy link
Contributor

The initial example confused me, as it uses blaze run, which doesn't use sandboxing. But with the modified BUILD file below, it does show this issue in a sandbox when run with bazel build py:runit --spawn_strategy=sandboxed, giving the output ['/usr/local/google/home/larsrc/git/issue-7091/py', '/usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/337e8539277b8db2a083a0be14a325a0/sandbox/linux-sandbox/1/execroot/__main__/bazel-out/host/bin/py/test.runfiles', '/usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/337e8539277b8db2a083a0be14a325a0/sandbox/linux-sandbox/1/execroot/__main__/bazel-out/host/bin/py/test.runfiles/__main__/py', '/usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/337e8539277b8db2a083a0be14a325a0/sandbox/linux-sandbox/1/execroot/__main__/bazel-out/host/bin/py/test.runfiles/__main__', '/usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/337e8539277b8db2a083a0be14a325a0/sandbox/linux-sandbox/1/execroot/__main__/bazel-out/host/bin/py/test.runfiles/bazel_tools', '/usr/lib/python39.zip', '/usr/lib/python3.9', '/usr/lib/python3.9/lib-dynload', '/usr/local/lib/python3.9/dist-packages', '/usr/lib/python3/dist-packages', '/usr/lib/python3.9/dist-packages']

$ cat py/BUILD
py_binary(
    name = "test",
    srcs = glob(
        ["**/*.py"],
        exclude = ["foo/baz.py"],
    ),
    imports = [""],
    main = "main.py",
)

genrule(
    name = "runit",
    outs = ["dummy"],
    cmd = "$(location :test) > $@",
    tools = [":test"],
)

@tpudlik
Copy link
Contributor

tpudlik commented Aug 30, 2022

This bug interacts particularly perniciously with py_proto_library, which uses Python imports not qualified with the repo name (which should not be allowed, #7067). The result is that imports in proto generated code may become ambiguous and resolve to incorrect packages at runtime (see https://issuetracker.google.com/issues/241456982#comment8 for an example and more details).

@tpudlik
Copy link
Contributor

tpudlik commented Sep 1, 2022

This bug was also reported as bazelbuild/rules_python#382, and f99cebe fixed it for Python 3.11+ (but note that Python 3.11 is not released yet, it's only coming in October).

@rickeylev
Copy link
Contributor

I had an idea for another possible solution here. It's not pretty, but should contain edits to the stub template and work with earlier versions.

Basically, use a combination of compile(), exec() and sys.modules (maybe __name__ and __loader__, too) to have some shim code that modifies sys.path, and then loads the originally intended main file, futzing with e.g. sys.modules to make sure the original main sees itself as __main__.

I'm pretty sure I got the idea here from PAR files loading things from zip files. It's implemented in Python, but has to play tricks to load the original main and trigger the if name == main block.

@larsrc-google
Copy link
Contributor

I take it @jvolkman 's PR fixed this.

@jvolkman
Copy link
Contributor

@larsrc-google that PR was never merged (it wasn't mine; I just commented). I think #15701 fixes this, but only for Python 3.11+.

janfeitsma added a commit to janfeitsma/MRA-prototype that referenced this issue Aug 12, 2023
@matts1
Copy link
Contributor

matts1 commented Sep 11, 2023

It appears I'm missing permissions to reopen the issue.

Could someone else do that?

@fmeum fmeum reopened this Sep 11, 2023
uri-canva pushed a commit to NixOS/nixpkgs that referenced this issue Oct 30, 2023
The tests started to fail after the repo-wide python 3.10 -> 3.11 update.

This is caused by Bazel's py_binary rule setting the [`PYTHONSAFEPATH`][1]
environment variable, which only has an effect for Python >= 3.11.

Setting this variable avoids prepending the current working directory and the
script's directory. The current test code relied on this behavior and thus
failed with:

```
Traceback (most recent call last):
  File "/build/.cache/bazel/_bazel_build/8bcfff1c77854f2a2b07d1413b0fc106/execroot/our_workspace/bazel-out/k8-fastbuild/bin/python/bin.runfiles/our_workspace/python/bin.py", line 6, in <module>
    from lib import foo
ModuleNotFoundError: No module named 'lib'
```

See also [bazelbuild/bazel#7091][2]

[1]: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH
[2]: bazelbuild/bazel#7091
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
P3 We're not considering working on this, but happy to review a PR. (No assignee) team-Rules-Python Native rules for Python type: bug
Projects
None yet
Development

No branches or pull requests