Un segfault dans la JVM et un EDR au fond de la stack
Un controller Jenkins sous Windows qui se termine en boucle sur un EXCEPTION_ACCESS_VIOLATION, c’est-à-dire un segfault côté Windows et qui redémarre toutes les minutes pendant une après-midi entière.
L’histoire est un bon cas d’école de deux choses: comment on ne traite pas une instabilité et comment on lit un crash dump sans sur-interpréter ce qu’on y voit.
Le piège dans lequel on était déjà tombé
Avant même mon diagnostic, la trace du problème était lisible dans l’historique. Des crashs similaires avaient eu lieu plus d’un an plus tôt, sans aucune modification côté équipe Jenkins, ce qui pointait déjà vers un facteur externe. La réponse apportée à l’époque avait été de migrer la JVM d’une version majeure à la suivante, sans analyse de cause racine ni post-mortem, dans l’espoir que ça règle le problème. Le service s’est stabilisé un temps après configuration, puis les crashs ont repris.
C’est le piège classique: changer un composant parce qu’on espère que ça aide, sans savoir pourquoi ça plante et confondre une accalmie avec une résolution. Une migration faite comme remédiation à l’aveugle n’est pas une remédiation, c’est un pari et quand le pari semble gagner on perd en plus l’occasion de comprendre, jusqu’à ce que ça replante.
Lire la stack et ne pas lui faire dire plus qu’elle ne dit
Les crash dumps étaient exploitables et deux threads de nature très différente plantaient, l’un dans le GC concurrent, l’autre dans le compilateur JIT. Dans les deux, la même DLL apparaissait:
V [jvm.dll+0x31acf5] <-- crash ici (haut de stack)
...
C [ucrtbase.dll+0x2268a]
C [cyinjct.dll+0x11e7] <-- l'injecteur de l'EDR (bas de stack)
C [KERNEL32.DLL+0x17ac4]
C [ntdll.dll+0x5a8c1]
La tentation, ici, est de pointer l’EDR et de crier victoire.
C’est là qu’il faut être honnête avec la stack: cyinjct.dll apparaît en bas, entre KERNEL32 et ucrtbase, ce qui est le motif d’un hook posé sur le point d’entrée du thread, pas celui d’une faute commise par l’EDR.
Le crash lui-même se produit en haut, dans jvm.dll, sur des opérations internes.
Autrement dit, la présence de l’EDR prouve qu’il s’injecte dans le processus Java, elle ne prouve pas qu’il est la cause directe de la corruption mémoire.
Confondre “présent dans la stack” et “coupable” est l’erreur la plus courante de lecture de crash dump et je me la suis interdite.
Ce qui rend l’hypothèse forte sans la démontrer
Cela dit, les présomptions s’accumulaient et il faut les peser pour ce qu’elles valent. La même DLL dans deux crashs, sur deux threads sans rapport, le GC et le JIT. Un offset identique dans les deux dumps, qui correspondait exactement à un crash reporté sur le forum de l’éditeur de l’EDR. Une instabilité qui avait démarré sans changement côté Jenkins, ce qui désigne un facteur externe. Et une interférence de cet EDR avec les processus, via cette injection, documentée publiquement par plusieurs éditeurs tiers, d’un éditeur de base de données à un navigateur, chacun ayant fini par bloquer l’injection ou publier un correctif.
À côté, je gardais ouvertes les hypothèses que je ne pouvais pas écarter: un bug de la JVM elle-même, une VM nettement sous-dimensionnée, de la mémoire dynamique côté hyperviseur retirant des pages au mauvais moment, un second agent de sécurité qui n’apparaissait dans aucune stack mais pouvait interférer sans s’y montrer. Une forte présomption n’est pas une preuve et le dire fait partie du travail.
Confirmer par élimination, pas par conviction
La seule façon propre de conclure, c’était un plan d’élimination séquentiel, du moins intrusif au plus intrusif, avec observation entre chaque étape, parce que chacune isole une cause différente.
D’abord retirer l’agent JVM chargé explicitement, action rapide et côté Jenkins, sans dépendre de personne. Si les crashs persistent, demander à l’équipe sécurité de désactiver le module d’exploit visant Java pour ce processus, une action ciblée qui garde la protection sur le reste. Si ça persiste encore, demander l’exclusion complète du processus dans la policy de l’EDR, ce qui supprime l’injection. Et si les trois étapes ne changent rien, la cause est ailleurs et on retombe sur la JVM, la pression mémoire ou l’hyperviseur.
L’intérêt de cette gradation, c’est que le résultat de chaque étape est un verdict: stable après l’étape un, c’est l’agent; stable après la deux, c’est le module d’exploit; stable après la trois, c’est l’injecteur; toujours instable, c’est autre chose. On ne cherche pas à avoir raison, on cherche à ce que l’environnement réponde à une question à la fois.
Les facteurs aggravants, qu’il ne faut pas confondre avec la cause
Deux conditions rendaient tout plus probable sans être le déclencheur. La VM était tendue, une mémoire totale à peine supérieure au heap de la JVM, ce qui ne laisse presque rien pour l’OS, le hors-heap, les builds et les agents et le GC concurrent qui remappe de la mémoire sous cette contrainte entre d’autant plus en collision avec le hooking de l’injecteur. Et la cohabitation de deux agents de sécurité sur la même machine, un EDR et un XDR, double les hooks dans les processus, facteur aggravant connu pour les applications qui manipulent la mémoire à bas niveau comme une JVM. Redimensionner la VM et déconflictualiser les agents ne “corrige” pas le crash, mais retire le terrain qui le favorise et il faut tenir la distinction pour ne pas vendre un redimensionnement comme une résolution, l’erreur exacte de la migration de JDK du début.
C’était l’EDR
Une migration faite sans post-mortem n’est pas une remédiation, c’est un pari dont une accalmie masque la perte et le prix du pari est l’occasion de comprendre, qu’on ne récupère qu’en replantant. Une DLL présente dans un crash dump n’est pas pour autant la cause: la position dans la stack dit si elle a fauté ou si elle était juste là et un hook en bas de stack prouve l’injection, pas la culpabilité. Une forte présomption se traite par un plan d’élimination où chaque étape fait répondre l’environnement, pas par la conviction qui saute à la conclusion la plus satisfaisante. Et un facteur aggravant n’est pas un déclencheur: la VM tendue et les deux agents rendaient le crash probable, mais les confondre avec la cause, c’est se préparer à être surpris quand on les aura corrigés et que ça plantera encore.
[edit: 2026] L’implémentation Ansible de tout ceci: le rôle jenkins-agent .