Running arbitrary unfriendly Python code on my server
Asked Answered
M

3

7

I'm making a game where users can write Python programs to control robots that fight each other. Every turn (in a thousand-turn game) their script will be run on my server to determine the robot's next move. How can I prevent these users from being mean to my server?

I've thought of/researched the following:

  • evaling their code in a limited environment (i.e. with __builtins__ disabled)
  • using OS-specific jails like chroot/ptrace
  • running the code in a VM of some kind

I have a Python program which runs a user's script a thousand times. How can I enforce a maximum total duration of one minute, restrict their memory resources, prevent them from accessing any files or network connections, and so on? What would be the ideal solution or combination of solutions?

Melatonin answered 28/10, 2013 at 17:28 Comment(4)
How about using Vagrant or Docker?Prevocalic
Oh my god that's incredible. Please post the link to your game when you're done, I'd love to participateBottle
make sure it's safe: Run it on someone else's server first? :) But really, Use context managers for this timing issue. #367182Caldarium
This might be an interesting read: nedbatchelder.com/blog/201206/eval_really_is_dangerous.htmlFate
M
6

I work on PythonAnywhere, which is a Python platform-as-a-service, which means that we run a lot of untrusted code on behalf of our users -- the new interactive console on the front page of Python.org is ours, and its constraints are probably not too far from yours.

I recommend using OS-level virtualisation; anything at the language level is likely to be less secure. We use chroot and cgroups for historical reasons, but if we were starting from scratch today I think we'd use Linux containers (LXC) or Docker. LXC is basically a bunch of clever wrappers around chroot and cgroups and so on that makes them easier to use, so that you can spin up an ephemeral virtual machine really really quickly. And Docker is an even more easy-to-use wrapper around LXC. Both are super-fast -- you can get a new VM up and running in less than a second.

Mackenziemackerel answered 19/3, 2014 at 12:58 Comment(0)
S
3

I have taken a peek at your profile and decided that this is a ongoing labour-of-love! so here goes..

Given your requirements I assume you are planning something that will be available over the internet.

Regarding your second line of enquiry:

I have a Python program which runs a user's script a thousand times. How can I enforce a maximum total duration of one minute, restrict their memory resources, prevent them from accessing any files or network connections, and so on? What would be the ideal solution or combination of solutions?

As you probably know, this is well trodden ground going all the way back to the 1980s. Re-invention is sometimes but not always best, from an informed perspective. For ideas on how to work out time-slicing of players instructions an so on, you might take a look at the sources of some related current projects already out there in the wilds. for example:

Spoiler:

- robocode
This project has a long history; has been in the hands of the community since 2005 and is actively maintained. the sources are java. essential reading imo.
https://github.com/robo-code/robocode

- fightcode:
This is a javascript flavour and works online. so it should be greatly useful in helping you along the way with your online pythonic approach. the site appears now to be commercial/closed-source. nevertheless, you can see the original sources that were submitted for the github 2012 game-off. At the very least, this might provide ideas on how to present your resulting game content to the world :)
https://github.com/timehome/game-off-2012
(incidentally, fightcode was a runner-up!)


Your first line of enquiry is all over SO. See here. However:

The cheapest (in every sense) option would be a chroot jail. The next cheapest option would be something like Linux-VServer. The performance is near native apparently, although I have no experience in using this software. Otherwise, yes, full-virtualisation using xen or whatever.

Opinion:

Full virtualisation will introduce head-aches and unnecessary overheads.

If (and only if) you're careful, you can do it with a simple chroot jail.

  • Have the jail solely as a disposable 'arena' in which to run the codes. ensure it runs with the minimum resources & privileges necessary to get the work done.

  • Totally decouple your web-app from the jail (for example there should be no need for the jail to have network access and so on).

So the work-flow might be like this:

  1. upload game-requests via your web-app.

  2. bless game requests. you said 'arbitrary unfriendly codes' but in reality the game will have some formal parameters; you're only going to be wanting a subset of Python functionality. at least check for it.

  3. queue-up apparently sane game requests and submit them to the arena (aka jail).

  4. if you're really paranoid, construct a new, clean arena/jail for each new game.

  5. run the game in the arena. saving each discrete game-state (in an internal low-cost format) to a database local to the jail.

  6. from outside the the jail, & when the game is complete, provide the web-app with the game-state database and then render it out to the internet.

Notes about setting up a Python jail.

This is how I got into this post. I was looking for specific instructions on how to jail python using (the very excellent) jailkit but couldn't find much. So having cooked my own, I then looked to see if there was a place for it on SO.

Be aware that some nix distros such as Ubuntu and Centos use python in their operation. In which case, to avoid clobbering your system, you might want to build whatever version of Python is required from sources.

Here is my recipe for installing Python 2.7 (assumes you already have jailkit installed) on Centos.

First, a more formal jailkit approach including creation of a dedicated user might be:

# Once-only set-up:

# as root user:

## get, build and alt-install required python onto host OS
mkdir -p /usr/local/src/python
cd /usr/local/src/python
wget http://www.python.org/ftp/python/2.7/Python-2.7.tgz
tar -vxzf Python-2.7.tgz
cd Python-2.7
./configure               #default ${prefix}="/usr/local"
make
make altinstall           #don't clobber systems' python; everythin installed under /urs/local and comes with 2.7 postfix.


## set-up a jailkit config for python 2.7 (& optionally to support some minimal devs)
cat <<OOOK >> /etc/jailkit/jk_init.ini

[python2.7]
comment = python 2.7 interpreter and libraries
paths = python2.7, /usr/local/lib/python2.7, /usr/local/include/python2.7, /etc/ld.so.conf
devices = /dev/null, /dev/zero, /dev/random, /dev/urandom 

OOOK

# Ad-infinitum:

