From e18ccc643a336567bca537cef8f53b87ddec124a Mon Sep 17 00:00:00 2001 From: ZnPdCo Date: Fri, 14 Aug 2026 22:10:35 +0800 Subject: [PATCH 1/3] fix(watcher): enforce time limit by wall clock instead of RLIMIT_CPU RLIMIT_CPU limits CPU time while the judge measures user time, and a program that merely sleeps never hits it, so a sleep-only program could run forever. The watcher now receives the extra wall-clock grace (argv[12], ms) from the main process and enforces timeLimit + extraTime with timerfd + pidfd (Linux) / kqueue (macOS), killing the child on timeout and reporting TLE. The sandbox stays in the main process (bwrap), which only keeps a 1s backstop against a hung watcher. --- src/core/processrunner_unix.cpp | 10 ++- unix/test/CMakeLists.txt | 2 + unix/test/scripts/mle_static.py | 2 +- unix/test/scripts/redirect.py | 2 +- unix/test/scripts/run.py | 2 +- unix/test/scripts/run_sh.py | 2 +- unix/test/scripts/runtimeerr.py | 2 +- unix/test/scripts/sleep_tle.py | 17 ++++ unix/test/scripts/space.py | 2 +- unix/test/scripts/symlink_abs.py | 2 +- unix/test/scripts/symlink_rel.py | 2 +- unix/test/scripts/tle.py | 2 +- unix/test/scripts/unlimit.py | 2 +- unix/test/sleep_tle.c | 6 ++ unix/watcher_unix.cpp | 137 +++++++++++++++++++++++++++++-- 15 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 unix/test/scripts/sleep_tle.py create mode 100644 unix/test/sleep_tle.c diff --git a/src/core/processrunner_unix.cpp b/src/core/processrunner_unix.cpp index c34cf546..c7e57fe6 100644 --- a/src/core/processrunner_unix.cpp +++ b/src/core/processrunner_unix.cpp @@ -100,6 +100,8 @@ ProcessRunnerResult UnixProcessRunner::run() { argumentsList << config.outputFileName; } + argumentsList << QString("%1").arg(extraTime); + qDebug() << argumentsList; QString bwrapPath = QStandardPaths::findExecutable("bwrap"); @@ -162,6 +164,8 @@ ProcessRunnerResult UnixProcessRunner::run() { argumentsList << config.outputFileName; } + argumentsList << QString("%1").arg(extraTime); + qDebug() << argumentsList; runner->setProcessEnvironment(config.environment); @@ -182,9 +186,9 @@ ProcessRunnerResult UnixProcessRunner::run() { QElapsedTimer timer; timer.start(); - // 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; + // The watcher itself enforces the wall clock (timeLimit + extraTime) and exits shortly + // after the timer fires; this loop only guards against a hung watcher, with 1s of slack. + long long killTimeLimit = 1LL * config.timeLimit + extraTime + 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..de2f1f58 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..63979c53 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..d4fcaea6 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..d4c830c2 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..d5c62401 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..a8bf947e --- /dev/null +++ b/unix/test/scripts/sleep_tle.py @@ -0,0 +1,17 @@ +import subprocess +import time +import os + +pid = os.getpid() +tmperr = f"_tmperr_{pid}" + +# `sleep` does not consume CPU time, so it can bypass RLIMIT_CPU; +# the watcher must time it out by wall clock. +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..a8554ed2 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..b10cdf82 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..6ecadfb0 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..5fab5bbf 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..23676ce8 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..f226b73b 100644 --- a/unix/watcher_unix.cpp +++ b/unix/watcher_unix.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include #include @@ -18,6 +19,14 @@ #include #include #include +#if defined(__linux__) +#include +#include +#include +#include +#elif defined(__APPLE__) +#include +#endif #include int pid; @@ -52,11 +61,12 @@ enum : int { * argv[9]: 原始(未经语言设置缩放的)空间限制(MiB) * argv[10]: 选手程序只读的文件 * argv[11]: 选手程序只写的文件 + * argv[12]: wall clock 额外超时时间(毫秒) */ auto main(int argc, char *argv[]) -> int { - if (argc != 12) { + if (argc != 13) { printf("-1\n-1\n"); - fprintf(stderr, "Expected 11 arguments, found %d\n", argc); + fprintf(stderr, "Expected 12 arguments, found %d\n", argc - 1); return RS_FAIL; } std::string fileName = argv[1]; @@ -70,6 +80,7 @@ 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]; + long long extraTimeMs = std::stoll(argv[12]); initWatcher(); @@ -97,7 +108,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 +133,110 @@ auto main(int argc, char *argv[]) -> int { struct rusage usage{}; int status = 0; + // Time limit is enforced by a wall-clock timer instead of `setrlimit(RLIMIT_CPU)`: + // RLIMIT_CPU counts CPU time while the judge measures user time, and a program that + // merely sleeps never hits it. + 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; + if (timerfd_settime(timerFd, 0, &ts, nullptr) < 0) { + perror("timerfd_settime"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + struct pollfd pfds[2]{}; + pfds[0].fd = childPfd; + pfds[0].events = POLLIN; + pfds[1].fd = timerFd; + pfds[1].events = POLLIN; + + int pollResult; + do { + pollResult = poll(pfds, 2, -1); + } while (pollResult < 0 && errno == EINTR); + + if (pollResult < 0) { + perror("poll"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + 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; + do { + n = kevent(kq, changes, 2, events, 2, NULL); + } while (n < 0 && errno == EINTR); + + if (n < 0) { + perror("kevent"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + 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 = @@ -144,6 +269,7 @@ auto main(int argc, char *argv[]) -> int { return RS_RE; } } else { + // Child process std::string finalStdinRedirect = stdinRedirect.empty() ? "/dev/null" : stdinRedirect; if (freopen(finalStdinRedirect.c_str(), "r", stdin) == NULL) { perror("freopen stdin"); @@ -160,7 +286,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 +297,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"); From 2dc202dced313814873afd9f69f2b5bb695120d7 Mon Sep 17 00:00:00 2001 From: ZnPdCo Date: Fri, 14 Aug 2026 22:10:35 +0800 Subject: [PATCH 2/3] fix(watcher): enforce time limit by wall clock instead of RLIMIT_CPU RLIMIT_CPU limits CPU time while the judge measures user time, and a program that merely sleeps never hits it, so a sleep-only program could run forever. The watcher now receives the extra wall-clock grace (argv[12], ms) from the main process and enforces timeLimit + extraTime with timerfd + pidfd (Linux) / kqueue (macOS & BSD), killing the child on timeout and reporting TLE. The sandbox stays in the main process (bwrap), which only keeps a 1s backstop against a hung watcher. --- src/core/processrunner_unix.cpp | 10 ++- unix/test/CMakeLists.txt | 2 + unix/test/scripts/mle_static.py | 2 +- unix/test/scripts/redirect.py | 2 +- unix/test/scripts/run.py | 2 +- unix/test/scripts/run_sh.py | 2 +- unix/test/scripts/runtimeerr.py | 2 +- unix/test/scripts/sleep_tle.py | 17 ++++ unix/test/scripts/space.py | 2 +- unix/test/scripts/symlink_abs.py | 2 +- unix/test/scripts/symlink_rel.py | 2 +- unix/test/scripts/tle.py | 2 +- unix/test/scripts/unlimit.py | 2 +- unix/test/sleep_tle.c | 6 ++ unix/watcher_unix.cpp | 139 +++++++++++++++++++++++++++++-- 15 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 unix/test/scripts/sleep_tle.py create mode 100644 unix/test/sleep_tle.c diff --git a/src/core/processrunner_unix.cpp b/src/core/processrunner_unix.cpp index c34cf546..c7e57fe6 100644 --- a/src/core/processrunner_unix.cpp +++ b/src/core/processrunner_unix.cpp @@ -100,6 +100,8 @@ ProcessRunnerResult UnixProcessRunner::run() { argumentsList << config.outputFileName; } + argumentsList << QString("%1").arg(extraTime); + qDebug() << argumentsList; QString bwrapPath = QStandardPaths::findExecutable("bwrap"); @@ -162,6 +164,8 @@ ProcessRunnerResult UnixProcessRunner::run() { argumentsList << config.outputFileName; } + argumentsList << QString("%1").arg(extraTime); + qDebug() << argumentsList; runner->setProcessEnvironment(config.environment); @@ -182,9 +186,9 @@ ProcessRunnerResult UnixProcessRunner::run() { QElapsedTimer timer; timer.start(); - // 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; + // The watcher itself enforces the wall clock (timeLimit + extraTime) and exits shortly + // after the timer fires; this loop only guards against a hung watcher, with 1s of slack. + long long killTimeLimit = 1LL * config.timeLimit + extraTime + 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..de2f1f58 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..63979c53 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..d4fcaea6 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..d4c830c2 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..d5c62401 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..a8bf947e --- /dev/null +++ b/unix/test/scripts/sleep_tle.py @@ -0,0 +1,17 @@ +import subprocess +import time +import os + +pid = os.getpid() +tmperr = f"_tmperr_{pid}" + +# `sleep` does not consume CPU time, so it can bypass RLIMIT_CPU; +# the watcher must time it out by wall clock. +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..a8554ed2 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..b10cdf82 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..6ecadfb0 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..5fab5bbf 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..23676ce8 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..64bdf72e 100644 --- a/unix/watcher_unix.cpp +++ b/unix/watcher_unix.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include #include @@ -18,6 +19,15 @@ #include #include #include +#if defined(__linux__) +#include +#include +#include +#include +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \ + defined(__DragonFly__) +#include +#endif #include int pid; @@ -52,11 +62,12 @@ enum : int { * argv[9]: 原始(未经语言设置缩放的)空间限制(MiB) * argv[10]: 选手程序只读的文件 * argv[11]: 选手程序只写的文件 + * argv[12]: wall clock 额外超时时间(毫秒) */ auto main(int argc, char *argv[]) -> int { - if (argc != 12) { + if (argc != 13) { printf("-1\n-1\n"); - fprintf(stderr, "Expected 11 arguments, found %d\n", argc); + fprintf(stderr, "Expected 12 arguments, found %d\n", argc - 1); return RS_FAIL; } std::string fileName = argv[1]; @@ -70,6 +81,7 @@ 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]; + long long extraTimeMs = std::stoll(argv[12]); initWatcher(); @@ -97,7 +109,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 +134,111 @@ auto main(int argc, char *argv[]) -> int { struct rusage usage{}; int status = 0; + // Time limit is enforced by a wall-clock timer instead of `setrlimit(RLIMIT_CPU)`: + // RLIMIT_CPU counts CPU time while the judge measures user time, and a program that + // merely sleeps never hits it. + 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; + if (timerfd_settime(timerFd, 0, &ts, nullptr) < 0) { + perror("timerfd_settime"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + struct pollfd pfds[2]{}; + pfds[0].fd = childPfd; + pfds[0].events = POLLIN; + pfds[1].fd = timerFd; + pfds[1].events = POLLIN; + + int pollResult; + do { + pollResult = poll(pfds, 2, -1); + } while (pollResult < 0 && errno == EINTR); + + if (pollResult < 0) { + perror("poll"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + 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__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || \ + defined(__DragonFly__) + 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; + do { + n = kevent(kq, changes, 2, events, 2, NULL); + } while (n < 0 && errno == EINTR); + + if (n < 0) { + perror("kevent"); + printf("-1\n-1\n"); + return RS_FAIL; + } + + 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 = @@ -144,6 +271,7 @@ auto main(int argc, char *argv[]) -> int { return RS_RE; } } else { + // Child process std::string finalStdinRedirect = stdinRedirect.empty() ? "/dev/null" : stdinRedirect; if (freopen(finalStdinRedirect.c_str(), "r", stdin) == NULL) { perror("freopen stdin"); @@ -160,7 +288,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 +299,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"); From d14ea8fbe3ba0e3a3cab235d38ea8d4410fa161a Mon Sep 17 00:00:00 2001 From: ZnPdCo Date: Sat, 15 Aug 2026 10:45:24 +0800 Subject: [PATCH 3/3] docs(readme): drop Linux distros older than kernel 5.3 The watcher now waits for the child with clone3 + CLONE_PIDFD, which requires Linux 5.3+. Distros with older kernels (Ubuntu 18.04, Debian 10, Mint 19.3, Deepin 15.11, openSUSE Leap 15.1) can no longer run the watcher, so remove them from the tested list and keep NOI Linux 2 as the baseline. --- README.md | 5 ----- 1 file changed, 5 deletions(-) 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 |