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:
upload game-requests via your web-app.
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.
queue-up apparently sane game requests and submit them to the arena (aka jail).
if you're really paranoid, construct a new, clean arena/jail for each new game.
run the game in the arena. saving each discrete game-state (in an internal low-cost format) to a database local to the jail.
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