import contextlib import logging import multiprocessing import os import random import signal import socket import string import struct import subprocess import sys import tempfile import traceback from libosue import * from libosue import CSnippet as C from libosue import FunctionPrototype as FP ################################################################################ # INITIALIZE THE EXAM OBJECT ################################################################################ # These functions are not allowed. not_allowed = [ "DEMO_setup_connection", "DEMO_server_request_handler", "DEMO_execute_command", ] prototypes = { "parse_arguments": FP("void")("int argc", "char **argv", "struct arguments *args"), "DEMO_setup_connection": FP("int")("const char *port_str"), "DEMO_server_request_handler": FP("void")("int sockfd", "const char *command", "int flags"), "DEMO_execute_command": FP("FILE *")("const char *command", "const char *argument"), } PORT = 692 exam.initialize( "./server", server_port=7220, not_allowed=not_allowed + exam.DEFAULT_RESTRICTED_FUNCTIONS, call_limit=200, prototypes=prototypes, report_file="report.txt", result_file="result.txt", debug_file="libosue.dbg", make_targets=["clean", "all"], initialize_stack=["setup_connection", "server_request_handler", "execute_command"], ) class FunctionAborted(EvalError): """Raised when a function should be aborted.""" pass # Kill programs on port 692. # This is slow, so we only do it once every deliver call and not for every session. utils.free_port(PORT) ################################################################################ # TASK 1: SETUP CONNECTION ################################################################################ def bind_cb(s, args): """Call setsockopt(...) before binding.""" reuse = Value.from_type("char") reuse.value = 1 s.c.setsockopt(args["sockfd"], socket.SOL_SOCKET, socket.SO_REUSEADDR, reuse.address, 1) with exam.task("Task 1: setup connection", 10) as task: with task.subtask("initialize server socket", 8) as st: with exam.session([]) as s: # Setup tracing for hints. s.c.socket.tracing = True s.c.bind.tracing = True s.c.listen.tracing = True # Make sure setsockopt(...) is called before bind(). s.c.bind.call_cb = bind_cb # Call the student's setup_connection connection function sockfd = s.c.setup_connection(str(PORT).encode()) # Check if the server socket works and creates valid connection sockets. utils.check_server_socket(s, sockfd, PORT) # Give the students some hints for their implementation if st.points != 8: if "socket" not in s.trace: exam.log("[INFO] socket() was never called") if "bind" not in s.trace: exam.log("[INFO] bind() was never called") if "listen" not in s.trace: exam.log("[INFO] listen() was never called") exam.log("The following subtasks are skipped because of errors:") exam.log("* error handling (2 points)") task.skip_subtasks() with task.subtask("error handling", 2) as st: with st.subtask("bind()", 1): with exam.session([]) as s: s.c.listen.tracing = True s.c.bind.override_return = C("((int) -1)") with exam.raises(ExitError, "program did not exit after bind() failed") as excinfo: sockfd = s.c.setup_connection(str(PORT).encode()) assert excinfo.value.exitcode == 1, "wrong exit code" assert "listen" not in s.trace, "listen() called after bind() failed" with st.subtask("listen()", 1): with exam.session([]) as s: s.c.listen.override_return = C("((int) -1)") with exam.raises(ExitError, "program did not exit after listen() failed") as excinfo: sockfd = s.c.setup_connection(str(PORT).encode()) assert excinfo.value.exitcode == 1, "wrong exit code" with exam.task("Task 2: handle requests", 20) as task: with task.subtask("read argument and execute command", 7) as st: # Check if the program correctly accepts a connection and reads the argument # from the connection socket. Execution stops when task 3 is called. def accept_return_cb(s, rv): rv = rv.convert() if rv > 0: s["connfd"] = rv def read_call_cb(s, args): connfd = args.get("sockfd") or args.get("fd") s["read"] = connfd.convert() == s["connfd"] s["buffer"] = args.get("buf") def read_return_cb(s, rv): if s["read"]: n = rv.convert() s["result"] += s["buffer"].convert(shape=n, cast="char *") def execute_command_cb(s, args): s["ec_called"] = True try: s["command"] = args["command"].convert() s["argument"] = args["argument"].convert() except InvalidMemoryError: pass raise FunctionAborted("This is where execution ends.") with exam.session([]) as s: s.c.accept.return_cb = accept_return_cb s.c.read.call_cb = read_call_cb s.c.read.return_cb = read_return_cb s.c.recv.call_cb = read_call_cb s.c.recv.return_cb = read_return_cb s.c.execute_command.call_cb = execute_command_cb s.c.DEMO_execute_command.call_cb = execute_command_cb s.c.DEMO_execute_command.allowed = True # Setup the tracing variables. s["connfd"] = None s["result"] = b"" s["ec_called"] = False s["command"] = None s["argument"] = None argument = "".join(random.choices(string.ascii_letters, k=24)).encode() sockfd = s.c.DEMO_setup_connection(str(PORT).encode()) with utils.SingleShotClient(PORT, argument) as client: with contextlib.suppress(FunctionAborted): s.c.server_request_handler(sockfd, b"./osue-tool") assert s["result"], "no argument read from client" assert s["result"] == argument, "read wrong argument from client" assert s["ec_called"], "execute_command() not called" assert s["command"] == b"./osue-tool", "invalid command passt to execute_command()" assert ( s["argument"] == argument ), "Invalid argument passed to execute_command()\nDid you terminate it?" with task.subtask("response", 8) as st: # Execute the full function and check if the client receives the correct result. def execute_command_cb(s, args): with s.c.fork.save(): s.c.fork.allowed = True return s.c.DEMO_execute_command(args["command"], args["argument"]) with st.subtask("single line output", 4) as pt: with exam.session([]) as s: s.c.execute_command.call_cb = execute_command_cb s.c.DEMO_execute_command.call_cb = execute_command_cb s.c.DEMO_execute_command.allowed = True sockfd = s.c.DEMO_setup_connection(str(PORT).encode()) argument = "".join(random.choices(string.ascii_letters, k=24)).encode() expected = argument + b"\n" with utils.SingleShotClient(PORT, argument) as client: s.c.server_request_handler(sockfd, b"./osue-tool") assert client.response, "didn't receive a response for single-line output" assert client.response == expected, "received wrong response for single-line output" with st.subtask("multi line output", 4) as pt: with exam.session([]) as s: s.c.execute_command.call_cb = execute_command_cb s.c.DEMO_execute_command.call_cb = execute_command_cb s.c.DEMO_execute_command.allowed = True sockfd = s.c.DEMO_setup_connection(str(PORT).encode()) argument = "".join(random.choices(string.ascii_letters, k=128)).encode() expected = b"\n".join(argument[i : i + 32] for i in range(0, len(argument), 32)) + b"\n" with utils.SingleShotClient(PORT, argument) as client: s.c.server_request_handler(sockfd, b"./osue-tool") assert client.response, "didn't receive a response for multi-line output" assert client.response == expected, "received wrong response for multi-line output" if task.points != 15: exam.log("The following subtasks are skipped because of errors:") exam.log("* repeated write/send calls (5 points)") task.skip_subtasks() with task.subtask("repeated write/send calls", 5) as st: def accept_return_cb(s, rv): s["connfd"] = rv.convert() def write_cb(s, args): fd = (args.get("fd") or args.get("sockfd")).convert() if fd == s["connfd"]: n = args.get("count") or args.get("len") buf = args.get("buf") s["messages"].append(buf.convert(shape=n.convert(), cast="char *")) return 1 with exam.session([]) as s: s.c.execute_command.call_cb = execute_command_cb s.c.DEMO_execute_command.call_cb = execute_command_cb s.c.DEMO_execute_command.allowed = True s.c.accept.return_cb = accept_return_cb s.c.write.call_cb = write_cb s.c.send.call_cb = write_cb s["connfd"] = None s["messages"] = [] argument = "".join(random.choices(string.ascii_letters, k=24)).encode() expected = argument + b"\n" sockfd = s.c.DEMO_setup_connection(str(PORT).encode()) with utils.SingleShotClient(PORT, argument) as client: s.c.server_request_handler(sockfd, b"./osue-tool") assert len(s["messages"]) > 1, "write/send not called multiple times" message = b"".join(m[:1] for m in s["messages"]) assert message == expected, "received wrong response" with exam.task("Task 3: execute command", 20) as task: with task.subtask("fork/exec pattern", 5) as st: pattern = utils.ForkNode(parent=None, child=utils.ExecNode("./osue-tool")) with utils.ForkExecTraces(pattern) as tracegen: for fork_exec_run in tracegen: with exam.session() as s: with fork_exec_run(s): s.c.execute_command(b"./osue-tool", b"just a random string") assert tracegen.correct, tracegen.default_message if task.points != 5: exam.log("The following subtasks are skipped because of errors:") exam.log("* wait for child process to finish (5 points)") exam.log("* result (10 points)") task.skip_subtasks() with task.subtask("wait for child process to finish", 5) as st: # Correctly wait for the child process to finish. # Follow the parent process and trace the return value of fork() and wait(). def fork_return_cb(s, rv): s["fork_result"] = rv.convert() def wait_return_cb(s, rv): s["wait_result"] = rv.convert() with exam.session([]) as s: s.c.fork.allowed = True s.c.fork.return_cb = fork_return_cb s.c.wait.return_cb = wait_return_cb s.c.waitpid.return_cb = wait_return_cb s.c.execute_command(b"./osue-tool", b"just a random string") assert "wait_result" in s, "wait never called" assert s.get("wait_result") == s.get("fork_result"), "program did not wait for child process" if task.points != 10: exam.log("The following subtasks are skipped because of errors:") exam.log("* result (10 points)") task.skip_subtasks() with task.subtask("result", 10) as st: # Handle a single request and check the result. with exam.session([]) as s: s.c.fork.allowed = True argument = "".join(random.choices(string.ascii_letters, k=24)).encode() expected = argument + b"\n" fp = s.c.execute_command(b"./osue-tool", argument) assert fp.intaddr(), "returned file pointer not valid" bufsize = 8192 try: buf = s.c.malloc(bufsize, cast="char *") n = s.c.fread(buf, 1, bufsize, fp) except FunctionKilledError: st.fail("reading from returned file pointer never terminated") except SignalError: st.fail("reading from returned file pointer failed") result = buf.convert(shape=n.convert()) assert result == expected, "invalid response"