Skip to content

Commit 8511dad

Browse files
authored
Merge pull request #398 from topcoder-platform/PM-4299-1
PM-4299: allow rerun dispatch when ECS listing is denied
2 parents b7546e6 + 8caa5d1 commit 8511dad

14 files changed

Lines changed: 897 additions & 183 deletions

File tree

ECS_RUNNER_LIFETIME.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Inside the task, the trusted Java parent runner:
9898
1. Fetches the challenge config, tester JAR, and tester metadata from `marathon-match-api-v6`.
9999
2. Downloads the submission from the configured Submission API URL.
100100
3. Posts an initial progress callback for `PROVISIONAL` and `SYSTEM` runs.
101-
4. Starts the tester in an isolated child JVM. Submitted solution commands run as the separate `scorer` user with scrubbed environment, restricted filesystem access, and no outbound INET/INET6 socket creation.
101+
4. Starts the tester in an isolated child JVM. Submitted solution commands run as the separate `scorer` user with scrubbed environment, restricted filesystem access, and no outbound INET/INET6 socket creation. For standard generic-runner seeds, the child invokes `mm-scorer-isolate --cleanup-scorer-state` before and after each test case so scorer-owned files in fixed writable roots are reset between seeds.
102102
5. Uploads public/private artifacts back through Submission API.
103103
6. Posts the final scoring callback to `POST /v6/marathon-match/internal/scoring-results`.
104104
7. Exits with code `0` only after the final callback succeeds.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ The ECS task still needs trusted outbound access to fetch challenge config, down
157157
- The trusted parent runner holds `ACCESS_TOKEN`, performs network calls, and never loads untrusted submission code directly.
158158
- The parent launches a separate child JVM through `mm-runner-isolate` with a scrubbed environment, so submission processes do not inherit the bearer token or other runner env vars.
159159
- Generic submitted solution commands run through `mm-scorer-isolate` as the separate non-root `scorer` user. Downloaded tester JARs and serialized scorer config are kept runner-owned mode `0400`, so submitted code cannot read or modify them from `/tmp`.
160+
- Standard generic-runner seed execution asks `mm-scorer-isolate` to reset scorer-owned writable state before and after each test case. The cleanup helper only scans fixed writable roots such as `/tmp`, `/var/tmp`, `/dev/shm`, and the scorer home, and only removes entries owned by the scorer UID.
160161
- Generic submitted solution commands also run under a filesystem allowlist that permits runtime/toolchain reads and scorer temp writes but does not permit reading infrastructure paths such as `/etc/hostname`, `/etc/resolv.conf`, `/proc/self/cgroup`, `/proc/self/mounts`, or proc network tables.
161162
- Native wrappers block `io_uring` and creation of non-`AF_UNIX` sockets for submitted solution processes and their fork/exec children, so submissions cannot open live outbound network connections.
162163
- Standard Topcoder Marathon testers run through the generic runner flow, which creates the callback score payload from trusted runner code. Custom tester `runTester(...)` result maps remain supported for advanced cases.

ecs-runner/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ This image is the runtime container for marathon match scoring tasks launched by
1919
- The container entrypoint starts as `root`. Do not override the ECS task-definition `user` for this container; root is needed for trusted bootstrap work and for preparing runner-owned files before the child JVM starts.
2020
- The trusted parent runner performs network bootstrap work: fetch challenge config, download tester/submission artifacts, upload artifacts, and post the scoring callback.
2121
- The tester executes in a separate child JVM launched through `mm-runner-isolate` as uid/gid `10001` (`runner`) with a scrubbed environment, so `ACCESS_TOKEN` and other runner env vars are not inherited by untrusted code.
22-
- Generic submitted solution commands execute through the setuid-root `mm-scorer-isolate` bridge as uid/gid `10002` (`scorer`). The bridge drops its supervisor back to the invoking `runner` uid after it forks the solution child, then supervises the solution process group so tester timeouts can still terminate lower-privilege processes.
22+
- Generic submitted solution commands execute through the setuid-root `mm-scorer-isolate` bridge as uid/gid `10002` (`scorer`). The bridge drops its supervisor back to the invoking `runner` uid after it forks the solution child, retaining only `CAP_KILL` so tester timeouts can still terminate the lower-privilege solution process group.
2323
- Downloaded tester JARs are mode `0400` runner-owned files. The serialized scorer config starts as a mode `0400` runner-owned handoff file and the child JVM deletes it immediately after loading it, before tester or submitted solution code executes.
24-
- Generic Marathon seed execution resets scorer-owned writable state, including `/tmp`, before and after every test case, so files written by one seed are not visible to later seeds in the same submission run.
24+
- Generic Marathon seed execution resets scorer-owned writable state before and after every test case, so files written by one seed are not visible to later seeds in the same submission run. The Java child invokes the setuid `mm-scorer-isolate --cleanup-scorer-state` mode, which scans only fixed writable roots such as `/tmp`, `/var/tmp`, `/dev/shm`, and the scorer home, and removes only entries owned by uid `10002`.
2525
- Generic submitted solution commands run under a Landlock filesystem allowlist. Runtime and toolchain files are readable, `/tmp` and the scorer home are writable, and infrastructure-revealing paths such as `/etc/hostname`, `/etc/resolv.conf`, `/proc/self/cgroup`, `/proc/self/mounts`, and proc network tables are not readable by submitted code.
2626
- Artifact previews and artifact zip uploads include only non-symlink regular files from the runner artifact directories. Submitted symlinks are ignored instead of being dereferenced by the trusted parent runner.
2727
- Submitted solution processes and their fork/exec children cannot use `io_uring` and can create only `AF_UNIX` sockets. These restrictions are kernel seccomp filters inherited across fork/exec, so clearing `LD_PRELOAD` in a spawned child process does not restore INET or INET6 socket access. Outbound network access from the submission itself is therefore blocked even though the parent runner still has the trusted egress it needs.

