Skip to content

Commit 1bb7aad

Browse files
committed
judgedaemon: Replace system and other command executions.
Introduce a safer and more readable variant that executes, logs and checks return values centrally.
1 parent 720b750 commit 1bb7aad

File tree

1 file changed

+84
-61
lines changed

1 file changed

+84
-61
lines changed

judge/judgedaemon.main.php

Lines changed: 84 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,39 @@ function read_judgehostlog(int $numLines = 20) : string
290290
return trim(ob_get_clean());
291291
}
292292

293+
/**
294+
* Execute a shell command given an array of strings forming the command.
295+
* The command and all its arguments are automatically escaped.
296+
*
297+
* @param array $command_parts The command and its arguments (e.g., ['ls', '-l', '/tmp/']).
298+
* @param int|null $retval The variable to store the command's exit code.
299+
* @param bool $log_nonzero_exitcode Whether non-zero exit codes should be logged.
300+
*
301+
* @return bool Returns true on success (exit code 0), false otherwise.
302+
*/
303+
function run_command_safe(array $command_parts, ?int &$retval = null, $log_nonzero_exitcode = true): bool
304+
{
305+
if (empty($command_parts)) {
306+
logmsg(LOG_WARNING, "Need at least the command that should be called.");
307+
$retval = -1;
308+
return false;
309+
}
310+
311+
$command = implode(' ', array_map('dj_escapeshellarg', $command_parts));
312+
313+
logmsg(LOG_DEBUG, "Executing command: $command");
314+
system($command, $retval);
315+
316+
if ($retval !== 0) {
317+
if ($log_nonzero_exitcode) {
318+
logmsg(LOG_WARNING, "Command failed with exit code $retval: $command");
319+
}
320+
return false;
321+
}
322+
323+
return true;
324+
}
325+
293326
// Fetches a new executable from database if not cached already, and runs build script to compile executable.
294327
// Returns an array with
295328
// - absolute path to run script
@@ -351,12 +384,10 @@ function fetch_executable_internal(
351384
$execrunjurypath = $execbuilddir . '/runjury';
352385
if (!is_dir($execdir) || !file_exists($execdeploypath) ||
353386
($combined_run_compare && file_get_contents(LIBJUDGEDIR . '/run-interactive.sh')!==file_get_contents($execrunpath))) {
354-
system('rm -rf ' . dj_escapeshellarg($execdir) . ' ' . dj_escapeshellarg($execbuilddir), $retval);
355-
if ($retval !== 0) {
387+
if (!run_command_safe(['rm', '-rf', $execdir, $execbuilddir], $retval)) {
356388
disable('judgehost', 'hostname', $myhost, "Deleting '$execdir' or '$execbuilddir' was unsuccessful.");
357389
}
358-
system('mkdir -p ' . dj_escapeshellarg($execbuilddir), $retval);
359-
if ($retval !== 0) {
390+
if (!run_command_safe(['mkdir', '-p', $execbuilddir], $retval)) {
360391
disable('judgehost', 'hostname', $myhost, "Could not create directory '$execbuilddir'");
361392
}
362393

@@ -466,8 +497,7 @@ function fetch_executable_internal(
466497
putenv('SCRIPTMEMLIMIT=' . djconfig_get_value('script_memory_limit'));
467498
putenv('SCRIPTFILELIMIT=' . djconfig_get_value('script_filesize_limit'));
468499

469-
system(LIBJUDGEDIR . '/build_executable.sh ' . dj_escapeshellarg($execdir), $retval);
470-
if ($retval !== 0) {
500+
if (!run_command_safe([LIBJUDGEDIR . '/build_executable.sh', $execdir], $retval)) {
471501
return [null, "Failed to build executable in $execdir.", "$execdir/build.log"];
472502
}
473503
chmod($execrunpath, 0755);
@@ -528,7 +558,11 @@ function fetch_executable_internal(
528558
exit(1);
529559
}
530560

531-
$myhost = trim(`hostname | cut -d . -f 1`);
561+
$hostname = gethostname();
562+
if ($hostname === false) {
563+
error("Could not determine hostname.");
564+
}
565+
$myhost = explode('.', $hostname)[0];
532566
if (isset($options['daemonid'])) {
533567
if (preg_match('/^\d+$/', $options['daemonid'])) {
534568
$myhost = $myhost . "-" . $options['daemonid'];
@@ -659,8 +693,7 @@ function fetch_executable_internal(
659693

660694
// Check basic prerequisites for chroot at judgehost startup
661695
logmsg(LOG_INFO, "🔏 Executing chroot script: '".CHROOT_SCRIPT." check'");
662-
system(LIBJUDGEDIR.'/'.CHROOT_SCRIPT.' check', $retval);
663-
if ($retval!==0) {
696+
if (!run_command_safe([LIBJUDGEDIR.'/'.CHROOT_SCRIPT, 'check'], $retval)) {
664697
error("chroot validation check exited with exitcode $retval");
665698
}
666699

@@ -753,8 +786,7 @@ function fetch_executable_internal(
753786
foreach ($candidateDirs as $d) {
754787
$cnt++;
755788
logmsg(LOG_INFO, " - deleting $d");
756-
system('rm -rf ' . dj_escapeshellarg($d), $retval);
757-
if ($retval !== 0) {
789+
if (!run_command_safe(['rm', '-rf', $d])) {
758790
logmsg(LOG_WARNING, "Deleting '$d' was unsuccessful.");
759791
}
760792
$after = disk_free_space(JUDGEDIR);
@@ -878,10 +910,7 @@ function fetch_executable_internal(
878910
$judgeTask['judgetaskid']
879911
);
880912

881-
$debug_cmd = implode(' ', array_map('dj_escapeshellarg',
882-
[$runpath, $workdir, $tmpfile]));
883-
system($debug_cmd, $retval);
884-
if ($retval !== 0) {
913+
if (!run_command_safe([$runpath, $workdir, $tmpfile])) {
885914
disable('run_script', 'run_script_id', $judgeTask['run_script_id'], "Running '$runpath' failed.");
886915
}
887916

@@ -950,8 +979,7 @@ function fetch_executable_internal(
950979
}
951980

952981

953-
system('mkdir -p ' . dj_escapeshellarg("$workdir/compile"), $retval);
954-
if ($retval !== 0) {
982+
if (!run_command_safe(['mkdir', '-p', "$workdir/compile"])) {
955983
error("Could not create '$workdir/compile'");
956984
}
957985

@@ -964,8 +992,7 @@ function fetch_executable_internal(
964992
if ($lastWorkdir !== $workdir) {
965993
// create chroot environment
966994
logmsg(LOG_INFO, " 🔒 Executing chroot script: '".CHROOT_SCRIPT." start'");
967-
system(LIBJUDGEDIR.'/'.CHROOT_SCRIPT.' start', $retval);
968-
if ($retval!==0) {
995+
if (!run_command_safe([LIBJUDGEDIR.'/'.CHROOT_SCRIPT, 'start'], $retval)) {
969996
logmsg(LOG_ERR, "chroot script exited with exitcode $retval");
970997
disable('judgehost', 'hostname', $myhost, "chroot script exited with exitcode $retval on $myhost");
971998
continue;
@@ -1027,8 +1054,7 @@ function registerJudgehost(string $myhost): void
10271054

10281055
// Create directory where to test submissions
10291056
$workdirpath = JUDGEDIR . "/$myhost/endpoint-$endpointID";
1030-
system('mkdir -p ' . dj_escapeshellarg("$workdirpath/testcase"), $retval);
1031-
if ($retval !== 0) {
1057+
if (!run_command_safe(['mkdir', '-p', "$workdirpath/testcase"])) {
10321058
error("Could not create $workdirpath");
10331059
}
10341060
chmod("$workdirpath/testcase", 0700);
@@ -1106,8 +1132,7 @@ function cleanup_judging(string $workdir) : void
11061132

11071133
// destroy chroot environment
11081134
logmsg(LOG_INFO, " 🔓 Executing chroot script: '".CHROOT_SCRIPT." stop'");
1109-
system(LIBJUDGEDIR.'/'.CHROOT_SCRIPT.' stop', $retval);
1110-
if ($retval!==0) {
1135+
if (!run_command_safe([LIBJUDGEDIR.'/'.CHROOT_SCRIPT, 'stop'], $retval)) {
11111136
logmsg(LOG_ERR, "chroot script exited with exitcode $retval");
11121137
disable('judgehost', 'hostname', $myhost, "chroot script exited with exitcode $retval on $myhost");
11131138
// Just continue here: even though we might continue a current
@@ -1117,8 +1142,7 @@ function cleanup_judging(string $workdir) : void
11171142
}
11181143

11191144
// Evict all contents of the workdir from the kernel fs cache
1120-
system(LIBJUDGEDIR . '/evict ' . dj_escapeshellarg($workdir), $retval);
1121-
if ($retval!==0) {
1145+
if (!run_command_safe([LIBJUDGEDIR . '/evict', $workdir], $retval)) {
11221146
warning("evict script exited with exitcode $retval");
11231147
}
11241148
}
@@ -1128,7 +1152,7 @@ function compile(
11281152
string $workdir,
11291153
string $workdirpath,
11301154
array $compile_config,
1131-
string $cpuset_opt,
1155+
?string $daemonid,
11321156
int $output_storage_limit
11331157
): bool {
11341158
global $myhost, $EXITCODES;
@@ -1148,26 +1172,24 @@ function compile(
11481172
$args = 'hostname=' . urlencode($myhost);
11491173
foreach (['compiler', 'runner'] as $type) {
11501174
if (isset($version_verification[$type . '_version_command'])) {
1175+
if (file_exists($version_output_file)) {
1176+
unlink($version_output_file);
1177+
}
1178+
11511179
$vcscript_content = $version_verification[$type . '_version_command'];
11521180
$vcscript = tempnam(TMPDIR, 'version_check-');
11531181
file_put_contents($vcscript, $vcscript_content);
11541182
chmod($vcscript, 0755);
1155-
$version_cmd = LIBJUDGEDIR . "/version_check.sh " .
1156-
implode(' ', array_map('dj_escapeshellarg', [
1157-
$vcscript,
1158-
$workdir,
1159-
]));
11601183

1161-
if (file_exists($version_output_file)) {
1162-
unlink($version_output_file);
1163-
}
1164-
system($version_cmd, $retval);
1184+
run_command_safe([LIBJUDGEDIR . "/version_check.sh", $vcscript, $workdir], $retval);
1185+
11651186
$versions[$type] = trim(file_get_contents($version_output_file));
11661187
if ($retval !== 0) {
11671188
$versions[$type] =
11681189
"Getting $type version failed with exit code $retval\n"
11691190
. $versions[$type];
11701191
}
1192+
11711193
unlink($vcscript);
11721194
}
11731195
if (isset($versions[$type])) {
@@ -1245,14 +1267,13 @@ function compile(
12451267
}
12461268

12471269
// Compile the program.
1248-
$compile_cmd = LIBJUDGEDIR . "/compile.sh $cpuset_opt " .
1249-
implode(' ', array_map('dj_escapeshellarg', array_merge([
1250-
$execrunpath,
1251-
$workdir,
1252-
], $files)));
1253-
logmsg(LOG_DEBUG, "Compile command: ".$compile_cmd);
1254-
system($compile_cmd, $retval);
1255-
if ($retval!==0) {
1270+
$compile_command_parts = [LIBJUDGEDIR . '/compile.sh'];
1271+
if (isset($daemonid)) {
1272+
$compile_command_parts[] = '-n';
1273+
$compile_command_parts[] = $daemonid;
1274+
}
1275+
array_push($compile_command_parts, $execrunpath, $workdir, ...$files);
1276+
if (!run_command_safe($compile_command_parts, $retval, log_nonzero_exitcode: false)) {
12561277
warning("compile script exited with exitcode $retval");
12571278
}
12581279

@@ -1355,7 +1376,7 @@ function judge(array $judgeTask): bool
13551376
}
13561377

13571378
$workdir = judging_directory($workdirpath, $judgeTask);
1358-
$compile_success = compile($judgeTask, $workdir, $workdirpath, $compile_config, $cpuset_opt, $output_storage_limit);
1379+
$compile_success = compile($judgeTask, $workdir, $workdirpath, $compile_config, $options['daemonid'] ?? null, $output_storage_limit);
13591380
if (!$compile_success) {
13601381
return false;
13611382
}
@@ -1457,37 +1478,39 @@ function judge(array $judgeTask): bool
14571478
// after the first (note that $passCnt starts at 1).
14581479
if ($passCnt > 1) {
14591480
$prevPassdir = $testcasedir . '/' . ($passCnt - 1) . '/feedback';
1460-
system('cp -R ' . dj_escapeshellarg($prevPassdir) . ' ' . dj_escapeshellarg($passdir . '/'));
1461-
system('rm ' . dj_escapeshellarg($passdir . '/feedback/nextpass.in'));
1481+
run_command_safe(['cp', '-R', $prevPassdir, $passdir . '/']);
1482+
run_command_safe(['rm', $passdir . '/feedback/nextpass.in']);
14621483
}
14631484

14641485
// Copy program with all possible additional files to testcase
14651486
// dir. Use hardlinks to preserve space with big executables.
14661487
$programdir = $passdir . '/execdir';
1467-
system('mkdir -p ' . dj_escapeshellarg($programdir), $retval);
1468-
if ($retval!==0) {
1488+
if (!run_command_safe(['mkdir', '-p', $programdir])) {
14691489
error("Could not create directory '$programdir'");
14701490
}
14711491

14721492
foreach (glob("$workdir/compile/*") as $compile_file) {
1473-
system('cp -PRl ' . dj_escapeshellarg($compile_file) . ' ' . dj_escapeshellarg($programdir), $retval);
1474-
if ($retval!==0) {
1493+
if (!run_command_safe(['cp', '-PRl', $compile_file, $programdir])) {
14751494
error("Could not copy program to '$programdir'");
14761495
}
14771496
}
14781497

14791498
$timelimit_str = implode(':', $timelimit['cpu']) . ',' . implode(':', $timelimit['wall']);
1480-
$test_run_cmd = LIBJUDGEDIR . "/testcase_run.sh $cpuset_opt " .
1481-
implode(' ', array_map('dj_escapeshellarg', [
1482-
$input,
1483-
$output,
1484-
$timelimit_str,
1485-
$passdir,
1486-
$run_runpath,
1487-
$compare_runpath,
1488-
$compare_config['compare_args']
1489-
]));
1490-
system($test_run_cmd, $retval);
1499+
$run_command_parts = [LIBJUDGEDIR . '/testcase_run.sh'];
1500+
if (isset($options['daemonid'])) {
1501+
$run_command_parts[] = '-n';
1502+
$run_command_parts[] = $options['daemonid'];
1503+
}
1504+
array_push($run_command_parts,
1505+
$input,
1506+
$output,
1507+
$timelimit_str,
1508+
$passdir,
1509+
$run_runpath,
1510+
$compare_runpath,
1511+
$compare_config['compare_args']
1512+
);
1513+
run_command_safe($run_command_parts, $retval, log_nonzero_exitcode: false);
14911514

14921515
// What does the exitcode mean?
14931516
if (!isset($EXITCODES[$retval])) {

0 commit comments

Comments
 (0)