From 67d155985a384f1d60b59eb6faa7309276899466 Mon Sep 17 00:00:00 2001 From: Reinier Zwitserloot Date: Thu, 3 Aug 2023 23:58:17 +0200 Subject: [PATCH] [loader] Fix an issue when using lombok as a module while using ObjectWeb ASM as dep. Evidently jigsaw's damage to java extends to our way of shading ObjectWeb ASM dep - it's a split package violation even though lombok loads its stuff in a separate loader. We fix it by using some on-the-fly extremely fast constant pool rewriting. --- .../lombok/bytecode/ClassFileMetaData.java | 21 +- .../lombok/launch/ClassFileMetaData.java | 482 ++++++++++++++++++ src/launch/lombok/launch/PackageShader.java | 109 ++++ .../lombok/launch/ShadowClassLoader.java | 10 +- 4 files changed, 620 insertions(+), 2 deletions(-) create mode 100644 src/launch/lombok/launch/ClassFileMetaData.java create mode 100644 src/launch/lombok/launch/PackageShader.java diff --git a/src/core/lombok/bytecode/ClassFileMetaData.java b/src/core/lombok/bytecode/ClassFileMetaData.java index 0510292d..71e0c56c 100644 --- a/src/core/lombok/bytecode/ClassFileMetaData.java +++ b/src/core/lombok/bytecode/ClassFileMetaData.java @@ -22,11 +22,15 @@ package lombok.bytecode; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Utility to read the constant pool, header, and inheritance information of any class file. + * + * THIS IS A COPY OF lombok.launch.ClassFileMetaData - it is used by the shadowclassloader which can't rely on anything from outside the lombok.launch package, but everything in the launch package has to be package private. + * If there is a bug or an update is needed here - make sure to also update it in the lombok.bytecode package. */ public class ClassFileMetaData { private static final byte UTF8 = 1; @@ -54,7 +58,7 @@ public class ClassFileMetaData { private final byte[] byteCode; - private final int maxPoolSize; + private final int maxPoolSize; private final int[] offsets; private final byte[] types; private final String[] utf8s; @@ -138,6 +142,18 @@ public class ClassFileMetaData { return new String(result, 0, length); } + public int[] getOffsets(byte type) { + int[] out = new int[types.length]; + int ptr = 0; + + for (int i = 0; i < types.length; i++) { + if (types[i] != type) continue; + out[ptr++] = offsets[i]; + } + + return Arrays.copyOf(out, ptr); + } + /** * Checks if the constant pool contains the provided 'raw' string. These are used as source material for further JVM types, such as string constants, type references, etcetera. */ @@ -457,6 +473,9 @@ public class ClassFileMetaData { return NOT_FOUND; } + /** + * Reads a 16-bit value at {@code position} + */ private int readValue(int position) { return ((byteCode[position] & 0xFF) << 8) | (byteCode[position + 1] & 0xFF); } diff --git a/src/launch/lombok/launch/ClassFileMetaData.java b/src/launch/lombok/launch/ClassFileMetaData.java new file mode 100644 index 00000000..be3bc865 --- /dev/null +++ b/src/launch/lombok/launch/ClassFileMetaData.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2010-2019 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.launch; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Utility to read the constant pool, header, and inheritance information of any class file. + * + * THIS IS A COPY OF lombok.bytecode.ClassFileMetaData - it is used by the shadowclassloader which can't rely on anything from outside the lombok.launch package, but everything in the launch package has to be package private. + * If there is a bug or an update is needed here - make sure to also update it in the lombok.bytecode package. + */ +class ClassFileMetaData { + private static final byte UTF8 = 1; + private static final byte INTEGER = 3; + private static final byte FLOAT = 4; + private static final byte LONG = 5; + private static final byte DOUBLE = 6; + private static final byte CLASS = 7; + private static final byte STRING = 8; + private static final byte FIELD = 9; + private static final byte METHOD = 10; + private static final byte INTERFACE_METHOD = 11; + private static final byte NAME_TYPE = 12; + // New in java7: support for methodhandles and invokedynamic + private static final byte METHOD_HANDLE = 15; + private static final byte METHOD_TYPE = 16; + private static final byte DYNAMIC = 17; + private static final byte INVOKE_DYNAMIC = 18; + // New in java9: support for modules + private static final byte MODULE = 19; + private static final byte PACKAGE = 20; + + private static final int NOT_FOUND = -1; + private static final int START_OF_CONSTANT_POOL = 8; + + private final byte[] byteCode; + + private final int maxPoolSize; + private final int[] offsets; + private final byte[] types; + private final String[] utf8s; + private final int endOfPool; + + public ClassFileMetaData(byte[] byteCode) { + this.byteCode = byteCode; + + maxPoolSize = readValue(START_OF_CONSTANT_POOL); + offsets = new int[maxPoolSize]; + types = new byte[maxPoolSize]; + utf8s = new String[maxPoolSize]; + int position = 10; + for (int i = 1; i < maxPoolSize; i++) { + byte type = byteCode[position]; + types[i] = type; + position++; + offsets[i] = position; + switch (type) { + case UTF8: + int length = readValue(position); + position += 2; + utf8s[i] = decodeString(position, length); + position += length; + break; + case CLASS: + case STRING: + case METHOD_TYPE: + case MODULE: + case PACKAGE: + position += 2; + break; + case METHOD_HANDLE: + position += 3; + break; + case INTEGER: + case FLOAT: + case FIELD: + case METHOD: + case INTERFACE_METHOD: + case NAME_TYPE: + case INVOKE_DYNAMIC: + case DYNAMIC: + position += 4; + break; + case LONG: + case DOUBLE: + position += 8; + i++; + break; + case 0: + break; + default: + throw new AssertionError("Unknown constant pool type " + type); + } + } + endOfPool = position; + } + + private String decodeString(int pos, int size) { + int end = pos + size; + + // the resulting string might be smaller + char[] result = new char[size]; + int length = 0; + while (pos < end) { + int first = (byteCode[pos++] & 0xFF); + if (first < 0x80) { + result[length++] = (char)first; + } else if ((first & 0xE0) == 0xC0) { + int x = (first & 0x1F) << 6; + int y = (byteCode[pos++] & 0x3F); + result[length++] = (char)(x | y); + } else { + int x = (first & 0x0F) << 12; + int y = (byteCode[pos++] & 0x3F) << 6; + int z = (byteCode[pos++] & 0x3F); + result[length++] = (char)(x | y | z); + } + } + return new String(result, 0, length); + } + + public int[] getOffsets(byte type) { + int[] out = new int[types.length]; + int ptr = 0; + + for (int i = 0; i < types.length; i++) { + if (types[i] != type) continue; + out[ptr++] = offsets[i]; + } + + return Arrays.copyOf(out, ptr); + } + + /** + * Checks if the constant pool contains the provided 'raw' string. These are used as source material for further JVM types, such as string constants, type references, etcetera. + */ + public boolean containsUtf8(String value) { + return findUtf8(value) != NOT_FOUND; + } + + /** + * Checks if the constant pool contains a reference to the provided class. + * + * NB: Most uses of a type do NOT show up as a class in the constant pool. + * For example, the parameter types and return type of any method you invoke or declare, are stored as signatures and not as type references, + * but the type to which any method you invoke belongs, is. Read the JVM Specification for more information. + * + * @param className must be provided JVM-style, such as {@code java/lang/String} + */ + public boolean usesClass(String className) { + return findClass(className) != NOT_FOUND; + } + + /** + * Checks if the constant pool contains a reference to a given field, either for writing or reading. + * + * @param className must be provided JVM-style, such as {@code java/lang/String} + */ + public boolean usesField(String className, String fieldName) { + int classIndex = findClass(className); + if (classIndex == NOT_FOUND) return false; + int fieldNameIndex = findUtf8(fieldName); + if (fieldNameIndex == NOT_FOUND) return false; + + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == FIELD && readValue(offsets[i]) == classIndex) { + int nameAndTypeIndex = readValue(offsets[i] + 2); + if (readValue(offsets[nameAndTypeIndex]) == fieldNameIndex) return true; + } + } + return false; + } + + /** + * Checks if the constant pool contains a reference to a given method, with any signature (return type and parameter types). + * + * @param className must be provided JVM-style, such as {@code java/lang/String} + */ + public boolean usesMethod(String className, String methodName) { + int classIndex = findClass(className); + if (classIndex == NOT_FOUND) return false; + int methodNameIndex = findUtf8(methodName); + if (methodNameIndex == NOT_FOUND) return false; + + for (int i = 1; i < maxPoolSize; i++) { + if (isMethod(i) && readValue(offsets[i]) == classIndex) { + int nameAndTypeIndex = readValue(offsets[i] + 2); + if (readValue(offsets[nameAndTypeIndex]) == methodNameIndex) return true; + } + } + return false; + } + + /** + * Checks if the constant pool contains a reference to a given method. + * + * @param className must be provided JVM-style, such as {@code java/lang/String} + * @param descriptor must be provided JVM-style, such as {@code (IZ)Ljava/lang/String;} + */ + public boolean usesMethod(String className, String methodName, String descriptor) { + int classIndex = findClass(className); + if (classIndex == NOT_FOUND) return false; + int nameAndTypeIndex = findNameAndType(methodName, descriptor); + if (nameAndTypeIndex == NOT_FOUND) return false; + + for (int i = 1; i < maxPoolSize; i++) { + if (isMethod(i) && + readValue(offsets[i]) == classIndex && + readValue(offsets[i] + 2) == nameAndTypeIndex) return true; + } + return false; + } + + /** + * Checks if the constant pool contains the provided string constant, which implies the constant is used somewhere in the code. + * + * NB: String literals get concatenated by the compiler. + * NB2: This method does NOT do any kind of normalization. + */ + public boolean containsStringConstant(String value) { + int index = findUtf8(value); + if (index == NOT_FOUND) return false; + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == STRING && readValue(offsets[i]) == index) return true; + } + return false; + } + + /** + * Checks if the constant pool contains the provided long constant, which implies the constant is used somewhere in the code. + * + * NB: compile-time constant expressions are evaluated at compile time. + */ + public boolean containsLong(long value) { + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == LONG && readLong(i) == value) return true; + } + return false; + } + + /** + * Checks if the constant pool contains the provided double constant, which implies the constant is used somewhere in the code. + * + * NB: compile-time constant expressions are evaluated at compile time. + */ + public boolean containsDouble(double value) { + boolean isNan = Double.isNaN(value); + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == DOUBLE) { + double d = readDouble(i); + if (d == value || (isNan && Double.isNaN(d))) return true; + } + } + return false; + } + + /** + * Checks if the constant pool contains the provided int constant, which implies the constant is used somewhere in the code. + * + * NB: compile-time constant expressions are evaluated at compile time. + */ + public boolean containsInteger(int value) { + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == INTEGER && readInteger(i) == value) return true; + } + return false; + } + + /** + * Checks if the constant pool contains the provided float constant, which implies the constant is used somewhere in the code. + * + * NB: compile-time constant expressions are evaluated at compile time. + */ + public boolean containsFloat(float value) { + boolean isNan = Float.isNaN(value); + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == FLOAT) { + float f = readFloat(i); + if (f == value || (isNan && Float.isNaN(f))) return true; + } + } + return false; + } + + private long readLong(int index) { + int pos = offsets[index]; + return ((long)read32(pos)) << 32 | (read32(pos + 4) & 0x00000000FFFFFFFFL); + } + + private double readDouble(int index) { + return Double.longBitsToDouble(readLong(index)); + } + + private int readInteger(int index) { + return read32(offsets[index]); + } + + private float readFloat(int index) { + return Float.intBitsToFloat(readInteger(index)); + } + + private int read32(int pos) { + return (byteCode[pos] & 0xFF) << 24 | (byteCode[pos + 1] & 0xFF) << 16 | (byteCode[pos + 2] & 0xFF) << 8 | (byteCode[pos + 3] & 0xFF); + } + + /** + * Returns the name of the class in JVM format, such as {@code java/lang/String} + */ + public String getClassName() { + return getClassName(readValue(endOfPool + 2)); + } + + /** + * Returns the name of the superclass in JVM format, such as {@code java/lang/Object} + * + * NB: If you try this on Object itself, you'll get {@code null}.
+ * NB2: For interfaces and annotation interfaces, you'll always get {@code java/lang/Object} + */ + public String getSuperClassName() { + return getClassName(readValue(endOfPool + 4)); + } + + /** + * Returns the name of all implemented interfaces. + */ + public List getInterfaces() { + int size = readValue(endOfPool + 6); + if (size == 0) return Collections.emptyList(); + + List result = new ArrayList(); + for (int i = 0; i < size; i++) { + result.add(getClassName(readValue(endOfPool + 8 + (i * 2)))); + } + return result; + } + + /** + * A {@code toString()} like utility to dump all contents of the constant pool into a string. + * + * NB: No guarantees are made about the exact layout of this string. It is for informational purposes only, don't try to parse it.
+ * NB2: After a double or long, there's a JVM spec-mandated gap, which is listed as {@code (cont.)} in the returned string. + */ + public String poolContent() { + StringBuilder result = new StringBuilder(); + for (int i = 1; i < maxPoolSize; i++) { + result.append(String.format("#%02x: ", i)); + int pos = offsets[i]; + switch(types[i]) { + case UTF8: + result.append("Utf8 ").append(utf8s[i]); + break; + case CLASS: + result.append("Class ").append(getClassName(i)); + break; + case STRING: + result.append("String \"").append(utf8s[readValue(pos)]).append("\""); + break; + case INTEGER: + result.append("int ").append(readInteger(i)); + break; + case FLOAT: + result.append("float ").append(readFloat(i)); + break; + case FIELD: + appendAccess(result.append("Field "), i); + break; + case METHOD: + case INTERFACE_METHOD: + appendAccess(result.append("Method "), i); + break; + case NAME_TYPE: + appendNameAndType(result.append("Name&Type "), i); + break; + case LONG: + result.append("long ").append(readLong(i)); + break; + case DOUBLE: + result.append("double ").append(readDouble(i)); + break; + case METHOD_HANDLE: + result.append("MethodHandle..."); + break; + case METHOD_TYPE: + result.append("MethodType..."); + break; + case DYNAMIC: + result.append("Dynamic..."); + break; + case INVOKE_DYNAMIC: + result.append("InvokeDynamic..."); + break; + case 0: + result.append("(cont.)"); + break; + } + result.append("\n"); + } + return result.toString(); + } + + private void appendAccess(StringBuilder result, int index) { + int pos = offsets[index]; + result.append(getClassName(readValue(pos))).append("."); + appendNameAndType(result, readValue(pos + 2)); + } + + private void appendNameAndType(StringBuilder result, int index) { + int pos = offsets[index]; + result.append(utf8s[readValue(pos)]).append(":").append(utf8s[readValue(pos + 2)]); + } + + private String getClassName(int classIndex) { + if (classIndex < 1) return null; + return utf8s[readValue(offsets[classIndex])]; + } + + private boolean isMethod(int i) { + byte type = types[i]; + return type == METHOD || type == INTERFACE_METHOD; + } + + private int findNameAndType(String name, String descriptor) { + int nameIndex = findUtf8(name); + if (nameIndex == NOT_FOUND) return NOT_FOUND; + int descriptorIndex = findUtf8(descriptor); + if (descriptorIndex == NOT_FOUND) return NOT_FOUND; + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == NAME_TYPE && + readValue(offsets[i]) == nameIndex && + readValue(offsets[i] + 2) == descriptorIndex) return i; + } + return NOT_FOUND; + } + + private int findUtf8(String value) { + for (int i = 1; i < maxPoolSize; i++) { + if (value.equals(utf8s[i])) { + return i; + } + } + return NOT_FOUND; + } + + private int findClass(String className) { + int index = findUtf8(className); + if (index == -1) return NOT_FOUND; + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == CLASS && readValue(offsets[i]) == index) return i; + } + return NOT_FOUND; + } + + /** + * Reads a 16-bit value at {@code position} + */ + private int readValue(int position) { + return ((byteCode[position] & 0xFF) << 8) | (byteCode[position + 1] & 0xFF); + } +} diff --git a/src/launch/lombok/launch/PackageShader.java b/src/launch/lombok/launch/PackageShader.java new file mode 100644 index 00000000..b47cd943 --- /dev/null +++ b/src/launch/lombok/launch/PackageShader.java @@ -0,0 +1,109 @@ +package lombok.launch; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +/** + * Low-level shader tool, that inline 'rewrites' bytecode so that a given package name acts like it has a different name. + * + * This is useful particularly in light of the java module system which disallows 'split packages' - different loader systems / class file sources + * that both have the same package. Generally you'd take one of your dependency libraries (such as lombok's dependency on ObjectWeb's ASM bytecode parser) and + * rename that dependency to avoid conflicts with other code that also wants to use ASM. + * + * This shader doesn't use ASM or any other bytecode manip library; it just scans constant pools. + * + * It's limited in its capabilities: A shading op (rename 'this' package to 'that') requires same-length strings so that the replacement can be done without moving things around. + */ +class PackageShader { + private final byte[][] froms; // contains package prefixes that need shading. + private final byte[][] tos; // contains the post-shading package prefixes. froms.length == tos.length. + + /** + * @param shadeOps provide pairs of strings; each pair is a from,to tuple listing binary package prefixes, e.g. "Lorg/objectweb/asm", "Lorg/lombokweb/asm". + */ + public PackageShader(String... shadeOps) { + if (shadeOps.length % 2 != 0) throw new IllegalArgumentException("Provide pairs: real package name to shaded package name (you provided an odd number of strings; even number required)"); + Charset ascii = Charset.forName("US-ASCII"); + int len = shadeOps.length / 2; + froms = new byte[len][]; + tos = new byte[len][]; + for (int i = 0; i < len; i ++) { + String in = shadeOps[i << 1]; + String out = shadeOps[(i << 1) | 1]; + if (in.contains(".") || out.contains(".")) throw new IllegalArgumentException("Binary name prefixes are required (use slashes and dollars instead of dots to separate type name elements); they look like e.g. 'java/util/'. Violating entry: " + in + " -> " + out); + froms[i] = in.getBytes(ascii); + tos[i] = out.getBytes(ascii); + if (froms[i].length != tos[i].length) throw new IllegalArgumentException("Pair [" + in + " -> " + out + "] is invalid: Both strings must be the same length"); + } + } + + private static final byte CONSTANTPOOLTYPE_UTF8 = 1; + /** + * Fuxxes the constant pool of the provided bytecode, rewriting any constant pool entries that represent types that start with any of the registered package shade requests to the shaded variant. + * + * The byte array is modified in-line. + * + * @param b Bytecode + * @return {@code true} if at least one shading modification has been applied. + */ + public boolean apply(byte[] b) { + ClassFileMetaData md = new ClassFileMetaData(b); + boolean changes = false; + + // We want to replace the package name but it can occur in the middle of a 'signature' string, e.g. ([I[Ljava/util/List;)Ljava/util/List; - so replace from start and _after_ every capital-L. + // 'startPoints' tracks every location inside the constant pool UTF8 value that _could_ contain a fully qualified type name: Position 0 plus after _every_ capital-L in the UTF-8. + int[] startPoints = new int[260]; + int maxStartPoints; + + for (int offset : md.getOffsets(CONSTANTPOOLTYPE_UTF8)) { + int len = readValue(b, offset); + offset += 2; + startPoints[0] = offset; + maxStartPoints = 1; + + for (int i = offset; i < offset + len; i++) { + if (b[i] == 'L') startPoints[maxStartPoints++] = offset + 1; + } + + outer: for (int i = 0; i < froms.length; i++) { + for (int startPointIdx = 0; startPointIdx < maxStartPoints; startPointIdx++) { + int indexIntoSignature = startPoints[startPointIdx]; + if (len - indexIntoSignature < froms[i].length) continue; + for (int p = 0; p < froms[i].length; p++) { + if (b[offset + indexIntoSignature + p] != froms[i][p]) continue outer; + } + System.arraycopy(tos[i], 0, b, offset + indexIntoSignature, tos[i].length); + changes = true; + } + } + } + + return changes; + } + + /** + * Reads a 16-bit value at {@code position} + */ + private static int readValue(byte[] b, int position) { + return ((b[position] & 0xFF) << 8) | (b[position + 1] & 0xFF); + } + + public String reverseResourceName(String name) { + outer: for (int i = 0; i < tos.length; i++) { + int len = tos[i].length; + if (name.length() < len) continue; + for (int p = 0; p < len; p++) { + if (name.charAt(p) != tos[i][p]) continue outer; + } + try { + String out = new String(froms[i], 0, froms[i].length, "US-ASCII") + name.substring(len); + return out; + } catch (UnsupportedEncodingException e) { + // Can't happen - US-ASCII is guaranteed by the JVM spec + return name; + } + } + + return name; + } +} diff --git a/src/launch/lombok/launch/ShadowClassLoader.java b/src/launch/lombok/launch/ShadowClassLoader.java index 929681ce..cb1db156 100644 --- a/src/launch/lombok/launch/ShadowClassLoader.java +++ b/src/launch/lombok/launch/ShadowClassLoader.java @@ -34,6 +34,7 @@ import java.net.URI; import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -105,6 +106,9 @@ class ShadowClassLoader extends ClassLoader { private final Set prependedParentLoaders = Collections.newSetFromMap(new IdentityHashMap()); + private final PackageShader shader = new PackageShader( + "org/objectweb/asm/", "org/lombokweb/asm/"); + public void prependParent(ClassLoader loader) { if (loader == null) return; if (loader == getParent()) return; @@ -470,7 +474,9 @@ class ShadowClassLoader extends ClassLoader { private URL getResource_(String name, boolean noSuper) { String altName = null; + name = shader.reverseResourceName(name); if (name.endsWith(".class")) altName = name.substring(0, name.length() - 6) + ".SCL." + sclSuffix; + for (File ce : override) { URL url = getResourceFromLocation(name, altName, ce); if (url != null) return url; @@ -542,7 +548,7 @@ class ShadowClassLoader extends ClassLoader { if (res == null) { if (!exclusionListMatch(fileNameOfClass)) { try { - // First search in the prepended classloaders, the class might be their already + // First search in the prepended classloaders, the class might be there already for (ClassLoader pre : prependedParentLoaders) { try { Class loadClass = pre.loadClass(name); @@ -589,6 +595,8 @@ class ShadowClassLoader extends ClassLoader { throw new ClassNotFoundException("I/O exception reading class " + name, e); } + shader.apply(b); + Class c; try { c = defineClass(name, b, 0, p);