From 22ef827e2cc2409f21ad5c26611cb13d39b5cb3e Mon Sep 17 00:00:00 2001 From: Adam Sotona Date: Fri, 31 May 2024 06:26:35 +0000 Subject: [PATCH] 8320396: Class-File API ClassModel::verify should include checks from hotspot/share/classfile/classFileParser.cpp Reviewed-by: liach, mcimadamore --- .../classfile/impl/ClassPrinterImpl.java | 43 +- .../impl/verifier/ParserVerifier.java | 494 ++++++++++++++++++ .../impl/verifier/VerificationWrapper.java | 6 + .../classfile/impl/verifier/VerifierImpl.java | 46 +- test/jdk/jdk/classfile/ClassPrinterTest.java | 4 +- test/jdk/jdk/classfile/VerifierSelfTest.java | 303 ++++++++++- 6 files changed, 862 insertions(+), 34 deletions(-) create mode 100644 src/java.base/share/classes/jdk/internal/classfile/impl/verifier/ParserVerifier.java diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/ClassPrinterImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/ClassPrinterImpl.java index 989589d0a0136..fac2eba95fe6b 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/ClassPrinterImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/ClassPrinterImpl.java @@ -28,6 +28,7 @@ import java.lang.constant.DirectMethodHandleDesc; import java.lang.reflect.AccessFlag; import java.util.AbstractList; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -35,7 +36,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.BiConsumer; import java.util.Set; import java.util.function.Consumer; @@ -75,16 +75,22 @@ public Stream walk() { } } - public static final class ListNodeImpl extends AbstractList implements ListNode { + public static sealed class ListNodeImpl extends AbstractList implements ListNode { private final Style style; private final ConstantDesc name; - private final Node[] nodes; + protected final List nodes; public ListNodeImpl(Style style, ConstantDesc name, Stream nodes) { this.style = style; this.name = name; - this.nodes = nodes.toArray(Node[]::new); + this.nodes = nodes.toList(); + } + + protected ListNodeImpl(Style style, ConstantDesc name, List nodes) { + this.style = style; + this.name = name; + this.nodes = nodes; } @Override @@ -103,18 +109,23 @@ public Style style() { @Override public Node get(int index) { - Objects.checkIndex(index, nodes.length); - return nodes[index]; + return nodes.get(index); } @Override public int size() { - return nodes.length; + return nodes.size(); } } public static final class MapNodeImpl implements MapNode { + private static final class PrivateListNodeImpl extends ListNodeImpl { + PrivateListNodeImpl(Style style, ConstantDesc name, Node... n) { + super(style, name, new ArrayList<>(List.of(n))); + } + } + private final Style style; private final ConstantDesc name; private final Map map; @@ -198,9 +209,19 @@ public Set> entrySet() { MapNodeImpl with(Node... nodes) { - for (var n : nodes) - if (n != null && map.put(n.name(), n) != null) - throw new AssertionError("Double entry of " + n.name() + " into " + name); + for (var n : nodes) { + if (n != null) { + var prev = map.putIfAbsent(n.name(), n); + if (prev != null) { + //nodes with duplicite keys are joined into a list + if (prev instanceof PrivateListNodeImpl list) { + list.nodes.add(n); + } else { + map.put(n.name(), new PrivateListNodeImpl(style, n.name(), prev, n)); + } + } + } + } return this; } } @@ -975,7 +996,7 @@ private static Node[] attributesToTree(List> attributes, Verbosity nodes.add(leaf("module main class", mmca.mainClass().name().stringValue())); case RecordAttribute ra -> nodes.add(new ListNodeImpl(BLOCK, "record components", ra.components().stream() - .map(rc -> new MapNodeImpl(BLOCK, "record") + .map(rc -> new MapNodeImpl(BLOCK, "component") .with(leafs( "name", rc.name().stringValue(), "type", rc.descriptor().stringValue())) diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/ParserVerifier.java b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/ParserVerifier.java new file mode 100644 index 0000000000000..4a2ffd3f25d3f --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/ParserVerifier.java @@ -0,0 +1,494 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.classfile.impl.verifier; + +import java.lang.classfile.Annotation; +import java.lang.classfile.AnnotationValue; +import java.lang.constant.ClassDesc; +import static java.lang.constant.ConstantDescs.CLASS_INIT_NAME; +import static java.lang.constant.ConstantDescs.INIT_NAME; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.lang.classfile.Attribute; +import java.lang.classfile.AttributedElement; +import java.lang.classfile.Attributes; +import java.lang.classfile.ClassModel; +import java.lang.classfile.ClassFileElement; +import java.lang.classfile.CodeModel; +import java.lang.classfile.CompoundElement; +import java.lang.classfile.CustomAttribute; +import java.lang.classfile.FieldModel; +import java.lang.classfile.MethodModel; +import java.lang.classfile.TypeAnnotation; +import java.lang.classfile.TypeKind; +import java.lang.classfile.attribute.*; +import java.lang.classfile.constantpool.*; +import java.lang.constant.ConstantDescs; +import java.lang.reflect.AccessFlag; +import java.util.Collection; +import java.util.function.Function; +import java.util.function.ToIntFunction; +import jdk.internal.classfile.impl.BoundAttribute; +import jdk.internal.classfile.impl.Util; + +/** + * ParserVerifier performs selected checks of the class file format according to + * {@jvms 4.8 Format Checking} + * + * @see hotspot/share/classfile/classFileParser.cpp + */ +public record ParserVerifier(ClassModel classModel) { + + List verify() { + var errors = new ArrayList(); + verifyConstantPool(errors); + verifyInterfaces(errors); + verifyFields(errors); + verifyMethods(errors); + verifyAttributes(classModel, errors); + return errors; + } + + private void verifyConstantPool(List errors) { + for (var cpe : classModel.constantPool()) { + Consumer check = c -> { + try { + c.run(); + } catch (VerifyError|Exception e) { + errors.add(new VerifyError("%s at constant pool index %d in %s".formatted(e.getMessage(), cpe.index(), toString(classModel)))); + } + }; + check.accept(switch (cpe) { + case DoubleEntry de -> de::doubleValue; + case FloatEntry fe -> fe::floatValue; + case IntegerEntry ie -> ie::intValue; + case LongEntry le -> le::longValue; + case Utf8Entry ue -> ue::stringValue; + case ConstantDynamicEntry cde -> cde::asSymbol; + case InvokeDynamicEntry ide -> ide::asSymbol; + case ClassEntry ce -> ce::asSymbol; + case StringEntry se -> se::stringValue; + case MethodHandleEntry mhe -> mhe::asSymbol; + case MethodTypeEntry mte -> mte::asSymbol; + case FieldRefEntry fre -> { + check.accept(fre.owner()::asSymbol); + check.accept(fre::typeSymbol); + yield () -> verifyFieldName(fre.name().stringValue()); + } + case InterfaceMethodRefEntry imre -> { + check.accept(imre.owner()::asSymbol); + check.accept(imre::typeSymbol); + yield () -> verifyMethodName(imre.name().stringValue()); + } + case MethodRefEntry mre -> { + check.accept(mre.owner()::asSymbol); + check.accept(mre::typeSymbol); + yield () -> verifyMethodName(mre.name().stringValue()); + } + case ModuleEntry me -> me::asSymbol; + case NameAndTypeEntry nate -> { + check.accept(nate.name()::stringValue); + yield () -> nate.type().stringValue(); + } + case PackageEntry pe -> pe::asSymbol; + }); + } + } + + private void verifyFieldName(String name) { + if (name.length() == 0 || name.chars().anyMatch(ch -> switch(ch) { + case '.', ';', '[', '/' -> true; + default -> false; + })) { + throw new VerifyError("Illegal field name %s in %s".formatted(name, toString(classModel))); + } + } + + private void verifyMethodName(String name) { + if (!name.equals(INIT_NAME) + && !name.equals(CLASS_INIT_NAME) + && (name.length() == 0 || name.chars().anyMatch(ch -> switch(ch) { + case '.', ';', '[', '/', '<', '>' -> true; + default -> false; + }))) { + throw new VerifyError("Illegal method name %s in %s".formatted(name, toString(classModel))); + } + } + + private void verifyInterfaces(List errors) { + var intfs = new HashSet(); + for (var intf : classModel.interfaces()) { + if (!intfs.add(intf)) { + errors.add(new VerifyError("Duplicate interface %s in %s".formatted(intf.asSymbol().displayName(), toString(classModel)))); + } + } + } + + private void verifyFields(List errors) { + record F(Utf8Entry name, Utf8Entry type) {}; + var fields = new HashSet(); + for (var f : classModel.fields()) try { + if (!fields.add(new F(f.fieldName(), f.fieldType()))) { + errors.add(new VerifyError("Duplicate field name %s with signature %s in %s".formatted(f.fieldName().stringValue(), f.fieldType().stringValue(), toString(classModel)))); + } + verifyFieldName(f.fieldName().stringValue()); + } catch (VerifyError ve) { + errors.add(ve); + } + } + + private void verifyMethods(List errors) { + record M(Utf8Entry name, Utf8Entry type) {}; + var methods = new HashSet(); + for (var m : classModel.methods()) try { + if (!methods.add(new M(m.methodName(), m.methodType()))) { + errors.add(new VerifyError("Duplicate method name %s with signature %s in %s".formatted(m.methodName().stringValue(), m.methodType().stringValue(), toString(classModel)))); + } + if (m.methodName().equalsString(CLASS_INIT_NAME) + && !m.flags().has(AccessFlag.STATIC)) { + errors.add(new VerifyError("Method is not static in %s".formatted(toString(classModel)))); + } + if (classModel.flags().has(AccessFlag.INTERFACE) + && m.methodName().equalsString(INIT_NAME)) { + errors.add(new VerifyError("Interface cannot have a method named in %s".formatted(toString(classModel)))); + } + verifyMethodName(m.methodName().stringValue()); + } catch (VerifyError ve) { + errors.add(ve); + } + } + + private void verifyAttributes(ClassFileElement cfe, List errors) { + if (cfe instanceof AttributedElement ae) { + var attrNames = new HashSet(); + for (var a : ae.attributes()) { + if (!a.attributeMapper().allowMultiple() && !attrNames.add(a.attributeName())) { + errors.add(new VerifyError("Multiple %s attributes in %s".formatted(a.attributeName(), toString(ae)))); + } + verifyAttribute(ae, a, errors); + } + } + switch (cfe) { + case CompoundElement comp -> { + for (var e : comp) verifyAttributes(e, errors); + } + case RecordAttribute ra -> { + for(var rc : ra.components()) verifyAttributes(rc, errors); + } + default -> {} + } + } + + private void verifyAttribute(AttributedElement ae, Attribute a, List errors) { + int size = switch (a) { + case AnnotationDefaultAttribute aa -> + valueSize(aa.defaultValue()); + case BootstrapMethodsAttribute bma -> + 2 + bma.bootstrapMethods().stream().mapToInt(bm -> 4 + 2 * bm.arguments().size()).sum(); + case CharacterRangeTableAttribute cra -> + 2 + 14 * cra.characterRangeTable().size(); + case CodeAttribute ca -> { + MethodModel mm = (MethodModel)ae; + if (mm.flags().has(AccessFlag.NATIVE) || mm.flags().has(AccessFlag.ABSTRACT)) { + errors.add(new VerifyError("Code attribute in native or abstract %s".formatted(toString(ae)))); + } + if (ca.maxLocals() < Util.maxLocals(mm.flags().flagsMask(), mm.methodTypeSymbol())) { + errors.add(new VerifyError("Arguments can't fit into locals in %s".formatted(toString(ae)))); + } + yield 10 + ca.codeLength() + 8 * ca.exceptionHandlers().size() + attributesSize(ca.attributes()); + } + case CompilationIDAttribute cida -> { + cida.compilationId(); + yield 2; + } + case ConstantValueAttribute cva -> { + ClassDesc type = ((FieldModel)ae).fieldTypeSymbol(); + ConstantValueEntry cve = cva.constant(); + if (!switch (TypeKind.from(type)) { + case BooleanType, ByteType, CharType, IntType, ShortType -> cve instanceof IntegerEntry; + case DoubleType -> cve instanceof DoubleEntry; + case FloatType -> cve instanceof FloatEntry; + case LongType -> cve instanceof LongEntry; + case ReferenceType -> type.equals(ConstantDescs.CD_String) && cve instanceof StringEntry; + case VoidType -> false; + }) { + errors.add(new VerifyError("Bad constant value type in %s".formatted(toString(ae)))); + } + yield 2; + } + case DeprecatedAttribute _ -> + 0; + case EnclosingMethodAttribute ema -> { + ema.enclosingClass(); + ema.enclosingMethod(); + yield 4; + } + case ExceptionsAttribute ea -> + 2 + 2 * ea.exceptions().size(); + case InnerClassesAttribute ica -> { + for (var ici : ica.classes()) { + if (ici.outerClass().isPresent() && ici.outerClass().get().equals(ici.innerClass())) { + errors.add(new VerifyError("Class is both outer and inner class in %s".formatted(toString(ae)))); + } + } + yield 2 + 8 * ica.classes().size(); + } + case LineNumberTableAttribute lta -> + 2 + 4 * lta.lineNumbers().size(); + case LocalVariableTableAttribute lvta -> + 2 + 10 * lvta.localVariables().size(); + case LocalVariableTypeTableAttribute lvta -> + 2 + 10 * lvta.localVariableTypes().size(); + case MethodParametersAttribute mpa -> + 1 + 4 * mpa.parameters().size(); + case ModuleAttribute ma -> + 16 + subSize(ma.exports(), ModuleExportInfo::exportsTo, 6, 2) + + subSize(ma.opens(), ModuleOpenInfo::opensTo, 6, 2) + + subSize(ma.provides(), ModuleProvideInfo::providesWith, 4, 2) + + 6 * ma.requires().size() + + 2 * ma.uses().size(); + case ModuleHashesAttribute mha -> + 2 + moduleHashesSize(mha.hashes()); + case ModuleMainClassAttribute mmca -> { + mmca.mainClass(); + yield 2; + } + case ModulePackagesAttribute mpa -> + 2 + 2 * mpa.packages().size(); + case ModuleResolutionAttribute mra -> + 2; + case ModuleTargetAttribute mta -> { + mta.targetPlatform(); + yield 2; + } + case NestHostAttribute nha -> { + nha.nestHost(); + yield 2; + } + case NestMembersAttribute nma -> { + if (ae.findAttribute(Attributes.nestHost()).isPresent()) { + errors.add(new VerifyError("Conflicting NestHost and NestMembers attributes in %s".formatted(toString(ae)))); + } + yield 2 + 2 * nma.nestMembers().size(); + } + case PermittedSubclassesAttribute psa -> { + if (classModel.flags().has(AccessFlag.FINAL)) { + errors.add(new VerifyError("PermittedSubclasses attribute in final %s".formatted(toString(ae)))); + } + yield 2 + 2 * psa.permittedSubclasses().size(); + } + case RecordAttribute ra -> + componentsSize(ra.components()); + case RuntimeVisibleAnnotationsAttribute aa -> + annotationsSize(aa.annotations()); + case RuntimeInvisibleAnnotationsAttribute aa -> + annotationsSize(aa.annotations()); + case RuntimeVisibleTypeAnnotationsAttribute aa -> + typeAnnotationsSize(aa.annotations()); + case RuntimeInvisibleTypeAnnotationsAttribute aa -> + typeAnnotationsSize(aa.annotations()); + case RuntimeVisibleParameterAnnotationsAttribute aa -> + parameterAnnotationsSize(aa.parameterAnnotations()); + case RuntimeInvisibleParameterAnnotationsAttribute aa -> + parameterAnnotationsSize(aa.parameterAnnotations()); + case SignatureAttribute sa -> { + sa.signature(); + yield 2; + } + case SourceDebugExtensionAttribute sda -> + sda.contents().length; + case SourceFileAttribute sfa -> { + sfa.sourceFile(); + yield 2; + } + case SourceIDAttribute sida -> { + sida.sourceId(); + yield 2; + } + case StackMapTableAttribute smta -> + 2 + subSize(smta.entries(), frame -> stackMapFrameSize(frame)); + case SyntheticAttribute _ -> + 0; + case UnknownAttribute _ -> + -1; + case CustomAttribute _ -> + -1; + default -> // should not happen if all known attributes are verified + throw new AssertionError(a); + }; + if (size >= 0 && size != ((BoundAttribute)a).payloadLen()) { + errors.add(new VerifyError("Wrong %s attribute length in %s".formatted(a.attributeName(), toString(ae)))); + } + } + + private static > int subSize(Collection entries, Function subMH, int entrySize, int subSize) { + return subSize(entries, (ToIntFunction) t -> entrySize + subSize * subMH.apply(t).size()); + } + + private static int subSize(Collection entries, ToIntFunction subMH) { + int l = 0; + for (T entry : entries) { + l += subMH.applyAsInt(entry); + } + return l; + } + + private static int componentsSize(List comps) { + int l = 2; + for (var rc : comps) { + l += 4 + attributesSize(rc.attributes()); + } + return l; + } + + private static int attributesSize(List> attrs) { + int l = 2; + for (var a : attrs) { + l += 6 + ((BoundAttribute)a).payloadLen(); + } + return l; + } + + private static int parameterAnnotationsSize(List> pans) { + int l = 1; + for (var ans : pans) { + l += annotationsSize(ans); + } + return l; + } + + private static int annotationsSize(List ans) { + int l = 2; + for (var an : ans) { + l += annotationSize(an); + } + return l; + } + + private static int typeAnnotationsSize(List ans) { + int l = 2; + for (var an : ans) { + l += 2 + an.targetInfo().size() + 2 * an.targetPath().size() + annotationSize(an); + } + return l; + } + + private static int annotationSize(Annotation an) { + int l = 4; + for (var el : an.elements()) { + l += 2 + valueSize(el.value()); + } + return l; + } + + private static int valueSize(AnnotationValue val) { + return 1 + switch (val) { + case AnnotationValue.OfAnnotation oan -> + annotationSize(oan.annotation()); + case AnnotationValue.OfArray oar -> { + int l = 2; + for (var v : oar.values()) { + l += valueSize(v); + } + yield l; + } + case AnnotationValue.OfConstant _, AnnotationValue.OfClass _ -> 2; + case AnnotationValue.OfEnum _ -> 4; + }; + } + + private static int moduleHashesSize(List hashes) { + int l = 2; + for (var h : hashes) { + h.moduleName(); + l += 4 + h.hash().length; + } + return l; + } + + private int stackMapFrameSize(StackMapFrameInfo frame) { + int ft = frame.frameType(); + if (ft < 64) return 1; + if (ft < 128) return 1 + verificationTypeSize(frame.stack().getFirst()); + if (ft > 246) { + if (ft == 247) return 3 + verificationTypeSize(frame.stack().getFirst()); + if (ft < 252) return 3; + if (ft < 255) { + var loc = frame.locals(); + int l = 3; + for (int i = loc.size() + 251 - ft; i < loc.size(); i++) { + l += verificationTypeSize(loc.get(i)); + } + return l; + } + if (ft == 255) { + int l = 7; + for (var vt : frame.stack()) { + l += verificationTypeSize(vt); + } + for (var vt : frame.locals()) { + l += verificationTypeSize(vt); + } + return l; + } + } + throw new IllegalArgumentException("Invalid stack map frame type " + ft); + } + + private static int verificationTypeSize(StackMapFrameInfo.VerificationTypeInfo vti) { + return switch (vti) { + case StackMapFrameInfo.SimpleVerificationTypeInfo _ -> 1; + case StackMapFrameInfo.ObjectVerificationTypeInfo ovti -> { + ovti.classSymbol(); + yield 3; + } + case StackMapFrameInfo.UninitializedVerificationTypeInfo _ -> 3; + }; + } + + private String className() { + return classModel.thisClass().asSymbol().displayName(); + } + + private String toString(AttributedElement ae) { + return switch (ae) { + case CodeModel m -> "Code attribute for " + toString(m.parent().get()); + case FieldModel m -> "field %s.%s".formatted( + className(), + m.fieldName().stringValue()); + case MethodModel m -> "method %s::%s(%s)".formatted( + className(), + m.methodName().stringValue(), + m.methodTypeSymbol().parameterList().stream().map(ClassDesc::displayName).collect(Collectors.joining(","))); + case RecordComponentInfo i -> "Record component %s of class %s".formatted( + i.name().stringValue(), + className()); + default -> "class " + className(); + }; + } +} diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerificationWrapper.java b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerificationWrapper.java index 2a5a4a9401599..d34f579b1f4f3 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerificationWrapper.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerificationWrapper.java @@ -25,6 +25,7 @@ */ package jdk.internal.classfile.impl.verifier; +import java.lang.constant.ClassDesc; import java.util.LinkedList; import java.util.List; @@ -33,6 +34,7 @@ import java.lang.classfile.constantpool.MemberRefEntry; import java.lang.classfile.constantpool.NameAndTypeEntry; import java.lang.reflect.AccessFlag; +import java.util.stream.Collectors; import java.lang.classfile.ClassModel; import java.lang.classfile.constantpool.ConstantPool; import java.lang.classfile.MethodModel; @@ -129,6 +131,10 @@ String descriptor() { return m.methodType().stringValue(); } + String parameters() { + return m.methodTypeSymbol().parameterList().stream().map(ClassDesc::displayName).collect(Collectors.joining(",")); + } + int codeLength() { return c == null ? 0 : c.codeLength(); } diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java index 8dfa726ec1963..11e7256751eeb 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/verifier/VerifierImpl.java @@ -41,6 +41,11 @@ import static jdk.internal.classfile.impl.verifier.VerificationFrame.FLAG_THIS_UNINIT; /** + * VerifierImpl performs selected checks and verifications of the class file + * format according to {@jvms 4.8 Format Checking}, + * {@jvms 4.9 Constraints on Java Virtual Machine code}, + * {@jvms 4.10 Verification of class Files} and {@jvms 6.5 Instructions} + * * @see java.base/share/native/include/classfile_constants.h.template * @see hotspot/share/classfile/verifier.hpp * @see hotspot/share/classfile/verifier.cpp @@ -110,22 +115,24 @@ public static List verify(ClassModel classModel, Consumer l public static List verify(ClassModel classModel, ClassHierarchyResolver classHierarchyResolver, Consumer logger) { var klass = new VerificationWrapper(classModel); - if (!is_eligible_for_verification(klass)) { - return List.of(); - } log_info(logger, "Start class verification for: %s", klass.thisClassName()); try { - if (klass.majorVersion() >= STACKMAP_ATTRIBUTE_MAJOR_VERSION) { - var errors = new VerifierImpl(klass, classHierarchyResolver, logger).verify_class(); - if (!errors.isEmpty() && klass.majorVersion() < NOFAILOVER_MAJOR_VERSION) { - log_info(logger, "Fail over class verification to old verifier for: %s", klass.thisClassName()); - return inference_verify(klass); + var errors = new ArrayList(); + errors.addAll(new ParserVerifier(classModel).verify()); + if (is_eligible_for_verification(klass)) { + if (klass.majorVersion() >= STACKMAP_ATTRIBUTE_MAJOR_VERSION) { + var verifierErrors = new VerifierImpl(klass, classHierarchyResolver, logger).verify_class(); + if (!verifierErrors.isEmpty() && klass.majorVersion() < NOFAILOVER_MAJOR_VERSION) { + log_info(logger, "Fail over class verification to old verifier for: %s", klass.thisClassName()); + errors.addAll(inference_verify(klass)); + } else { + errors.addAll(verifierErrors); + } } else { - return errors; + errors.addAll(inference_verify(klass)); } - } else { - return inference_verify(klass); } + return errors; } finally { log_info(logger, "End class verification for: %s", klass.thisClassName()); } @@ -281,7 +288,7 @@ void create_method_sig_entry(sig_as_verification_types sig_verif_types, String m void verify_method(VerificationWrapper.MethodWrapper m, List errorsCollector) { try { - verify_method(m, m.maxLocals(), m.maxStack(), m.stackMapTableRawData()); + verify_method(m); } catch (VerifyError err) { errorsCollector.add(err); } catch (Error | Exception e) { @@ -290,9 +297,14 @@ void verify_method(VerificationWrapper.MethodWrapper m, List errors } @SuppressWarnings("fallthrough") - void verify_method(VerificationWrapper.MethodWrapper m, int max_locals, int max_stack, byte[] stackmap_data) { + void verify_method(VerificationWrapper.MethodWrapper m) { _method = m; log_info(_logger, "Verifying method %s%s", m.name(), m.descriptor()); + byte[] codeArray = m.codeArray(); + if (codeArray == null) verifyError("Missing Code attribute"); + int max_locals = m.maxLocals(); + int max_stack = m.maxStack(); + byte[] stackmap_data = m.stackMapTableRawData(); var cp = m.constantPool(); if (!VerificationSignature.isValidMethodSignature(m.descriptor())) verifyError("Invalid method signature"); VerificationFrame current_frame = new VerificationFrame(max_locals, max_stack, this); @@ -302,7 +314,7 @@ void verify_method(VerificationWrapper.MethodWrapper m, int max_locals, int max_ if (code_length < 1 || code_length > MAX_CODE_SIZE) { verifyError(String.format("Invalid method Code length %d", code_length)); } - var code = ByteBuffer.wrap(_method.codeArray(), 0, _method.codeLength()); + var code = ByteBuffer.wrap(codeArray, 0, _method.codeLength()); byte[] code_data = generate_code_data(code, code_length); int ex_minmax[] = new int[] {code_length, -1}; verify_exception_handler_table(code_length, code_data, ex_minmax); @@ -1816,16 +1828,16 @@ private void dumpMethod() { void verifyError(String msg) { dumpMethod(); - throw new VerifyError(String.format("%s at %s.%s%s @%d %s", msg, _klass.thisClassName(), _method.name(), _method.descriptor(), bci, errorContext)); + throw new VerifyError(String.format("%s in %s::%s(%s) @%d %s", msg, _klass.thisClassName(), _method.name(), _method.parameters(), bci, errorContext).trim()); } void verifyError(String msg, VerificationFrame from, VerificationFrame target) { dumpMethod(); - throw new VerifyError(String.format("%s at %s.%s%s @%d %s%n while assigning %s%n to %s", msg, _klass.thisClassName(), _method.name(), _method.descriptor(), bci, errorContext, from, target)); + throw new VerifyError(String.format("%s in %s::%s(%s) @%d %s%n while assigning %s%n to %s", msg, _klass.thisClassName(), _method.name(), _method.parameters(), bci, errorContext, from, target)); } void classError(String msg) { dumpMethod(); - throw new ClassFormatError(String.format("%s at %s.%s%s", msg, _klass.thisClassName(), _method.name(), _method.descriptor())); + throw new VerifyError(String.format("%s in %s::%s(%s)", msg, _klass.thisClassName(), _method.name(), _method.parameters())); } } diff --git a/test/jdk/jdk/classfile/ClassPrinterTest.java b/test/jdk/jdk/classfile/ClassPrinterTest.java index 6400dc4f5dfbe..9c25ff7e13af7 100644 --- a/test/jdk/jdk/classfile/ClassPrinterTest.java +++ b/test/jdk/jdk/classfile/ClassPrinterTest.java @@ -730,13 +730,13 @@ void testPrintXmlTraceAll() throws IOException { Phee PhooBooBee - + fee LPhoo; SignatureRuntimeInvisibleTypeAnnotations LPhoo; - LBoo;FIELD + LBoo;FIELD LPhoo;flfl2.0frfl3.0 BooPhoo diff --git a/test/jdk/jdk/classfile/VerifierSelfTest.java b/test/jdk/jdk/classfile/VerifierSelfTest.java index 8cbc90d2ff6fd..ef1bc3cab30ec 100644 --- a/test/jdk/jdk/classfile/VerifierSelfTest.java +++ b/test/jdk/jdk/classfile/VerifierSelfTest.java @@ -24,20 +24,30 @@ /* * @test * @summary Testing ClassFile Verifier. + * @enablePreview * @run junit VerifierSelfTest */ import java.io.IOException; +import java.lang.constant.ClassDesc; +import static java.lang.constant.ConstantDescs.*; +import java.lang.invoke.MethodHandleInfo; import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; -import java.lang.classfile.ClassHierarchyResolver; -import java.lang.classfile.ClassFile; -import java.lang.classfile.CodeModel; -import java.lang.classfile.MethodModel; +import java.lang.classfile.*; +import java.lang.classfile.attribute.*; +import java.lang.classfile.components.ClassPrinter; +import java.lang.constant.ModuleDesc; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.fail; class VerifierSelfTest { @@ -83,4 +93,289 @@ void testFailed() throws IOException { throw new AssertionError("expected verification failure"); } } + + @Test + void testParserVerification() { + var cc = ClassFile.of(); + var cd_test = ClassDesc.of("ParserVerificationTestClass"); + var indexes = new Object[9]; + var clm = cc.parse(cc.build(cd_test, clb -> { + clb.withFlags(ClassFile.ACC_INTERFACE | ClassFile.ACC_FINAL); + var cp = clb.constantPool(); + var ce_valid = cp.classEntry(cd_test); + var ce_invalid = cp.classEntry(cp.utf8Entry("invalid.class.name")); + indexes[0] = ce_invalid.index(); + var nate_invalid_field = cp.nameAndTypeEntry("field;", CD_int); + var nate_invalid_method = cp.nameAndTypeEntry("method;", MTD_void); + var bsme = cp.bsmEntry(BSM_INVOKE, List.of()); + indexes[1] = cp.methodTypeEntry(cp.utf8Entry("invalid method type")).index(); + indexes[2] = cp.constantDynamicEntry(bsme, nate_invalid_method).index(); + indexes[3] = cp.invokeDynamicEntry(bsme, nate_invalid_field).index(); + indexes[4] = cp.fieldRefEntry(ce_invalid, nate_invalid_method).index(); + indexes[5] = cp.methodRefEntry(ce_invalid, nate_invalid_field).index(); + indexes[6] = cp.interfaceMethodRefEntry(ce_invalid, nate_invalid_field).index(); + indexes[7] = cp.methodHandleEntry(MethodHandleInfo.REF_getField, cp.methodRefEntry(cd_test, "method", MTD_void)).index(); + indexes[8] = cp.methodHandleEntry(MethodHandleInfo.REF_invokeVirtual, cp.fieldRefEntry(cd_test, "field", CD_int)).index(); + patch(clb, + CompilationIDAttribute.of("12345"), + DeprecatedAttribute.of(), + EnclosingMethodAttribute.of(cd_test, Optional.empty(), Optional.empty()), + InnerClassesAttribute.of(InnerClassInfo.of(cd_test, Optional.of(cd_test), Optional.of("inner"), 0)), + ModuleAttribute.of(ModuleDesc.of("m"), mab -> {}), + ModuleHashesAttribute.of("alg", List.of()), + ModuleMainClassAttribute.of(cd_test), + ModulePackagesAttribute.of(), + ModuleResolutionAttribute.of(0), + ModuleTargetAttribute.of("t"), + NestHostAttribute.of(cd_test), + NestMembersAttribute.ofSymbols(cd_test), + PermittedSubclassesAttribute.ofSymbols(cd_test), + RecordAttribute.of(RecordComponentInfo.of("c", CD_String, patch( + SignatureAttribute.of(Signature.of(CD_String)), + RuntimeVisibleAnnotationsAttribute.of(), + RuntimeInvisibleAnnotationsAttribute.of(), + RuntimeVisibleTypeAnnotationsAttribute.of(), + RuntimeInvisibleTypeAnnotationsAttribute.of()))), + RuntimeVisibleAnnotationsAttribute.of(), + RuntimeInvisibleAnnotationsAttribute.of(), + RuntimeVisibleTypeAnnotationsAttribute.of(), + RuntimeInvisibleTypeAnnotationsAttribute.of(), + SignatureAttribute.of(ClassSignature.of(Signature.ClassTypeSig.of(cd_test))), + SourceDebugExtensionAttribute.of("sde".getBytes()), + SourceFileAttribute.of("ParserVerificationTestClass.java"), + SourceIDAttribute.of("sID"), + SyntheticAttribute.of()) + .withInterfaceSymbols(CD_List, CD_List) + .withField("f", CD_String, fb -> patch(fb, + ConstantValueAttribute.of(0), + DeprecatedAttribute.of(), + RuntimeVisibleAnnotationsAttribute.of(), + RuntimeInvisibleAnnotationsAttribute.of(), + RuntimeVisibleTypeAnnotationsAttribute.of(), + RuntimeInvisibleTypeAnnotationsAttribute.of(), + SignatureAttribute.of(Signature.of(CD_String)), + SyntheticAttribute.of())) + .withField("/", CD_int, 0) + .withField("/", CD_int, 0) + .withMethod("m", MTD_void, ClassFile.ACC_ABSTRACT | ClassFile.ACC_STATIC, mb -> patch(mb, + AnnotationDefaultAttribute.of(AnnotationValue.ofInt(0)), + DeprecatedAttribute.of(), + ExceptionsAttribute.ofSymbols(CD_Exception), + MethodParametersAttribute.of(MethodParameterInfo.ofParameter(Optional.empty(), 0)), + RuntimeVisibleAnnotationsAttribute.of(), + RuntimeInvisibleAnnotationsAttribute.of(), + RuntimeVisibleParameterAnnotationsAttribute.of(List.of()), + RuntimeInvisibleParameterAnnotationsAttribute.of(List.of()), + SignatureAttribute.of(MethodSignature.of(MTD_void)), + SyntheticAttribute.of()) + .withCode(cob -> + cob.iconst_0() + .ifThen(CodeBuilder::nop) + .return_() + .with(new CloneAttribute(StackMapTableAttribute.of(List.of()))) + .with(new CloneAttribute(CharacterRangeTableAttribute.of(List.of()))) + .with(new CloneAttribute(LineNumberTableAttribute.of(List.of()))) + .with(new CloneAttribute(LocalVariableTableAttribute.of(List.of()))) + .with(new CloneAttribute(LocalVariableTypeTableAttribute.of(List.of()))))) + .withMethod("<>", MTD_void, ClassFile.ACC_NATIVE, mb -> {}) + .withMethod("<>", MTD_void, ClassFile.ACC_NATIVE, mb -> {}) + .withMethod(INIT_NAME, MTD_void, 0, mb -> {}) + .withMethod(CLASS_INIT_NAME, MTD_void, 0, mb -> {}); + })); + var found = cc.verify(clm).stream().map(VerifyError::getMessage).collect(Collectors.toCollection(LinkedList::new)); + var expected = """ + Invalid class name: invalid.class.name at constant pool index %1$d in class ParserVerificationTestClass + Bad method descriptor: invalid method type at constant pool index %2$d in class ParserVerificationTestClass + not a valid reference type descriptor: ()V at constant pool index %3$d in class ParserVerificationTestClass + Bad method descriptor: I at constant pool index %4$d in class ParserVerificationTestClass + not a valid reference type descriptor: ()V at constant pool index %5$d in class ParserVerificationTestClass + Invalid class name: invalid.class.name at constant pool index %5$d in class ParserVerificationTestClass + Illegal field name method; in class ParserVerificationTestClass at constant pool index %5$d in class ParserVerificationTestClass + Bad method descriptor: I at constant pool index %6$d in class ParserVerificationTestClass + Invalid class name: invalid.class.name at constant pool index %6$d in class ParserVerificationTestClass + Illegal method name field; in class ParserVerificationTestClass at constant pool index %6$d in class ParserVerificationTestClass + Bad method descriptor: I at constant pool index %7$d in class ParserVerificationTestClass + Invalid class name: invalid.class.name at constant pool index %7$d in class ParserVerificationTestClass + Illegal method name field; in class ParserVerificationTestClass at constant pool index %7$d in class ParserVerificationTestClass + not a valid reference type descriptor: ()V at constant pool index %8$d in class ParserVerificationTestClass + Bad method descriptor: I at constant pool index %9$d in class ParserVerificationTestClass + Duplicate interface List in class ParserVerificationTestClass + Illegal field name / in class ParserVerificationTestClass + Duplicate field name / with signature I in class ParserVerificationTestClass + Illegal field name / in class ParserVerificationTestClass + Illegal method name <> in class ParserVerificationTestClass + Duplicate method name <> with signature ()V in class ParserVerificationTestClass + Illegal method name <> in class ParserVerificationTestClass + Interface cannot have a method named in class ParserVerificationTestClass + Method is not static in class ParserVerificationTestClass + Multiple CompilationID attributes in class ParserVerificationTestClass + Wrong CompilationID attribute length in class ParserVerificationTestClass + Wrong Deprecated attribute length in class ParserVerificationTestClass + Multiple EnclosingMethod attributes in class ParserVerificationTestClass + Wrong EnclosingMethod attribute length in class ParserVerificationTestClass + Class is both outer and inner class in class ParserVerificationTestClass + Multiple InnerClasses attributes in class ParserVerificationTestClass + Class is both outer and inner class in class ParserVerificationTestClass + Wrong InnerClasses attribute length in class ParserVerificationTestClass + Multiple Module attributes in class ParserVerificationTestClass + Wrong Module attribute length in class ParserVerificationTestClass + Multiple ModuleHashes attributes in class ParserVerificationTestClass + Wrong ModuleHashes attribute length in class ParserVerificationTestClass + Multiple ModuleMainClass attributes in class ParserVerificationTestClass + Wrong ModuleMainClass attribute length in class ParserVerificationTestClass + Multiple ModulePackages attributes in class ParserVerificationTestClass + Wrong ModulePackages attribute length in class ParserVerificationTestClass + Multiple ModuleResolution attributes in class ParserVerificationTestClass + Wrong ModuleResolution attribute length in class ParserVerificationTestClass + Multiple ModuleTarget attributes in class ParserVerificationTestClass + Wrong ModuleTarget attribute length in class ParserVerificationTestClass + Multiple NestHost attributes in class ParserVerificationTestClass + Wrong NestHost attribute length in class ParserVerificationTestClass + Conflicting NestHost and NestMembers attributes in class ParserVerificationTestClass + Multiple NestMembers attributes in class ParserVerificationTestClass + Conflicting NestHost and NestMembers attributes in class ParserVerificationTestClass + Wrong NestMembers attribute length in class ParserVerificationTestClass + PermittedSubclasses attribute in final class ParserVerificationTestClass + Multiple PermittedSubclasses attributes in class ParserVerificationTestClass + PermittedSubclasses attribute in final class ParserVerificationTestClass + Wrong PermittedSubclasses attribute length in class ParserVerificationTestClass + Multiple Record attributes in class ParserVerificationTestClass + Wrong Record attribute length in class ParserVerificationTestClass + Multiple RuntimeVisibleAnnotations attributes in class ParserVerificationTestClass + Wrong RuntimeVisibleAnnotations attribute length in class ParserVerificationTestClass + Multiple RuntimeInvisibleAnnotations attributes in class ParserVerificationTestClass + Wrong RuntimeInvisibleAnnotations attribute length in class ParserVerificationTestClass + Multiple RuntimeVisibleTypeAnnotations attributes in class ParserVerificationTestClass + Wrong RuntimeVisibleTypeAnnotations attribute length in class ParserVerificationTestClass + Multiple RuntimeInvisibleTypeAnnotations attributes in class ParserVerificationTestClass + Wrong RuntimeInvisibleTypeAnnotations attribute length in class ParserVerificationTestClass + Multiple Signature attributes in class ParserVerificationTestClass + Wrong Signature attribute length in class ParserVerificationTestClass + Multiple SourceDebugExtension attributes in class ParserVerificationTestClass + Multiple SourceFile attributes in class ParserVerificationTestClass + Wrong SourceFile attribute length in class ParserVerificationTestClass + Multiple SourceID attributes in class ParserVerificationTestClass + Wrong SourceID attribute length in class ParserVerificationTestClass + Wrong Synthetic attribute length in class ParserVerificationTestClass + Bad constant value type in field ParserVerificationTestClass.f + Multiple ConstantValue attributes in field ParserVerificationTestClass.f + Bad constant value type in field ParserVerificationTestClass.f + Wrong ConstantValue attribute length in field ParserVerificationTestClass.f + Wrong Deprecated attribute length in field ParserVerificationTestClass.f + Multiple RuntimeVisibleAnnotations attributes in field ParserVerificationTestClass.f + Wrong RuntimeVisibleAnnotations attribute length in field ParserVerificationTestClass.f + Multiple RuntimeInvisibleAnnotations attributes in field ParserVerificationTestClass.f + Wrong RuntimeInvisibleAnnotations attribute length in field ParserVerificationTestClass.f + Multiple RuntimeVisibleTypeAnnotations attributes in field ParserVerificationTestClass.f + Wrong RuntimeVisibleTypeAnnotations attribute length in field ParserVerificationTestClass.f + Multiple RuntimeInvisibleTypeAnnotations attributes in field ParserVerificationTestClass.f + Wrong RuntimeInvisibleTypeAnnotations attribute length in field ParserVerificationTestClass.f + Multiple Signature attributes in field ParserVerificationTestClass.f + Wrong Signature attribute length in field ParserVerificationTestClass.f + Wrong Synthetic attribute length in field ParserVerificationTestClass.f + Multiple AnnotationDefault attributes in method ParserVerificationTestClass::m() + Wrong AnnotationDefault attribute length in method ParserVerificationTestClass::m() + Wrong Deprecated attribute length in method ParserVerificationTestClass::m() + Multiple Exceptions attributes in method ParserVerificationTestClass::m() + Wrong Exceptions attribute length in method ParserVerificationTestClass::m() + Multiple MethodParameters attributes in method ParserVerificationTestClass::m() + Wrong MethodParameters attribute length in method ParserVerificationTestClass::m() + Multiple RuntimeVisibleAnnotations attributes in method ParserVerificationTestClass::m() + Wrong RuntimeVisibleAnnotations attribute length in method ParserVerificationTestClass::m() + Multiple RuntimeInvisibleAnnotations attributes in method ParserVerificationTestClass::m() + Wrong RuntimeInvisibleAnnotations attribute length in method ParserVerificationTestClass::m() + Multiple RuntimeVisibleParameterAnnotations attributes in method ParserVerificationTestClass::m() + Wrong RuntimeVisibleParameterAnnotations attribute length in method ParserVerificationTestClass::m() + Multiple RuntimeInvisibleParameterAnnotations attributes in method ParserVerificationTestClass::m() + Wrong RuntimeInvisibleParameterAnnotations attribute length in method ParserVerificationTestClass::m() + Multiple Signature attributes in method ParserVerificationTestClass::m() + Wrong Signature attribute length in method ParserVerificationTestClass::m() + Wrong Synthetic attribute length in method ParserVerificationTestClass::m() + Code attribute in native or abstract method ParserVerificationTestClass::m() + Wrong StackMapTable attribute length in Code attribute for method ParserVerificationTestClass::m() + Wrong CharacterRangeTable attribute length in Code attribute for method ParserVerificationTestClass::m() + Wrong LineNumberTable attribute length in Code attribute for method ParserVerificationTestClass::m() + Wrong LocalVariableTable attribute length in Code attribute for method ParserVerificationTestClass::m() + Wrong LocalVariableTypeTable attribute length in Code attribute for method ParserVerificationTestClass::m() + Multiple StackMapTable attributes in Code attribute for method ParserVerificationTestClass::m() + Multiple Signature attributes in Record component c of class ParserVerificationTestClass + Wrong Signature attribute length in Record component c of class ParserVerificationTestClass + Multiple RuntimeVisibleAnnotations attributes in Record component c of class ParserVerificationTestClass + Wrong RuntimeVisibleAnnotations attribute length in Record component c of class ParserVerificationTestClass + Multiple RuntimeInvisibleAnnotations attributes in Record component c of class ParserVerificationTestClass + Wrong RuntimeInvisibleAnnotations attribute length in Record component c of class ParserVerificationTestClass + Multiple RuntimeVisibleTypeAnnotations attributes in Record component c of class ParserVerificationTestClass + Wrong RuntimeVisibleTypeAnnotations attribute length in Record component c of class ParserVerificationTestClass + Multiple RuntimeInvisibleTypeAnnotations attributes in Record component c of class ParserVerificationTestClass + Wrong RuntimeInvisibleTypeAnnotations attribute length in Record component c of class ParserVerificationTestClass + Multiple Signature attributes in Record component c of class ParserVerificationTestClass + Wrong Signature attribute length in Record component c of class ParserVerificationTestClass + Multiple RuntimeVisibleAnnotations attributes in Record component c of class ParserVerificationTestClass + Wrong RuntimeVisibleAnnotations attribute length in Record component c of class ParserVerificationTestClass + Multiple RuntimeInvisibleAnnotations attributes in Record component c of class ParserVerificationTestClass + Wrong RuntimeInvisibleAnnotations attribute length in Record component c of class ParserVerificationTestClass + Multiple RuntimeVisibleTypeAnnotations attributes in Record component c of class ParserVerificationTestClass + Wrong RuntimeVisibleTypeAnnotations attribute length in Record component c of class ParserVerificationTestClass + Multiple RuntimeInvisibleTypeAnnotations attributes in Record component c of class ParserVerificationTestClass + Wrong RuntimeInvisibleTypeAnnotations attribute length in Record component c of class ParserVerificationTestClass + Missing Code attribute in ParserVerificationTestClass::() @0 + Missing Code attribute in ParserVerificationTestClass::() @0 + """.formatted(indexes).lines().filter(exp -> !found.remove(exp)).toList(); + if (!found.isEmpty() || !expected.isEmpty()) { + ClassPrinter.toYaml(clm, ClassPrinter.Verbosity.TRACE_ALL, System.out::print); + fail(""" + + Expected: + %s + + Found: + %s + """.formatted(expected.stream().collect(Collectors.joining("\n ")), found.stream().collect(Collectors.joining("\n ")))); + } + } + + private static class CloneAttribute extends CustomAttribute { + CloneAttribute(Attribute a) { + super(new AttributeMapper(){ + @Override + public String name() { + return a.attributeName(); + } + + @Override + public CloneAttribute readAttribute(AttributedElement enclosing, ClassReader cf, int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public void writeAttribute(BufWriter buf, CloneAttribute attr) { + int start = buf.size(); + a.attributeMapper().writeAttribute(buf, a); + buf.writeU1(0); //writes additional byte to the attribute payload + buf.patchInt(start + 2, 4, buf.size() - start - 6); + } + + @Override + public AttributeMapper.AttributeStability stability() { + return a.attributeMapper().stability(); + } + }); + } + } + + private static B patch(B b, Attribute... attrs) { + for (var a : attrs) { + b.with(a).with(new CloneAttribute(a)); + } + return b; + } + + private static List> patch(Attribute... attrs) { + var lst = new ArrayList>(attrs.length * 2); + for (var a : attrs) { + lst.add(a); + lst.add(new CloneAttribute(a)); + } + return lst; + } }