Skip to content

Commit fd17d22

Browse files
committed
refactor patchJar, add (optional) match on class bytes SHA256 digest
2 parents 47e352f + f9980f2 commit fd17d22

File tree

4 files changed

+194
-76
lines changed

4 files changed

+194
-76
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.gradle.internal.dependencies.patches;
11+
12+
import org.objectweb.asm.ClassVisitor;
13+
import org.objectweb.asm.ClassWriter;
14+
15+
import java.util.Arrays;
16+
import java.util.HexFormat;
17+
import java.util.function.Function;
18+
19+
public record PatcherInfo(String jarEntryName, byte[] classSha256, Function<ClassWriter, ClassVisitor> visitorFactory) {
20+
/**
21+
* Creates a patcher info entry, linking a jar entry path name and its SHA256 digest to a patcher factory (a factory to create an ASM
22+
* visitor)
23+
* @param jarEntryName the jar entry path, as a string
24+
* @param classSha256 the SHA256 digest of the class bytes, as a HEX string
25+
* @param visitorFactory the factory to create an ASM visitor from a ASM writer
26+
*/
27+
public static PatcherInfo classPatcher(String jarEntryName, String classSha256, Function<ClassWriter, ClassVisitor> visitorFactory) {
28+
return new PatcherInfo(jarEntryName, HexFormat.of().parseHex(classSha256), visitorFactory);
29+
}
30+
31+
boolean matches(byte[] otherClassSha256) {
32+
if (this.classSha256 == null) {
33+
return true;
34+
}
35+
return Arrays.equals(this.classSha256, otherClassSha256);
36+
}
37+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.gradle.internal.dependencies.patches;
11+
12+
import org.objectweb.asm.ClassReader;
13+
import org.objectweb.asm.ClassWriter;
14+
15+
import java.io.File;
16+
import java.io.FileOutputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.security.MessageDigest;
20+
import java.security.NoSuchAlgorithmException;
21+
import java.util.Collection;
22+
import java.util.Enumeration;
23+
import java.util.HexFormat;
24+
import java.util.Locale;
25+
import java.util.function.Function;
26+
import java.util.jar.JarEntry;
27+
import java.util.jar.JarFile;
28+
import java.util.jar.JarOutputStream;
29+
import java.util.stream.Collectors;
30+
31+
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
32+
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
33+
34+
public class Utils {
35+
36+
private static final MessageDigest SHA_256;
37+
38+
static {
39+
try {
40+
SHA_256 = MessageDigest.getInstance("SHA-256");
41+
} catch (NoSuchAlgorithmException e) {
42+
throw new RuntimeException(e);
43+
}
44+
}
45+
46+
/**
47+
* Patches the classes in the input JAR file, using the collection of patchers. If the patcher info specify a SHA256 digest, and
48+
* the class to patch does not match it, an IllegalArgumentException is thrown.
49+
* If the input file does not contain all the classes to patch specified in the patcher info collection, an IllegalArgumentException
50+
* is also thrown.
51+
* @param inputFile the JAR file to patch
52+
* @param outputFile the output (patched) JAR file
53+
* @param patchers list of patcher info (classes to patch (jar entry name + optional SHA256 digest) and ASM visitor to transform them)
54+
*/
55+
public static void patchJar(File inputFile, File outputFile, Collection<PatcherInfo> patchers) {
56+
var classPatchers = patchers.stream().collect(Collectors.toMap(PatcherInfo::jarEntryName, Function.identity()));
57+
try (JarFile jarFile = new JarFile(inputFile); JarOutputStream jos = new JarOutputStream(new FileOutputStream(outputFile))) {
58+
Enumeration<JarEntry> entries = jarFile.entries();
59+
while (entries.hasMoreElements()) {
60+
JarEntry entry = entries.nextElement();
61+
String entryName = entry.getName();
62+
// Add the entry to the new JAR file
63+
jos.putNextEntry(new JarEntry(entryName));
64+
65+
var classPatcher = classPatchers.remove(entryName);
66+
if (classPatcher != null) {
67+
byte[] classToPatch = jarFile.getInputStream(entry).readAllBytes();
68+
var classSha256 = SHA_256.digest(classToPatch);
69+
70+
if (classPatcher.matches(classSha256) == false) {
71+
throw new IllegalArgumentException(
72+
String.format(
73+
Locale.ROOT,
74+
"error patching JAR [%s]: SHA256 digest mismatch for class [%s] (expected: [%s], found: [%s]). "
75+
+ "This JAR was updated to a version that contains a different class, for which this patcher was not "
76+
+ "designed for. Please check if the patcher still applies correctly to this class, and update its "
77+
+ "SHA256 digest.",
78+
inputFile.getName(),
79+
classPatcher.jarEntryName(),
80+
HexFormat.of().formatHex(classPatcher.classSha256()),
81+
HexFormat.of().formatHex(classSha256)
82+
)
83+
);
84+
}
85+
86+
ClassReader classReader = new ClassReader(classToPatch);
87+
ClassWriter classWriter = new ClassWriter(classReader, COMPUTE_MAXS | COMPUTE_FRAMES);
88+
classReader.accept(classPatcher.visitorFactory().apply(classWriter), 0);
89+
jos.write(classWriter.toByteArray());
90+
} else {
91+
// Read the entry's data and write it to the new JAR
92+
try (InputStream is = jarFile.getInputStream(entry)) {
93+
is.transferTo(jos);
94+
}
95+
}
96+
jos.closeEntry();
97+
}
98+
} catch (IOException ex) {
99+
throw new RuntimeException(ex);
100+
}
101+
102+
if (classPatchers.isEmpty() == false) {
103+
throw new IllegalArgumentException(
104+
String.format(
105+
Locale.ROOT,
106+
"error patching [%s]: the jar does not contain [%s]",
107+
inputFile.getName(),
108+
String.join(", ", classPatchers.keySet())
109+
)
110+
);
111+
}
112+
}
113+
}

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/hdfs/HdfsClassPatcher.java

Lines changed: 42 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
package org.elasticsearch.gradle.internal.dependencies.patches.hdfs;
1111

12+
import org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo;
13+
import org.elasticsearch.gradle.internal.dependencies.patches.Utils;
1214
import org.gradle.api.artifacts.transform.CacheableTransform;
1315
import org.gradle.api.artifacts.transform.InputArtifact;
1416
import org.gradle.api.artifacts.transform.TransformAction;
@@ -20,52 +22,64 @@
2022
import org.gradle.api.tasks.Input;
2123
import org.gradle.api.tasks.Optional;
2224
import org.jetbrains.annotations.NotNull;
23-
import org.objectweb.asm.ClassReader;
24-
import org.objectweb.asm.ClassVisitor;
25-
import org.objectweb.asm.ClassWriter;
2625

2726
import java.io.File;
28-
import java.io.FileOutputStream;
29-
import java.io.IOException;
30-
import java.io.InputStream;
31-
import java.util.Enumeration;
32-
import java.util.HashMap;
3327
import java.util.List;
34-
import java.util.Locale;
35-
import java.util.Map;
36-
import java.util.function.Function;
37-
import java.util.jar.JarEntry;
38-
import java.util.jar.JarFile;
39-
import java.util.jar.JarOutputStream;
4028
import java.util.regex.Pattern;
4129

42-
import static java.util.Map.entry;
43-
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
44-
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
30+
import static org.elasticsearch.gradle.internal.dependencies.patches.PatcherInfo.classPatcher;
4531

4632
@CacheableTransform
4733
public abstract class HdfsClassPatcher implements TransformAction<HdfsClassPatcher.Parameters> {
4834

49-
record JarPatchers(String artifactTag, Pattern artifactPattern, Map<String, Function<ClassWriter, ClassVisitor>> jarPatchers) {}
35+
record JarPatchers(String artifactTag, Pattern artifactPattern, List<PatcherInfo> jarPatchers) {}
5036

5137
static final List<JarPatchers> allPatchers = List.of(
5238
new JarPatchers(
5339
"hadoop-common",
5440
Pattern.compile("hadoop-common-(?!.*tests)"),
55-
Map.ofEntries(
56-
entry("org/apache/hadoop/util/ShutdownHookManager.class", ShutdownHookManagerPatcher::new),
57-
entry("org/apache/hadoop/util/Shell.class", ShellPatcher::new),
58-
entry("org/apache/hadoop/security/UserGroupInformation.class", SubjectGetSubjectPatcher::new)
41+
List.of(
42+
classPatcher(
43+
"org/apache/hadoop/util/ShutdownHookManager.class",
44+
"90641e0726fc9372479728ef9b7ae2be20fb7ab4cddd4938e55ffecadddd4d94",
45+
ShutdownHookManagerPatcher::new
46+
),
47+
classPatcher(
48+
"org/apache/hadoop/util/Shell.class",
49+
"8837c7f3eeda3f658fc3d6595f18e77a4558220ff0becdf3e175fa4397a6fd0c",
50+
ShellPatcher::new
51+
),
52+
classPatcher(
53+
"org/apache/hadoop/security/UserGroupInformation.class",
54+
"3c34bbc2716a6c8f4e356e78550599b0a4f01882712b4f7787d032fb10527212",
55+
SubjectGetSubjectPatcher::new
56+
)
5957
)
6058
),
6159
new JarPatchers(
6260
"hadoop-client-api",
6361
Pattern.compile("hadoop-client-api.*"),
64-
Map.ofEntries(
65-
entry("org/apache/hadoop/util/ShutdownHookManager.class", ShutdownHookManagerPatcher::new),
66-
entry("org/apache/hadoop/util/Shell.class", ShellPatcher::new),
67-
entry("org/apache/hadoop/security/UserGroupInformation.class", SubjectGetSubjectPatcher::new),
68-
entry("org/apache/hadoop/security/authentication/client/KerberosAuthenticator.class", SubjectGetSubjectPatcher::new)
62+
List.of(
63+
classPatcher(
64+
"org/apache/hadoop/util/ShutdownHookManager.class",
65+
"90641e0726fc9372479728ef9b7ae2be20fb7ab4cddd4938e55ffecadddd4d94",
66+
ShutdownHookManagerPatcher::new
67+
),
68+
classPatcher(
69+
"org/apache/hadoop/util/Shell.class",
70+
"8837c7f3eeda3f658fc3d6595f18e77a4558220ff0becdf3e175fa4397a6fd0c",
71+
ShellPatcher::new
72+
),
73+
classPatcher(
74+
"org/apache/hadoop/security/UserGroupInformation.class",
75+
"3c34bbc2716a6c8f4e356e78550599b0a4f01882712b4f7787d032fb10527212",
76+
SubjectGetSubjectPatcher::new
77+
),
78+
classPatcher(
79+
"org/apache/hadoop/security/authentication/client/KerberosAuthenticator.class",
80+
"6bab26c1032a38621c20050ec92067226d1d67972d0d370e412ca25f1df96b76",
81+
SubjectGetSubjectPatcher::new
82+
)
6983
)
7084
)
7185
);
@@ -95,55 +109,9 @@ public void transform(@NotNull TransformOutputs outputs) {
95109
} else {
96110
patchersToApply.forEach(patchers -> {
97111
System.out.println("Patching " + inputFile.getName());
98-
99-
Map<String, Function<ClassWriter, ClassVisitor>> jarPatchers = new HashMap<>(patchers.jarPatchers());
100112
File outputFile = outputs.file(inputFile.getName().replace(".jar", "-patched.jar"));
101-
102-
patchJar(inputFile, outputFile, jarPatchers);
103-
104-
if (jarPatchers.isEmpty() == false) {
105-
throw new IllegalArgumentException(
106-
String.format(
107-
Locale.ROOT,
108-
"error patching [%s] with [%s]: the jar does not contain [%s]",
109-
inputFile.getName(),
110-
patchers.artifactPattern().toString(),
111-
String.join(", ", jarPatchers.keySet())
112-
)
113-
);
114-
}
113+
Utils.patchJar(inputFile, outputFile, patchers.jarPatchers());
115114
});
116115
}
117116
}
118-
119-
private static void patchJar(File inputFile, File outputFile, Map<String, Function<ClassWriter, ClassVisitor>> jarPatchers) {
120-
try (JarFile jarFile = new JarFile(inputFile); JarOutputStream jos = new JarOutputStream(new FileOutputStream(outputFile))) {
121-
Enumeration<JarEntry> entries = jarFile.entries();
122-
while (entries.hasMoreElements()) {
123-
JarEntry entry = entries.nextElement();
124-
String entryName = entry.getName();
125-
// Add the entry to the new JAR file
126-
jos.putNextEntry(new JarEntry(entryName));
127-
128-
Function<ClassWriter, ClassVisitor> classPatcher = jarPatchers.remove(entryName);
129-
if (classPatcher != null) {
130-
byte[] classToPatch = jarFile.getInputStream(entry).readAllBytes();
131-
132-
ClassReader classReader = new ClassReader(classToPatch);
133-
ClassWriter classWriter = new ClassWriter(classReader, COMPUTE_FRAMES | COMPUTE_MAXS);
134-
classReader.accept(classPatcher.apply(classWriter), 0);
135-
136-
jos.write(classWriter.toByteArray());
137-
} else {
138-
// Read the entry's data and write it to the new JAR
139-
try (InputStream is = jarFile.getInputStream(entry)) {
140-
is.transferTo(jos);
141-
}
142-
}
143-
jos.closeEntry();
144-
}
145-
} catch (IOException ex) {
146-
throw new RuntimeException(ex);
147-
}
148-
}
149117
}

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/patches/hdfs/SubjectGetSubjectPatcher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class SubjectGetSubjectPatcher extends ClassVisitor {
2525

2626
@Override
2727
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
28-
return new ReplaceCallMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions), name, access, descriptor);
28+
return new ReplaceCallMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions));
2929
}
3030

3131
/**
@@ -35,7 +35,7 @@ private static class ReplaceCallMethodVisitor extends MethodVisitor {
3535
private static final String SUBJECT_CLASS_INTERNAL_NAME = "javax/security/auth/Subject";
3636
private static final String METHOD_NAME = "getSubject";
3737

38-
ReplaceCallMethodVisitor(MethodVisitor methodVisitor, String name, int access, String descriptor) {
38+
ReplaceCallMethodVisitor(MethodVisitor methodVisitor) {
3939
super(ASM9, methodVisitor);
4040
}
4141

0 commit comments

Comments
 (0)