More work on the safe python AIs:

- Using safe.py from the given SVN repository, which is much cleaner,
  no more notes or tests.

- Added a preprocessor, as a way to allow a limited import again.

- Made runtime docs work again, with a --python-api switch instead of
  the previous AI hack.
This commit is contained in:
Elias Pschernig 2007-03-21 15:24:48 +00:00
parent 69d5e52e60
commit 37ec6e8aaf
6 changed files with 115 additions and 284 deletions

View file

@ -80,16 +80,11 @@ def output(topics, level):
(doc and doc.replace("\n", "\n\n") or "..."))
if __name__ == "__main__":
# If we are run as script, start a scenario with this python AI and a single turn,
# so the documentation will gets printed.
import os, sys
scenario = "multiplayer_Blitz" # doesn't matter, just needs to be valid
controllers = "--controller1=ai --algorithm1=python_ai " +\
"--parm1=python_script:documentation.py --controller2=ai"
os.system("src/wesnoth --nogui --multiplayer --turns=1 --scenario=%s %s" %
(scenario, controllers))
import os
# If we are run as script, run wesnoth with the --python-api switch.
os.system("src/wesnoth --python-api")
else:
# If we are run as a python AI, simply output the documentation to stdout.
# If we are run as a python script, output the documentation to stdout.
import wesnoth
topics = []
myhelp("wesnoth", topics)

39
data/ais/parse.py Normal file
View file

@ -0,0 +1,39 @@
import re, os, safe
def include(matchob):
"""
Regular expression callback. Handles a single import statement, returning
the included code.
"""
names = [x.strip() for x in matchob.group(1).split(",")]
r = ""
for name in names:
for path in pathes:
includefile = os.path.join(path, name)
try:
code = parse_file(includefile + ".py")
break
except IOError:
pass
else:
raise safe.SafeException("Could not include %s." % name)
return None
r += code
return r
def parse_file(name):
"""
Simple pre-parsing of scripts, all it does is allow importing other scripts.
"""
abspath = os.path.abspath(name)
if abspath in already: return ""
already[abspath] = 1
code = file(abspath).read()
code = re.sub(r"^import\s+(.*)", include, code, re.M)
return code
def parse(name):
global already
already = {}
return parse_file(name)

View file

