Signed-off-by: Joseph Umana <joseph@josephumana.com>
This commit is contained in:
Joseph Umana 2025-07-25 16:32:30 -04:00
commit da34a7b62e
Signed by: joeuint
GPG key ID: 05A7B3877ADEC27C
9 changed files with 378 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.out
gojrt
.cache/
compile_commands.json

2
Makefile Normal file
View file

@ -0,0 +1,2 @@
all:
gcc main.c structs.h serialization.c serialization.h -o gojrt -Wall -std=c23 -g

12
README.md Normal file
View file

@ -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 <compile_command> <run_command> <runtime_seconds> <memory_limit>`

180
main.c Normal file
View file

@ -0,0 +1,180 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
#include <errno.h>
#include <wchar.h>
#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 <em>runtimeSeconds</em>
* as a time limit and <em>memoryLimit
* </em> 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);
}

60
serialization.c Normal file
View file

@ -0,0 +1,60 @@
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#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;
}

4
serialization.h Normal file
View file

@ -0,0 +1,4 @@
#pragma once
#include "structs.h"
SerializationResult serializeResult(RunResult* result);

70
structs.h Normal file
View file

@ -0,0 +1,70 @@
#pragma once
#include <time.h>
#include <stdint.h>
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;

4
testprogs/nonzero.c Normal file
View file

@ -0,0 +1,4 @@
int main(void) {
return 69;
}

42
tools/decodeFlags.py Executable file
View file

@ -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]})')