Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -33,7 +33,7 @@ public RuntimeInstrumenter getInstrumenter(Filter<ClassInfo> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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);
}
Expand All @@ -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<ClassDesc> 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);
Expand All @@ -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()));
});
});
}
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Annotation> 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;
Expand All @@ -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;
Expand All @@ -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<Annotation> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> safetyFilter;

public BytecodeHierarchyResolver(Filter<String> safetyFilter) {
this.safetyFilter = safetyFilter;
}

@Override
public Set<ParentMethod> resolveUncheckedMethods(ClassModel model, ClassLoader loader) {
Set<ParentMethod> bridgesNeeded = new HashSet<>();
Set<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.eisop.runtimeframework.resolution;

import java.lang.classfile.ClassModel;
import java.lang.reflect.Method;
import java.util.Set;

/**
Expand All @@ -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<Method> resolveUncheckedMethods(ClassModel model, ClassLoader loader);
Set<ParentMethod> resolveUncheckedMethods(ClassModel model, ClassLoader loader);
}
Original file line number Diff line number Diff line change
@@ -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) {}
Loading