# as root user:

cd /home

## create a python jail by passing jk_init the name of the sction created in /etc/jailkit/jk_init.ini
jk_init -v -j /home/jail_py python2.7

## optionally create a dedicated user
useradd -M -s /sbin/nologin prisoner
passwd prisoner

## quick sanity-check for setuids
find /home/jail_py -perm -4000 -exec ls -ldb {} \;

## test
echo "import sys; print sys.version" | jk_chrootlaunch -u prisoner -j /home/jail_py -x /home/jail_py/usr/local/bin/python2.7
echo "import os; print 'cwd:{} uid:{}'.format(os.getcwd(),os.getuid())" | jk_chrootlaunch -u prisoner -j /home/jail_py -x /home/jail_py/usr/local/bin/python2.7
jk_chrootlaunch -u prisoner -j /home/jail_py -x /home/jail_py/usr/local/bin/python2.7

## optional, if we need /proc
mkdir -p /home/jail_py/proc
mount -t proc /proc /home/jail_py/proc


## nuke everything
umount /home/jail_py/proc
rm -fr /home/jail_py
userdel prisoner

And here's an non-recommended, more ad-hoc approach

# Once-only set-up:

# as root user:

## get and build required python
mkdir -p /usr/local/src/python
cd /usr/local/src/python
wget http://www.python.org/ftp/python/2.7/Python-2.7.tgz
tar -vxzf Python-2.7.tgz
cd Python-2.7
./configure --prefix="/usr"         # optionally override default ${prefix} ~ we don't really need /local/ indirection for our jail
make

## find out minimal python lib dependencies so we can add them to the jail - see jk_cp -j below..
ldd /home/jail_py/usr/bin/python                 

# Ad-infinitum:

# as root user:

## create a user
useradd -M -s /sbin/nologin prisoner
passwd prisoner

## manually create a jail
mkdir -p /home/jail_py

## deploy python into jail
# IMPORTANT: be sure to export DESTDIR to avoid stomping on your system python ;)
cd /usr/local/src/python/Python-2.7
export DESTDIR="/home/jail_py"      # point install to /home/jail_py/${prefix}    
make install                        # no need for altinstall since we're deploying directly to the jail
cd /home


## provision jail with a copy of /etc/ld.so.conf to prevent jk_cp form chucking out warnings
jk_cp -j /home/jail_py  /etc/ld.so.conf

## copy minimal python lib dependencies to the jail - see ldd above
jk_cp -j /home/jail_py  /lib/libpthread.so.0 /lib/libdl.so.2 /lib/libutil.so.1 /lib/libm.so.6 /lib/libc.so.6 /lib/ld-linux.so.2

## quick sanity-check for setuids
find /home/jail_py -perm -4000 -exec ls -ldb {} \;

## test running jailed python as user 'prisoner'
echo "import sys; print sys.version" | jk_chrootlaunch -u prisoner -j /home/jail_py -x /home/jail_py/usr/bin/python
echo "import os; print 'cwd:{} uid:{}'.format(os.getcwd(),os.getuid())" | jk_chrootlaunch -u prisoner -j /home/jail_py -x /home/jail_py/usr/bin/python
jk_chrootlaunch -u prisoner -j /home/jail_py -x /home/jail_py/usr/bin/python


## optional, if we need /proc
mkdir -p /home/jail_py/proc
mount -t proc /proc /home/jail_py/proc

#optional, if we need some /dev/x
jk_cp -j /home/jail_py /dev/null    


#nuke everything
umount /home/jail_py/proc
rm -fr /home/jail_py
userdel prisoner

Note: depending upon your requirements you might also want/need to mount /proc. For example, running something like this will fail if /proc is *not mounted.

cat <<OOOK | jk_chrootlaunch -u prisoner -j /home/jail_py -x /home/jail_py/usr/local/bin/python2.7 -

#thx: https://raw.github.com/pixelb/ps_mem/master/ps_mem.py

import os
import sys
import errno


class Proc:
    def __init__(self):
        uname = os.uname()
        if uname[0] == "FreeBSD":
            self.proc = '/compat/linux/proc'
        else:
            self.proc = '/proc'
    def path(self, *args):
        return os.path.join(self.proc, *(str(a) for a in args))
    def open(self, *args):
        try:
            return open(self.path(*args))
        except (IOError, OSError):
            val = sys.exc_info()[1]
            if (val.errno == errno.ENOENT or # kernel thread or process gone
                val.errno == errno.EPERM):
                raise LookupError
            raise
    def meminfo(self):
        fd=self.open("meminfo")
        for next in iter(fd.readline, ""):
            print next.replace('\n', '')


Proc().meminfo()

OOOK

You should certainly not be needing an interactive session. But it may still be worth running your arena under a dedicated user. But you don't have to. In any case, jailkit takes much of the pain out of setting up a decent chroot jail.

You can grab the sources and tinker with them to suit your own needs (i did). or you can just use jk_chrootlaunch to safely invoke your arena from outside the jail.

(NB: the reason I needed to mess with the jailkit sources is because it employs the same "/./" login hack as pure-ftp)

Finally you might want to peruse: http://www.unixwiz.net/techtips/chroot-practices.html

Scraggly answered 14/3, 2014 at 20:18 Comment(1)
..if you guessed.. yes. my *real motivation here is getting the coveted SO necromacer badge =) mwhahaha ~or at the very least a revival, rotfl.Scraggly
C
0

There's no built in way to run sandboxed code in cpython but there is in pypy.

http://pypy.org/features.html#sandboxing

There are a few other ways described on the python wiki (like using jailkit) but they seem to have various disadvantages.

https://wiki.python.org/moin/SandboxedPython

I would go the pypy route.

Conservator answered 28/10, 2013 at 19:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.