def execute_two_player_game(submit_id, test: TestInfo, run_config: RunConfig, player_one_executable_path, player_two_executable_path) -> RunResult: log_file = "interaction.log" tester_executable_name = "tester." + run_config.tester_path.split(".")[-1] player_one_executable_name = "solution1." + player_one_executable_path.split( ".")[-1] player_two_executable_name = "solution2." + player_two_executable_path.split( ".")[-1] args = { "tester_run_command": Runner.get_run_command( language=common.get_language_by_exec_name(tester_executable_name), executable=tester_executable_name, memory_limit=config.MAX_TESTER_MEMORY), "player_one_run_command": Runner.get_run_command(language=common.get_language_by_exec_name( player_one_executable_name), executable=player_one_executable_name, memory_limit=run_config.memory_limit), "player_two_run_command": Runner.get_run_command(language=common.get_language_by_exec_name( player_two_executable_name), executable=player_two_executable_name, memory_limit=run_config.memory_limit), "tester_timeout": config.MAX_GAME_LENGTH, "solution_timeout": run_config.timeout, "time_limit": run_config.time_limit, "memory_limit": run_config.memory_limit, "log_file": log_file } empty_file = NamedTemporaryFile(mode="w+t", delete=False) empty_file_path = os.path.abspath(empty_file.name) # Copy all the needed files to the sandbox directory: # 1) wrapper.py # 2) the tester executable # 3) player one's solution executable # 4) player two's solution executable # 5) the input file (read by the tester) # 6) game log (empty file with write permissions) sandbox = Sandbox() sandbox.put_file(os.path.join(config.ROOT_DIR, "wrapper.py")) sandbox.put_file(run_config.tester_path, target_name=tester_executable_name) sandbox.put_file(player_one_executable_path, target_name=player_one_executable_name) sandbox.put_file(player_two_executable_path, target_name=player_two_executable_name) sandbox.put_file(test.inpPath, target_name="input.txt", mode=0o777) sandbox.put_file(empty_file_path, target_name=log_file, mode=0o777) # Run the executable, while measuring CPU and Memory consumption run_result = Runner.run_program( sandbox=sandbox, executable_path=os.path.join(config.ROOT_DIR, "interactor_2P.py"), memory_limit=config.MAX_EXECUTION_MEMORY, timeout=config.MAX_GAME_LENGTH, input_bytes=json.dumps(args, indent=4, sort_keys=True).encode()) interaction_log = sandbox.read_file(log_file) del sandbox # Record the game log to the /replays folder replay_id = save_replay_log(interaction_log) # The interactor crashed or got killed # Don't check memory limit, as it can be caused by child processes (including solution) if run_result.exec_time >= config.MAX_GAME_LENGTH: message = "Interactor took too much time to complete ({:.3f}s).".format( run_result.exec_time) logger.error("Submit {id} | {message}".format(id=submit_id, message=message)) return RunResult(status=TestStatus.INTERNAL_ERROR, error=message, replay_id=replay_id) if run_result.exit_code != 0: message = "Interactor exited with non-zero exit code ({}).".format( run_result.exit_code) logger.error("Submit {id} | {message}".format(id=submit_id, message=message)) return RunResult(status=TestStatus.INTERNAL_ERROR, error=message, replay_id=replay_id) # Parse the response from the interactor try: results = json.loads(run_result.output.decode(config.OUTPUT_ENCODING)) except ValueError: message = "Could not decode interactor's output: {}!".format( run_result.output.decode()) logger.error("Submit {id} | {message}".format(id=submit_id, message=message)) return RunResult(status=TestStatus.INTERNAL_ERROR, error=message, replay_id=replay_id) # print("RESULTS:\n{}".format(json.dumps(results, indent=4, sort_keys=True))) # Okay, let's assume the interactor was okay. Now check if the tester crashed. if results["internal_error"]: message = "Tester crashed or some other internal error happened. Result from interactor:\n{}".format( json.dumps(results, indent=4, sort_keys=True)) logger.error("Submit {id} | {message}".format(id=submit_id, message=message)) return RunResult(status=TestStatus.INTERNAL_ERROR, error=message, replay_id=replay_id) # Everything with the system seems okay. # Leave the caller function decide what the test status will be return RunResult(status=TestStatus.ACCEPTED, output=run_result.output, replay_id=replay_id)
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)
def execute_interactive(submit_id, test: TestInfo, run_config: RunConfig) -> RunResult: log_file = "interaction.log" tester_executable_name = "tester." + run_config.tester_path.split(".")[-1] solution_executable_name = "solution." + run_config.executable_path.split( ".")[-1] # Both the tester and the solution get extra time in order to account for time tester # processes input/output. The tester gets additional time to make sure it can print # the log and results properly after the solution completes its execution. tester_communication_time = min(1.0, run_config.time_limit * 2) tester_postprocessing_time = 1.0 solution_timeout = run_config.timeout + tester_communication_time tester_timeout = solution_timeout + tester_postprocessing_time args = { "tester_timeout": tester_timeout, "solution_timeout": solution_timeout, "tester_run_command": Runner.get_run_command( language=common.get_language_by_exec_name(tester_executable_name), executable=tester_executable_name, memory_limit=config.MAX_TESTER_MEMORY), "solution_run_command": Runner.get_run_command(language=common.get_language_by_exec_name( solution_executable_name), executable=solution_executable_name, memory_limit=run_config.memory_limit), "log_file": log_file } # print(json.dumps(args, indent=4, sort_keys=True)) empty_file = NamedTemporaryFile(mode="w+t", delete=False) empty_file_path = os.path.abspath(empty_file.name) # Copy all the needed files to the sandbox directory: # 1) wrapper.py # 2) the tester executable # 3) the solution executable # 4) the input file (read by the tester) # 5) game log (empty file with write permissions) sandbox = Sandbox() sandbox.put_file(os.path.join(config.ROOT_DIR, "wrapper.py")) sandbox.put_file(run_config.tester_path, target_name=tester_executable_name) sandbox.put_file(run_config.executable_path, target_name=solution_executable_name) sandbox.put_file(test.inpPath, target_name="input.txt", mode=0o777) sandbox.put_file(empty_file_path, target_name=log_file, mode=0o777) # Run the executable, while measuring CPU and Memory consumption run_result = Runner.run_program( sandbox=sandbox, executable_path=os.path.join(config.ROOT_DIR, "interactor_1P.py"), memory_limit=config.MAX_EXECUTION_MEMORY, timeout=config.MAX_GAME_LENGTH, input_bytes=json.dumps(args, indent=4, sort_keys=True).encode()) interaction_log = sandbox.read_file(log_file) del sandbox # Record the game log to the /replays folder replay_id = save_replay_log(interaction_log) # The interactor crashed or got killed # Don't check memory limit, as it can be caused by child processes (including solution) if run_result.exec_time >= config.MAX_GAME_LENGTH: message = "Interactor took too much time to complete ({:.3f}s).".format( run_result.exec_time) logger.error("Submit {id} | {message}".format(id=submit_id, message=message)) return RunResult(status=TestStatus.INTERNAL_ERROR, error=message, replay_id=replay_id) if run_result.exit_code != 0: message = "Interactor exited with non-zero exit code ({}).".format( run_result.exit_code) logger.error("Submit {id} | {message}".format(id=submit_id, message=message)) return RunResult(status=TestStatus.INTERNAL_ERROR, error=message, replay_id=replay_id) # Parse the response from the interactor try: results = json.loads(run_result.output.decode(config.OUTPUT_ENCODING)) except ValueError: message = "Could not decode interactor's output: {}!".format( run_result.output.decode()) logger.error("Submit {id} | {message}".format(id=submit_id, message=message)) return RunResult(status=TestStatus.INTERNAL_ERROR, error=message, replay_id=replay_id) # print("RESULTS:\n{}".format(json.dumps(results, indent=4, sort_keys=True))) # Okay, let's assume the interactor was okay. Now check if the tester crashed. if results["internal_error"] or results["tester_exit_code"] != 0: message = "Tester crashed or some other internal error happened. Result from interactor:\n{}".format( json.dumps(results, indent=4, sort_keys=True)) logger.error("Submit {id} | {message}".format(id=submit_id, message=message)) return RunResult(status=TestStatus.INTERNAL_ERROR, error=message, replay_id=replay_id) # Everything with the system seems okay. # Get the score and the solution's exit_code, exec_time, and exec_memory exit_code = int(results["solution_exit_code"]) # Calculate final time and memory (offset for VM start-up time) solution_language = common.get_language_by_exec_name( run_config.executable_path) exec_time = max( 0, float(results["solution_exec_time"]) - common.get_time_offset(solution_language)) exec_memory = max( 0, float(results["solution_exec_memory"]) - common.get_memory_offset(solution_language)) # Get tester's output output = ("" if "tester_message" not in results else results["tester_message"]).encode() # Leave the caller function decide what the test status will be return RunResult(exit_code=exit_code, exec_time=exec_time, exec_memory=exec_memory, output=output, replay_id=replay_id)