{"record":{"id":"dcc9708de16b0ba1","repo":"elastic/elasticsearch","slug":"error-patching-jar-s-sha256-digest-mismatch","errorCode":null,"errorMessage":"Error patching JAR [%s]: SHA256 digest mismatch (%s). This JAR was updated to a version that contains different classes, for which this patcher was not designed. Please check if the patcher still applies correctly, and update the SHA256 digest(s).","messagePattern":"Error patching JAR \\[(.+?)\\]: SHA256 digest mismatch \\((.+?)\\)\\. This JAR was updated to a version that contains different classes, for which this patcher was not designed\\. Please check if the patcher still applies correctly, and update the SHA256 digest\\(s\\)\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/Utils.java","lineNumber":139,"sourceCode":"                                    }\n                                }\n                                manifestEntry.setValue(nonSignatureAttributes);\n                            }\n                            manifest.write(jos);\n                        } else if (unsignJar == false || entryName.matches(\"META-INF/.*\\\\.SF\") == false) {\n                            // Read the entry's data and write it to the new JAR\n                            is.transferTo(jos);\n                        }\n                    }\n                }\n                jos.closeEntry();\n            }\n        } catch (IOException ex) {\n            throw new RuntimeException(ex);\n        }\n\n        if (mismatchedClasses.isEmpty() == false) {\n            throw new IllegalArgumentException(\n                String.format(\n                    Locale.ROOT,\n                    \"\"\"\n                        Error patching JAR [%s]: SHA256 digest mismatch (%s). This JAR was updated to a version that contains different \\\n                        classes, for which this patcher was not designed. Please check if the patcher still \\\n                        applies correctly, and update the SHA256 digest(s).\"\"\",\n                    inputFile.getName(),\n                    mismatchedClasses.stream().map(MismatchInfo::toString).collect(Collectors.joining())\n                )\n            );\n        }\n\n        if (classPatchers.isEmpty() == false) {\n            throw new IllegalArgumentException(\n                String.format(\n                    Locale.ROOT,\n                    \"error patching [%s]: the jar does not contain [%s]\",\n                    inputFile.getName(),","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/Utils.java#L121-L157","documentation":"Thrown by Utils.patchJar after rewriting a JAR when one or more classes targeted by a patcher have a SHA-256 digest that does not match the digest recorded in the PatcherInfo. The patcher is an ASM bytecode transform authored against a specific class shape; the digest guard ensures it is only applied to the exact bytes it was validated on. A mismatch means the upstream dependency shipped different class bytes (a version bump or repackaging) and the patch must be re-validated or the digest updated.","triggerScenarios":"patchJar(inputFile, outputFile, patchers, unsignJar) iterates JAR entries; for each entry matching a PatcherInfo.jarEntryName it computes SHA_256.digest(classBytes) and compares via classPatcher.matches(classSha256). On mismatch it records a MismatchInfo(jarEntryName, expected, found) without applying the transform. After the JAR is fully copied, if mismatchedClasses is non-empty it throws with each class's expected vs found hex digests.","commonSituations":"A dependency version was bumped (e.g. lucene-core, or a shaded jar) so the target class bytes changed; the upstream published a repackaged/rebuilt artifact with the same version but different bytes (reproducibility gap); the patcher's recorded digest was for a different classifier/os; a transitive resolution pulled a different jar than the one the patcher targeted.","solutions":["Read the message: for each mismatched class it prints [class='...', expected='<hex>', found='<hex>']. Decide whether the new bytes are acceptable.","Verify the patcher's ASM transform still applies correctly to the new class bytes (run the build's patch tests / re-derive the transform against the current jar).","If the transform still holds, update the PatcherInfo's classSha256 for each affected class to the 'found' digest and re-run.","Pin the dependency version back to the one the patcher was authored for if the bump was unintentional.","If using unsignJar=true on a signed JAR, confirm the input jar is the exact signed artifact the patcher expects (signature side-channels aside)."],"exampleFix":"// before: patcher recorded digest for lucene-core 9.10.0\nnew PatcherInfo(\"org/apache/lucene/util/Foo.class\",\n    HexFormat.of().parseHex(\"aaaa...\"),  // old expected\n    visitor);\n// after: bump the digest to the 'found' value once the transform is re-validated\nnew PatcherInfo(\"org/apache/lucene/util/Foo.class\",\n    HexFormat.of().parseHex(\"bbbb...\"),  // matches new jar bytes\n    visitor);","handlingStrategy":"validation","validationCode":"import java.security.MessageDigest;\nimport java.util.HexFormat;\nimport java.util.jar.JarFile;\nboolean digestsMatch(File jar, Collection<PatcherInfo> patchers) throws Exception {\n    MessageDigest sha = MessageDigest.getInstance(\"SHA-256\");\n    var byName = patchers.stream().collect(\n        java.util.stream.Collectors.toMap(PatcherInfo::jarEntryName, p -> p));\n    try (var jf = new JarFile(jar)) {\n        for (var name : byName.keySet()) {\n            var e = jf.getJarEntry(name);\n            if (e == null) return false;\n            byte[] bytes = jf.getInputStream(e).readAllBytes();\n            if (!byName.get(name).matches(sha.digest(bytes))) return false;\n        }\n    }\n    return true;\n}\n// assert digestsMatch(inputJar, patchers);  // before invoking Utils.patchJar","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pin dependency versions so the patched jar does not drift underneath the patcher.","When bumping a patched dependency, re-derive and re-test the ASM transform, then update the recorded SHA-256 digests.","Add a test that asserts every PatcherInfo's digest matches the resolved jar bytes.","Beware non-reproducible upstream rebuilds that change bytes at the same version."],"tags":["gradle","jar-patching","asm","sha256","dependencies","bytecode"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T12:17:08.281Z"}