commit da34a7b62e4c7e7e987a126bf8c1331d29c7d5bf Author: Joseph Umana Date: Fri Jul 25 16:32:30 2025 -0400 init Signed-off-by: Joseph Umana 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]})')