From da34a7b62e4c7e7e987a126bf8c1331d29c7d5bf Mon Sep 17 00:00:00 2001 From: Joseph Umana Date: Fri, 25 Jul 2025 16:32:30 -0400 Subject: [PATCH] init Signed-off-by: Joseph Umana --- .gitignore | 4 + Makefile | 2 + README.md | 12 +++ main.c | 180 +++++++++++++++++++++++++++++++++++++++++++ serialization.c | 60 +++++++++++++++ serialization.h | 4 + structs.h | 70 +++++++++++++++++ testprogs/nonzero.c | 4 + tools/decodeFlags.py | 42 ++++++++++ 9 files changed, 378 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 main.c create mode 100644 serialization.c create mode 100644 serialization.h create mode 100644 structs.h create mode 100644 testprogs/nonzero.c create mode 100755 tools/decodeFlags.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edf9737 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.out +gojrt +.cache/ +compile_commands.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..453ceb5 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +all: + gcc main.c structs.h serialization.c serialization.h -o gojrt -Wall -std=c23 -g diff --git a/README.md b/README.md new file mode 100644 index 0000000..7075abe --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# GOJ-Runtime +A lightweight, simple competitive programmer runtime and grader. + +## Features +[x] Time limits +[x] Memory limits +[x] Result Serialization (results) +[] Grading +[] Multiple Test Cases + +## Usage +`# gojrt ` diff --git a/main.c b/main.c new file mode 100644 index 0000000..6a3d3ff --- /dev/null +++ b/main.c @@ -0,0 +1,180 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "structs.h" +#include "serialization.h" + +#define COMPILE_TIMEOUT 120 +#define COMPILE_MEMORYLIMIT 1000000 // 1GB + +#define RUNNER_UID 1000 +#define RUNNER_GID 1000 + +#ifndef RUNNER_UID + #error "RUNNER_UID must be set" +#endif + +#ifndef RUNNER_GID + #error "RUNNER_GID must be set" +#endif + +// TODO: Better Panics +// Give an OE +_Noreturn void panic(const char* msg, ...) { + va_list vargs; + va_start(vargs, msg); + + fprintf(stderr, "panic: "); + vfprintf(stderr, msg, vargs); + + va_end(vargs); + + exit(1); +} + +void execCommand(char* command) { + setgid(RUNNER_GID); + setuid(RUNNER_UID); + char* args[] = {"sh", "-c", command, NULL}; + execvp("sh", args); + + // If exec returns, we're beyond cooked so panic + panic("Failed to exec in process! Errno: %d", errno); +} + +void getExecStats(pid_t pid, RunResult* result) { + puts("getExecStats"); + struct rusage usage; + time_t start = time(NULL); + + if (wait4(pid, &result->exitCode, 0, &usage) <= 0) { + panic("wait4 non-zero! Errno: %d", errno); + } + + result->exitCode = WEXITSTATUS(result->exitCode); + + result->maxRss = usage.ru_maxrss; + + time_t end = time(NULL); + + time_t executionTime = end - start; + + result->runtime = executionTime; +} + +void setExecFlags(RunResult* result, time_t runtimeSeconds, size_t memoryLimit) { + printf("%d\n", result->exitCode); + if (result->exitCode == 69) { + result->flags |= Nice; + strcpy(result->feedback, "Nice!"); + } + + bool errorFlag = false; + + if (result->exitCode != 0) { + result->flags |= ProgramError; + errorFlag = true; + } + + if (result->runtime > runtimeSeconds) { + result->flags |= TimeLimitExceeded; + errorFlag = true; + } + + if (result->maxRss > memoryLimit) { + result->flags |= MemoryLimitExceeded; + errorFlag = true; + } + + if (errorFlag) { + result->flags |= Err; + } else { + result->flags |= Ok; + } +} + +void getRunSummary() { +} + +/** + * @brief Runs commands with given constraints + * + * @details + * executeCommand will run the command string with runtimeSeconds + * as a time limit and memoryLimit + * as max RSS in KB. If time + * is exceeded, the function will immediately return error. However, + * a program that exceeds the memory limit will continue to run but, it's max + * RSS usage will be checked and will fail if it exceeds the memory limit + * + * Command strings are also passed in as an argument to "shell -c" allowing + * shell syntax to be used. This can be particularly useful for multi-step + * compilation. + * + * @param[in] command The shell commmand/syntax to run + * @param[in] runtimeSeconds Timeout in seconds + * @param[in] memoryLimit Max RSS limit in KB + * + */ +RunResult executeCommand(char* command, time_t runtimeSeconds, size_t memoryLimit) { + int process = fork(); + + RunResult result; + + // stop undefined behavior + // from thinking a string is present + result.feedback[0] = '\0'; + + if (process > 0) { + getExecStats(process, &result); + } else if (process == 0) { + execCommand(command); + } else { + panic("Failed to spawn process"); + } + + setExecFlags(&result, runtimeSeconds, memoryLimit); + + result.flags |= ExecutionFinal; + + return result; +} + +int main(int argc, char* argv[]) { + if (getuid() != 0) { + panic("Must run as root"); + } + + if (argc < 5) { + panic("Not enough args"); + } + + size_t runMemoryLimit = atol(argv[3]); + time_t runRuntimeSeconds = atol(argv[4]); + + if (runMemoryLimit <= 0 || runRuntimeSeconds <= 0) { + panic("invalid args"); + } + + RunResult compileResult = executeCommand(argv[1], COMPILE_TIMEOUT, COMPILE_MEMORYLIMIT); + RunResult runResult = executeCommand(argv[2], runRuntimeSeconds, runMemoryLimit); + + SerializationResult serialResult = serializeResult(&runResult); + + write(STDOUT_FILENO, serialResult.data, serialResult.size); + + free(serialResult.data); +} diff --git a/serialization.c b/serialization.c new file mode 100644 index 0000000..10b485b --- /dev/null +++ b/serialization.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include + +#include "structs.h" + +#define WATERMARK {0x69, 0x6E, 0x79, 0x72, 0x74, 0x62, 0x75} +#define VERSION 0x00 + +// Number of chars required to represent any 64 bit int/uint + NUL +#define INT_BUFFER_SIZE 21 + +#define SUM_BYTES(x) totalBytes += x + +#define CONCAT_BYTES(dst, src, size) memcpy(dst, src, size); \ + dst += size; + +// len_fb | fb | runtime + +// WARNING: This approach assumes uniform endiness and architechture +// TODO: Make it more portable? +// TODO: this is horrible. i need a better way. maybe a serialization lib or c++ +// rewrite? + +uint64_t calcSize(RunResult* result) { + uint64_t totalBytes = 0; + + size_t feedbackLen = strlen(result->feedback); + totalBytes += sizeof(feedbackLen); + totalBytes += feedbackLen; + + totalBytes += sizeof(result->runtime); + totalBytes += sizeof(result->maxRss); + totalBytes += sizeof(result->flags); + totalBytes += sizeof(result->exitCode); + return totalBytes; +} + +SerializationResult serializeResult(RunResult* result) { + uint64_t size = calcSize(result); + uint8_t* buf = malloc(size); + uint8_t* bufCursor = buf; + + size_t feedbackLen = strlen(result->feedback); + CONCAT_BYTES(bufCursor, &feedbackLen, sizeof(feedbackLen)); + CONCAT_BYTES(bufCursor, result->feedback, feedbackLen); + CONCAT_BYTES(bufCursor, &result->runtime, sizeof(result->runtime)); + CONCAT_BYTES(bufCursor, &result->maxRss, sizeof(result->maxRss)); + CONCAT_BYTES(bufCursor, &result->flags, sizeof(result->flags)); + CONCAT_BYTES(bufCursor, &result->exitCode, sizeof(result->exitCode)); + + SerializationResult serialResult = { + .data = buf, + .size = size + }; + + return serialResult; +} diff --git a/serialization.h b/serialization.h new file mode 100644 index 0000000..e4a733f --- /dev/null +++ b/serialization.h @@ -0,0 +1,4 @@ +#pragma once +#include "structs.h" + +SerializationResult serializeResult(RunResult* result); diff --git a/structs.h b/structs.h new file mode 100644 index 0000000..5b7b94a --- /dev/null +++ b/structs.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +enum ResultFlags { + /// Execution Result /// + // No other error occured, output may be graded + // OK + Ok = 1 << 0, + // At least one error occured + // See "Error" for all of them + // ERR + Err = 1 << 1, + + /// Grader Result /// + // The program gave correct output with no errors + // AC + Accepted = 1 << 2, + // The program got Err OR Wrong Answer + // RJ + Rejected = 1 << 3, + + /// Errors /// + // Set if memory limit is exceeded + // MLE + MemoryLimitExceeded = 1 << 4, + // Set if timelimit triggered termination + // TLE + TimeLimitExceeded = 1 << 5, + // Code gave the wrong result + // WA + WrongAnswer = 1 << 6, + // Given if program tampers with the runtime (unused) + // TD + TamperingDetected = 1 << 7, + // Set if program retuns non-zero exit code + // PE + ProgramError = 1 << 8, + // Occurs if a non-listed error occurs + // (Details given in comment) + // OE + OtherError = 1 << 9, + + /// Status /// + // Set if the run result is final (all flags set) + Final = 1 << 10, + // If all Execution flags are set + ExecutionFinal = 1 << 11, + + /// Misc. /// + // Nice! + // NIC + Nice = 1 << 31, +}; + +struct { + // Specifies extra feedback + char feedback[256]; + time_t runtime; + int maxRss; + int flags; + int exitCode; +} typedef RunResult; + +typedef struct { + uint8_t* data; + uint64_t size; +} SerializationResult; + diff --git a/testprogs/nonzero.c b/testprogs/nonzero.c new file mode 100644 index 0000000..ae239f5 --- /dev/null +++ b/testprogs/nonzero.c @@ -0,0 +1,4 @@ +int main(void) { + return 69; +} + diff --git a/tools/decodeFlags.py b/tools/decodeFlags.py new file mode 100755 index 0000000..e4bb7df --- /dev/null +++ b/tools/decodeFlags.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import argparse +flag_dict = { + 0: ['OK', 'Ok'], + 1: ['ERR', 'Err'], + 2: ['AC', 'Accepted'], + 3: ['RJ', 'Rejected'], + 4: ['MLE', 'MemoryLimitExceeded'], + 5: ['TLE', 'TimeLimitExceeded'], + 6: ['WA', 'WrongAnswer'], + 7: ['TD', 'TamperingDetected'], + 8: ['PE', 'ProgramError'], + 9: ['OE', 'OtherError'], + 10: [None, 'Final'], + 11: [None, 'ExecutionFinal'], + 31: ['NIC', 'Nice'], +} + +parser = argparse.ArgumentParser( + prog='GOJ Flag Decoder', + description='Decoder for GOJ status flags' + ) + +parser.add_argument('flags', help='the flags you would like to decode (as a 32 bit signed int)') + +args = parser.parse_args() + +flags = int(args.flags) + +for flag in range(0, 32): + if (flags & 2 ** flag) == 2 ** flag: + desc_list = flag_dict.get(flag) + + if desc_list is None: + print(f'{flag}: Unknown') + continue + + if desc_list[0] is None: + print(f'{flag}: {desc_list[1]}') + else: + print(f'{flag}: {desc_list[0]} ({desc_list[1]})')