@ -5,61 +5,10 @@ This code is not guaranteed to work. Use at your own risk!
Beware! Trust no one!
Please e-mail philhassey@yahoo.com if you find any security holes.
svn://www.imitationpickles.org/pysafe/trunk
Known limitations:
- Safe doesn't have any testing for timeouts/DoS. One-liners
like these will lock up the system: "while 1: pass", "234234**234234"
- Lots of (likely) safe builtins and safe AST Nodes are not allowed.
I suppose you can add them to the whitelist if you want them. I
trimmed it down as much as I thought I could get away with and still
have useful python code.
- Might not work with future versions of python - this is made with
python 2.4 in mind. _STR_NOT_BEGIN might have to be extended
in the future with more magic variable prefixes. Or you can
switch to conservative mode, but then even variables like "my_var"
won't work, which is sort of a nuisance.
- If you get data back from a safe_exec, don't call any functions
or methods - they might not be safe with __builtin__ restored
to its normal state. Work with them again via an additional safe_exec.
- The "context" sent to the functions is not tested at all. If you
pass in a dangerous function {'myfile':file} the code will be able
to call it.
See README.txt, NOTES.txt, CHANGES.txt for more details.
"""
# Built-in Objects
# http://docs.python.org/lib/builtin.html
# AST Nodes - compiler
# http://docs.python.org/lib/module-compiler.ast.html
# Types and members - inspection
# http://docs.python.org/lib/inspect-types.html
# The standard type heirarchy
# http://docs.python.org/ref/types.html
# Based loosely on - Restricted "safe" eval - by Babar K. Zafar
# (it isn't very safe, but it got me started)
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496746
# Securing Python: Controlling the abilities of the interpreter
# (or - why even trying this is likely to end in tears)
# http://us.pycon.org/common/talkdata/PyCon2007/062/PyCon_2007.pdf
# Changes
# 2007-03-13: added test for unicode strings that contain __, etc
# 2007-03-09: renamed safe_eval to safe_exec, since that's what it is.
# 2007-03-09: use "exec code in context" , because of test_misc_recursive_fnc
# 2007-03-09: Removed 'type' from _BUILTIN_OK - see test_misc_type_escape
# 2007-03-08: Cleaned up the destroy / restore mechanism, added more tests
# 2007-03-08: Fixed how contexts work.
# 2007-03-07: Added test for global node
# 2007-03-07: Added test for SyntaxError
# 2007-03-07: Fixed an issue where the context wasn't being reset (added test)
# 2007-03-07: Added unittest for dir()
# 2007-03-07: Removed 'isinstance', 'issubclass' from builtins whitelist
# 2007-03-07: Removed 'EmptyNode', 'Global' from AST whitelist
# 2007-03-07: Added import __builtin__; s/__builtins__/__builtin__
import compiler
import __builtin__
@ -118,23 +67,16 @@ def _check_ast(code):
ast = compiler.parse(code)
_check_node(ast)
# r = [v for v in dir(__builtin__) if v[0] != '_' and v[0] == v[0].upper()] ; r.sort() ; print r
_BUILTIN_OK = [
'__debug__','quit','exit',
'ArithmeticError', 'AssertionError', 'AttributeError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'IOError', 'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'OverflowWarning', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError',
'abs', 'bool', 'cmp', 'complex', 'dict', 'divmod', 'filter', 'float', 'frozenset', 'hex', 'int', 'len', 'list', 'long', 'map', 'max', 'min', 'object', 'oct', 'pow', 'range', 'reduce', 'repr', 'round', 'set', 'slice', 'str', 'sum', 'tuple', 'xrange', 'zip',
'isinstance', 'issubclass']
#this is zope's list...
#in ['False', 'None', 'True', 'abs', 'basestring', 'bool', 'callable',
#'chr', 'cmp', 'complex', 'divmod', 'float', 'hash',
#'hex', 'id', 'int', 'isinstance', 'issubclass', 'len',
#'long', 'oct', 'ord', 'pow', 'range', 'repr', 'round',
#'str', 'tuple', 'unichr', 'unicode', 'xrange', 'zip']:
'Warning',
'None','True','False',
'abs', 'bool', 'callable', 'cmp', 'complex', 'dict', 'divmod', 'filter',
'float', 'frozenset', 'hex', 'int', 'isinstance', 'issubclass', 'len',
'list', 'long', 'map', 'max', 'min', 'object', 'oct', 'pow', 'range',
'repr', 'round', 'set', 'slice', 'str', 'sum', 'tuple', 'xrange', 'zip',
]
_BUILTIN_STR = [
'copyright','credits','license','__name__','__doc__',
]
@ -187,173 +129,3 @@ def safe_exec(code,context = None):
safe_check(code)
safe_run(code,context)
if __name__ == '__main__':
import unittest
class TestSafe(unittest.TestCase):
def test_check_node_import(self):
self.assertRaises(CheckNodeException,safe_exec,"import os")
def test_check_node_from(self):
self.assertRaises(CheckNodeException,safe_exec,"from os import *")
def test_check_node_exec(self):
self.assertRaises(CheckNodeException,safe_exec,"exec 'None'")
def test_check_node_raise(self):
self.assertRaises(CheckNodeException,safe_exec,"raise Exception")
def test_check_node_global(self):
self.assertRaises(CheckNodeException,safe_exec,"global abs")
def test_check_str_x(self):
self.assertRaises(CheckStrException,safe_exec,"x__ = 1")
def test_check_str_str(self):
self.assertRaises(CheckStrException,safe_exec,"x = '__'")
def test_check_str_class(self):
self.assertRaises(CheckStrException,safe_exec,"None.__class__")
def test_check_str_func_globals(self):
self.assertRaises(CheckStrException,safe_exec,"def x(): pass; x.func_globals")
def test_check_str_init(self):
safe_exec("def __init__(self): pass")
def test_check_str_subclasses(self):
self.assertRaises(CheckStrException,safe_exec,"object.__subclasses__")
def test_check_str_properties(self):
code = """
class X(object):
def __get__(self,k,t=None):
1/0
"""
self.assertRaises(CheckStrException,safe_exec,code)
def test_check_str_unicode(self):
self.assertRaises(CheckStrException,safe_exec,"u'__'")
def test_run_builtin_open(self):
self.assertRaises(RunBuiltinException,safe_exec,"open('test.txt','w')")
def test_run_builtin_getattr(self):
self.assertRaises(RunBuiltinException,safe_exec,"getattr(None,'x')")
def test_run_builtin_abs(self):
safe_exec("abs(-1)")
def test_run_builtin_open_fnc(self):
def test():
f = open('test.txt','w')
self.assertRaises(RunBuiltinException,safe_exec,"test()",{'test':test})
def test_run_builtin_open_context(self):
#this demonstrates how python jumps into some mystical
#restricted mode at this point .. causing this to throw
#an IOError. a bit strange, if you ask me.
self.assertRaises(IOError,safe_exec,"test('test.txt','w')",{'test':open})
def test_run_builtin_type_context(self):
#however, even though this is also a very dangerous function
#python's mystical restricted mode doesn't throw anything.
safe_exec("test(1)",{'test':type})
def test_run_builtin_dir(self):
self.assertRaises(RunBuiltinException,safe_exec,"dir(None)")
def test_run_exeception_div(self):
self.assertRaises(ZeroDivisionError,safe_exec,"1/0")
def test_run_exeception_i(self):
self.assertRaises(ValueError,safe_exec,"(-1)**0.5")
def test_misc_callback(self):
self.value = None
def test(): self.value = 1
safe_exec("test()", {'test':test})
self.assertEqual(self.value, 1)
def test_misc_safe(self):
self.value = None
def test(v): self.value = v
code = """
class Test:
def __init__(self,value):
self.x = value
self.y = 4
def run(self):
for n in xrange(0,34):
self.x += n
self.y *= n
return self.x+self.y
b = Test(value)
r = b.run()
test(r)
"""
safe_exec(code,{'value':3,'test':test})
self.assertEqual(self.value, 564)
def test_misc_context_reset(self):
#test that local contact is reset
safe_exec("abs = None")
safe_exec("abs(-1)")
safe_run("abs = None")
safe_run("abs(-1)")
def test_misc_syntax_error(self):
self.assertRaises(SyntaxError,safe_exec,"/")
def test_misc_context_switch(self):
self.value = None
def test(v): self.value = v
safe_exec("""
def test2():
open('test.txt','w')
test(test2)
""",{'test':test})
self.assertRaises(RunBuiltinException,safe_exec,"test()",{'test':self.value})
def test_misc_context_junk(self):
#test that stuff isn't being added into *my* context
#except what i want in it..
c = {}
safe_exec("b=1",c)
self.assertEqual(c['b'],1)
def test_misc_context_later(self):
#honestly, i'd rec that people don't do this, but
#at least we've got it covered ...
c = {}
safe_exec("def test(): open('test.txt','w')",c)
self.assertRaises(RunBuiltinException,c['test'])
#def test_misc_test(self):
#code = "".join(open('test.py').readlines())
#safe_check(code)
def test_misc_builtin_globals_write(self):
#check that a user can't modify the special _builtin_globals stuff
safe_exec("abs = None")
self.assertNotEqual(_builtin_globals['abs'],None)
#def test_misc_builtin_globals_used(self):
##check that the same builtin globals are always used
#c1,c2 = {},{}
#safe_exec("def test(): pass",c1)
#safe_exec("def test(): pass",c2)
#self.assertEqual(c1['test'].func_globals,c2['test'].func_globals)
#self.assertEqual(c1['test'].func_globals,_builtin_globals)
def test_misc_builtin_globals_used(self):
#check that the same builtin globals are always used
c = {}
safe_exec("def test1(): pass",c)
safe_exec("def test2(): pass",c)
self.assertEqual(c['test1'].func_globals,c['test2'].func_globals)
self.assertEqual(c['test1'].func_globals['__builtins__'],_builtin_globals)
self.assertEqual(c['__builtins__'],_builtin_globals)
def test_misc_type_escape(self):
#tests that 'type' isn't allowed anymore
#with type defined, you could create magical classes like this:
code = """
def delmethod(self): 1/0
foo=type('Foo', (object,), {'_' + '_del_' + '_':delmethod})()
foo.error
"""
try:
self.assertRaises(RunBuiltinException,safe_exec,code)
finally:
pass
def test_misc_recursive_fnc(self):
code = "def test():test()\ntest()"
self.assertRaises(RuntimeError,safe_exec,code)
unittest.main()
#safe_exec('print locals()')

View file

@ -47,7 +47,6 @@
static python_ai* running_instance;
bool python_ai::init_ = false;
PyObject* python_ai::python_error_ = NULL;
#define return_none do {Py_INCREF(Py_None); return Py_None;} while(false)
@ -1667,28 +1666,50 @@ static PyMethodDef wesnoth_python_methods[] = {
PyObject* pyob = reinterpret_cast<PyObject *>(type); \
PyModule_AddObject(module, const_cast<char *>(n), pyob); }
void python_ai::initialize_python()
{
if (init_) return;
init_ = true;
Py_Initialize( );
PyObject* module = Py_InitModule3("wesnoth", wesnoth_python_methods,
"This is the wesnoth AI module. "
"The python script will be executed once for each turn of the side with the "
"python AI using the script.");
Py_Register(wesnoth_unit_type, "unit");
Py_Register(wesnoth_location_type, "location");
Py_Register(wesnoth_gamemap_type, "gamemap");
Py_Register(wesnoth_unittype_type, "unittype");
Py_Register(wesnoth_team_type, "team");
Py_Register(wesnoth_attacktype_type, "attacktype");
Py_Register(wesnoth_gamestatus_type, "gamestatus");
}
void python_ai::invoke(std::string name)
{
initialize_python();
PyErr_Clear();
PyObject* globals = PyDict_New();
PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins());
std::string python_code;
python_code +=
"import sys\n"
"backup = sys.path[:]\n"
"sys.path.append(\"" + game_config::path + "/data/ais\")\n"
"try:\n"
" import " + name + "\n"
"finally:\n"
" sys.path = backup\n";
PyObject *ret = PyRun_String(python_code.c_str(), Py_file_input, globals,
globals);
Py_XDECREF(ret);
Py_DECREF(globals);
}
python_ai::python_ai(ai_interface::info& info) : ai_interface(info), exception(QUIT)
{
LOG_AI << "Running Python instance.\n";
running_instance = this;
if ( !init_ )
{
Py_Initialize( );
PyObject* module = Py_InitModule3("wesnoth", wesnoth_python_methods,
"A script must call 'import wesnoth' to import all Wesnoth-related objects. "
"The python script will be executed once for each turn of the side with the "
"python AI using the script.");
Py_Register(wesnoth_unit_type, "unit");
Py_Register(wesnoth_location_type, "location");
Py_Register(wesnoth_gamemap_type, "gamemap");
Py_Register(wesnoth_unittype_type, "unittype");
Py_Register(wesnoth_team_type, "team");
Py_Register(wesnoth_attacktype_type, "attacktype");
Py_Register(wesnoth_gamestatus_type, "gamestatus");
python_error_ = PyErr_NewException("wesnoth.error",NULL,NULL);
PyDict_SetItemString(PyModule_GetDict(module),"error",python_error_);
init_ = true;
}
initialize_python();
calculate_possible_moves(possible_moves_,src_dst_,dst_src_,false);
calculate_possible_moves(enemy_possible_moves_,enemy_src_dst_,enemy_dst_src_,true);
}
@ -1721,21 +1742,20 @@ void python_ai::play_turn()
LOG_AI << "Executing Python script \"" << script << "\".\n";
// Run the python script. We actually execute a short inline python
// script, which sets up the module search path to the current binary
// pathes, runs the script with execfile, and then resets the path.
// script, which sets up the module search path to the data path,
// runs the script, and then resets the path.
std::string python_code;
python_code += "import sys\n"
"backup = sys.path[:]; sys.path.extend([";
const std::vector<std::string>& paths = get_binary_paths("data");
std::vector<std::string>::const_iterator i;
for (i = paths.begin(); i != paths.end(); ++i) {
python_code += "'" + *i + "ais', ";
}
python_code += "])\n"
"import wesnoth, safe, heapq, random\n"
python_code +=
"import sys\n"
"backup = sys.path[:]\n"
"sys.path.append(\"" + game_config::path + "/data/ais\")\n"
"try:\n"
" code = file(\"" + script + "\").read()\n"
" safe.safe_exec(code, {\"wesnoth\" : wesnoth, \"heapq\" : heapq, \"random\" : random})\n"
" import wesnoth, parse, safe, heapq, random\n"
" code = parse.parse(\"" + script + "\")\n"
" safe.safe_exec(code, {\n"
" \"wesnoth\" : wesnoth,\n"
" \"heapq\" : heapq,\n"
" \"random\" : random})\n"
"finally:\n"
" sys.path = backup\n";
PyObject *ret = PyRun_String(python_code.c_str(), Py_file_input,

View file

@ -56,14 +56,14 @@ public:
static PyObject* wrapper_unit_find_path( wesnoth_unit* unit, PyObject* args );
static PyObject* wrapper_unit_attack_statistics(wesnoth_unit* unit, PyObject* args);
static void set_error(const char *fmt, ...);
static bool is_unit_valid(const unit* unit);
std::vector<team>& get_teams() { return get_info().teams; }
static std::vector<std::string> get_available_scripts();
protected:
static bool init_;
static PyObject* python_error_;
static void initialize_python();
static void invoke(std::string name);
private:
static bool init_;
end_level_exception exception;
ai_interface::move_map src_dst_;
ai_interface::move_map dst_src_;

View file

@ -52,6 +52,7 @@
#include "serialization/binary_wml.hpp"
#include "serialization/parser.hpp"
#include "serialization/preprocessor.hpp"
#include "ai_python.hpp"
#include "wesconfig.h"
@ -1655,6 +1656,7 @@ int play_game(int argc, char** argv)
<< " --max-fps the maximum fps the game tries to run at the value\n"
<< " should be between the 1 and 1000, the default is 50.\n"
<< " --path prints the name of the game data directory and exits.\n"
<< " --python-api prints the runtime documentation for the python API.\n"
<< " -r, --resolution XxY sets the screen resolution. Example: -r 800x600\n"
<< " -t, --test runs the game in a small test scenario.\n"
<< " -v, --version prints the game's version number and exits.\n"
@ -1685,6 +1687,9 @@ int play_game(int argc, char** argv)
std::cout << game_config::path
<< "\n";
return 0;
} else if(val == "--python-api") {
python_ai::invoke("documentation.py");
return 0;
} else if (val.substr(0, 6) == "--log-") {
size_t p = val.find('=');
if (p == std::string::npos) {