ecs-runner/boilerplate/src/main/java/com/topcoder/marathon/MarathonTester.java

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public abstract class MarathonTester {
3535
private final List<BufferedWriter> solInputWriters = new ArrayList<BufferedWriter>();
3636
private static final int maxSolutionOutputLength = 10_000_000;
3737
private static final long processDrainTimeoutMillis = 250;
38+
private static final long processTimeoutDestroyMillis = 250;
3839
private static final long errorReaderDrainTimeoutMillis = 250;
3940
private BufferedWriter solOutputWriter;
4041
private BufferedWriter solErrorWriter;
@@ -110,17 +111,20 @@ protected final void startTime() {
110111
lastTimeoutThread = new Thread() {
111112
public void run() {
112113
try {
113-
boolean finished = process.waitFor(timeLimit - elapsedTime, TimeUnit.NANOSECONDS);
114+
long remainingTime = Math.max(1L, timeLimit - elapsedTime);
115+
boolean finished = process.waitFor(remainingTime, TimeUnit.NANOSECONDS);
114116
if (!finished) {
117+
boolean notifyTimeout = false;
115118
synchronized (timeLock) {
116119
if (lastStart > 0) elapsedTime += System.nanoTime() - lastStart;
117120
lastStart = 0;
118-
if (process != null) process.destroyForcibly();
119121
if (!timeout) {
120122
timeout = true;
121-
timeout();
123+
notifyTimeout = true;
122124
}
123125
}
126+
terminateTimedOutProcess();
127+
if (notifyTimeout) timeout();
124128
}
125129
} catch (Exception e) {
126130
}
@@ -186,6 +190,73 @@ public final double runTest() {
186190
return score;
187191
}
188192

193+
/**
194+
* Terminates the submitted solution process after its measured execution
195+
* time expires and closes runner-side streams so blocked tester reads unwind.
196+
*
197+
* <p>The ECS scorer command is wrapped by a small native supervisor. A
198+
* graceful destroy gives that wrapper a catchable signal it can relay to the
199+
* low-privilege scorer process group before this method escalates to a hard
200+
* process kill.
201+
*/
202+
private void terminateTimedOutProcess() {
203+
Process processToStop = process;
204+
if (processToStop != null) {
205+
processToStop.destroy();
206+
try {
207+
if (!processToStop.waitFor(processTimeoutDestroyMillis, TimeUnit.MILLISECONDS)) {
208+
processToStop.destroyForcibly();
209+
processToStop.waitFor(processDrainTimeoutMillis, TimeUnit.MILLISECONDS);
210+
}
211+
} catch (InterruptedException e) {
212+
Thread.currentThread().interrupt();
213+
processToStop.destroyForcibly();
214+
} catch (Exception e) {
215+
processToStop.destroyForcibly();
216+
}
217+
}
218+
219+
closeSolutionStreamsAfterTimeout();
220+
}
221+
222+
/**
223+
* Closes solution pipes after timeout termination.
224+
*
225+
* <p>Some failed process-tree terminations can leave descendant processes
226+
* holding the stdout pipe open. Closing the Java-side reader prevents
227+
* {@link #readLine()} from blocking forever after the timeout has already
228+
* been recorded.
229+
*/
230+
private void closeSolutionStreamsAfterTimeout() {
231+
for (BufferedWriter out : solInputWriters) {
232+
try {
233+
out.close();
234+
} catch (Exception e) {
235+
}
236+
}
237+
if (solOutputReader != null) {
238+
try {
239+
solOutputReader.close();
240+
} catch (Exception e) {
241+
}
242+
}
243+
if (solOutputWriter != null) {
244+
try {
245+
solOutputWriter.close();
246+
} catch (Exception e) {
247+
}
248+
}
249+
if (solErrorReader != null) {
250+
solErrorReader.closeAndWait(errorReaderDrainTimeoutMillis);
251+
solutionError = solErrorReader.getOutput();
252+
} else if (solErrorWriter != null) {
253+
try {
254+
solErrorWriter.close();
255+
} catch (Exception e) {
256+
}
257+
}
258+
}
259+
189260
protected final void writeLine(int v) throws Exception {
190261
writeLine(String.valueOf(v));
191262
}

0 commit comments

Comments
 (0)