diff --git a/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessRuntimeChecker.java b/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessRuntimeChecker.java index 87fd2e2..184b0cc 100644 --- a/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessRuntimeChecker.java +++ b/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessRuntimeChecker.java @@ -8,8 +8,8 @@ import io.github.eisop.runtimeframework.filter.ClassInfo; import io.github.eisop.runtimeframework.filter.Filter; import io.github.eisop.runtimeframework.policy.EnforcementPolicy; +import io.github.eisop.runtimeframework.resolution.BytecodeHierarchyResolver; import io.github.eisop.runtimeframework.resolution.HierarchyResolver; -import io.github.eisop.runtimeframework.resolution.ReflectionHierarchyResolver; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -33,7 +33,7 @@ public RuntimeInstrumenter getInstrumenter(Filter filter) { EnforcementPolicy policy = createPolicy(config, filter); HierarchyResolver resolver = - new ReflectionHierarchyResolver( + new BytecodeHierarchyResolver( className -> filter.test(new ClassInfo(className.replace('.', '/'), null, null))); return new AnnotationInstrumenter(policy, resolver, filter); diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeTransformer.java b/framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeTransformer.java index dbb6f60..bdb2fe3 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeTransformer.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeTransformer.java @@ -5,6 +5,7 @@ import io.github.eisop.runtimeframework.filter.ClassInfo; import io.github.eisop.runtimeframework.filter.Filter; import io.github.eisop.runtimeframework.qual.AnnotatedFor; +import java.io.InputStream; import java.lang.classfile.Annotation; import java.lang.classfile.AnnotationValue; import java.lang.classfile.Attributes; @@ -118,7 +119,7 @@ private boolean hasPackageLevelAnnotation(String className, ClassLoader loader, String packageInfoPath = packageName + "/package-info.class"; boolean found = false; - try (java.io.InputStream is = + try (InputStream is = (loader != null) ? loader.getResourceAsStream(packageInfoPath) : ClassLoader.getSystemResourceAsStream(packageInfoPath)) { diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/core/AnnotationInstrumenter.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/AnnotationInstrumenter.java index af620f7..efd39f9 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/AnnotationInstrumenter.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/AnnotationInstrumenter.java @@ -4,6 +4,7 @@ import io.github.eisop.runtimeframework.filter.Filter; import io.github.eisop.runtimeframework.policy.EnforcementPolicy; import io.github.eisop.runtimeframework.resolution.HierarchyResolver; +import io.github.eisop.runtimeframework.resolution.ParentMethod; import java.lang.classfile.ClassBuilder; import java.lang.classfile.ClassModel; import java.lang.classfile.CodeBuilder; @@ -19,8 +20,8 @@ import java.lang.classfile.instruction.StoreInstruction; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; -import java.lang.reflect.Method; -import java.util.Arrays; +import java.lang.reflect.Modifier; +import java.util.List; public class AnnotationInstrumenter extends RuntimeInstrumenter { @@ -159,7 +160,7 @@ protected void generateMethodCallCheck(CodeBuilder b, InvokeInstruction invoke) @Override protected void generateBridgeMethods(ClassBuilder builder, ClassModel model, ClassLoader loader) { - for (Method parentMethod : hierarchyResolver.resolveUncheckedMethods(model, loader)) { + for (ParentMethod parentMethod : hierarchyResolver.resolveUncheckedMethods(model, loader)) { if (policy.shouldGenerateBridge(parentMethod)) { emitBridge(builder, parentMethod); } @@ -186,28 +187,23 @@ protected void generateStoreCheck( } } - private void emitBridge(ClassBuilder builder, Method parentMethod) { - String methodName = parentMethod.getName(); - MethodTypeDesc desc = - MethodTypeDesc.of( - ClassDesc.ofDescriptor(parentMethod.getReturnType().descriptorString()), - Arrays.stream(parentMethod.getParameterTypes()) - .map(c -> ClassDesc.ofDescriptor(c.descriptorString())) - .toArray(ClassDesc[]::new)); + private void emitBridge(ClassBuilder builder, ParentMethod parentMethod) { + MethodModel method = parentMethod.method(); + String methodName = method.methodName().stringValue(); + MethodTypeDesc desc = method.methodTypeSymbol(); builder.withMethod( methodName, desc, - java.lang.reflect.Modifier.PUBLIC, + Modifier.PUBLIC, methodBuilder -> { methodBuilder.withCode( codeBuilder -> { int slotIndex = 1; - Class[] paramTypes = parentMethod.getParameterTypes(); + List paramTypes = desc.parameterList(); - for (int i = 0; i < paramTypes.length; i++) { - TypeKind type = - TypeKind.from(ClassDesc.ofDescriptor(paramTypes[i].descriptorString())); + for (int i = 0; i < paramTypes.size(); i++) { + TypeKind type = TypeKind.from(paramTypes.get(i)); RuntimeVerifier target = policy.getBridgeParameterCheck(parentMethod, i); if (target != null) { codeBuilder.aload(slotIndex); @@ -219,15 +215,18 @@ private void emitBridge(ClassBuilder builder, Method parentMethod) { codeBuilder.aload(0); slotIndex = 1; - for (Class pType : paramTypes) { - TypeKind type = TypeKind.from(ClassDesc.ofDescriptor(pType.descriptorString())); + for (ClassDesc pType : paramTypes) { + TypeKind type = TypeKind.from(pType); loadLocal(codeBuilder, type, slotIndex); slotIndex += type.slotSize(); } - ClassDesc parentDesc = ClassDesc.of(parentMethod.getDeclaringClass().getName()); + ClassDesc parentDesc = + ClassDesc.of( + parentMethod.owner().thisClass().asInternalName().replace('/', '.')); codeBuilder.invokespecial(parentDesc, methodName, desc); - returnResult(codeBuilder, parentMethod.getReturnType()); + returnResult( + codeBuilder, ClassDesc.ofDescriptor(desc.returnType().descriptorString())); }); }); } @@ -253,12 +252,17 @@ private void loadLocal(CodeBuilder b, TypeKind type, int slot) { } } - private void returnResult(CodeBuilder b, Class returnType) { - if (returnType == void.class) b.return_(); - else if (returnType == int.class || returnType == boolean.class) b.ireturn(); - else if (returnType == long.class) b.lreturn(); - else if (returnType == float.class) b.freturn(); - else if (returnType == double.class) b.dreturn(); + private void returnResult(CodeBuilder b, ClassDesc returnType) { + String desc = returnType.descriptorString(); + if (desc.equals("V")) b.return_(); + else if (desc.equals("I") + || desc.equals("Z") + || desc.equals("B") + || desc.equals("S") + || desc.equals("C")) b.ireturn(); + else if (desc.equals("J")) b.lreturn(); + else if (desc.equals("F")) b.freturn(); + else if (desc.equals("D")) b.dreturn(); else b.areturn(); } } diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/policy/EnforcementPolicy.java b/framework/src/main/java/io/github/eisop/runtimeframework/policy/EnforcementPolicy.java index 0b8a3a4..c43a62a 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/policy/EnforcementPolicy.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/policy/EnforcementPolicy.java @@ -1,12 +1,12 @@ package io.github.eisop.runtimeframework.policy; import io.github.eisop.runtimeframework.core.RuntimeVerifier; +import io.github.eisop.runtimeframework.resolution.ParentMethod; import java.lang.classfile.ClassModel; import java.lang.classfile.FieldModel; import java.lang.classfile.MethodModel; import java.lang.classfile.TypeKind; import java.lang.constant.MethodTypeDesc; -import java.lang.reflect.Method; /** Defines the rules for WHEN to inject a runtime check. */ public interface EnforcementPolicy { @@ -39,10 +39,10 @@ default RuntimeVerifier getBoundaryFieldWriteCheck( RuntimeVerifier getBoundaryFieldReadCheck(String owner, String fieldName, TypeKind type); /** Should we generate a bridge for this inherited method? */ - boolean shouldGenerateBridge(Method parentMethod); + boolean shouldGenerateBridge(ParentMethod parentMethod); /** For a bridge we are generating, what check applies to this parameter? */ - RuntimeVerifier getBridgeParameterCheck(Method parentMethod, int paramIndex); + RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int paramIndex); /** Should we check an value being stored into an array? */ RuntimeVerifier getArrayStoreCheck(TypeKind componentType); diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/policy/StandardEnforcementPolicy.java b/framework/src/main/java/io/github/eisop/runtimeframework/policy/StandardEnforcementPolicy.java index 897fa83..5f21c31 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/policy/StandardEnforcementPolicy.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/policy/StandardEnforcementPolicy.java @@ -5,15 +5,14 @@ import io.github.eisop.runtimeframework.core.ValidationKind; import io.github.eisop.runtimeframework.filter.ClassInfo; import io.github.eisop.runtimeframework.filter.Filter; +import io.github.eisop.runtimeframework.resolution.ParentMethod; import java.lang.classfile.Annotation; import java.lang.classfile.Attributes; import java.lang.classfile.FieldModel; import java.lang.classfile.MethodModel; import java.lang.classfile.TypeAnnotation; import java.lang.classfile.TypeKind; -import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -137,18 +136,21 @@ public RuntimeVerifier getBoundaryFieldReadCheck(String owner, String fieldName, // --- 3. Inheritance Logic --- @Override - public boolean shouldGenerateBridge(Method parentMethod) { - if (parentMethod.getDeclaringClass() == Object.class) return false; - Class[] paramTypes = parentMethod.getParameterTypes(); - java.lang.annotation.Annotation[][] paramAnnos = parentMethod.getParameterAnnotations(); + public boolean shouldGenerateBridge(ParentMethod parentMethod) { + if (parentMethod.owner().thisClass().asInternalName().equals("java/lang/Object")) return false; - for (int i = 0; i < paramTypes.length; i++) { - // Check specific parameter annotations + MethodModel method = parentMethod.method(); + // MethodTypeDesc param parsing + var paramTypes = method.methodTypeSymbol().parameterList(); + + for (int i = 0; i < paramTypes.size(); i++) { boolean explicitNoop = false; boolean explicitEnforce = false; - for (java.lang.annotation.Annotation anno : paramAnnos[i]) { - String desc = "L" + anno.annotationType().getName().replace('.', '/') + ";"; + List annos = getMethodParamAnnotations(method, i); + + for (Annotation anno : annos) { + String desc = anno.classSymbol().descriptorString(); TypeSystemConfiguration.ConfigEntry entry = configuration.find(desc); if (entry != null) { if (entry.kind() == ValidationKind.ENFORCE) explicitEnforce = true; @@ -159,8 +161,8 @@ public boolean shouldGenerateBridge(Method parentMethod) { if (explicitEnforce) return true; // If no explicit decision, check default if it's a reference type - ClassDesc pTypeDesc = ClassDesc.ofDescriptor(paramTypes[i].descriptorString()); - if (TypeKind.from(pTypeDesc) == TypeKind.REFERENCE && !explicitNoop) { + TypeKind pType = TypeKind.from(paramTypes.get(i)); + if (pType == TypeKind.REFERENCE && !explicitNoop) { TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { return true; @@ -171,26 +173,33 @@ public boolean shouldGenerateBridge(Method parentMethod) { } @Override - public RuntimeVerifier getBridgeParameterCheck(Method parentMethod, int paramIndex) { - java.lang.annotation.Annotation[] annos = parentMethod.getParameterAnnotations()[paramIndex]; - Class paramType = parentMethod.getParameterTypes()[paramIndex]; + public RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int paramIndex) { + MethodModel method = parentMethod.method(); + List annos = getMethodParamAnnotations(method, paramIndex); - // 1. Explicit Configuration - for (java.lang.annotation.Annotation anno : annos) { - String desc = "L" + anno.annotationType().getName().replace('.', '/') + ";"; - TypeSystemConfiguration.ConfigEntry entry = configuration.find(desc); - if (entry != null) { - if (entry.kind() == ValidationKind.ENFORCE) return entry.verifier(); - if (entry.kind() == ValidationKind.NOOP) return null; + RuntimeVerifier verifier = resolveVerifier(annos); + if (verifier != null) return verifier; + + // Check default + var paramTypes = method.methodTypeSymbol().parameterList(); + TypeKind pType = TypeKind.from(paramTypes.get(paramIndex)); + + if (pType == TypeKind.REFERENCE) { + // Need to ensure we don't default if it was explicitly NOOPed (resolvedVerifier handles NOOP + // by returning null if found) + // Re-checking NOOP logic because resolveVerifier returns null for BOTH "Noop" and "Not Found" + boolean isExplicitNoop = false; + for (Annotation a : annos) { + TypeSystemConfiguration.ConfigEntry entry = + configuration.find(a.classSymbol().descriptorString()); + if (entry != null && entry.kind() == ValidationKind.NOOP) isExplicitNoop = true; } - } - // 2. Default - ClassDesc pTypeDesc = ClassDesc.ofDescriptor(paramType.descriptorString()); - if (TypeKind.from(pTypeDesc) == TypeKind.REFERENCE) { - TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); - if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { - return defaultEntry.verifier(); + if (!isExplicitNoop) { + TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); + if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { + return defaultEntry.verifier(); + } } } return null; diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/resolution/BytecodeHierarchyResolver.java b/framework/src/main/java/io/github/eisop/runtimeframework/resolution/BytecodeHierarchyResolver.java new file mode 100644 index 0000000..d98fb62 --- /dev/null +++ b/framework/src/main/java/io/github/eisop/runtimeframework/resolution/BytecodeHierarchyResolver.java @@ -0,0 +1,80 @@ +package io.github.eisop.runtimeframework.resolution; + +import io.github.eisop.runtimeframework.filter.Filter; +import java.io.IOException; +import java.io.InputStream; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.MethodModel; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Set; + +public class BytecodeHierarchyResolver implements HierarchyResolver { + + private final Filter safetyFilter; + + public BytecodeHierarchyResolver(Filter safetyFilter) { + this.safetyFilter = safetyFilter; + } + + @Override + public Set resolveUncheckedMethods(ClassModel model, ClassLoader loader) { + Set bridgesNeeded = new HashSet<>(); + Set implementedSignatures = new HashSet<>(); + + for (MethodModel mm : model.methods()) { + implementedSignatures.add( + mm.methodName().stringValue() + mm.methodTypeSymbol().descriptorString()); + } + + String superName = + model + .superclass() + .map(sc -> sc.asInternalName().replace('/', '.')) + .orElse("java.lang.Object"); + + if ("java.lang.Object".equals(superName)) return bridgesNeeded; + + String currentName = superName; + while (currentName != null && !currentName.equals("java.lang.Object")) { + if (safetyFilter.test(currentName)) { + break; + } + + try (InputStream is = loader.getResourceAsStream(currentName.replace('.', '/') + ".class")) { + if (is == null) { + break; + } + + ClassModel parentModel = ClassFile.of().parse(is.readAllBytes()); + + for (MethodModel m : parentModel.methods()) { + int flags = m.flags().flagsMask(); + + if (Modifier.isPrivate(flags) + || Modifier.isStatic(flags) + || Modifier.isFinal(flags) + || (flags & 0x1000) != 0 /* SYNTHETIC */ + || (flags & 0x0040) != 0 /* BRIDGE */) { + continue; + } + + String sig = m.methodName().stringValue() + m.methodTypeSymbol().descriptorString(); + if (implementedSignatures.contains(sig)) continue; + + implementedSignatures.add(sig); + bridgesNeeded.add(new ParentMethod(parentModel, m)); + } + + currentName = + parentModel.superclass().map(sc -> sc.asInternalName().replace('/', '.')).orElse(null); + + } catch (IOException e) { + System.err.println("[RuntimeFramework] Error reading bytecode for: " + currentName); + break; + } + } + return bridgesNeeded; + } +} diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/resolution/HierarchyResolver.java b/framework/src/main/java/io/github/eisop/runtimeframework/resolution/HierarchyResolver.java index 32a757c..c1ddeda 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/resolution/HierarchyResolver.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/resolution/HierarchyResolver.java @@ -1,7 +1,6 @@ package io.github.eisop.runtimeframework.resolution; import java.lang.classfile.ClassModel; -import java.lang.reflect.Method; import java.util.Set; /** @@ -15,8 +14,8 @@ public interface HierarchyResolver { * class. 2. Are NOT final/private/static. 3. Come from an "Unchecked" (unsafe) ancestor. * * @param model The class currently being instrumented. - * @param loader The ClassLoader to use for loading parent classes. - * @return A set of java.lang.reflect.Method objects representing the targets for bridging. + * @param loader The ClassLoader to use for loading parent classes (as resources). + * @return A set of ParentMethod objects representing the targets for bridging. */ - Set resolveUncheckedMethods(ClassModel model, ClassLoader loader); + Set resolveUncheckedMethods(ClassModel model, ClassLoader loader); } diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/resolution/ParentMethod.java b/framework/src/main/java/io/github/eisop/runtimeframework/resolution/ParentMethod.java new file mode 100644 index 0000000..2c40a5a --- /dev/null +++ b/framework/src/main/java/io/github/eisop/runtimeframework/resolution/ParentMethod.java @@ -0,0 +1,10 @@ +package io.github.eisop.runtimeframework.resolution; + +import java.lang.classfile.ClassModel; +import java.lang.classfile.MethodModel; + +/** + * Represents a method found in a parent class during hierarchy resolution. Wraps the ClassModel of + * the parent and the MethodModel of the method. + */ +public record ParentMethod(ClassModel owner, MethodModel method) {} diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/resolution/ReflectionHierarchyResolver.java b/framework/src/main/java/io/github/eisop/runtimeframework/resolution/ReflectionHierarchyResolver.java deleted file mode 100644 index 2b23e7f..0000000 --- a/framework/src/main/java/io/github/eisop/runtimeframework/resolution/ReflectionHierarchyResolver.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.github.eisop.runtimeframework.resolution; - -import io.github.eisop.runtimeframework.filter.Filter; -import java.lang.classfile.ClassModel; -import java.lang.classfile.MethodModel; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.HashSet; -import java.util.Set; - -public class ReflectionHierarchyResolver implements HierarchyResolver { - - private final Filter safetyFilter; - - public ReflectionHierarchyResolver(Filter safetyFilter) { - this.safetyFilter = safetyFilter; - } - - @Override - public Set resolveUncheckedMethods(ClassModel model, ClassLoader loader) { - Set bridgesNeeded = new HashSet<>(); - Set implementedSignatures = new HashSet<>(); - - for (MethodModel mm : model.methods()) { - implementedSignatures.add( - mm.methodName().stringValue() + mm.methodTypeSymbol().descriptorString()); - } - - String superName = - model - .superclass() - .map(sc -> sc.asInternalName().replace('/', '.')) - .orElse("java.lang.Object"); - if ("java.lang.Object".equals(superName)) return bridgesNeeded; - - try { - Class currentAncestor = Class.forName(superName, false, loader); - - while (currentAncestor != null && currentAncestor != Object.class) { - if (safetyFilter.test(currentAncestor.getName())) { - break; - } - - for (Method m : currentAncestor.getDeclaredMethods()) { - int mods = m.getModifiers(); - if (Modifier.isFinal(mods) || Modifier.isStatic(mods) || Modifier.isPrivate(mods)) - continue; - if (m.isSynthetic() || m.isBridge()) continue; - String sig = m.getName() + getMethodDescriptor(m); - if (implementedSignatures.contains(sig)) continue; - - implementedSignatures.add(sig); - bridgesNeeded.add(m); - } - currentAncestor = currentAncestor.getSuperclass(); - } - } catch (ClassNotFoundException e) { - System.err.println( - "[RuntimeFramework] Could not resolve hierarchy for: " - + model.thisClass().asInternalName()); - } - return bridgesNeeded; - } - - private String getMethodDescriptor(Method m) { - StringBuilder sb = new StringBuilder("("); - for (Class p : m.getParameterTypes()) { - sb.append(p.descriptorString()); - } - sb.append(")"); - sb.append(m.getReturnType().descriptorString()); - return sb.toString(); - } -}