# Surefire forked VM terminated

Classification: environment
Frameworks: JUnit, Maven, CI
Canonical: https://automationtester.in/failures/maven-surefire-forked-vm-terminated

## Recognizable error

```text
The forked VM terminated without properly saying goodbye. VM crash or System.exit called?
```

## What it means

Surefire runs tests in a forked JVM that reports back to Maven over a channel and says goodbye at the end. This fork exited without saying it, so Maven cannot tell what passed and fails the build.

## Minimal repro

Run on 2026-10-23 with maven-surefire-plugin 3.6.0, JUnit Jupiter 6.1.3, Apache Maven 3.10.0, Eclipse Temurin 21.0.12.1, image maven:3-eclipse-temurin-21, linux aarch64.

pom.xml:

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>surefire-goodbye</artifactId>
  <version>1.0</version>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.6.0</version>
      </plugin>
    </plugins>
  </build>
</project>
```

src/test/java/example/ExitTest.java:

```java
package example;

import org.junit.jupiter.api.Test;

class ExitTest {
    @Test
    void callsSystemExit() {
        System.exit(3);
    }
}
```

command:

```shell
docker run --rm -v "$PWD":/w -w /w maven:3-eclipse-temurin-21 mvn -q test
```

output:

```text
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.6.0:test (default-test) on project surefire-goodbye:
[ERROR]
[ERROR] See /w/target/surefire-reports for the individual test results.
[ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
[ERROR] The forked VM terminated without properly saying goodbye. VM crash or System.exit called?
[ERROR] Command was /bin/sh -c cd '/w' && '/opt/java/openjdk/bin/java' '-jar' '/w/target/surefire/surefirebooter-<timestamp>.jar' ...
[ERROR] Error occurred in starting fork, check output in log
[ERROR] Process Exit Code: 3
[ERROR] Crashed tests:
[ERROR] example.ExitTest

# The SurefireBooterForkException stack trace that follows is trimmed.
```

## Ranked causes, checks, and fixes

### 1. Code under test, or a library, calls System.exit

Surefire's FAQ says it does not support tests or referenced libraries calling System.exit() at any time. The fork just ends, and Process Exit Code is whatever was passed to exit. CLI entry points and some plugin-style libraries do this on errors.

Diagnostic: Read Process Exit Code and the Crashed tests: list that Surefire appends to the message. An exit code you recognise from your own code (1, 2, 3) points here.

Fix: Keep exit calls in main only, have the testable code return a status or throw, and test that. On JDK 24 and later you cannot trap exit with a SecurityManager in tests: JEP 486 disabled it permanently.

### 2. The container or OS killed the fork for memory

An exit code of 137 is 128 + 9, the number of SIGKILL, which is how the Linux OOM killer and container memory limits end a process; a shell killed with kill -9 reports exactly 137. With forkCount above 1, every fork brings its own heap.

Diagnostic: Check for 137 in Process Exit Code and for an OOM kill in the runner or container events. Add up forkCount × (heap + metaspace + thread stacks) against the job's memory limit.

Fix: Set an explicit -Xmx through argLine so the forks fit the limit, lower forkCount, or give the job more memory.

### 3. The JVM itself crashed

A native library, a JIT bug or a corrupted JDK can crash the fork. The FAQ says to look for hs_err files and the console output, and that a crash that only appears after a CI agent has been running a while often means the suite leaks an OS-level resource.

Diagnostic: Look for hs_err_pid*.log files, which the JVM writes to its working directory, and for *.dump and *.dumpstream files in target/surefire-reports.

Fix: Fix or update whatever the hs_err stack names (often a native dependency), or pin a known-good JDK build.

## Verified fix

Here the exit code was 3, the value the test passed to System.exit, so the fix is to keep the exit in main and test the status it would return.

src/main/java/example/Cli.java:

```java
package example;

public final class Cli {
    static int run(String[] args) {
        return 3;
    }

    public static void main(String[] args) {
        System.exit(run(args));
    }
}
```

src/test/java/example/ExitTest.java:

```java
package example;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class ExitTest {
    @Test
    void returnsTheExitStatus() {
        assertEquals(3, Cli.run(new String[0]));
    }
}
```

Evidence: Run on 2026-10-23 in the maven:3-eclipse-temurin-21 image (Maven 3.10.0, Temurin 21.0.12.1) on a colima VM: with the test asserting on Cli.run instead of calling System.exit, the same mvn -q test exited 0. Adding forkCount 2 and argLine -Xmx768m to the Surefire configuration first, with the exit still in the test, changed nothing: the build failed again with Process Exit Code: 3. Sizing the forks is the fix for an exit code of 137, which this run did not reproduce.

## When this is not the cause

- **Failing assertions.** Ordinary test failures are reported per test, with their assertion messages, by a fork that then exits normally. The goodbye message means the fork died; the tests in Crashed tests: did not get to report a result at all.
- **The corrupted channel warning.** Corrupted channel by directly writing to native stream in forked JVM is a separate FAQ entry about output written straight to the native stream. It is a warning about reporting, not the crash itself.
- **Maven's own memory.** MAVEN_OPTS sizes the Maven process, not the forks. Surefire documents argLine as the JVM options for forked executions, so that is where fork heap goes.

## Prevention

- Keep System.exit out of anything a test can reach.
- Set -Xmx for forks explicitly instead of relying on container ergonomics.
- Archive target/surefire-reports and hs_err files from failed CI runs.

## Primary references

- [Surefire FAQ: the forked VM terminated without saying goodbye](https://maven.apache.org/surefire/maven-surefire-plugin/faq.html)
- [Surefire source: ForkStarter.java](https://github.com/apache/maven-surefire/blob/master/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java)
- [Surefire: test goal parameters (argLine, forkCount)](https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html)
- [JEP 486: Permanently disable the Security Manager](https://openjdk.org/jeps/486)

Triage this failure locally: https://automationtester.in/tools/test-failure-triage