2018-12-16

开发OJ之沙箱 — 设想和开始

学校现用的 OJ 是HUSTOJ的魔改版, 沙箱自然也是用的它的. 说为沙箱其实并不是很合理, 判题进程包含了构建沙箱这个东西, 他将所有的过程整合在一起, 使得二次开发不是很方便. 所以 专门 立了一个 SandBox 的项目, 新 OJ 将沙箱作为一个单独的项目独立出来, 以方便以后出现 bug 的修复.

我们并不打算考虑多平台, 开发过程中仅在Linux上进行测试, BSD 等 Unix 系统可能需要修改测试, 开发过程中 参考了 QingdaoU judger, EOJ judger, DMOJ judge 对于 system call 的限制以及项目构建, 危险代码等一些方面, 在此表示对他们的感谢.

项目采用 CMake 构建(因为不想写 makefile), 并没有选择使用C语言, 而是在某些方面使用了 C++ ,比如命令行参数获取, 某些地方使用 C++ 11 的特性将会使代码更易于阅读, 开发过程中使用 CLion, 编译要求C++11 GCC 4.8 就已经支持.

设想

沙箱应该 整个 OJ 中最危险的部分. 它需要 编译, 运行 用户提交的代码, 而用户可以恶意构造代码, 使系统崩溃或硬件受损. 整个判题 Judger 将会放在 Docker 中, 如果一旦 Docker 受到破坏, 重启 对应容器则应该可以解决问题.

资源限制通过 setrlimit实现.

涉及到文件, 网络, 用户, 进程等的操作, 在Linux中都是需要在内核态操作, Linux 通过 system call 来调用, 可以在调用前让系统检查调用的合法性, 例如 ptrace, seccomp, 考虑到OnlineJudge 沙箱实现思路 中 提到的 ptrace 效率较低, 所以还是采用 seccomp. seccomp策略则考虑使用 白名单模式, 防止由于疏漏, 没有封禁部分危险的系统调用.

代码的编译和运行则是exec系函数来执行命令, exec 会继承 pid, 也就是说 setlimit 和 seccomp 之前的设定在接下来能起作用. 通过父进程监控子进程, 若出现超时就 kill 掉, 子进程 使用exec命令前会将自身降到一个低权限账号上, 通过用户来限制权限

获取程序的状态和资源使用则可以使用 wait4 等相关函数, 这方还没详细了解.

开始

我使用的是 CLion, 但就算是拿个 VS code 也照样能写, 只不过调试什么的稍微麻烦一些. 由于Ubuntu 默认没有seccomp.h, 还需要安装libseccomp-dev才能使用, 编译时还需要指定链接库.

cmake

我这边只贴配置, cmake 可以认为是个跨平台的 make, 由于打算是作为一个库被集成使用的, 编译的时候需要将函数地址相对化, 也就是要加上参数 -pie -fPIC.
这里贴下cmake的代码

cmake_minimum_required(VERSION 3.0)
project(sandbox)

#set(CMAKE_VERBOSE_MAKEFILE ON)

add_compile_options(-pie)
add_compile_options(-Wall)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

add_executable(sandbox.so src/main.cpp {more file})

target_link_libraries(sandbox.so seccomp)

add_test(test test/)

日志模块

一切都需要有日志模块; 如果多个沙箱同时运行, 有可能出现写日志冲突的情况, 这时候需要对文件进行加锁, 文件锁可以参考这篇 Linux 2.6 中的文件锁, 原本应该可以直接使用pwrite进行原子写, 但pwrite并不会改变文件当前的偏移量, 就有可能出现写完覆盖了的情况, 最后使用write和文件锁来完成跟这个问题.

为了使用log::debug之类的方式调用, 将一些函数放在了struct中.

通过openLog和close控制文件结构体指针, 如果结构体为空, 则会将日志直接打到stderr上.

代码如下

log.h

//
// Created by Boxjan on Dec 15, 2018 13:32.
//

#ifndef SANDBOX_LOG_H
#define SANDBOX_LOG_H

