5

I'm interested in executing potentially untrusted tests with pytest in some kind of sandbox, like docker, similarly to what continuous integration services do.

I understand that to properly sandbox a python process you need OS-level isolation, like running the tests in a disposable chroot/container, but in my use case I don't need to protect against intentionally malicious code, only from dangerous behaviour of pairing "randomly" functions with arguments. So lesser strict sandboxing may still be acceptable. But I didn't find any plugin that enables any form of sandboxing.

What is the best way to sandbox tests execution in pytest?

Update: This question is not about python sandboxing in general as the tests' code is run by pytest and I can't change the way it is executed to use exec or ast or whatever. Also using pypy-sandbox is not an option unfortunately as it is "a prototype only" as per the PyPy feature page.

Update 2: Hoger Krekel on the pytest-dev mailing list suggests using a dedicated testuser via pytest-xdist for user-level isolation:

py.test --tx ssh=OTHERUSER@localhost --dist=each

which made me realise that for my CI-like use case:

having a "disposable" environment is as important as having a isolated one, so that every test or every session runs from the same initial state and it is not influenced by what older sessions might have left on folders writable by the testuser (/home/testuser, /tmp, /var/tmp, etc).

So the testuser+xdist is close to a solution, but not quite there.

Just for context I need isolation to run pytest-nodev.

Community
  • 1
  • 1
alexamici
  • 754
  • 5
  • 10
  • 1
    This is a unique question, and it has been gathering insightful answers on the pytest mailing list: https://mail.python.org/pipermail/pytest-dev/2016-February/003391.html – Petr Viktorin Feb 11 '16 at 10:29

2 Answers2

8

After quite a bit of research I didn't find any ready-made way for pytest to run a project tests with OS-level isolation and in a disposable environment. Many approaches are possible and have advantages and disadvantages, but most of them have more moving parts that I would feel comfortable with.

The absolute minimal (but opinionated) approach I devised is the following:

  • build a python docker image with:
    • a dedicated non-root user: pytest
    • all project dependencies from requirements.txt
    • the project installed in develop mode
  • run py.test in a container that mounts the project folder on the host as the home of pytest user

To implement the approach add the following Dockerfile to the top folder of the project you want to test next to the requirements.txt and setup.py files:

FROM python:3

# setup pytest user
RUN adduser --disabled-password --gecos "" --uid 7357 pytest
COPY ./ /home/pytest
WORKDIR /home/pytest

# setup the python and pytest environments
RUN pip install --upgrade pip setuptools pytest
RUN pip install --upgrade -r requirements.txt
RUN python setup.py develop

# setup entry point
USER pytest
ENTRYPOINT ["py.test"]

Build the image once with:

docker build -t pytest .

Run py.test inside the container mounting the project folder as volume on /home/pytest with:

docker run --rm -it -v `pwd`:/home/pytest pytest [USUAL_PYTEST_OPTIONS]

Note that -v mounts the volume as uid 1000 so host files are not writable by the pytest user with uid forced to 7357.

Now you should be able to develop and test your project with OS-level isolation.

Update: If you also run the test on the host you may need to remove the python and pytest caches that are not writable inside the container. On the host run:

rm -rf .cache/ && find . -name __pycache__  | xargs rm -rf
alexamici
  • 754
  • 5
  • 10
  • 2
    The use case you describe in your question has actually been my most common use for Docker for a while now. This answer covers most of the finer points of making this approach work. +1 One thing I'm currently struggling with is how to spawn a Docker container *for each individual test* to get test-and-os-level isolation, as well as the ability to parallelise with something like the pytest-xdist plugin... – JimmidyJoo Jun 28 '16 at 18:27
  • 2
    To the best of my knowledge such a use case can only be supported with a dedicated plugin, but I didn't find anything similar. The closest is https://github.com/nvbn/pytest-docker-pexpect that let's you write explicit spawn of images. Writing the core functionality as a pytest plugin should not be difficult, but integrating image setup and maintenance in the plugin itself looks very tricky. – alexamici Jul 01 '16 at 06:18
  • 1
    I agree. I spent a bit of time investigating this but wrote off the idea as too much work in the end. Besides container management, another problem that would need to be addressed is how to get something like xdist to connect to the container with `--tx socket=[...]` or similar. It would appear that `--tx ssh=[...]` is not an option, but using a socket server is yet more complexity. Perhaps I'm wrong, but I feel like an extension to xdist itself would be the ideal way to do this, given the `--tx` abstraction currently in place there. – JimmidyJoo Jul 01 '16 at 13:47
2

To be honest, this seems like a great use case for something like docker. Sure, you're not handling it completely cleanly using only python, but you can abuse the host OS to your heart's desire and not have to worry about long-term damage. Plus, unlike a lot of CI solutions, it can run comfortably on your development machine.

Also note that whether or not your code is intentionally malicious or not, having that sort of isolation is still beneficial to prevent accidents like:

rm -rf /usr/local/share/ myapp
Ryan Gooler
  • 2,025
  • 1
  • 13
  • 14
  • 1
    Using a disposable container is what I'm researching right now and to my surprise but I can't find any tool to help automate that for pytest. – alexamici Feb 18 '16 at 11:26
  • The container would be completely wrapped around python, and would execute pytest. https://hub.docker.com/_/python/ would be a good start, and your docker file would look like: FROM python:2-onbuild CMD [ "python", "./test.py" ] – Ryan Gooler Feb 20 '16 at 04:52
  • 1
    There are a lot of moving parts. You don't want to bake the tests into your image, but send them with `--rsyncdir` and execute them with `--tx ssh=`. Unfortunately you can't use non-standard ports with `--tx` and exposed ports are always placed in non-standard locations. – alexamici Feb 20 '16 at 22:03