A segfault in the JVM and an EDR at the bottom of the stack
A Jenkins controller on Windows terminating in a loop on an EXCEPTION_ACCESS_VIOLATION, that is, a segfault on the Windows side, restarting every minute for a whole afternoon.
The story is a good teaching case for two things: how you do not treat an instability and how you read a crash dump without over-reading what you see in it.
The trap we had already fallen into
Even before my diagnosis, the trace of the problem was legible in the history. Similar crashes had happened more than a year earlier, with no change on the Jenkins team’s side, which already pointed to an external factor. The answer given at the time had been to migrate the JVM from one major version to the next, with no root-cause analysis and no post-mortem, in the hope that it would fix the problem. The service stabilised for a while after configuration, then the crashes came back.
That is the classic trap: changing a component because you hope it helps, without knowing why it crashes and mistaking a lull for a resolution. A migration done as blind remediation is not remediation, it is a bet and when the bet seems to win you also lose the chance to understand, until it crashes again.
Read the stack and do not make it say more than it does
The crash dumps were usable and two threads of very different nature were crashing, one in the concurrent GC, the other in the JIT compiler. In both, the same DLL appeared:
V [jvm.dll+0x31acf5] <-- crash here (top of stack)
...
C [ucrtbase.dll+0x2268a]
C [cyinjct.dll+0x11e7] <-- the EDR's injector (bottom of stack)
C [KERNEL32.DLL+0x17ac4]
C [ntdll.dll+0x5a8c1]
The temptation here is to point at the EDR and declare victory.
This is where you have to be honest with the stack: cyinjct.dll appears at the bottom, between KERNEL32 and ucrtbase, which is the pattern of a hook placed on the thread’s entry point, not that of a fault committed by the EDR.
The crash itself happens at the top, in jvm.dll, on internal operations.
In other words, the EDR’s presence proves it injects into the Java process, it does not prove it is the direct cause of the memory corruption.
Confusing “present in the stack” with “guilty” is the most common error in reading a crash dump and I forbade it to myself.
What makes the hypothesis strong without proving it
That said, the presumptions were piling up and they have to be weighed for what they are worth. The same DLL in two crashes, on two unrelated threads, the GC and the JIT. An identical offset in both dumps, matching exactly a crash reported on the EDR vendor’s own forum. An instability that had started with no change on the Jenkins side, which points to an external factor. And an interference of that EDR with processes, through this injection, documented publicly by several third-party vendors, from a database vendor to a browser, each of which ended up blocking the injection or shipping a fix.
Alongside, I kept open the hypotheses I could not rule out: a bug in the JVM itself, a markedly under-sized VM, dynamic memory on the hypervisor side pulling pages at the wrong moment, a second security agent that appeared in no stack but could interfere without showing up in one. A strong presumption is not a proof and saying so is part of the job.
Confirm by elimination, not by conviction
The only clean way to conclude was a sequential elimination plan, from least to most intrusive, with observation between each step, because each isolates a different cause.
First remove the explicitly loaded JVM agent, a quick action on the Jenkins side, depending on no one. If the crashes persist, ask the security team to disable the Java-targeting exploit module for that process, a targeted action that keeps protection on the rest. If it still persists, ask for a full exclusion of the process in the EDR policy, which removes the injection. And if the three steps change nothing, the cause is elsewhere and you fall back on the JVM, memory pressure or the hypervisor.
The value of this gradation is that the result of each step is a verdict: stable after step one, it is the agent; stable after two, it is the exploit module; stable after three, it is the injector; still unstable, it is something else. You are not trying to be right, you are trying to make the environment answer one question at a time.
The aggravating factors, not to be confused with the cause
Two conditions made everything more likely without being the trigger. The VM was tight, total memory barely above the JVM heap, which leaves almost nothing for the OS, the off-heap, the builds and the agents and the concurrent GC remapping memory under that pressure collides all the more with the injector’s hooking. And the coexistence of two security agents on the same machine, an EDR and an XDR, doubles the hooks in the processes, a known aggravating factor for applications that manipulate memory at a low level like a JVM. Resizing the VM and de-conflicting the agents does not “fix” the crash, but removes the ground that favours it and you have to hold that distinction so as not to sell a resize as a resolution, the exact mistake of the JDK migration at the start.
It was the EDR
A migration done without a post-mortem is not remediation, it is a bet whose loss a lull hides and the price of the bet is the chance to understand, which you only get back by crashing again. A DLL present in a crash dump is not thereby the cause: its position in the stack says whether it faulted or was just there and a hook at the bottom of the stack proves injection, not guilt. A strong presumption is handled by an elimination plan where each step makes the environment answer, not by the conviction that leaps to the most satisfying conclusion. And an aggravating factor is not a trigger: the tight VM and the two agents made the crash likely, but confusing them with the cause is setting yourself up to be surprised when you have fixed them and it crashes still.
[edit: 2026] The Ansible implementation of all this: the jenkins-agent role .