#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <sys/file.h>
#include <unistd.h>

enum LEVEL {DEBUG, INFO, WARN, ERROR};
const char LEVEL_STR[][8] = {"DEBUG", "INFO", "WARN", "ERROR"};

const int one_line_max_size = 10240;

class Log {
private:
    FILE *log_file;
    bool is_debug;
    static Log *instance;

    static void writeLog(const LEVEL level, const char *format, va_list &args);
    Log();
    ~Log();

public:
    static Log *getInstance();

    static void openFile(const char *file_path);
    static void closeFile();

    static void debug(const char *format, ...);
    static void info(const char *format, ...);
    static void warn(const char *format, ...);
    static void error(const char *format, ...);

    static void isDebug();


};

typedef Log log;
#endif //SANDBOX_LOG_H

log.cpp

//
// Created by Boxjan on Dec 19, 2018 16:38.
//

#include "log.h"

Log *Log::instance = nullptr;

Log::Log()  {
    log_file = nullptr;
    is_debug = false;
}

Log::~Log()  {
    closeFile();
}

Log* Log::getInstance() {
    if (instance == nullptr) {
        instance = new Log();
    }
    return instance;
}

void Log::isDebug()  {
    getInstance()->is_debug = true;
}

void Log::openFile(const char *file_path) {
    if (getInstance()->log_file != nullptr)
        closeFile();

    getInstance()->log_file = fopen(file_path, "a+");
}

void Log::closeFile() {
    fclose(getInstance()->log_file);
    getInstance()->log_file = nullptr;
}

void Log::debug(const char *format, ...) {
    va_list args;
    va_start(args, format);
    writeLog(DEBUG, format, args);
    va_end(args);
}

void Log::info(const char *format, ...) {
    va_list args;
    va_start(args, format);
    writeLog(INFO, format, args);
    va_end(args);
}

void Log::warn(const char *format, ...) {
    va_list args;
    va_start(args, format);
    writeLog(WARN, format, args);
    va_end(args);
}

void Log::error(const char *format, ...) {
    va_list args;
    va_start(args, format);
    writeLog(ERROR, format, args);
    va_end(args);
}

void Log::writeLog(const LEVEL level, const char *format, va_list &args) {

    if (level == DEBUG && !getInstance()->is_debug) return;

    // format time
    static char datetime[64];
    static time_t now;
    now = time(nullptr);
    strftime(datetime, 63, "%Y-%m-%d %H:%M:%S", localtime(&now));

    //
    static char message[one_line_max_size];
    vsnprintf(message, one_line_max_size, format, args);

    // get one record
    static char log_str[one_line_max_size];
    int count = snprintf(log_str, one_line_max_size, "%s [%s] - %s\n", datetime, LEVEL_STR[level], message);

    // file
    if (getInstance()->log_file == nullptr) {
        fprintf(stderr, "%s", log_str);
        return;
    }

    int log_fd = fileno((FILE *) getInstance()->log_file);
    if (flock(log_fd, LOCK_EX) == 0) {
        if (write(log_fd, log_str, (size_t) count) < 0) {
            fprintf(stderr, "write error\n");
            fprintf(stderr, "%s", log_str);
            return;
        }
        flock(log_fd, LOCK_UN);
    } else {
        fprintf(stderr, "lock file error\n");
        fprintf(stderr, "%s", log_str);
    }
}

代码中 使用了 C/C++不定参数的处理, 涉及 va_list, va_start, vprint系 等函数, 这些可以到 Cplusplus 找找.

命令行参数解析

由于是被设计为可以单独使用, 也可以被包含在库中, 所以需要解析命令行参数, 用的是 args 这个库. 看readme学着用即可.

Update

Dec 22 2018

由于命令行参数解析库存在设计上的问题, 故改用 cmdline.
由于日志摸块存在编译冲突, static 不可变更问题, 故重写为类, 并分为两文件.

-- EOF --

comments

如果无法加载 请将 disqus.com | disquscdn.com 加入代理