Skip to content

Commit a5200b5

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 a5200b5

File tree

1 file changed

+86
-64
lines changed

1 file changed

+86
-64
lines changed

judge/judgedaemon.main.php

Lines changed: 86 additions & 64 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])) {
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])) {
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])) {
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,9 +693,8 @@ 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) {
664-
error("chroot validation check exited with exitcode $retval");
696+
if (!run_command_safe([LIBJUDGEDIR.'/'.CHROOT_SCRIPT, 'check'])) {
697+
error("chroot validation check failed");
665698
}
666699

667700
foreach ($endpoints as $id => $endpoint) {
@@ -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,9 +1142,8 @@ 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) {
1122-
warning("evict script exited with exitcode $retval");
1145+
if (!run_command_safe([LIBJUDGEDIR . '/evict', $workdir])) {
1146+
warning("evict script failed, continuing gracefully");
11231147
}
11241148
}
11251149

@@ -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,16 +1267,14 @@ 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) {
1256-
warning("compile script exited with exitcode $retval");
1270+
$compile_command_parts = [LIBJUDGEDIR . '/compile.sh'];
1271+
if (isset($daemonid)) {
1272+
$compile_command_parts[] = '-n';
1273+
$compile_command_parts[] = $daemonid;
12571274
}
1275+
array_push($compile_command_parts, $execrunpath, $workdir, ...$files);
1276+
// Note that the $retval is handled further down after reading/writing metadata.
1277+
run_command_safe($compile_command_parts, $retval, log_nonzero_exitcode: false);
12581278

12591279
$compile_output = '';
12601280
if (is_readable($workdir . '/compile.out')) {
@@ -1355,7 +1375,7 @@ function judge(array $judgeTask): bool
13551375
}
13561376

13571377
$workdir = judging_directory($workdirpath, $judgeTask);
1358-
$compile_success = compile($judgeTask, $workdir, $workdirpath, $compile_config, $cpuset_opt, $output_storage_limit);
1378+
$compile_success = compile($judgeTask, $workdir, $workdirpath, $compile_config, $options['daemonid'] ?? null, $output_storage_limit);
13591379
if (!$compile_success) {
13601380
return false;
13611381
}
@@ -1457,37 +1477,39 @@ function judge(array $judgeTask): bool
14571477
// after the first (note that $passCnt starts at 1).
14581478
if ($passCnt > 1) {
14591479
$prevPassdir = $testcasedir . '/' . ($passCnt - 1) . '/feedback';
1460-
system('cp -R ' . dj_escapeshellarg($prevPassdir) . ' ' . dj_escapeshellarg($passdir . '/'));
1461-
system('rm ' . dj_escapeshellarg($passdir . '/feedback/nextpass.in'));
1480+
run_command_safe(['cp', '-R', $prevPassdir, $passdir . '/']);
1481+
run_command_safe(['rm', $passdir . '/feedback/nextpass.in']);
14621482
}
14631483

14641484
// Copy program with all possible additional files to testcase
14651485
// dir. Use hardlinks to preserve space with big executables.
14661486
$programdir = $passdir . '/execdir';
1467-
system('mkdir -p ' . dj_escapeshellarg($programdir), $retval);
1468-
if ($retval!==0) {
1487+
if (!run_command_safe(['mkdir', '-p', $programdir])) {
14691488
error("Could not create directory '$programdir'");
14701489
}
14711490

14721491
foreach (glob("$workdir/compile/*") as $compile_file) {
1473-
system('cp -PRl ' . dj_escapeshellarg($compile_file) . ' ' . dj_escapeshellarg($programdir), $retval);
1474-
if ($retval!==0) {
1492+
if (!run_command_safe(['cp', '-PRl', $compile_file, $programdir])) {
14751493
error("Could not copy program to '$programdir'");
14761494
}
14771495
}
14781496

14791497
$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);
1498+
$run_command_parts = [LIBJUDGEDIR . '/testcase_run.sh'];
1499+
if (isset($options['daemonid'])) {
1500+
$run_command_parts[] = '-n';
1501+
$run_command_parts[] = $options['daemonid'];
1502+
}
1503+
array_push($run_command_parts,
1504+
$input,
1505+
$output,
1506+
$timelimit_str,
1507+
$passdir,
1508+
$run_runpath,
1509+
$compare_runpath,
1510+
$compare_config['compare_args']
1511+
);
1512+
run_command_safe($run_command_parts, $retval, log_nonzero_exitcode: false);
14911513

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

0 commit comments

Comments
 (0)