diff --git a/.github/workflows/watcher_linux.yml b/.github/workflows/watcher_linux.yml index 0967e6ca..72f98ea1 100644 --- a/.github/workflows/watcher_linux.yml +++ b/.github/workflows/watcher_linux.yml @@ -12,6 +12,7 @@ on: env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release + LEMONLIME_SANDBOXED: 1 jobs: build: diff --git a/README.md b/README.md index e0a35eb5..eb1cc93b 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,9 @@ A tiny judging environment for OI contest based on Lemon + LemonPlus | Arch | 2021-07-11 | amd64 | KDE-Xorg | | Arch | 2020-10-31 | amd64 | KDE-Wayland | | Ubuntu | 20.04 | amd64 | GNOME 3 | -| Ubuntu | 18.04.4 | amd64 | GNOME 3 | | NOI Linux 2(Ubuntu) | 20.04 | amd64 | GNOME 3 | -| Linux Mint | 19.3 | amd64 | Cinnamon | -| Deepin | 15.11 | amd64 | DDE | | Deepin | 20 (1000) | amd64 | DDE | -| Debian | 10.3.0 | amd64 | LXQt; KDE-Xorg | | Fedora | 31-1.9 | amd64 | XFCE | -| openSUSE | Leap 15.1 | amd64 | iceWM | | openSUSE | Tumbleweed | amd64 | KDE-Xorg | | macOS | 15.2 Beta | arm64 | Aqua | diff --git a/src/core/processrunner_unix.cpp b/src/core/processrunner_unix.cpp index c34cf546..810d4eb8 100644 --- a/src/core/processrunner_unix.cpp +++ b/src/core/processrunner_unix.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -29,10 +28,7 @@ ProcessRunnerResult UnixProcessRunner::run() { ProcessRunnerResult res; res.result = CorrectAnswer; - int extraTime = qCeil(qMax(2000, config.timeLimit * 2) * config.extraTimeRatio); -#ifdef Q_OS_LINUX - // TODO: rewrite with cgroup QFile watcher(config.workingDirectory + QUuid::createUuid().toString(QUuid::Id128)); if (config.interpreterAsWatcher) { @@ -45,28 +41,6 @@ ProcessRunnerResult UnixProcessRunner::run() { auto *runner = new QProcess(); QStringList argumentsList; - argumentsList << "--dev" << "/dev"; - argumentsList << "--proc" << "/proc"; - argumentsList << "--ro-bind" << "/usr" << "/usr"; - argumentsList << "--symlink" << "/usr/lib" << "/lib"; - argumentsList << "--symlink" << "/usr/lib64" << "/lib64"; - argumentsList << "--symlink" << "/usr/bin" << "/bin"; - argumentsList << "--symlink" << "/usr/sbin" << "/sbin"; - argumentsList << "--tmpfs" << "/tmp"; - - argumentsList << "--unshare-all" << "--die-with-parent"; - - argumentsList << "--chdir" << config.workingDirectory; - - argumentsList << "--bind" << config.workingDirectory << config.workingDirectory; - - if (config.standardInputCheck) { - argumentsList << "--ro-bind" << QFileInfo(config.inputFile).absoluteFilePath() - << QFileInfo(config.inputFile).absoluteFilePath(); - } - - argumentsList << watcher.fileName(); - argumentsList << config.executableFile; argumentsList << config.arguments; @@ -100,67 +74,10 @@ ProcessRunnerResult UnixProcessRunner::run() { argumentsList << config.outputFileName; } - qDebug() << argumentsList; + argumentsList << config.workingDirectory; - QString bwrapPath = QStandardPaths::findExecutable("bwrap"); - if (bwrapPath.isEmpty()) { - res.score = 0; - res.result = CannotStartProgram; - res.message = QObject::tr("bwrap not found. Please install bubblewrap."); - delete runner; - return res; - } - - runner->setProcessEnvironment(config.environment); - runner->setWorkingDirectory(config.workingDirectory); - runner->start(bwrapPath, argumentsList); - -#else - - QFile watcher(config.workingDirectory + QUuid::createUuid().toString(QUuid::Id128)); - - if (config.interpreterAsWatcher) { - QFile::copy(config.executableFile, watcher.fileName()); - } else { - QFile::copy(":/watcher/watcher_unix", watcher.fileName()); - } - - watcher.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner); - auto *runner = new QProcess(); - QStringList argumentsList; - - argumentsList << config.executableFile; - argumentsList << config.arguments; - - if (config.standardInputCheck) { - argumentsList << QFileInfo(config.inputFile).absoluteFilePath(); - } else { - argumentsList << ""; - } - - if (config.standardOutputCheck) { - argumentsList << "_tmpout"; - } else { - argumentsList << ""; - } - - argumentsList << "_tmperr"; - argumentsList << QString("%1").arg(config.timeLimit); - argumentsList << QString("%1").arg(config.memoryLimit); - argumentsList << QString("%1").arg(config.rawTimeLimit); - argumentsList << QString("%1").arg(config.rawMemoryLimit); - - if (config.standardInputCheck) { - argumentsList << ""; - } else { - argumentsList << config.inputFileName; - } - - if (config.standardOutputCheck) { - argumentsList << ""; - } else { - argumentsList << config.outputFileName; - } + int extraTimeMs = qCeil(qMax(2000, config.timeLimit * 2) * config.extraTimeRatio); + argumentsList << QString("%1").arg(extraTimeMs); qDebug() << argumentsList; @@ -168,8 +85,6 @@ ProcessRunnerResult UnixProcessRunner::run() { runner->setWorkingDirectory(config.workingDirectory); runner->start(watcher.fileName(), argumentsList); -#endif - if (! runner->waitForStarted(-1)) { delete runner; res.score = 0; @@ -184,7 +99,7 @@ ProcessRunnerResult UnixProcessRunner::run() { // Using rlimit to limit CPU time can only be accurate to seconds, // so here it is rounded up to an integer second. - long long killTimeLimit = (config.timeLimit + 999) / 1000 * 1000 + extraTime; + long long killTimeLimit = (config.timeLimit + 999) / 1000 * 1000 + extraTimeMs + 1000; while (timer.elapsed() <= killTimeLimit) { if (runner->waitForFinished(10)) { isProgramFinishedInExtraTimeLimit = true; diff --git a/unix/test/CMakeLists.txt b/unix/test/CMakeLists.txt index 844f8713..78991477 100644 --- a/unix/test/CMakeLists.txt +++ b/unix/test/CMakeLists.txt @@ -18,6 +18,7 @@ file(COPY hello.sh DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) add_executable(tle tle.c) add_executable(add add.c) add_executable(re re.c) +add_executable(sleep_tle sleep_tle.c) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/scripts DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) @@ -27,6 +28,7 @@ add_test(NAME watcher_run_sh_test COMMAND python3 scripts/run_sh.py) add_test(NAME watcher_MLE_static_test COMMAND python3 scripts/mle_static.py) add_test(NAME watcher_unlimit_memory_test COMMAND python3 scripts/unlimit.py) add_test(NAME watcher_TLE_test COMMAND python3 scripts/tle.py) +add_test(NAME watcher_wall_clock_TLE_test COMMAND python3 scripts/sleep_tle.py) add_test(NAME watcher_filename_with_space_test COMMAND python3 scripts/space.py) add_test(NAME watcher_symlink_abs_test COMMAND python3 scripts/symlink_abs.py) add_test(NAME watcher_symlink_rel_test COMMAND python3 scripts/symlink_rel.py) diff --git a/unix/test/scripts/mle_static.py b/unix/test/scripts/mle_static.py index f53fedf4..06385c92 100644 --- a/unix/test/scripts/mle_static.py +++ b/unix/test/scripts/mle_static.py @@ -4,7 +4,7 @@ pid = os.getpid() tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./mle_static", "", "", "", tmperr, "1000", "380", "1000", "380", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./mle_static", "", "", "", tmperr, "1000", "380", "1000", "380", "", "", ".", "0"], shell=False, stdout=subprocess.PIPE) stdout, _ = p.communicate() diff --git a/unix/test/scripts/redirect.py b/unix/test/scripts/redirect.py index 4a2af928..262da41c 100644 --- a/unix/test/scripts/redirect.py +++ b/unix/test/scripts/redirect.py @@ -10,7 +10,7 @@ with open(tmpin, 'w') as f: f.writelines(['1 1']) -p = subprocess.Popen(["./watcher_unix", "./add", "", tmpin, tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False) +p = subprocess.Popen(["./watcher_unix", "./add", "", tmpin, tmpout, tmperr, "1000", "100", "1000", "100", "", "", ".", "0"], shell=False) time.sleep(2) p.kill() diff --git a/unix/test/scripts/run.py b/unix/test/scripts/run.py index 2ae512b8..950b5e87 100644 --- a/unix/test/scripts/run.py +++ b/unix/test/scripts/run.py @@ -5,7 +5,7 @@ tmpout = f"_tmpout_{pid}" tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./hello", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./hello", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", ".", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/run_sh.py b/unix/test/scripts/run_sh.py index 0ae2612c..450390dc 100644 --- a/unix/test/scripts/run_sh.py +++ b/unix/test/scripts/run_sh.py @@ -5,7 +5,7 @@ tmpout = f"_tmpout_{pid}" tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "/bin/sh", "hello.sh", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "/bin/sh", "hello.sh", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", ".", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/runtimeerr.py b/unix/test/scripts/runtimeerr.py index d69fbd9d..de68c626 100644 --- a/unix/test/scripts/runtimeerr.py +++ b/unix/test/scripts/runtimeerr.py @@ -4,6 +4,6 @@ pid = os.getpid() tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./re", "", "", "", tmperr, "1000", "100", "1000", "100", "", ""], shell=False) +p = subprocess.Popen(["./watcher_unix", "./re", "", "", "", tmperr, "1000", "100", "1000", "100", "", "", ".", "0"], shell=False) assert(p.wait() == 2) diff --git a/unix/test/scripts/sleep_tle.py b/unix/test/scripts/sleep_tle.py new file mode 100644 index 00000000..cca950b5 --- /dev/null +++ b/unix/test/scripts/sleep_tle.py @@ -0,0 +1,15 @@ +import subprocess +import time +import os + +pid = os.getpid() +tmperr = f"_tmperr_{pid}" + +p = subprocess.Popen(["./watcher_unix", "./sleep_tle", "", "", "", tmperr, + "1000", "100", "1000", "100", "", "", ".", "0"], + shell=False) + +time.sleep(3) +p.kill() + +assert(p.returncode == 3) diff --git a/unix/test/scripts/space.py b/unix/test/scripts/space.py index 15a5a70a..81fb9726 100644 --- a/unix/test/scripts/space.py +++ b/unix/test/scripts/space.py @@ -8,7 +8,7 @@ shutil.copy("./hello", "./he llo") -p = subprocess.Popen(["./watcher_unix", "./he llo", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./he llo", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", ".", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/symlink_abs.py b/unix/test/scripts/symlink_abs.py index 66e9c10e..201b9c89 100644 --- a/unix/test/scripts/symlink_abs.py +++ b/unix/test/scripts/symlink_abs.py @@ -10,7 +10,7 @@ os.symlink(os.path.join(os.getcwd(), "hello"), "hello_s_abs") -p = subprocess.Popen(["./watcher_unix", "./hello_s_abs", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./hello_s_abs", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", ".", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/symlink_rel.py b/unix/test/scripts/symlink_rel.py index b13ab1b2..6c2a97e3 100644 --- a/unix/test/scripts/symlink_rel.py +++ b/unix/test/scripts/symlink_rel.py @@ -10,7 +10,7 @@ os.symlink("hello", "hello_s_rel") -p = subprocess.Popen(["./watcher_unix", "./hello_s_rel", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./hello_s_rel", "", "", tmpout, tmperr, "1000", "100", "1000", "100", "", "", ".", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/scripts/tle.py b/unix/test/scripts/tle.py index a2ea1fed..12a51539 100644 --- a/unix/test/scripts/tle.py +++ b/unix/test/scripts/tle.py @@ -6,7 +6,7 @@ tmpout = f"_tmpout_{pid}" tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./tle", "", "", "", tmperr, "1000", "100", "1000", "100", "", ""], shell=False) +p = subprocess.Popen(["./watcher_unix", "./tle", "", "", "", tmperr, "1000", "100", "1000", "100", "", "", ".", "0"], shell=False) time.sleep(5) p.kill() diff --git a/unix/test/scripts/unlimit.py b/unix/test/scripts/unlimit.py index 68268c4c..727d3f14 100644 --- a/unix/test/scripts/unlimit.py +++ b/unix/test/scripts/unlimit.py @@ -5,7 +5,7 @@ tmpout = f"_tmpout_{pid}" tmperr = f"_tmperr_{pid}" -p = subprocess.Popen(["./watcher_unix", "./mle_static", "", "", tmpout, tmperr, "1000", "-1", "1000", "-1", "", ""], shell=False, stdout=subprocess.PIPE) +p = subprocess.Popen(["./watcher_unix", "./mle_static", "", "", tmpout, tmperr, "1000", "-1", "1000", "-1", "", "", ".", "0"], shell=False, stdout=subprocess.PIPE) assert(p.wait() == 0) assert(os.path.exists(tmpout)) diff --git a/unix/test/sleep_tle.c b/unix/test/sleep_tle.c new file mode 100644 index 00000000..e13b08b0 --- /dev/null +++ b/unix/test/sleep_tle.c @@ -0,0 +1,6 @@ +#include + +int main() { + sleep(10); + return 0; +} diff --git a/unix/watcher_unix.cpp b/unix/watcher_unix.cpp index 096042e6..971fd0d8 100644 --- a/unix/watcher_unix.cpp +++ b/unix/watcher_unix.cpp @@ -7,10 +7,12 @@ */ #include +#include #include #include #include #include +#include #include #include #include @@ -18,7 +20,16 @@ #include #include #include +#if defined(__linux__) +#include +#include +#include +#include +#elif defined(__APPLE__) +#include +#endif #include +#include int pid; @@ -52,11 +63,13 @@ enum : int { * argv[9]: 原始(未经语言设置缩放的)空间限制(MiB) * argv[10]: 选手程序只读的文件 * argv[11]: 选手程序只写的文件 + * argv[12]: 工作目录 + * argv[13]: wall clock 额外超时时间(毫秒) */ auto main(int argc, char *argv[]) -> int { - if (argc != 12) { + if (argc != 14) { printf("-1\n-1\n"); - fprintf(stderr, "Expected 11 arguments, found %d\n", argc); + fprintf(stderr, "Expected 13 arguments, found %d\n", argc - 1); return RS_FAIL; } std::string fileName = argv[1]; @@ -70,6 +83,53 @@ auto main(int argc, char *argv[]) -> int { [[maybe_unused]] long long rawMemoryLimitMib = std::stoll(argv[9]); [[maybe_unused]] std::string readableFile = argv[10]; [[maybe_unused]] std::string writableFile = argv[11]; + std::string workdir = argv[12]; + long long extraTimeMs = std::stoll(argv[13]); + +#ifdef __linux__ + if (! getenv("LEMONLIME_SANDBOXED")) { + char selfExe[4096] = {}; + ssize_t len = readlink("/proc/self/exe", selfExe, sizeof(selfExe) - 1); + if (len <= 0) { + fprintf(stderr, "Cannot determine self executable path\n"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + std::vector args; + + auto add = [&](auto... xs) { ((args.push_back(xs)), ...); }; + + add("bwrap"); + add("--dev", "/dev"); + add("--proc", "/proc"); + add("--ro-bind", "/usr", "/usr"); + add("--symlink", "/usr/lib", "/lib"); + add("--symlink", "/usr/lib64", "/lib64"); + add("--symlink", "/usr/bin", "/bin"); + add("--symlink", "/usr/sbin", "/sbin"); + add("--tmpfs", "/tmp"); + add("--unshare-all"); + add("--die-with-parent"); + add("--chdir", workdir.c_str()); + add("--bind", workdir.c_str(), workdir.c_str()); + + if (! stdinRedirect.empty()) { + add("--ro-bind", stdinRedirect.c_str(), stdinRedirect.c_str()); + } + + add("--", selfExe); + for (int i = 1; i < argc; ++i) + add(argv[i]); + add(nullptr); + + setenv("LEMONLIME_SANDBOXED", "1", 1); + execvp("bwrap", const_cast(args.data())); + fprintf(stderr, "bwrap: %s\n", strerror(errno)); + printf("-1\n-1\n"); + return RS_FAIL; + } +#endif initWatcher(); @@ -97,7 +157,22 @@ auto main(int argc, char *argv[]) -> int { ssize_t actualMemoryRLimit = getMemoryRLimit(memoryLimitMib); +#if defined(__linux__) + int childPfd = -1; + struct clone_args args{}; + args.flags = CLONE_PIDFD; + args.pidfd = (unsigned long long)&childPfd; + args.exit_signal = SIGCHLD; + pid = syscall(SYS_clone3, &args, sizeof(args)); +#else pid = fork(); +#endif + + if (pid < 0) { + perror("fork"); + printf("-1\n-1\n"); + return RS_FAIL; + } if (pid > 0) { // Parent process @@ -107,11 +182,85 @@ auto main(int argc, char *argv[]) -> int { struct rusage usage{}; int status = 0; + long long wallClockMs = timeLimitMs + extraTimeMs; +#if defined(__linux__) + int timerFd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if (timerFd < 0) { + perror("timerfd_create"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + struct itimerspec ts{}; + ts.it_value.tv_sec = wallClockMs / 1000; + ts.it_value.tv_nsec = (wallClockMs % 1000) * 1000000; + timerfd_settime(timerFd, 0, &ts, nullptr); + + struct pollfd pfds[2]{}; + pfds[0].fd = childPfd; + pfds[0].events = POLLIN; + pfds[1].fd = timerFd; + pfds[1].events = POLLIN; + + poll(pfds, 2, -1); + + bool childExited = pfds[0].revents & POLLIN; + bool timedOut = pfds[1].revents & POLLIN; + + if (! childExited) + kill(pid, SIGKILL); + + wait4(pid, &status, 0, &usage); + + close(childPfd); + close(timerFd); + + if (timedOut) { + printf("-1\n-1\n"); + return RS_TLE; + } +#elif defined(__APPLE__) + int kq = kqueue(); + if (kq < 0) { + perror("kqueue"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + struct kevent changes[2]; + EV_SET(&changes[0], pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, NULL); + EV_SET(&changes[1], 0, EVFILT_TIMER, EV_ADD | EV_ONESHOT, 0, wallClockMs, NULL); + + struct kevent events[2]; + int n = kevent(kq, changes, 2, events, 2, NULL); + + bool childExited = false; + bool timedOut = false; + for (int i = 0; i < n; i++) { + if (events[i].filter == EVFILT_PROC) + childExited = true; + else if (events[i].filter == EVFILT_TIMER) + timedOut = true; + } + + if (timedOut && ! childExited) + kill(pid, SIGKILL); + + wait4(pid, &status, 0, &usage); + + close(kq); + + if (timedOut) { + printf("-1\n-1\n"); + return RS_TLE; + } +#else if (wait4(pid, &status, 0, &usage) == -1) { printf("-1\n-1\n"); perror("wait4"); return RS_FAIL; } +#endif if (WIFEXITED(status)) { long long timeUsedMs = @@ -160,7 +309,7 @@ auto main(int argc, char *argv[]) -> int { exit(RS_FAIL); } - rlimit memlim{}, stalim{}, timlim{}; + rlimit memlim{}, stalim{}; if (memoryLimitMib > 0) { memlim = (rlimit){(rlim_t)actualMemoryRLimit, (rlim_t)actualMemoryRLimit}; @@ -171,13 +320,8 @@ auto main(int argc, char *argv[]) -> int { stalim = (rlimit){(rlim_t)2147483647LL, (rlim_t)2147483647LL}; } - // Calculate time limit in seconds, rounding up - rlim_t soft_time_limit_sec = (timeLimitMs + 999) / 1000; - timlim = (rlimit){soft_time_limit_sec, soft_time_limit_sec + 1}; // Soft limit + 1 for hard limit - setrlimit(RLIMIT_AS, &memlim); setrlimit(RLIMIT_STACK, &stalim); - setrlimit(RLIMIT_CPU, &timlim); if (execlp("bash", "bash", "-c", runCmd.c_str(), NULL) == -1) { perror("execlp");