def sandbox_helper(sandbox: Sandbox, command, privileged=False): stdout, stderr = TemporaryFile("wb+"), TemporaryFile("wb+") sandbox.execute(command=command, stdin_fd=None, stdout_fd=stdout, stderr_fd=stderr, privileged=privileged) stdout.flush() stdout.seek(0) stdout_text = stdout.read().decode().strip() stdout.close() stderr.flush() stderr.seek(0) stderr_text = stderr.read().decode().strip() stderr.close() # If running java or javac or jar the JVM prints an annoying message: # "Picked up JAVA_TOOL_OPTIONS: <actual options set by sandbox environment> # Remove it from the stderr if it is there if any(java in command for java in ["java", "javac", "jar"]): stdout_text = "\n".join([ line for line in stdout_text.splitlines() if not line.startswith("Picked up JAVA_TOOL_OPTIONS") ]) stderr_text = "\n".join([ line for line in stderr_text.splitlines() if not line.startswith("Picked up JAVA_TOOL_OPTIONS") ]) return stdout_text, stderr_text
def main(): config, argv = parseOptions() config.allowModule('sys', 'argv') with open(argv[0], "rb") as fp: content = fp.read() sys.argv = list(argv) sandbox = Sandbox(config) sandbox.execute(content)
def run_python(code): # call and run code sandbox = Sandbox(SandboxConfig('stdout', use_subprocess=True, timeout=5)) backup = sys.stdout try: sys.stdout = StringIO() # capture output sandbox.execute(code) results = sys.stdout.getvalue() # release output finally: sys.stdout.close() # close the stream sys.stdout = backup return results
class Player(greenlet): def __init__(self, robot_controller): super(Player, self).__init__() self.robot_controller = robot_controller config = SandboxConfig(use_subprocess=False) config.enable('traceback') config.enable('stdout') config.enable('stderr') config.enable('time') # TODO need to allow *all* imports from team package config.allowModule(robot_controller.robot.team + '.player', 'RobotPlayer') # TODO need a better method for override the sys_path config.sys_path = config.sys_path + (os.getcwd(),) # add additional builtins to the config # - increment_clock this = self def increment_clock(amt): Scheduler.instance().increment_bytecode(amt) # TODO need a better method to add builtins additions config._builtins_additions = { 'increment_clock': increment_clock, } self.sandbox = Sandbox(config) self.running = False def resume(self, throw=False): return self.switch(throw) def pause(self): # break out of the sandbox self.sandbox.disable_protections() # return execution to the scheduler throw = self.parent.switch() if throw: raise RobotDeathException('killed by engine') # re-enable sandbox protections self.sandbox.enable_protections() def run(self, *args): statement = LOADER_CODE.format(team=self.robot_controller.robot.team) safelocals = { 'rc': self.robot_controller } self.running = True self.sandbox.execute(statement, globals={}, locals=safelocals)
def run_scenario(redis, scenario): """ Run Python scenario with ``redis`` instance. """ data = {'redis': redis} results = [] sandbox = Sandbox() start = time.time() for raw in scenario.splitlines(): line = 'result = {0}'.format(raw) if not '=' in raw else raw sandbox.execute(line, {}, data) results.append((raw, data['result'] if 'result' in data else None)) return {'results': results, 'time': time.time() - start}
def run_scenario(redis, scenario): """ Run Python scenario with ``redis`` instance. """ data = {'redis': redis} results = [] sandbox = Sandbox() start = time.time() for raw in scenario.splitlines(): line = 'result = {0}'.format(raw) if '=' not in raw else raw sandbox.execute(line, {}, data) results.append((raw, data['result'] if 'result' in data else None)) return {'results': results, 'time': time.time() - start}
def run(sandbox: Sandbox, command, input_bytes=None, privileged=False) -> (bytes, bytes): # Make the input and output go through pipes as they don't require hard drive I/O stdin = Runner.get_pipe( max_size=0 if input_bytes is None else len(input_bytes)) stdout = Runner.get_pipe(max_size=config.MAX_EXECUTION_OUTPUT) stderr = Runner.get_pipe(max_size=config.MAX_EXECUTION_OUTPUT) # If there is input, write the data into the stdin pipe if input_bytes is not None: with open(stdin[1], "wb") as inp: inp.write(input_bytes) else: os.close(stdin[1]) # Run the command in the sandboxed environment sandbox.execute(command=command, stdin_fd=stdin[0], stdout_fd=stdout[1], stderr_fd=stderr[1], blocking=True, privileged=privileged) os.close(stdin[0]) # Store the execution's stdout and stderr os.close(stdout[1]) with open(stdout[0], "rb") as out: stdout_bytes = out.read() os.close(stderr[1]) with open(stderr[0], "rb") as err: stderr_bytes = err.read() # If running java or javac or jar the JVM prints an annoying message: # "Picked up JAVA_TOOL_OPTIONS: <actual options set by sandbox environment> # Remove it from the stderr if it is there if any(java in command for java in ["java", "javac", "jar"]): stdout_bytes = "\n".join([ line for line in stdout_bytes.decode().splitlines() if not line.startswith("Picked up JAVA_TOOL_OPT") ]).encode() stderr_bytes = "\n".join([ line for line in stderr_bytes.decode().splitlines() if not line.startswith("Picked up JAVA_TOOL_OPT") ]).encode() return stdout_bytes, stderr_bytes
def validateFull(request): #Limit runtime, feature only useable in unix environment if not sys.platform.startswith('win'): #signal.signal(signal.SIGALRM, timehandler) #signal.alarm(5) #will signal SIGALRM in 5 seconds resource.setrlimit(resource.RLIMIT_STACK, (3145728, 3145728)) #limit stack to 3mb resource.setrlimit(resource.RLIMIT_DATA, (3145728, 3145728)) #limit heap to 3mb #Set sandbox environment sandbox = Sandbox(SandboxConfig('math', 'random', 'datetime', 'time')) sbHeader = "from random import *\nfrom math import *" try: sandbox.execute(sbHeader+request.GET['code']) except MemoryError: return HttpResponse("Your code consumed too much memory, look for non-terminating loops.") except Exception as e: return HttpResponse("Full Code did not validate! Here is the error message produced:\n" + str(e)) return HttpResponse(0)
def json_turnin(request): # Make sure the request is in POST if request.method != "POST": raise Http404 # Get vars answer_id = request.POST['answer_id'] result = request.POST['content'] # Find some objects answer = get_object_or_404(Answer, id=answer_id) # Make sure answer belongs to the user if answer.user != request.user: raise Http404 done = 0 if answer.lesson.type == 0: # Run answer through sandbox, and capture stdout stdout_backup = sys.stdout sys.stdout = StringIO() sandbox = Sandbox(SandboxConfig('stdout','math')) sandbox.execute(result) output = sys.stdout.getvalue() sys.stdout = stdout_backup # Test string test = unicode(output.replace("\r", ""), "utf-8") expect = answer.lesson.expect.replace("\r","") # Test result against expected output if answer.lesson.type == 0 and test == expect: done = 1 else: done = 1 # Save answer text to database, just in case answer.result = result if done == 1: answer.status = 1 answer.save() return JSONResponse({'done': done, 'redirect': reverse('main:lessons')})
def runcode(id, locals_dict={}, debug=False, follow_links=True): print locals_dict for k,v in locals_dict.items(): print type(v) node = Node.objects.get(id=id) if node == None: raise Exception("Node not found") code = node.code.encode('ascii').replace("\r","") print "Running node %s"%node.title if os.path.exists(os.path.join('/home/oyvinbak/python/django_ticker/ticker/nodes', code)): path = os.path.join('/home/oyvinbak/python/django_ticker/ticker/nodes', code) print path f = open(path) exec f in locals_dict, locals_dict f.close() else: sandbox = Sandbox(config) sandbox.execute(code, globals=locals_dict) for k,obj in locals_dict.items(): try: pickle.dumps(obj) except: del locals_dict[k] if debug: return if follow_links: for link in node.next.filter(enabled=True): res = runcode.apply_async([link.dst.id], {"locals_dict":locals_dict}) if link.debug: ld = LinkDump() ld.link = link ld.dump = pickle.dumps(locals_dict) ld.result_id = res.id ld.save() else: print "Not following links" return locals_dict
def execute(session_id, code): global modules if code.strip() == '': return '' buff = StringIO() __stdout = sys.stdout sys.stdout = buff sandbox = Sandbox(SandboxConfig('stdout')) old_commands = get_old_commands(key=session_id) old_code = '\n'.join(old_commands) try: sandbox.execute(old_code + '\n' + code) except Exception: buff = StringIO() traceback.print_exc(file=buff) # Remove the stack trace stack = buff.getvalue().replace('"<string>"', '"<JSON-RPC>"').split('\n') return '\n'.join([stack[0]] + stack[3:]) else: full_output = buff.getvalue() output_len = get_old_output_len(key=session_id) write_code(key=session_id, code=code) expire_key(key=session_id, timeout=SESSION_TIMEOUT) if output_len: buff.seek(int(output_len)) output = buff.read() else: output = buff.getvalue() write_output_len(key=session_id, output=full_output) return output finally: sys.stdout = __stdout
def json_execute(request): # Make sure the request is in POST if request.method != "POST": raise Http404 # Get vars answer_id = request.POST['answer_id'] result = request.POST['content'] # Find some objects answer = get_object_or_404(Answer, id=answer_id) # Make sure answer belongs to the user if answer.user != request.user: raise Http404 # Run answer through sandbox, and capture stdout stdout_backup = sys.stdout sys.stdout = StringIO() sandbox = Sandbox(SandboxConfig('stdout','math')) try: sandbox.execute(result) except Exception, e: return JSONResponse({'output': str(e), 'done': 0})
import sys from sandbox import Sandbox, SandboxConfig if __name__ == "__main__": ''' Will run a given code in sandbox (stream stdio from cmd) arg[1] = code given to run python run_python_secure.py <code> < inputfile ''' if len(sys.argv) != 2: print "Not enough args" print sys.argv sys.exit() args = sys.argv code = args[1] try: sandbox = Sandbox(SandboxConfig('stdin', 'stdout')) sandbox.execute(code) except Exception, e: print e
from sandbox import Sandbox from sandbox.config import SandboxConfig config = SandboxConfig('stderr','stdout','interpreter') config.enable('regex') config.enable('time') config.enable('datetime') config.enable('math') config.enable('random') items = ['urllib2','urllib','struct','base64','httplib','socket','_socket'] for item in items: config.allowModule(item) config.allowModule('httplib') config.allowModule('array','array') config.allowModule('StringIO', 'StringIO') config.allowModule('os','_get_exports_list') config.allowModule('json','scanner','decoder') config.recursion_limit = 3000 sandbox = Sandbox(config) import pickle f = open('/tmp/data') code, locals_dict = pickle.load(f) f.close() sandbox.execute(code, locals=locals_dict)
from __future__ import print_function from sandbox import Sandbox import os sbox=Sandbox() string =""" print ("Memory Leak") (lambda fc=( lambda n: [ c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == n ][0] ): fc("function")( fc("code")( 0,0,0,0,"KABOOM",(),(),(),"","",0,"" ),{} )() )() """ sbox.execute(string)
print(n1) else: print("Fibonacci sequence:") print(n1,",",n2,end=', ') while count < nterms: nth = n1 + n2 print(nth,end=' , ') # update values n1 = n2 n2 = nth count += 1 """ s.execute(fibonnaci) print("\n") #using the sandbox to execute a print statement code=""" print('Hello world!') """ s.execute(code)
""" A bad implementation of the Fibonacci function was written in which redundant recursive functions are called. The parameter '9' was entered to test if the program will actually run and print something. Nothing was printed. '35' was inputted as a parameter next. Due to computing constraints, this will take about 6 seconds to compute on my computer, however, the sandbox just exists in less than a second when I enter it. """ from sandbox import Sandbox s = Sandbox() code = """ def fib(x): if x == 0: return 0 elif x == 1 or x == 2: return 1 else: return fib(x-1)+ fib(x-2) fib(9) fib(35) print fib(9) print fib(35) """ s.execute(code)
class TestSandbox(TestCase): PATH_FIXTURES = os.path.abspath("tests/fixtures/sandbox/") @classmethod def setUpClass(cls): initializer.init() @classmethod def tearDownClass(cls): pass def setUp(self) -> None: self.sandbox = None def tearDown(self) -> None: if self.sandbox is not None: self.sandbox.wait(0.1) del self.sandbox self.sandbox = None @staticmethod def sandbox_helper(sandbox: Sandbox, command, privileged=False): stdout, stderr = TemporaryFile("wb+"), TemporaryFile("wb+") sandbox.execute(command=command, stdin_fd=None, stdout_fd=stdout, stderr_fd=stderr, privileged=privileged) stdout.flush() stdout.seek(0) stdout_text = stdout.read().decode().strip() stdout.close() stderr.flush() stderr.seek(0) stderr_text = stderr.read().decode().strip() stderr.close() # If running java or javac or jar the JVM prints an annoying message: # "Picked up JAVA_TOOL_OPTIONS: <actual options set by sandbox environment> # Remove it from the stderr if it is there if any(java in command for java in ["java", "javac", "jar"]): stdout_text = "\n".join([ line for line in stdout_text.splitlines() if not line.startswith("Picked up JAVA_TOOL_OPTIONS") ]) stderr_text = "\n".join([ line for line in stderr_text.splitlines() if not line.startswith("Picked up JAVA_TOOL_OPTIONS") ]) return stdout_text, stderr_text # ================================= # # Chroot Setup # # ================================= # def test_working_directory_is_empty(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="ls") self.assertEqual("", stderr) self.assertEqual("", stdout) def test_working_directory_is_home(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="pwd") self.assertEqual("", stderr) self.assertEqual("/home", stdout) def test_chroot_has_proper_fs_tree(self): # Root directory has a proper chroot structure # List all entries in "/" folder (but skip "total...", thus not using -la) stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="ls -ld /* /.*") self.assertEqual("", stderr) # All required system directories are present for mount_dir in [ "bin", "dev", "etc", "lib", "lib64", "proc", "sys", "usr" ]: self.assertIn(mount_dir, stdout) # All dirs (".", "..", "/home" and mounted directories) have the correct permissions self.assertEqual(stdout.count("xr-xr-x"), len(stdout.splitlines())) def test_sys_structure_is_mounted(self): # There are files in the mounted directories stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="cat /proc/uptime") self.assertEqual("", stderr) self.assertNotEqual("", stdout) # Sanity check that an error is printed on a missing file stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="cat /proc/foobarbaz") self.assertNotEqual("", stderr) self.assertEqual("", stdout) def test_cannot_chroot_second_time(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="chroot ..") self.assertIn("Operation not permitted", stderr) stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="sudo chroot ..") self.assertIn("is not allowed to execute '/usr/sbin/chroot ..'", stderr) # ================================= # # File System Access # # ================================= # def test_cant_touch_this(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="touch /proc/uptime") self.assertNotEqual("", stderr) self.assertEqual("", stdout) def test_cannot_make_directories(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="mkdir foo") self.assertNotEqual("", stderr) self.assertEqual("", stdout) def test_cannot_remove_directories(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="cd .. && rmdir home") self.assertNotEqual("", stderr) self.assertEqual("", stdout) def test_cannot_create_files(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="touch foo.txt") self.assertNotEqual("", stderr) self.assertEqual("", stdout) def test_cannot_redirect_to_files(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="echo bla > foo.txt") self.assertNotEqual("", stderr) self.assertEqual("", stdout) def test_cannot_rm_rf(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="rm -rf /") self.assertNotEqual("", stderr) stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="sudo rm -rf /") self.assertNotEqual("", stderr) def test_cp(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="cp /bin/bash .") self.assertIn("Permission denied", stderr) def test_mv(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="mv /bin/bash .") self.assertIn("Permission denied", stderr) def test_create_symlink(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="ln -s /bin/bash bash") self.assertIn("Permission denied", stderr) def test_mount(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="mount /bin /usr") self.assertIn("only root can do that", stderr) # ================================= # # Network Access # # ================================= # @mock.patch("config.MAX_EXECUTION_TIME", 1.0) def test_no_ping_dns_resolving(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="ping www.google.com") self.assertIn("Temporary failure in name resolution", stderr) @mock.patch("config.MAX_EXECUTION_TIME", 1.0) def test_no_ping_to_ip_address(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="ping 8.8.8.8") self.assertIn("Operation not permitted", stderr) @mock.patch("config.MAX_EXECUTION_TIME", 1.0) def test_no_wget_dns_resolving(self): stdout, stderr = self.sandbox_helper( sandbox=Sandbox(), command="wget www.google.com/robots.txt") self.assertIn("Temporary failure in name resolution", stderr) @mock.patch("config.MAX_EXECUTION_TIME", 1.0) def test_no_wget_from_ip_address(self): stdout, stderr = self.sandbox_helper( sandbox=Sandbox(), command="wget 216.58.212.4/robots.txt") # The process should reach the MAX_EXECUTION_TIME and be killed (being stuck on "Connecting to...") self.assertEqual("Connecting to 216.58.212.4:80...", stderr.splitlines()[-1]) @mock.patch("config.MAX_EXECUTION_TIME", 1.0) def test_no_localhost_access(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="ping localhost") self.assertIn("Operation not permitted", stderr) stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="ping 127.0.0.1") self.assertIn("Operation not permitted", stderr) # ================================= # # User and process info # # ================================= # def test_priority_in_boundaries(self): self.assertGreaterEqual(config.PROCESS_PRIORITY_REAL, os.sched_get_priority_min(os.SCHED_RR)) self.assertLessEqual(config.PROCESS_PRIORITY_REAL, os.sched_get_priority_max(os.SCHED_RR)) def test_cannot_run_commands_with_sudo(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="sudo ls -la") self.assertNotEqual("", stderr) self.assertEqual("", stdout) def test_prlimit_output(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="prlimit") expected = { "AS": [ config.MAX_EXECUTION_MEMORY, config.MAX_EXECUTION_MEMORY, "bytes" ], "CPU": [config.MAX_EXECUTION_TIME, config.MAX_EXECUTION_TIME, "seconds"], "DATA": [ config.MAX_EXECUTION_MEMORY, config.MAX_EXECUTION_MEMORY, "bytes" ], "FSIZE": [ config.MAX_EXECUTION_OUTPUT, config.MAX_EXECUTION_OUTPUT, "bytes" ], "NOFILE": [config.MAX_OPEN_FILES, config.MAX_OPEN_FILES, "files"], "NPROC": [config.MAX_PROCESSES, config.MAX_PROCESSES, "processes"], "RSS": [ config.MAX_EXECUTION_MEMORY, config.MAX_EXECUTION_MEMORY, "bytes" ], "STACK": [config.MAX_EXECUTION_STACK, config.MAX_EXECUTION_STACK, "bytes"], } lines = stdout.splitlines() for line in lines: tokens = line.split() if tokens[0] in expected: self.assertEqual(int(tokens[-3]), expected[tokens[0]][0]) self.assertEqual(int(tokens[-2]), expected[tokens[0]][1]) self.assertEqual(tokens[-1], expected[tokens[0]][2]) def test_niceness_level(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="nice") self.assertEqual(str(config.PROCESS_PRIORITY_NICE), stdout) def test_worker_id(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="id -u") self.assertGreaterEqual(int(stdout), 1000) def test_worker_user(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="whoami") self.assertTrue("worker" in stdout) def test_scheduling_algorithm(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="chrt -p $$") self.assertIn("scheduling policy: SCHED_OTHER", stdout.splitlines()[0]) # Standard UNIX scheduler def test_process_info(self): stdout, stderr = self.sandbox_helper( sandbox=Sandbox(), command="ps -o uid,pid,ppid,cls,pri,ni,rtprio -p $$") process_info = stdout.splitlines()[1].split() self.assertGreaterEqual(int(process_info[0]), 1000) # User ID (again) self.assertEqual( process_info[3], "TS" ) # Scheduling algorithm, RR = real-time round robin, TS = standard self.assertEqual(process_info[5], str(config.PROCESS_PRIORITY_NICE)) # Nice level self.assertEqual(process_info[6], "-") # Priority # ================================= # # Sandbox API # # ================================= # def test_has_file(self): self.sandbox = Sandbox() self.assertFalse(self.sandbox.has_file("foo.txt")) self.assertTrue(self.sandbox.has_file("../usr/bin/timeout")) def test_get_file(self): self.sandbox = Sandbox() self.assertFalse(os.path.isfile("./time_binary")) self.sandbox.get_file("../usr/bin/timeout", "./timeout_binary") self.assertTrue(os.path.isfile("./timeout_binary")) os.remove("./timeout_binary") def test_put_file(self): self.sandbox = Sandbox() self.assertFalse(self.sandbox.has_file("foo.txt")) self.sandbox.put_file("install_steps.txt", "foo.txt") self.assertTrue(self.sandbox.has_file("foo.txt")) def test_put_file_with_permissions_write(self): self.sandbox = Sandbox() self.assertFalse(self.sandbox.has_file("foo.txt")) self.sandbox.put_file("install_steps.txt", "foo.txt") self.assertTrue(self.sandbox.has_file("foo.txt")) stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="echo bar > foo.txt") self.assertNotEqual(stderr, "") # No permissions to write self.sandbox.put_file("install_steps.txt", "foo.txt", 0o777) stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="echo bar > foo.txt") self.assertEqual(stderr, "") # This time has permissions stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="cat foo.txt") self.assertEqual(stdout, "bar") # Double check by printing the file's contents def test_put_file_with_permissions_exec(self): self.sandbox = Sandbox() self.sandbox.put_file("/bin/ls", "ls_binary") stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="./ls_binary ..") self.assertEqual(stderr, "") self.assertIn("bin", stdout) # Should list parent folder self.sandbox.put_file("/bin/ls", "ls_binary", 0o766) stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="./ls_binary ..") self.assertIn("Permission denied", stderr) # Should be unable to execute self.assertEqual(stdout, "") def test_del_file(self): self.sandbox = Sandbox() self.assertFalse(self.sandbox.has_file("foo.txt")) self.sandbox.put_file("install_steps.txt", "foo.txt") self.assertTrue(self.sandbox.has_file("foo.txt")) self.sandbox.del_file("foo.txt") self.assertFalse(self.sandbox.has_file("foo.txt")) def test_read_file(self): self.sandbox = Sandbox() self.sandbox.put_file("install_steps.txt", "foo.txt") contents = self.sandbox.read_file("foo.txt").decode() self.assertIn("python", contents) def test_execute_blocking(self): self.sandbox = Sandbox() output = TemporaryFile(mode="w+b") start_time = perf_counter() self.sandbox.execute(command="sleep 0.2 ; echo foo", stdin_fd=None, stdout_fd=output, stderr_fd=None, blocking=True) self.assertGreaterEqual(perf_counter() - start_time, 0.2) self.assertEqual(output.tell(), 4) # Already printed "foo\n" def test_execute_non_blocking(self): self.sandbox = Sandbox() output = TemporaryFile(mode="w+b") start_time = perf_counter() self.sandbox.execute(command="sleep 0.2 ; echo foo", stdin_fd=None, stdout_fd=output, stderr_fd=None, blocking=False) self.assertLess(perf_counter() - start_time, 0.1) self.assertEqual(output.tell(), 0) # Haven't yet printed anything sleep(0.3) self.assertEqual(output.tell(), 4) # But printing it eventually def test_privileged_execution(self): self.sandbox = Sandbox() self.sandbox.put_file("/bin/ls", "ls_binary", 0o744) stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="./ls_binary ..") self.assertIn("Permission denied", stderr) # Should be unable to execute self.assertEqual("", stdout) stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="./ls_binary ..", privileged=True) self.assertEqual("", stderr) # But privileged user should be able to do it self.assertIn("bin", stdout) def test_privileged_deletion(self): self.sandbox = Sandbox() self.sandbox.put_file("/bin/ls", "ls_binary", 0o744) stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="rm ls_binary") self.assertIn("Permission denied", stderr) # Should be unable to execute self.assertTrue(self.sandbox.has_file("ls_binary")) stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command="rm ls_binary", privileged=True) self.assertEqual("", stderr) # But privileged user should be able to do it self.assertFalse(self.sandbox.has_file("ls_binary")) # ================================= # # Applied ulimits # # ================================= # def test_output_limit(self): self.sandbox = Sandbox() file_size = 1000000 # 1MB output = NamedTemporaryFile(mode="w+", delete=True) for i in range(file_size // 10): output.write("test test\n") output.flush() self.sandbox.put_file(output.name, "foo.txt") target_size = 0 while target_size + file_size <= config.MAX_EXECUTION_OUTPUT: target_size += file_size stdout, stderr = self.sandbox_helper( sandbox=self.sandbox, command="for i in {{1..{}}}; do cat foo.txt; done;".format( target_size // file_size)) self.assertEqual("", stderr) self.assertEqual(len(stdout), target_size - 1) target_size += file_size stdout, stderr = self.sandbox_helper( sandbox=self.sandbox, command="for i in {{1..{}}}; do cat foo.txt; done;".format( target_size // file_size)) self.assertIn("File size limit exceeded", stderr) self.assertEqual(len(stdout), config.MAX_EXECUTION_OUTPUT) def test_no_input_limit(self): self.sandbox = Sandbox() file_size = 50000000 # 50MB output = NamedTemporaryFile(mode="w+", delete=True) message = "Without IT I'm just espr\n" for i in range(file_size // len(message)): output.write(message) output.flush() self.sandbox.put_file(output.name, "foo.txt") stdout, stderr = self.sandbox_helper( sandbox=self.sandbox, command="wc -c < foo.txt && wc -l < foo.txt") self.assertEqual("", stderr) self.assertEqual(len(stdout.splitlines()), 2) self.assertEqual(int(stdout.splitlines()[0]), file_size) self.assertEqual(int(stdout.splitlines()[1]), file_size // len(message)) @mock.patch("config.MAX_EXECUTION_TIME", 0.5) def test_hard_timeout(self): start_time = perf_counter() stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="sleep 3; echo foo") self.assertEqual("", stdout) self.assertEqual("", stderr) exec_time = perf_counter() - start_time self.assertGreaterEqual(exec_time, 0.5) self.assertLess(exec_time, 0.7) @mock.patch("config.MAX_EXECUTION_TIME", 1.0) def test_fork_bomb(self): # Run to see if it crashes the system and how many processes it spawns start_time = perf_counter() stdout, stderr = TemporaryFile(mode="w+"), TemporaryFile(mode="w+") self.sandbox = Sandbox() self.sandbox.execute(command=":(){ :|:& };:", stdin_fd=None, stdout_fd=stdout, stderr_fd=stderr, blocking=False) # Check number of processes by this worker and its CPU usage continuously # (but sleep for 0.01 seconds so we don't do it more than 100 times) iteration = 0 max_cpu = 0.0 max_processes = 0 while perf_counter() - start_time < config.MAX_EXECUTION_TIME: if iteration % 2 == 0: ps_info = os.popen("ps -U {}".format( self.sandbox._worker.name)).read() max_processes = max(max_processes, len(ps_info.splitlines()) - 1) else: cpu_info = os.popen( "top -b -n 1 -u {} | awk 'NR>7 {{ sum += $9; }} END {{ print sum; }}'" .format(self.sandbox._worker.name)).read() max_cpu = max(max_cpu, float(cpu_info)) iteration += 1 sleep(0.01) self.assertLess(perf_counter() - start_time, 1.2) self.assertLessEqual(max_processes, config.MAX_PROCESSES) self.assertLessEqual(max_cpu, 100.0) stdout.seek(0) self.assertEqual("", stdout.read()) stderr.seek(0) self.assertIn("fork: retry: Resource temporarily unavailable", stderr.read()) # At this point the sandbox is still running (as the fork bomb processes are detached) # Make sure that wait() kills it entirely self.assertTrue(self.sandbox.is_running()) self.sandbox.wait(0.1) self.assertFalse(self.sandbox.is_running()) @mock.patch("config.MAX_EXECUTION_TIME", 0.3) def test_sandbox_wait_kills_sleepers(self): stdout, stderr = TemporaryFile(mode="w+"), TemporaryFile(mode="w+") self.sandbox = Sandbox() self.sandbox.execute(command=":(){ :|:& };:", stdin_fd=None, stdout_fd=stdout, stderr_fd=stderr, blocking=False) # While the program is within its time limit it is at max processes sleep(0.2) self.assertTrue(self.sandbox.is_running()) ps_info = os.popen("ps -U {}".format(self.sandbox._worker.name)).read() self.assertEqual(len(ps_info.splitlines()) - 1, config.MAX_PROCESSES) # What's worse, even after that they are still alive # (as they don't use much CPU, so are not affected by MAX_EXECUTION_TIME) sleep(0.2) self.assertTrue(self.sandbox.is_running()) ps_info = os.popen("ps -U {}".format(self.sandbox._worker.name)).read() self.assertEqual(len(ps_info.splitlines()) - 1, config.MAX_PROCESSES) # However, wait() should terminate everything self.sandbox.wait(0.1) self.assertFalse(self.sandbox.is_running()) ps_info = os.popen("ps -U {}".format(self.sandbox._worker.name)).read() self.assertEqual(len(ps_info.splitlines()) - 1, 0) def test_cpu_usage(self): self.sandbox = Sandbox() self.sandbox.put_file(os.path.join(self.PATH_FIXTURES, "factor.py")) stdout, stderr = self.sandbox_helper( sandbox=self.sandbox, command="/usr/bin/time --format '%U %P' pypy3 factor.py") self.assertIn("1000000000000037", stdout) self.assertEqual(len(stderr.splitlines()), 1) user_time = float(stderr.split()[0]) percent_cpu = int(stderr.split()[1].split('%')[0]) # Took at least half a second self.assertGreater(user_time, 0.5) # CPU usage was around 100% self.assertTrue(95 < percent_cpu <= 100) def test_memory_usage_heap(self): self.sandbox = Sandbox() self.sandbox.put_file( os.path.join(self.PATH_FIXTURES, "mem_allocator.cpp")) self.sandbox_helper( sandbox=self.sandbox, command= "g++ -O2 -std=c++17 -w -s -o mem_allocator mem_allocator.cpp", privileged=True) self.assertTrue(self.sandbox.has_file("mem_allocator")) command = "/usr/bin/time --quiet --format='%M' /bin/bash -c \"./mem_allocator heap {}\"" +\ " ; code=$? ; >&2 echo $code ; exit $code" targets = [10000000, 100000000, 500000000, 1000000000, 2000000000] # 10MB, 100MB, 500MB, 1GB, 2GB for target in targets: stdout, stderr = self.sandbox_helper( sandbox=self.sandbox, command=command.format(target)) exit_code, exec_memory = int(stderr.splitlines()[-1]), int( stderr.splitlines()[-2]) self.assertEqual(exit_code, 0) self.assertTrue(target <= exec_memory * 1024 <= target + 5000000) # Up to 5MB overhead for C++ libraries # Twenty megabytes less than the threshold is okay target = config.MAX_EXECUTION_MEMORY - 20000000 stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command=command.format(target)) exit_code, exec_memory = int(stderr.splitlines()[-1]), int( stderr.splitlines()[-2]) self.assertEqual(exit_code, 0) self.assertTrue(target <= exec_memory * 1024 <= target + 5000000) # Up to 5MB overhead for C++ libraries # Allocating around the threshold is no longer okay target = config.MAX_EXECUTION_MEMORY stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command=command.format(target)) exit_code, exec_memory = int(stderr.splitlines()[-1]), int( stderr.splitlines()[-2]) self.assertNotEqual(exit_code, 0) self.assertTrue( exec_memory * 1024 <= config.MAX_EXECUTION_MEMORY + 1024) def test_memory_usage_stack(self): self.sandbox = Sandbox() self.sandbox.put_file( os.path.join(self.PATH_FIXTURES, "mem_allocator.cpp")) self.sandbox_helper( sandbox=self.sandbox, command= "g++ -O2 -std=c++17 -w -s -o mem_allocator mem_allocator.cpp", privileged=True) self.assertTrue(self.sandbox.has_file("mem_allocator")) command = "/usr/bin/time --quiet --format='%M' /bin/bash -c \"./mem_allocator stack {}\"" +\ " ; code=$? ; >&2 echo $code ; exit $code" # Test different target stack sizes (should all be okay) targets = [1000000, 10000000, 50000000] # 1MB, 10MB, 50MB for target in targets: stdout, stderr = self.sandbox_helper( sandbox=self.sandbox, command=command.format(target)) exit_code, exec_memory = int(stderr.splitlines()[-1]), int( stderr.splitlines()[-2]) self.assertEqual(exit_code, 0) self.assertTrue(target <= exec_memory * 1024 <= target + 5000000) # Up to 5MB overhead for C++ libraries # Half a megabyte less than the threshold is okay target = config.MAX_EXECUTION_STACK - 500000 stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command=command.format(target)) exit_code, exec_memory = int(stderr.splitlines()[-1]), int( stderr.splitlines()[-2]) self.assertEqual(exit_code, 0) self.assertTrue(target <= exec_memory * 1024 <= target + 5000000) # Up to 5MB overhead for C++ libraries # Half a megabyte more than the threshold is not okay target = config.MAX_EXECUTION_STACK + 500000 stdout, stderr = self.sandbox_helper(sandbox=self.sandbox, command=command.format(target)) exit_code, exec_memory = int(stderr.splitlines()[-1]), int( stderr.splitlines()[-2]) self.assertNotEqual(exit_code, 0) self.assertTrue(target <= exec_memory * 1024 <= target + 5000000) # Up to 5MB overhead for C++ libraries # ================================= # # High-level prerequisites # # ================================= # def test_languages_are_available(self): stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="g++") self.assertIn("g++: fatal error: no input files", stderr) stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="java") self.assertIn("Usage: java [options]", stderr) stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="javac") self.assertIn("Usage: javac <options> <source files>", stdout) stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="jar") self.assertIn("Usage: jar [OPTION...]", stderr) stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="pypy3 --version") self.assertIn("Python 3.", stdout) def test_time_command_available(self): stdout, stderr = self.sandbox_helper( sandbox=Sandbox(), command="ls -la /usr/bin | grep -w time") self.assertEqual("", stderr) self.assertTrue(stdout.startswith("-rwx") and stdout.endswith("time")) command = "/usr/bin/time --quiet --format='%U %S %e %M' /bin/bash -c 'sleep 0.33; echo foo'" + \ " ; code=$? ; >&2 echo $code ; exit $code" stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command=command) self.assertEqual(stdout, "foo") self.assertEqual(int(stderr.splitlines()[-1]), 0) # Exit code self.assertLess(float(stderr.splitlines()[-2].split()[0]), 0.1) # User time self.assertLess(float(stderr.splitlines()[-2].split()[1]), 0.1) # Kernel time self.assertAlmostEqual(float(stderr.splitlines()[-2].split()[2]), 0.33, delta=0.1) # Clock time def test_timeout_command_available(self): stdout, stderr = self.sandbox_helper( sandbox=Sandbox(), command="ls -la /usr/bin | grep -w timeout") self.assertEqual("", stderr) self.assertTrue( stdout.startswith("-rwx") and stdout.endswith("timeout")) start_time = perf_counter() command = "/usr/bin/timeout 0.3s /bin/bash -c 'sleep 1.0; echo foo'" stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command=command) self.assertEqual(stdout, "") # No output, killed before that self.assertLess(perf_counter() - start_time, 0.5) # Killed shortly after the timeout # Up to MAX_PARALLEL_WORKERS run in parallel. # We expect if we run less than or equal to MAX_PARALLEL_WORKERS to take clock time equal to the longest # of them. If we run even a single one more, we expect the clock time to be roughly twice as long. def dummy_sleep_helper(self): start_time = perf_counter() sandbox = Sandbox() waiting_time = perf_counter() - start_time self.sandbox_helper(sandbox=sandbox, command="sleep 0.5 ; echo foo") return waiting_time def test_workers_under_limit(self): start_time = perf_counter() pool = ThreadPoolExecutor(max_workers=config.MAX_PARALLEL_WORKERS) futures = [ pool.submit(self.dummy_sleep_helper) for _ in range(config.MAX_PARALLEL_WORKERS) ] # Each of the processes runs in ~0.5s, but since we schedule them through a thread pool # we should reach this point much earlier. self.assertLess(perf_counter() - start_time, 0.1) # Wait for all workers to complete and get the maximum waiting time for a Sandbox object max_waiting_time = 0.0 for future in futures: max_waiting_time = max(max_waiting_time, future.result()) # Expecting none of the workers to wait for another one to finish, this get a Sandbox object immediately self.assertLess(max_waiting_time, 0.2) # Waiting all of them to complete takes at least 0.5 seconds self.assertGreaterEqual(perf_counter() - start_time, 0.5) # But not much more than 0.5 seconds self.assertLess(perf_counter() - start_time, 0.7) def test_workers_over_limit(self): start_time = perf_counter() pool = ThreadPoolExecutor(max_workers=config.MAX_PARALLEL_WORKERS + 1) futures = [ pool.submit(self.dummy_sleep_helper) for _ in range(config.MAX_PARALLEL_WORKERS + 1) ] # Each of the processes runs in ~0.5s, but since we schedule them through a thread pool # we should reach this point much earlier. One of the threads should be blocked on waiting # for a sandbox, though. self.assertLess(perf_counter() - start_time, 0.1) # Wait for all workers to complete and get the maximum waiting time for a Sandbox object max_waiting_time = 0.0 for future in futures: max_waiting_time = max(max_waiting_time, future.result()) # Expecting one of the workers to wait for another one to finish before getting a Sandbox object self.assertGreaterEqual(max_waiting_time, 0.5) self.assertLess(max_waiting_time, 0.7) # Waiting all of them to complete takes at least 1 second (twice as much) self.assertGreaterEqual(perf_counter() - start_time, 1.0) # But not much more than 1 second self.assertLess(perf_counter() - start_time, 1.4)
from __future__ import print_function from sandbox import Sandbox s = Sandbox() string = """ from __future__ import print_function import math x= math.factorial(20) print (x) """ s.execute(string)
class Bot: def __init__(self): """Constructor """ self.__state = STATE_DISCONNECTED self.__load_defaults() self.parser = Parser() self.logger = Logger() self.sandbox = Sandbox() def __load_defaults(self): """Loads default settings """ self.username = os.getlogin() self.password = None self.nicks = ["nick", "altnick"] self.realname = "Default pynsour user" self.handlers = [] self.localhost = 'localhost' self.on_connect = [] self.ops = [] self.name = "" def asDict(self): """Return object as dictionary Ignores doc variables """ info = {} for attr in dir(self): if attr[0] == "_" or attr[:2] == "__": continue i = getattr(self, attr) if type(i).__name__ == "instancemethod": continue else: info[attr] = i return info def connect(self): """Connect the bot to the IRC server """ self.__state = STATE_CONNECTING self.__connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.logger.console("+++ Connecting to %s:%s" % (self.hostname, self.port)) self.__connection.connect((self.hostname, self.port)) def event(self): """Event fire """ if self.__state == STATE_DISCONNECTED: return elif self.__state == STATE_CONNECTING: if self.password: self.write("PASS %s" % self.password) self.write("NICK %s" % self.nicks[0]) self.write("USER %s %s %s :%s" % (self.username, self.localhost, self.hostname, self.realname)) self.__state = STATE_HANDSHAKE elif self.__state == STATE_HANDSHAKE: pass self.read() self.ops += self.parser.parse() self.execute() def execute(self): """Execute botcode """ # Expand meta-ops, e.g. connect events new_ops = [] for operation in self.ops: if operation[0] == botcode.OP_EVENT_CONNECT: new_ops += self.on_connect self.__state = STATE_ONLINE elif operation[0] == botcode.OP_EVENT_PRIVMSG: sandbox_ops = self.filter_eval(operation[1]) if sandbox_ops: new_ops += self.sandbox.execute(sandbox_ops) else: new_ops.append(operation) self.ops = new_ops while len(self.ops) > 0: new_ops = [] for operation in self.ops: if operation[0] == botcode.OP_PONG: self.write("PONG :%s" % operation[1]) elif operation[0] == botcode.OP_JOIN: if len(operation) == 2: self.write("JOIN %s :%s" % operation[1]) elif len(operation) == 1: self.write("JOIN %s" % operation[1]) elif operation[0] == botcode.OP_MODE: self.write("MODE %s" % operation[1]) elif operation[0] == botcode.OP_PRIVMSG: self.write("PRIVMSG %s :%s" % operation[1:3]) elif operation[0] == botcode.OP_ERROR: self.logger.console("ERR\n" "%s" % operation[1]) self.ops = new_ops # self.ops will be empty by here def filter_eval(self, line): """Filter based on channel """ ops = [] words = line.split(":", 1) if len(words) == 1: return ops args, msg = words argv = args.split(" ") if len(argv) < 4: return ops sender, action, recipient = argv[:3] path = "%s/%s" % (self.name, recipient) for handler in self.handlers: re = handler['channel_re'] if re.match(path): # self.logger.console("F: %s %s" % (path, argv)) script_path = re.sub(handler['script'].replace("$", "\\"), path) ops += (botcode.OP_EVENT_SCRIPT, script_path, (sender, action, recipient, msg)), return ops def read(self): """Reading from connection """ if self.__state > STATE_DISCONNECTED: incoming = self.__connection.recv(BUFFER_SIZE) self.parser.append(incoming) read_bytes = len(incoming) first_line = incoming.split("\n")[0] if len(first_line) > MAX_CONSOLE_LEN: first_line = "%s..." % first_line[:MAX_CONSOLE_LEN] self.logger.console(" IN [%4d] %s" % (read_bytes, first_line)) def write(self, outgoing): """Writing to connection """ first_line = outgoing outgoing = "".join((outgoing, "\r\n")) write_bytes = len(outgoing) if len(first_line) > MAX_CONSOLE_LEN: first_line = "%s..." % first_line[:MAX_CONSOLE_LEN] self.logger.console("OUT [%4d] %s" % (write_bytes, first_line)) self.__connection.send(outgoing)