From 211798fdbf50664f5aa42946c03d7cb9b00f05b1 Mon Sep 17 00:00:00 2001 From: Alex Cook Date: Fri, 30 Jan 2026 14:11:29 -0500 Subject: [PATCH 1/6] feat: add bridge return checks --- .../InheritanceBridgeTest.java | 9 ++- .../core/AnnotationInstrumenter.java | 10 +++ .../policy/EnforcementPolicy.java | 5 ++ .../policy/StandardEnforcementPolicy.java | 76 +++++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/checker/src/test/resources/test-cases/nullness-bridge/InheritanceBridgeTest.java b/checker/src/test/resources/test-cases/nullness-bridge/InheritanceBridgeTest.java index 2414ee8..5658b17 100644 --- a/checker/src/test/resources/test-cases/nullness-bridge/InheritanceBridgeTest.java +++ b/checker/src/test/resources/test-cases/nullness-bridge/InheritanceBridgeTest.java @@ -28,9 +28,14 @@ public static void main(String[] args) { test.finalAction(null); // cannot bridge final methods, no error here - String unsafe = test.returnAction(); + // :: error: (Return value of inherited method returnAction must be NonNull) + test.returnAction(); + + // :: error: (Return value of inherited method returnAction must be NonNull) // :: error: (Local Variable Assignment (Slot 2) must be NonNull) + String unsafe = test.returnAction(); + // :: error: (Return value of inherited method returnAction must be NonNull) @Nullable String again = test.returnAction(); } @@ -38,4 +43,4 @@ public static void main(String[] args) { public void overrideMe(@NonNull String inputA, @Nullable String inputB) { System.out.println("safe version of this method" + inputA + inputB); } -} \ No newline at end of file +} 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 efd39f9..ab082f4 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 @@ -225,6 +225,16 @@ private void emitBridge(ClassBuilder builder, ParentMethod parentMethod) { ClassDesc.of( parentMethod.owner().thisClass().asInternalName().replace('/', '.')); codeBuilder.invokespecial(parentDesc, methodName, desc); + + RuntimeVerifier returnTarget = policy.getBridgeReturnCheck(parentMethod); + if (returnTarget != null) { + codeBuilder.dup(); + returnTarget.generateCheck( + codeBuilder, + TypeKind.REFERENCE, + "Return value of inherited method " + methodName); + } + returnResult( codeBuilder, ClassDesc.ofDescriptor(desc.returnType().descriptorString())); }); 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 c43a62a..da9fdc5 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 @@ -44,6 +44,11 @@ default RuntimeVerifier getBoundaryFieldWriteCheck( /** For a bridge we are generating, what check applies to this parameter? */ RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int paramIndex); + /** For a bridge we are generating, what check applies to the return value? */ + default RuntimeVerifier getBridgeReturnCheck(ParentMethod parentMethod) { + return null; + } + /** 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 35db6d8..252d44c 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 @@ -146,6 +146,7 @@ public boolean shouldGenerateBridge(ParentMethod parentMethod) { // MethodTypeDesc param parsing var paramTypes = method.methodTypeSymbol().parameterList(); + // 1. Check Parameters for (int i = 0; i < paramTypes.size(); i++) { boolean explicitNoop = false; boolean explicitEnforce = false; @@ -172,6 +173,34 @@ public boolean shouldGenerateBridge(ParentMethod parentMethod) { } } } + + // 2. Check Return Type + TypeKind returnType = TypeKind.from(method.methodTypeSymbol().returnType()); + if (returnType == TypeKind.REFERENCE) { + boolean explicitNoop = false; + boolean explicitEnforce = false; + + List annos = getMethodReturnAnnotations(method); + + 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; + if (entry.kind() == ValidationKind.NOOP) explicitNoop = true; + } + } + + if (explicitEnforce) return true; + + if (!explicitNoop) { + TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); + if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { + return true; + } + } + } + return false; } @@ -208,6 +237,35 @@ public RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int pa return null; } + @Override + public RuntimeVerifier getBridgeReturnCheck(ParentMethod parentMethod) { + MethodModel method = parentMethod.method(); + TypeKind returnType = TypeKind.from(method.methodTypeSymbol().returnType()); + if (returnType != TypeKind.REFERENCE) return null; + + List annos = getMethodReturnAnnotations(method); + + RuntimeVerifier verifier = resolveVerifier(annos); + if (verifier != null) return verifier.withAttribution(AttributionKind.CALLER); + + if (returnType == TypeKind.REFERENCE) { // Redundant but keeps structure similar + boolean isExplicitNoop = false; + for (Annotation a : annos) { + TypeSystemConfiguration.ConfigEntry entry = + configuration.find(a.classSymbol().descriptorString()); + if (entry != null && entry.kind() == ValidationKind.NOOP) isExplicitNoop = true; + } + + if (!isExplicitNoop) { + TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); + if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { + return defaultEntry.verifier().withAttribution(AttributionKind.CALLER); + } + } + } + return null; + } + // --- Parsing Helpers --- private List getMethodParamAnnotations(MethodModel method, int paramIndex) { List result = new ArrayList<>(); @@ -230,6 +288,24 @@ private List getMethodParamAnnotations(MethodModel method, int param return result; } + private List getMethodReturnAnnotations(MethodModel method) { + List result = new ArrayList<>(); + method + .findAttribute(Attributes.runtimeVisibleAnnotations()) + .ifPresent(attr -> result.addAll(attr.annotations())); + method + .findAttribute(Attributes.runtimeVisibleTypeAnnotations()) + .ifPresent( + attr -> { + for (TypeAnnotation ta : attr.annotations()) { + if (ta.targetInfo().targetType() == TypeAnnotation.TargetType.METHOD_RETURN) { + result.add(ta.annotation()); + } + } + }); + return result; + } + private List getFieldAnnotations(FieldModel field) { List result = new ArrayList<>(); field From 8c4b24b49dc3a5dcd2e451abc6fd29b0d1b62b60 Mon Sep 17 00:00:00 2001 From: Alex Cook Date: Fri, 30 Jan 2026 14:20:39 -0500 Subject: [PATCH 2/6] feat: add field read check --- .../nullness-field-read/InstanceFieldRead.java | 16 +++++++++++++--- .../nullness-field-read/StaticFieldRead.java | 13 +++++++++++-- .../nullness-parameter/FieldArgument.java | 9 ++++++--- .../core/RuntimeInstrumenter.java | 8 +------- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/checker/src/test/resources/test-cases/nullness-field-read/InstanceFieldRead.java b/checker/src/test/resources/test-cases/nullness-field-read/InstanceFieldRead.java index 66ca7fd..77f30a2 100644 --- a/checker/src/test/resources/test-cases/nullness-field-read/InstanceFieldRead.java +++ b/checker/src/test/resources/test-cases/nullness-field-read/InstanceFieldRead.java @@ -8,12 +8,22 @@ static class UncheckedLib { public String poison = null; } + public static void consume(@Nullable Object o) {} + public static void main(String[] args) { UncheckedLib lib = new UncheckedLib(); - String s = lib.poison; + // 1. Read without storage (Argument passing) + // :: error: (Read Field 'poison' must be NonNull) + consume(lib.poison); + + // 2. Assignment + // :: error: (Read Field 'poison' must be NonNull) // :: error: (Local Variable Assignment (Slot 2) must be NonNull) + String s = lib.poison; - @Nullable String q = lib.poison; + // 3. Nullable Assignment (Still checks read) + // :: error: (Read Field 'poison' must be NonNull) + @Nullable String q = lib.poison; } -} +} \ No newline at end of file diff --git a/checker/src/test/resources/test-cases/nullness-field-read/StaticFieldRead.java b/checker/src/test/resources/test-cases/nullness-field-read/StaticFieldRead.java index 99673fb..c33c48d 100644 --- a/checker/src/test/resources/test-cases/nullness-field-read/StaticFieldRead.java +++ b/checker/src/test/resources/test-cases/nullness-field-read/StaticFieldRead.java @@ -1,3 +1,4 @@ +import org.checkerframework.checker.nullness.qual.Nullable; import io.github.eisop.runtimeframework.qual.AnnotatedFor; @AnnotatedFor("nullness") @@ -7,8 +8,16 @@ static class UncheckedLib { public static String POISON = null; } + public static void consume(@Nullable Object o) {} + public static void main(String[] args) { - String s = UncheckedLib.POISON; + // 1. Read without storage + // :: error: (Read Field 'POISON' must be NonNull) + consume(UncheckedLib.POISON); + + // 2. Assignment + // :: error: (Read Field 'POISON' must be NonNull) // :: error: (Local Variable Assignment (Slot 1) must be NonNull) + String s = UncheckedLib.POISON; } -} +} \ No newline at end of file diff --git a/checker/src/test/resources/test-cases/nullness-parameter/FieldArgument.java b/checker/src/test/resources/test-cases/nullness-parameter/FieldArgument.java index cdd6bbe..4050956 100644 --- a/checker/src/test/resources/test-cases/nullness-parameter/FieldArgument.java +++ b/checker/src/test/resources/test-cases/nullness-parameter/FieldArgument.java @@ -10,14 +10,17 @@ static class UncheckedLib { } public static void main(String[] args) { + // :: error: (Read Field 'POISON' must be NonNull) + // :: error: (Parameter 0 must be NonNull) consume(UncheckedLib.POISON); - nullableConsume(UncheckedLib.POISON); + + // :: error: (Read Field 'POISON' must be NonNull) + nullableConsume(UncheckedLib.POISON); } public static void consume(@NonNull String arg) { - // :: error: (Parameter 0 must be NonNull) } public static void nullableConsume(@Nullable String arg) { } -} +} \ No newline at end of file diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java index 785570d..7856704 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java @@ -66,13 +66,7 @@ public void accept(ClassBuilder classBuilder, ClassElement classElement) { } else if (isFieldRead(fInst)) { codeBuilder.with(element); if (isCheckedScope) { - // generateFieldReadCheck(codeBuilder, fInst, classModel); - // Currently disabling field read checks as the GETFIELD - // and GETSTATIC instructions are not actually dangerous - // on their own. Its when we STORE a field we read from - // that an issue could arise - // TODO: consider method of turning on and off different - // boundary sites + generateFieldReadCheck(codeBuilder, fInst, classModel); } } } else if (element instanceof ReturnInstruction rInst) { From fc9436172af7d3b59b52e240eaa1cb92c4cc6232 Mon Sep 17 00:00:00 2001 From: Alex Cook Date: Fri, 30 Jan 2026 14:52:41 -0500 Subject: [PATCH 3/6] feat: add unchecked method return checks --- .../test-cases/nullness-invoke/InstanceBoundary.java | 3 ++- .../test-cases/nullness-invoke/NullableBoundary.java | 1 + .../test-cases/nullness-invoke/StaticBoundary.java | 1 + .../runtimeframework/core/AnnotationInstrumenter.java | 10 +++++++++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/checker/src/test/resources/test-cases/nullness-invoke/InstanceBoundary.java b/checker/src/test/resources/test-cases/nullness-invoke/InstanceBoundary.java index 7443f37..b7eb271 100644 --- a/checker/src/test/resources/test-cases/nullness-invoke/InstanceBoundary.java +++ b/checker/src/test/resources/test-cases/nullness-invoke/InstanceBoundary.java @@ -13,10 +13,11 @@ public static void main(String[] args) { UncheckedLib lib = new UncheckedLib(); String s = lib.getNull(); + // :: error: (Return value of getNull (Boundary) must be NonNull) // :: error: (Local Variable Assignment (Slot 2) must be NonNull) lib.getNull(); - // currently no explicit check on the return if its not stored + // :: error: (Return value of getNull (Boundary) must be NonNull) } } diff --git a/checker/src/test/resources/test-cases/nullness-invoke/NullableBoundary.java b/checker/src/test/resources/test-cases/nullness-invoke/NullableBoundary.java index 38b6cc2..2a7ca5b 100644 --- a/checker/src/test/resources/test-cases/nullness-invoke/NullableBoundary.java +++ b/checker/src/test/resources/test-cases/nullness-invoke/NullableBoundary.java @@ -12,5 +12,6 @@ public static String getNull() { public static void main(String[] args) { @Nullable String s = UncheckedLib.getNull(); + // :: error: (Return value of getNull (Boundary) must be NonNull) } } diff --git a/checker/src/test/resources/test-cases/nullness-invoke/StaticBoundary.java b/checker/src/test/resources/test-cases/nullness-invoke/StaticBoundary.java index 3aa1212..010b75a 100644 --- a/checker/src/test/resources/test-cases/nullness-invoke/StaticBoundary.java +++ b/checker/src/test/resources/test-cases/nullness-invoke/StaticBoundary.java @@ -12,6 +12,7 @@ public static String getNull() { public static void main(String[] args) { String s = UncheckedLib.getNull(); + // :: error: (Return value of getNull (Boundary) must be NonNull) // :: error: (Local Variable Assignment (Slot 1) must be NonNull) } 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 ab082f4..d083924 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 @@ -154,8 +154,16 @@ protected void generateUncheckedReturnCheck( @Override protected void generateMethodCallCheck(CodeBuilder b, InvokeInstruction invoke) { - // empty for now, only need to generate checks when a method call is stored somehwhere + RuntimeVerifier target = + policy.getBoundaryCallCheck(invoke.owner().asInternalName(), invoke.typeSymbol()); + if (target != null) { + b.dup(); + target.generateCheck( + b, + TypeKind.REFERENCE, + "Return value of " + invoke.name().stringValue() + " (Boundary)"); + } } @Override From 729afb602b7d8a4891f962a70f1e9e8e17eb7a0c Mon Sep 17 00:00:00 2001 From: Alex Cook Date: Fri, 30 Jan 2026 15:00:39 -0500 Subject: [PATCH 4/6] chore: spotless --- .../checker/nullness/NullnessVerifier.java | 3 +-- .../core/AnnotationInstrumenter.java | 4 +-- .../policy/StandardEnforcementPolicy.java | 25 ++++++++----------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessVerifier.java b/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessVerifier.java index 4db17bf..4444328 100644 --- a/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessVerifier.java +++ b/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessVerifier.java @@ -10,8 +10,7 @@ public class NullnessVerifier implements RuntimeVerifier { private static final ClassDesc VERIFIER = ClassDesc.of(NullnessRuntimeVerifier.class.getName()); - private static final ClassDesc ATTRIBUTION_KIND = - ClassDesc.of(AttributionKind.class.getName()); + private static final ClassDesc ATTRIBUTION_KIND = ClassDesc.of(AttributionKind.class.getName()); private static final String METHOD_DEFAULT = "checkNotNull"; private static final MethodTypeDesc DESC_DEFAULT = 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 d083924..adf349e 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 @@ -160,9 +160,7 @@ protected void generateMethodCallCheck(CodeBuilder b, InvokeInstruction invoke) if (target != null) { b.dup(); target.generateCheck( - b, - TypeKind.REFERENCE, - "Return value of " + invoke.name().stringValue() + " (Boundary)"); + b, TypeKind.REFERENCE, "Return value of " + invoke.name().stringValue() + " (Boundary)"); } } 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 252d44c..0b843bb 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,9 +5,8 @@ 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.policy.EnforcementPolicy; -import io.github.eisop.runtimeframework.runtime.AttributionKind; import io.github.eisop.runtimeframework.resolution.ParentMethod; +import io.github.eisop.runtimeframework.runtime.AttributionKind; import java.lang.classfile.Annotation; import java.lang.classfile.Attributes; import java.lang.classfile.FieldModel; @@ -248,19 +247,17 @@ public RuntimeVerifier getBridgeReturnCheck(ParentMethod parentMethod) { RuntimeVerifier verifier = resolveVerifier(annos); if (verifier != null) return verifier.withAttribution(AttributionKind.CALLER); - if (returnType == TypeKind.REFERENCE) { // Redundant but keeps structure similar - boolean isExplicitNoop = false; - for (Annotation a : annos) { - TypeSystemConfiguration.ConfigEntry entry = - configuration.find(a.classSymbol().descriptorString()); - if (entry != null && entry.kind() == ValidationKind.NOOP) isExplicitNoop = true; - } + boolean isExplicitNoop = false; + for (Annotation a : annos) { + TypeSystemConfiguration.ConfigEntry entry = + configuration.find(a.classSymbol().descriptorString()); + if (entry != null && entry.kind() == ValidationKind.NOOP) isExplicitNoop = true; + } - if (!isExplicitNoop) { - TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); - if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { - return defaultEntry.verifier().withAttribution(AttributionKind.CALLER); - } + if (!isExplicitNoop) { + TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); + if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { + return defaultEntry.verifier().withAttribution(AttributionKind.CALLER); } } return null; From 8c336e99acd7346495a170bb6c32af591cd34032 Mon Sep 17 00:00:00 2001 From: Alex Cook Date: Mon, 2 Feb 2026 13:29:16 -0500 Subject: [PATCH 5/6] refactor: Instrumentation pipeline refactor --- ...ifier.java => NullnessCheckGenerator.java} | 13 +- .../nullness/NullnessRuntimeChecker.java | 10 +- .../runtimeframework/agent/RuntimeAgent.java | 11 +- .../agent/RuntimeTransformer.java | 10 +- .../core/AnnotationInstrumenter.java | 284 ----------------- ...ntimeVerifier.java => CheckGenerator.java} | 4 +- .../core/EnforcementInstrumenter.java | 129 ++++++++ .../core/EnforcementTransform.java | 291 ++++++++++++++++++ .../runtimeframework/core/RuntimeChecker.java | 16 +- .../core/RuntimeInstrumenter.java | 129 +------- .../core/TypeSystemConfiguration.java | 6 +- .../runtimeframework/core/ValidationKind.java | 6 +- ...ementPolicy.java => BoundaryStrategy.java} | 65 ++-- ...licy.java => InstrumentationStrategy.java} | 31 +- ...olicy.java => StrictBoundaryStrategy.java} | 25 +- .../util/SysOutInstrumenter.java | 113 +++---- 16 files changed, 565 insertions(+), 578 deletions(-) rename checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/{NullnessVerifier.java => NullnessCheckGenerator.java} (82%) delete mode 100644 framework/src/main/java/io/github/eisop/runtimeframework/core/AnnotationInstrumenter.java rename framework/src/main/java/io/github/eisop/runtimeframework/core/{RuntimeVerifier.java => CheckGenerator.java} (92%) create mode 100644 framework/src/main/java/io/github/eisop/runtimeframework/core/EnforcementInstrumenter.java create mode 100644 framework/src/main/java/io/github/eisop/runtimeframework/core/EnforcementTransform.java rename framework/src/main/java/io/github/eisop/runtimeframework/policy/{StandardEnforcementPolicy.java => BoundaryStrategy.java} (80%) rename framework/src/main/java/io/github/eisop/runtimeframework/policy/{EnforcementPolicy.java => InstrumentationStrategy.java} (58%) rename framework/src/main/java/io/github/eisop/runtimeframework/policy/{GlobalEnforcementPolicy.java => StrictBoundaryStrategy.java} (85%) diff --git a/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessVerifier.java b/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessCheckGenerator.java similarity index 82% rename from checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessVerifier.java rename to checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessCheckGenerator.java index 4444328..95de358 100644 --- a/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessVerifier.java +++ b/checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessCheckGenerator.java @@ -1,13 +1,13 @@ package io.github.eisop.runtimeframework.checker.nullness; -import io.github.eisop.runtimeframework.core.RuntimeVerifier; +import io.github.eisop.runtimeframework.core.CheckGenerator; import io.github.eisop.runtimeframework.runtime.AttributionKind; import java.lang.classfile.CodeBuilder; import java.lang.classfile.TypeKind; import java.lang.constant.ClassDesc; import java.lang.constant.MethodTypeDesc; -public class NullnessVerifier implements RuntimeVerifier { +public class NullnessCheckGenerator implements CheckGenerator { private static final ClassDesc VERIFIER = ClassDesc.of(NullnessRuntimeVerifier.class.getName()); private static final ClassDesc ATTRIBUTION_KIND = ClassDesc.of(AttributionKind.class.getName()); @@ -23,18 +23,17 @@ public class NullnessVerifier implements RuntimeVerifier { private final AttributionKind attribution; - public NullnessVerifier() { + public NullnessCheckGenerator() { this(AttributionKind.LOCAL); } - public NullnessVerifier(AttributionKind attribution) { + public NullnessCheckGenerator(AttributionKind attribution) { this.attribution = attribution; } @Override - public RuntimeVerifier withAttribution(AttributionKind kind) { - if (this.attribution == kind) return this; - return new NullnessVerifier(kind); + public CheckGenerator withAttribution(AttributionKind kind) { + return new NullnessCheckGenerator(kind); } @Override 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 184b0cc..cf21b71 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 @@ -1,13 +1,13 @@ package io.github.eisop.runtimeframework.checker.nullness; -import io.github.eisop.runtimeframework.core.AnnotationInstrumenter; +import io.github.eisop.runtimeframework.core.EnforcementInstrumenter; import io.github.eisop.runtimeframework.core.RuntimeChecker; import io.github.eisop.runtimeframework.core.RuntimeInstrumenter; import io.github.eisop.runtimeframework.core.TypeSystemConfiguration; 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.policy.EnforcementPolicy; +import io.github.eisop.runtimeframework.policy.InstrumentationStrategy; import io.github.eisop.runtimeframework.resolution.BytecodeHierarchyResolver; import io.github.eisop.runtimeframework.resolution.HierarchyResolver; import org.checkerframework.checker.nullness.qual.NonNull; @@ -22,7 +22,7 @@ public String getName() { @Override public RuntimeInstrumenter getInstrumenter(Filter filter) { - NullnessVerifier verifier = new NullnessVerifier(); + NullnessCheckGenerator verifier = new NullnessCheckGenerator(); TypeSystemConfiguration config = new TypeSystemConfiguration() @@ -30,12 +30,12 @@ public RuntimeInstrumenter getInstrumenter(Filter filter) { .onNoop(Nullable.class) .withDefault(ValidationKind.ENFORCE, verifier); - EnforcementPolicy policy = createPolicy(config, filter); + InstrumentationStrategy strategy = createStrategy(config, filter); HierarchyResolver resolver = new BytecodeHierarchyResolver( className -> filter.test(new ClassInfo(className.replace('.', '/'), null, null))); - return new AnnotationInstrumenter(policy, resolver, filter); + return new EnforcementInstrumenter(strategy, resolver, filter); } } diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeAgent.java b/framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeAgent.java index ec87ebc..05328bc 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeAgent.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeAgent.java @@ -14,7 +14,7 @@ public final class RuntimeAgent { public static void premain(String args, Instrumentation inst) { Filter safeFilter = new FrameworkSafetyFilter(); - Filter policyFilter = safeFilter; + Filter strategyFilter = safeFilter; String checkedClasses = System.getProperty("runtime.classes"); boolean isGlobalMode = Boolean.getBoolean("runtime.global"); @@ -23,12 +23,12 @@ public static void premain(String args, Instrumentation inst) { if (checkedClasses != null && !checkedClasses.isBlank()) { System.out.println("[RuntimeAgent] Checked Scope restricted to: " + checkedClasses); Filter listFilter = new ClassListFilter(Arrays.asList(checkedClasses.split(","))); - policyFilter = info -> safeFilter.test(info) && listFilter.test(info); + strategyFilter = info -> safeFilter.test(info) && listFilter.test(info); } else if (trustAnnotatedFor) { - policyFilter = info -> false; + strategyFilter = info -> false; } - Filter scanFilter = policyFilter; + Filter scanFilter = strategyFilter; boolean scanAll = false; if (trustAnnotatedFor) { @@ -83,7 +83,8 @@ public static void premain(String args, Instrumentation inst) { } inst.addTransformer( - new RuntimeTransformer(scanFilter, policyFilter, checker, trustAnnotatedFor, isGlobalMode), + new RuntimeTransformer( + scanFilter, strategyFilter, checker, trustAnnotatedFor, isGlobalMode), false); } } 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 be50a28..462d163 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 @@ -13,7 +13,7 @@ public class RuntimeTransformer implements ClassFileTransformer { private final Filter scanFilter; - private final Filter policyFilter; + private final Filter strategyFilter; private final RuntimeChecker checker; private final boolean trustAnnotatedFor; private final boolean isGlobalMode; @@ -21,12 +21,12 @@ public class RuntimeTransformer implements ClassFileTransformer { public RuntimeTransformer( Filter scanFilter, - Filter policyFilter, + Filter strategyFilter, RuntimeChecker checker, boolean trustAnnotatedFor, boolean isGlobalMode) { this.scanFilter = scanFilter; - this.policyFilter = policyFilter; + this.strategyFilter = strategyFilter; this.checker = checker; this.trustAnnotatedFor = trustAnnotatedFor; this.isGlobalMode = isGlobalMode; @@ -62,7 +62,7 @@ public byte[] transform( ClassFile cf = ClassFile.of(); ClassModel classModel = cf.parse(classfileBuffer); - boolean isChecked = policyFilter.test(info); + boolean isChecked = strategyFilter.test(info); if (!isChecked && trustAnnotatedFor && annotatedForFilter != null) { if (annotatedForFilter.test(classModel, loader)) { @@ -94,7 +94,7 @@ public byte[] transform( && annotatedForFilter.test(effectiveCtx)) { return true; } - return policyFilter.test(effectiveCtx); + return strategyFilter.test(effectiveCtx); }; RuntimeInstrumenter instrumenter = checker.getInstrumenter(dynamicFilter); 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 deleted file mode 100644 index adf349e..0000000 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/AnnotationInstrumenter.java +++ /dev/null @@ -1,284 +0,0 @@ -package io.github.eisop.runtimeframework.core; - -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.HierarchyResolver; -import io.github.eisop.runtimeframework.resolution.ParentMethod; -import java.lang.classfile.ClassBuilder; -import java.lang.classfile.ClassModel; -import java.lang.classfile.CodeBuilder; -import java.lang.classfile.FieldModel; -import java.lang.classfile.MethodModel; -import java.lang.classfile.Opcode; -import java.lang.classfile.TypeKind; -import java.lang.classfile.instruction.ArrayLoadInstruction; -import java.lang.classfile.instruction.ArrayStoreInstruction; -import java.lang.classfile.instruction.FieldInstruction; -import java.lang.classfile.instruction.InvokeInstruction; -import java.lang.classfile.instruction.ReturnInstruction; -import java.lang.classfile.instruction.StoreInstruction; -import java.lang.constant.ClassDesc; -import java.lang.constant.MethodTypeDesc; -import java.lang.reflect.Modifier; -import java.util.List; - -public class AnnotationInstrumenter extends RuntimeInstrumenter { - - private final EnforcementPolicy policy; - private final HierarchyResolver hierarchyResolver; - - public AnnotationInstrumenter( - EnforcementPolicy policy, - HierarchyResolver hierarchyResolver, - Filter safetyFilter) { - super(safetyFilter); - this.policy = policy; - this.hierarchyResolver = hierarchyResolver; - } - - @Override - protected void generateArrayStoreCheck(CodeBuilder b, ArrayStoreInstruction instruction) { - if (instruction.opcode() == Opcode.AASTORE) { - RuntimeVerifier target = policy.getArrayStoreCheck(TypeKind.REFERENCE); - if (target != null) { - b.dup(); - target.generateCheck(b, TypeKind.REFERENCE, "Array Element Write"); - } - } - } - - @Override - protected void generateArrayLoadCheck(CodeBuilder b, ArrayLoadInstruction instruction) { - if (instruction.opcode() == Opcode.AALOAD) { - RuntimeVerifier target = policy.getArrayLoadCheck(TypeKind.REFERENCE); - if (target != null) { - b.dup(); - target.generateCheck(b, TypeKind.REFERENCE, "Array Element Read"); - } - } - } - - @Override - protected void generateParameterCheck( - CodeBuilder b, int slotIndex, TypeKind type, MethodModel method, int paramIndex) { - RuntimeVerifier target = policy.getParameterCheck(method, paramIndex, type); - if (target != null) { - b.aload(slotIndex); - target.generateCheck(b, type, "Parameter " + paramIndex); - } - } - - @Override - protected void generateFieldWriteCheck( - CodeBuilder b, FieldInstruction field, ClassModel classModel) { - RuntimeVerifier target = null; - TypeKind type = TypeKind.fromDescriptor(field.typeSymbol().descriptorString()); - - if (field.owner().asInternalName().equals(classModel.thisClass().asInternalName())) { - FieldModel targetField = findField(classModel, field); - if (targetField != null) { - target = policy.getFieldWriteCheck(targetField, type); - } - } else { - target = - policy.getBoundaryFieldWriteCheck( - field.owner().asInternalName(), field.name().stringValue(), type); - } - - if (target != null) { - if (field.opcode() == Opcode.PUTSTATIC) { - b.dup(); - target.generateCheck(b, type, "Static Field '" + field.name().stringValue() + "'"); - } else if (field.opcode() == Opcode.PUTFIELD) { - b.dup_x1(); - target.generateCheck(b, type, "Field '" + field.name().stringValue() + "'"); - b.swap(); - } - } - } - - @Override - protected void generateFieldReadCheck( - CodeBuilder b, FieldInstruction field, ClassModel classModel) { - RuntimeVerifier target = null; - TypeKind type = TypeKind.fromDescriptor(field.typeSymbol().descriptorString()); - - if (field.owner().asInternalName().equals(classModel.thisClass().asInternalName())) { - FieldModel targetField = findField(classModel, field); - if (targetField != null) { - target = policy.getFieldReadCheck(targetField, type); - } - } else { - target = - policy.getBoundaryFieldReadCheck( - field.owner().asInternalName(), field.name().stringValue(), type); - } - - if (target != null) { - if (type.slotSize() == 1) { - b.dup(); - target.generateCheck(b, type, "Read Field '" + field.name().stringValue() + "'"); - } - } - } - - @Override - protected void generateReturnCheck(CodeBuilder b, ReturnInstruction ret, MethodModel method) { - RuntimeVerifier target = policy.getReturnCheck(method); - if (target != null) { - b.dup(); - target.generateCheck( - b, TypeKind.REFERENCE, "Return value of " + method.methodName().stringValue()); - } - } - - @Override - protected void generateUncheckedReturnCheck( - CodeBuilder b, - ReturnInstruction ret, - MethodModel method, - ClassModel classModel, - ClassLoader loader) { - if (ret.opcode() != Opcode.ARETURN) return; - RuntimeVerifier target = policy.getUncheckedOverrideReturnCheck(classModel, method, loader); - - if (target != null) { - b.dup(); - target.generateCheck( - b, - TypeKind.REFERENCE, - "Return value of overridden method " + method.methodName().stringValue()); - } - } - - @Override - protected void generateMethodCallCheck(CodeBuilder b, InvokeInstruction invoke) { - RuntimeVerifier target = - policy.getBoundaryCallCheck(invoke.owner().asInternalName(), invoke.typeSymbol()); - - if (target != null) { - b.dup(); - target.generateCheck( - b, TypeKind.REFERENCE, "Return value of " + invoke.name().stringValue() + " (Boundary)"); - } - } - - @Override - protected void generateBridgeMethods(ClassBuilder builder, ClassModel model, ClassLoader loader) { - for (ParentMethod parentMethod : hierarchyResolver.resolveUncheckedMethods(model, loader)) { - if (policy.shouldGenerateBridge(parentMethod)) { - emitBridge(builder, parentMethod); - } - } - } - - @Override - protected void generateStoreCheck( - CodeBuilder b, StoreInstruction instruction, MethodModel method) { - boolean isRefStore = - switch (instruction.opcode()) { - case ASTORE, ASTORE_0, ASTORE_1, ASTORE_2, ASTORE_3 -> true; - default -> false; - }; - - if (!isRefStore) return; - - int slot = instruction.slot(); - RuntimeVerifier target = policy.getLocalVariableWriteCheck(method, slot, TypeKind.REFERENCE); - - if (target != null) { - b.dup(); - target.generateCheck(b, TypeKind.REFERENCE, "Local Variable Assignment (Slot " + slot + ")"); - } - } - - private void emitBridge(ClassBuilder builder, ParentMethod parentMethod) { - MethodModel method = parentMethod.method(); - String methodName = method.methodName().stringValue(); - MethodTypeDesc desc = method.methodTypeSymbol(); - - builder.withMethod( - methodName, - desc, - Modifier.PUBLIC, - methodBuilder -> { - methodBuilder.withCode( - codeBuilder -> { - int slotIndex = 1; - List paramTypes = desc.parameterList(); - - 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); - target.generateCheck( - codeBuilder, type, "Parameter " + i + " in inherited method " + methodName); - } - slotIndex += type.slotSize(); - } - - codeBuilder.aload(0); - slotIndex = 1; - for (ClassDesc pType : paramTypes) { - TypeKind type = TypeKind.from(pType); - loadLocal(codeBuilder, type, slotIndex); - slotIndex += type.slotSize(); - } - - ClassDesc parentDesc = - ClassDesc.of( - parentMethod.owner().thisClass().asInternalName().replace('/', '.')); - codeBuilder.invokespecial(parentDesc, methodName, desc); - - RuntimeVerifier returnTarget = policy.getBridgeReturnCheck(parentMethod); - if (returnTarget != null) { - codeBuilder.dup(); - returnTarget.generateCheck( - codeBuilder, - TypeKind.REFERENCE, - "Return value of inherited method " + methodName); - } - - returnResult( - codeBuilder, ClassDesc.ofDescriptor(desc.returnType().descriptorString())); - }); - }); - } - - private FieldModel findField(ClassModel classModel, FieldInstruction field) { - for (FieldModel fm : classModel.fields()) { - if (fm.fieldName().stringValue().equals(field.name().stringValue()) - && fm.fieldType().stringValue().equals(field.type().stringValue())) { - return fm; - } - } - return null; - } - - private void loadLocal(CodeBuilder b, TypeKind type, int slot) { - switch (type) { - case INT, BYTE, CHAR, SHORT, BOOLEAN -> b.iload(slot); - case LONG -> b.lload(slot); - case FLOAT -> b.fload(slot); - case DOUBLE -> b.dload(slot); - case REFERENCE -> b.aload(slot); - default -> throw new IllegalArgumentException("Unknown type"); - } - } - - 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/core/RuntimeVerifier.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/CheckGenerator.java similarity index 92% rename from framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeVerifier.java rename to framework/src/main/java/io/github/eisop/runtimeframework/core/CheckGenerator.java index a82c6fb..59a8bbb 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeVerifier.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/CheckGenerator.java @@ -11,7 +11,7 @@ * operand stack satisfies a specific property. */ @FunctionalInterface -public interface RuntimeVerifier { +public interface CheckGenerator { /** * Generates bytecode to verify a property. @@ -33,7 +33,7 @@ public interface RuntimeVerifier { * @param kind The attribution strategy. * @return A verifier with the specified attribution. */ - default RuntimeVerifier withAttribution(AttributionKind kind) { + default CheckGenerator withAttribution(AttributionKind kind) { return this; } } diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/core/EnforcementInstrumenter.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/EnforcementInstrumenter.java new file mode 100644 index 0000000..4834b3d --- /dev/null +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/EnforcementInstrumenter.java @@ -0,0 +1,129 @@ +package io.github.eisop.runtimeframework.core; + +import io.github.eisop.runtimeframework.filter.ClassInfo; +import io.github.eisop.runtimeframework.filter.Filter; +import io.github.eisop.runtimeframework.policy.InstrumentationStrategy; +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; +import java.lang.classfile.CodeTransform; +import java.lang.classfile.MethodModel; +import java.lang.classfile.TypeKind; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.lang.reflect.Modifier; +import java.util.List; + +public class EnforcementInstrumenter extends RuntimeInstrumenter { + + private final InstrumentationStrategy strategy; + private final HierarchyResolver hierarchyResolver; + + public EnforcementInstrumenter( + InstrumentationStrategy strategy, + HierarchyResolver hierarchyResolver, + Filter safetyFilter) { + super(safetyFilter); + this.strategy = strategy; + this.hierarchyResolver = hierarchyResolver; + } + + @Override + protected CodeTransform createCodeTransform( + ClassModel classModel, MethodModel methodModel, boolean isCheckedScope, ClassLoader loader) { + return new EnforcementTransform(strategy, classModel, methodModel, isCheckedScope, loader); + } + + @Override + protected void generateBridgeMethods(ClassBuilder builder, ClassModel model, ClassLoader loader) { + for (ParentMethod parentMethod : hierarchyResolver.resolveUncheckedMethods(model, loader)) { + if (strategy.shouldGenerateBridge(parentMethod)) { + emitBridge(builder, parentMethod); + } + } + } + + private void emitBridge(ClassBuilder builder, ParentMethod parentMethod) { + MethodModel method = parentMethod.method(); + String methodName = method.methodName().stringValue(); + MethodTypeDesc desc = method.methodTypeSymbol(); + + builder.withMethod( + methodName, + desc, + Modifier.PUBLIC, + methodBuilder -> { + methodBuilder.withCode( + codeBuilder -> { + int slotIndex = 1; + List paramTypes = desc.parameterList(); + + // 1. Parameter Checks (Bridge acts as entry point) + for (int i = 0; i < paramTypes.size(); i++) { + TypeKind type = TypeKind.from(paramTypes.get(i)); + CheckGenerator target = strategy.getBridgeParameterCheck(parentMethod, i); + if (target != null) { + codeBuilder.aload(slotIndex); + target.generateCheck( + codeBuilder, type, "Parameter " + i + " in inherited method " + methodName); + } + slotIndex += type.slotSize(); + } + + // 2. Call Super + codeBuilder.aload(0); + slotIndex = 1; + for (ClassDesc pType : paramTypes) { + TypeKind type = TypeKind.from(pType); + loadLocal(codeBuilder, type, slotIndex); + slotIndex += type.slotSize(); + } + + ClassDesc parentDesc = + ClassDesc.of( + parentMethod.owner().thisClass().asInternalName().replace('/', '.')); + codeBuilder.invokespecial(parentDesc, methodName, desc); + + // 3. Return Check (Bridge acts as exit point) + CheckGenerator returnTarget = strategy.getBridgeReturnCheck(parentMethod); + if (returnTarget != null) { + codeBuilder.dup(); + returnTarget.generateCheck( + codeBuilder, + TypeKind.REFERENCE, + "Return value of inherited method " + methodName); + } + + returnResult( + codeBuilder, ClassDesc.ofDescriptor(desc.returnType().descriptorString())); + }); + }); + } + + private void loadLocal(CodeBuilder b, TypeKind type, int slot) { + switch (type) { + case INT, BYTE, CHAR, SHORT, BOOLEAN -> b.iload(slot); + case LONG -> b.lload(slot); + case FLOAT -> b.fload(slot); + case DOUBLE -> b.dload(slot); + case REFERENCE -> b.aload(slot); + default -> throw new IllegalArgumentException("Unknown type"); + } + } + + 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/core/EnforcementTransform.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/EnforcementTransform.java new file mode 100644 index 0000000..15a43ee --- /dev/null +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/EnforcementTransform.java @@ -0,0 +1,291 @@ +package io.github.eisop.runtimeframework.core; + +import io.github.eisop.runtimeframework.policy.InstrumentationStrategy; +import java.lang.classfile.ClassModel; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeTransform; +import java.lang.classfile.FieldModel; +import java.lang.classfile.Instruction; +import java.lang.classfile.MethodModel; +import java.lang.classfile.Opcode; +import java.lang.classfile.TypeKind; +import java.lang.classfile.instruction.ArrayLoadInstruction; +import java.lang.classfile.instruction.ArrayStoreInstruction; +import java.lang.classfile.instruction.FieldInstruction; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.LineNumber; +import java.lang.classfile.instruction.ReturnInstruction; +import java.lang.classfile.instruction.StoreInstruction; +import java.lang.constant.MethodTypeDesc; +import java.lang.reflect.Modifier; + +/** A CodeTransform that injects runtime checks based on an InstrumentationStrategy. */ +public class EnforcementTransform implements CodeTransform { + + private final InstrumentationStrategy strategy; + private final ClassModel classModel; + private final MethodModel methodModel; + private final boolean isCheckedScope; + private final ClassLoader loader; + private boolean entryChecksEmitted; + + public EnforcementTransform( + InstrumentationStrategy strategy, + ClassModel classModel, + MethodModel methodModel, + boolean isCheckedScope, + ClassLoader loader) { + this.strategy = strategy; + this.classModel = classModel; + this.methodModel = methodModel; + this.isCheckedScope = isCheckedScope; + this.loader = loader; + this.entryChecksEmitted = !isCheckedScope; + } + + @Override + public void accept(CodeBuilder builder, CodeElement element) { + if (maybeEmitEntryChecks(builder, element)) { + return; + } + + switch (element) { + case FieldInstruction f -> handleField(builder, f); + case ReturnInstruction r -> handleReturn(builder, r); + case InvokeInstruction i -> handleInvoke(builder, i); + case ArrayStoreInstruction a -> handleArrayStore(builder, a); + case ArrayLoadInstruction a -> handleArrayLoad(builder, a); + case StoreInstruction s -> handleStore(builder, s); + default -> builder.with(element); + } + } + + private boolean maybeEmitEntryChecks(CodeBuilder builder, CodeElement element) { + if (entryChecksEmitted) { + return false; + } + + if (element instanceof LineNumber) { + builder.with(element); + emitParameterChecks(builder); + entryChecksEmitted = true; + return true; + } else if (element instanceof Instruction) { + emitParameterChecks(builder); + entryChecksEmitted = true; + return false; + } + + return false; + } + + private void handleField(CodeBuilder b, FieldInstruction f) { + if (isFieldWrite(f)) { + emitFieldWriteCheck(b, f); + b.with(f); + } else if (isFieldRead(f)) { + b.with(f); + if (isCheckedScope) { + emitFieldReadCheck(b, f); + } + } else { + b.with(f); + } + } + + private void handleReturn(CodeBuilder b, ReturnInstruction r) { + if (isCheckedScope) { + emitReturnCheck(b); + } else { + emitUncheckedReturnCheck(b, r); + } + b.with(r); + } + + private void handleInvoke(CodeBuilder b, InvokeInstruction i) { + b.with(i); + if (isCheckedScope) { + emitMethodCallCheck(b, i); + } + } + + private void handleArrayStore(CodeBuilder b, ArrayStoreInstruction a) { + emitArrayStoreCheck(b, a); + b.with(a); + } + + private void handleArrayLoad(CodeBuilder b, ArrayLoadInstruction a) { + b.with(a); + if (isCheckedScope) { + emitArrayLoadCheck(b, a); + } + } + + private void handleStore(CodeBuilder b, StoreInstruction s) { + if (isCheckedScope) { + emitStoreCheck(b, s); + } + b.with(s); + } + + public void emitParameterChecks(CodeBuilder builder) { + boolean isStatic = (methodModel.flags().flagsMask() & Modifier.STATIC) != 0; + int slotIndex = isStatic ? 0 : 1; + MethodTypeDesc methodDesc = methodModel.methodTypeSymbol(); + int paramCount = methodDesc.parameterList().size(); + + for (int i = 0; i < paramCount; i++) { + TypeKind type = TypeKind.from(methodDesc.parameterList().get(i)); + CheckGenerator target = strategy.getParameterCheck(methodModel, i, type); + + if (target != null) { + builder.aload(slotIndex); + target.generateCheck(builder, type, "Parameter " + i); + } + + slotIndex += type.slotSize(); + } + } + + private void emitFieldWriteCheck(CodeBuilder b, FieldInstruction field) { + CheckGenerator target = null; + TypeKind type = TypeKind.fromDescriptor(field.typeSymbol().descriptorString()); + + if (field.owner().asInternalName().equals(classModel.thisClass().asInternalName())) { + FieldModel targetField = findField(classModel, field); + if (targetField != null) { + target = strategy.getFieldWriteCheck(targetField, type); + } + } else { + target = + strategy.getBoundaryFieldWriteCheck( + field.owner().asInternalName(), field.name().stringValue(), type); + } + + if (target != null) { + if (field.opcode() == Opcode.PUTSTATIC) { + b.dup(); + target.generateCheck(b, type, "Static Field '" + field.name().stringValue() + "'"); + } else if (field.opcode() == Opcode.PUTFIELD) { + b.dup_x1(); + target.generateCheck(b, type, "Field '" + field.name().stringValue() + "'"); + b.swap(); + } + } + } + + private void emitFieldReadCheck(CodeBuilder b, FieldInstruction field) { + CheckGenerator target = null; + TypeKind type = TypeKind.fromDescriptor(field.typeSymbol().descriptorString()); + + if (field.owner().asInternalName().equals(classModel.thisClass().asInternalName())) { + FieldModel targetField = findField(classModel, field); + if (targetField != null) { + target = strategy.getFieldReadCheck(targetField, type); + } + } else { + target = + strategy.getBoundaryFieldReadCheck( + field.owner().asInternalName(), field.name().stringValue(), type); + } + + if (target != null) { + if (type.slotSize() == 1) { + b.dup(); + target.generateCheck(b, type, "Read Field '" + field.name().stringValue() + "'"); + } + } + } + + private void emitReturnCheck(CodeBuilder b) { + CheckGenerator target = strategy.getReturnCheck(methodModel); + if (target != null) { + b.dup(); + target.generateCheck( + b, TypeKind.REFERENCE, "Return value of " + methodModel.methodName().stringValue()); + } + } + + private void emitUncheckedReturnCheck(CodeBuilder b, ReturnInstruction ret) { + if (ret.opcode() != Opcode.ARETURN) return; + CheckGenerator target = + strategy.getUncheckedOverrideReturnCheck(classModel, methodModel, loader); + + if (target != null) { + b.dup(); + target.generateCheck( + b, + TypeKind.REFERENCE, + "Return value of overridden method " + methodModel.methodName().stringValue()); + } + } + + private void emitMethodCallCheck(CodeBuilder b, InvokeInstruction invoke) { + CheckGenerator target = + strategy.getBoundaryCallCheck(invoke.owner().asInternalName(), invoke.typeSymbol()); + + if (target != null) { + b.dup(); + target.generateCheck( + b, TypeKind.REFERENCE, "Return value of " + invoke.name().stringValue() + " (Boundary)"); + } + } + + private void emitArrayStoreCheck(CodeBuilder b, ArrayStoreInstruction instruction) { + if (instruction.opcode() == Opcode.AASTORE) { + CheckGenerator target = strategy.getArrayStoreCheck(TypeKind.REFERENCE); + if (target != null) { + b.dup(); + target.generateCheck(b, TypeKind.REFERENCE, "Array Element Write"); + } + } + } + + private void emitArrayLoadCheck(CodeBuilder b, ArrayLoadInstruction instruction) { + if (instruction.opcode() == Opcode.AALOAD) { + CheckGenerator target = strategy.getArrayLoadCheck(TypeKind.REFERENCE); + if (target != null) { + b.dup(); + target.generateCheck(b, TypeKind.REFERENCE, "Array Element Read"); + } + } + } + + private void emitStoreCheck(CodeBuilder b, StoreInstruction instruction) { + boolean isRefStore = + switch (instruction.opcode()) { + case ASTORE, ASTORE_0, ASTORE_1, ASTORE_2, ASTORE_3 -> true; + default -> false; + }; + + if (!isRefStore) return; + + int slot = instruction.slot(); + CheckGenerator target = + strategy.getLocalVariableWriteCheck(methodModel, slot, TypeKind.REFERENCE); + + if (target != null) { + b.dup(); + target.generateCheck(b, TypeKind.REFERENCE, "Local Variable Assignment (Slot " + slot + ")"); + } + } + + private boolean isFieldWrite(FieldInstruction f) { + return f.opcode() == Opcode.PUTFIELD || f.opcode() == Opcode.PUTSTATIC; + } + + private boolean isFieldRead(FieldInstruction f) { + return f.opcode() == Opcode.GETFIELD || f.opcode() == Opcode.GETSTATIC; + } + + private FieldModel findField(ClassModel classModel, FieldInstruction field) { + for (FieldModel fm : classModel.fields()) { + if (fm.fieldName().stringValue().equals(field.name().stringValue()) + && fm.fieldType().stringValue().equals(field.type().stringValue())) { + return fm; + } + } + return null; + } +} diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeChecker.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeChecker.java index 4d09bf7..5839306 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeChecker.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeChecker.java @@ -2,9 +2,9 @@ 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.policy.GlobalEnforcementPolicy; -import io.github.eisop.runtimeframework.policy.StandardEnforcementPolicy; +import io.github.eisop.runtimeframework.policy.BoundaryStrategy; +import io.github.eisop.runtimeframework.policy.InstrumentationStrategy; +import io.github.eisop.runtimeframework.policy.StrictBoundaryStrategy; /** * Represents a specific type system or check to be enforced (e.g., Nullness, Immutability). This @@ -24,23 +24,23 @@ public abstract class RuntimeChecker { public abstract RuntimeInstrumenter getInstrumenter(Filter filter); /** - * Helper method to create the appropriate EnforcementPolicy based on the framework's + * Helper method to create the appropriate InstrumentationStrategy based on the framework's * configuration (e.g., -Druntime.global=true). * *

Subclasses should use this instead of manually checking system properties. * * @param config The TypeSystemConfiguration for this checker. * @param filter The filter defining the boundary between Checked and Unchecked code. - * @return A configured EnforcementPolicy (Standard or Global). + * @return A configured InstrumentationStrategy (Standard or Global). */ - protected EnforcementPolicy createPolicy( + protected InstrumentationStrategy createStrategy( TypeSystemConfiguration config, Filter filter) { boolean isGlobalMode = Boolean.getBoolean("runtime.global"); if (isGlobalMode) { - return new GlobalEnforcementPolicy(config, filter); + return new StrictBoundaryStrategy(config, filter); } else { - return new StandardEnforcementPolicy(config, filter); + return new BoundaryStrategy(config, filter); } } } diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java index 7856704..19b0bbf 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java @@ -6,22 +6,9 @@ import java.lang.classfile.ClassElement; import java.lang.classfile.ClassModel; import java.lang.classfile.ClassTransform; -import java.lang.classfile.CodeBuilder; -import java.lang.classfile.CodeElement; -import java.lang.classfile.Instruction; +import java.lang.classfile.CodeTransform; import java.lang.classfile.MethodModel; -import java.lang.classfile.Opcode; -import java.lang.classfile.TypeKind; import java.lang.classfile.attribute.CodeAttribute; -import java.lang.classfile.instruction.ArrayLoadInstruction; -import java.lang.classfile.instruction.ArrayStoreInstruction; -import java.lang.classfile.instruction.FieldInstruction; -import java.lang.classfile.instruction.InvokeInstruction; -import java.lang.classfile.instruction.LineNumber; -import java.lang.classfile.instruction.ReturnInstruction; -import java.lang.classfile.instruction.StoreInstruction; -import java.lang.constant.MethodTypeDesc; -import java.lang.reflect.Modifier; public abstract class RuntimeInstrumenter { @@ -43,67 +30,9 @@ public void accept(ClassBuilder classBuilder, ClassElement classElement) { methodModel, (methodBuilder, methodElement) -> { if (methodElement instanceof CodeAttribute codeModel) { - methodBuilder.withCode( - codeBuilder -> { - boolean entryChecksDone = !isCheckedScope; - - for (CodeElement element : codeModel) { - if (!entryChecksDone && element instanceof LineNumber) { - codeBuilder.with(element); - instrumentMethodEntry(codeBuilder, methodModel); - entryChecksDone = true; - continue; - } - if (!entryChecksDone && element instanceof Instruction) { - instrumentMethodEntry(codeBuilder, methodModel); - entryChecksDone = true; - } - - if (element instanceof FieldInstruction fInst) { - if (isFieldWrite(fInst)) { - generateFieldWriteCheck(codeBuilder, fInst, classModel); - codeBuilder.with(element); - } else if (isFieldRead(fInst)) { - codeBuilder.with(element); - if (isCheckedScope) { - generateFieldReadCheck(codeBuilder, fInst, classModel); - } - } - } else if (element instanceof ReturnInstruction rInst) { - if (isCheckedScope) { - generateReturnCheck(codeBuilder, rInst, methodModel); - } else { - generateUncheckedReturnCheck( - codeBuilder, rInst, methodModel, classModel, loader); - } - codeBuilder.with(element); - } else if (element instanceof InvokeInstruction invoke) { - codeBuilder.with(element); - if (isCheckedScope) { - generateMethodCallCheck(codeBuilder, invoke); - } - } else if (element instanceof ArrayStoreInstruction astore) { - generateArrayStoreCheck(codeBuilder, astore); - codeBuilder.with(element); - } else if (element instanceof ArrayLoadInstruction aload) { - codeBuilder.with(element); - if (isCheckedScope) { - generateArrayLoadCheck(codeBuilder, aload); - } - } else if (element instanceof StoreInstruction store) { - if (isCheckedScope) { - generateStoreCheck(codeBuilder, store, methodModel); - } - codeBuilder.with(element); - } else { - codeBuilder.with(element); - } - } - - if (!entryChecksDone && isCheckedScope) { - instrumentMethodEntry(codeBuilder, methodModel); - } - }); + methodBuilder.transformCode( + codeModel, + createCodeTransform(classModel, methodModel, isCheckedScope, loader)); } else { methodBuilder.with(methodElement); } @@ -122,55 +51,9 @@ public void atEnd(ClassBuilder builder) { }; } - protected boolean isFieldWrite(FieldInstruction f) { - return f.opcode() == Opcode.PUTFIELD || f.opcode() == Opcode.PUTSTATIC; - } - - protected boolean isFieldRead(FieldInstruction f) { - return f.opcode() == Opcode.GETFIELD || f.opcode() == Opcode.GETSTATIC; - } - - protected void instrumentMethodEntry(CodeBuilder builder, MethodModel method) { - boolean isStatic = (method.flags().flagsMask() & Modifier.STATIC) != 0; - int slotIndex = isStatic ? 0 : 1; - MethodTypeDesc methodDesc = method.methodTypeSymbol(); - int paramCount = methodDesc.parameterList().size(); - - for (int i = 0; i < paramCount; i++) { - TypeKind type = TypeKind.from(methodDesc.parameterList().get(i)); - generateParameterCheck(builder, slotIndex, type, method, i); - slotIndex += type.slotSize(); - } - } - - protected abstract void generateParameterCheck( - CodeBuilder b, int slotIndex, TypeKind type, MethodModel method, int paramIndex); - - protected abstract void generateFieldWriteCheck( - CodeBuilder b, FieldInstruction field, ClassModel classModel); - - protected abstract void generateFieldReadCheck( - CodeBuilder b, FieldInstruction field, ClassModel classModel); - - protected abstract void generateReturnCheck( - CodeBuilder b, ReturnInstruction ret, MethodModel method); - - protected abstract void generateUncheckedReturnCheck( - CodeBuilder b, - ReturnInstruction ret, - MethodModel method, - ClassModel classModel, - ClassLoader loader); - - protected abstract void generateMethodCallCheck(CodeBuilder b, InvokeInstruction invoke); + protected abstract CodeTransform createCodeTransform( + ClassModel classModel, MethodModel methodModel, boolean isCheckedScope, ClassLoader loader); protected abstract void generateBridgeMethods( ClassBuilder builder, ClassModel model, ClassLoader loader); - - protected abstract void generateArrayStoreCheck(CodeBuilder b, ArrayStoreInstruction instruction); - - protected abstract void generateArrayLoadCheck(CodeBuilder b, ArrayLoadInstruction instruction); - - protected abstract void generateStoreCheck( - CodeBuilder b, StoreInstruction instruction, MethodModel method); } diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/core/TypeSystemConfiguration.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/TypeSystemConfiguration.java index 515fed6..27ed05c 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/TypeSystemConfiguration.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/TypeSystemConfiguration.java @@ -27,7 +27,7 @@ public TypeSystemConfiguration() { * @return this configuration (fluent). */ public TypeSystemConfiguration onEnforce( - Class annotation, RuntimeVerifier verifier) { + Class annotation, CheckGenerator verifier) { registry.put(annotation.descriptorString(), new ConfigEntry(ValidationKind.ENFORCE, verifier)); return this; } @@ -50,7 +50,7 @@ public TypeSystemConfiguration onNoop(Class annotation) { * @param verifier The verifier (required if kind is ENFORCE). * @return this configuration (fluent). */ - public TypeSystemConfiguration withDefault(ValidationKind kind, RuntimeVerifier verifier) { + public TypeSystemConfiguration withDefault(ValidationKind kind, CheckGenerator verifier) { this.defaultEntry = new ConfigEntry(kind, verifier); return this; } @@ -67,5 +67,5 @@ public ConfigEntry getDefault() { return defaultEntry; } - public record ConfigEntry(ValidationKind kind, RuntimeVerifier verifier) {} + public record ConfigEntry(ValidationKind kind, CheckGenerator verifier) {} } diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/core/ValidationKind.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/ValidationKind.java index 0bf6705..6a228b5 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/ValidationKind.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/ValidationKind.java @@ -1,10 +1,10 @@ package io.github.eisop.runtimeframework.core; -/** Defines the semantic behavior of a qualifier in the runtime system. */ +/** Defines the type of validation logic to apply for a specific annotation. */ public enum ValidationKind { /** - * The qualifier requires runtime verification. The associated {@link RuntimeVerifier} will be - * invoked. + * The qualifier requires runtime verification. The associated {@link CheckGenerator} will be + * invoked to generate the check logic. */ ENFORCE, diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/policy/StandardEnforcementPolicy.java b/framework/src/main/java/io/github/eisop/runtimeframework/policy/BoundaryStrategy.java similarity index 80% rename from framework/src/main/java/io/github/eisop/runtimeframework/policy/StandardEnforcementPolicy.java rename to framework/src/main/java/io/github/eisop/runtimeframework/policy/BoundaryStrategy.java index 0b843bb..30f19eb 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/policy/StandardEnforcementPolicy.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/policy/BoundaryStrategy.java @@ -1,6 +1,6 @@ package io.github.eisop.runtimeframework.policy; -import io.github.eisop.runtimeframework.core.RuntimeVerifier; +import io.github.eisop.runtimeframework.core.CheckGenerator; import io.github.eisop.runtimeframework.core.TypeSystemConfiguration; import io.github.eisop.runtimeframework.core.ValidationKind; import io.github.eisop.runtimeframework.filter.ClassInfo; @@ -17,23 +17,17 @@ import java.util.ArrayList; import java.util.List; -public class StandardEnforcementPolicy implements EnforcementPolicy { +public class BoundaryStrategy implements InstrumentationStrategy { protected final TypeSystemConfiguration configuration; protected final Filter safetyFilter; - public StandardEnforcementPolicy( - TypeSystemConfiguration configuration, Filter safetyFilter) { + public BoundaryStrategy(TypeSystemConfiguration configuration, Filter safetyFilter) { this.configuration = configuration; this.safetyFilter = safetyFilter; } - /** - * Resolves the verifier for a given list of annotations. Logic: 1. Check for any annotation that - * explicitly ENFORCES. 2. Check for any annotation that explicitly NOOPs. 3. Fallback to default. - */ - protected RuntimeVerifier resolveVerifier(List annotations) { - // 1. Look for explicit configuration + protected CheckGenerator resolveGenerator(List annotations) { for (Annotation a : annotations) { String desc = a.classSymbol().descriptorString(); TypeSystemConfiguration.ConfigEntry entry = configuration.find(desc); @@ -41,12 +35,11 @@ protected RuntimeVerifier resolveVerifier(List annotations) { if (entry.kind() == ValidationKind.ENFORCE) { return entry.verifier(); } else if (entry.kind() == ValidationKind.NOOP) { - return null; // Explicitly skipped + return null; } } } - // 2. Fallback to default TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { return defaultEntry.verifier(); @@ -56,39 +49,39 @@ protected RuntimeVerifier resolveVerifier(List annotations) { } @Override - public RuntimeVerifier getParameterCheck(MethodModel method, int paramIndex, TypeKind type) { + public CheckGenerator getParameterCheck(MethodModel method, int paramIndex, TypeKind type) { if (type != TypeKind.REFERENCE) return null; List annos = getMethodParamAnnotations(method, paramIndex); - RuntimeVerifier verifier = resolveVerifier(annos); - return (verifier != null) ? verifier.withAttribution(AttributionKind.CALLER) : null; + CheckGenerator generator = resolveGenerator(annos); + return (generator != null) ? generator.withAttribution(AttributionKind.CALLER) : null; } @Override - public RuntimeVerifier getFieldWriteCheck(FieldModel field, TypeKind type) { + public CheckGenerator getFieldWriteCheck(FieldModel field, TypeKind type) { return null; } @Override - public RuntimeVerifier getFieldReadCheck(FieldModel field, TypeKind type) { + public CheckGenerator getFieldReadCheck(FieldModel field, TypeKind type) { if (type != TypeKind.REFERENCE) return null; List annos = getFieldAnnotations(field); - return resolveVerifier(annos); + return resolveGenerator(annos); } @Override - public RuntimeVerifier getReturnCheck(MethodModel method) { + public CheckGenerator getReturnCheck(MethodModel method) { return null; } @Override - public RuntimeVerifier getLocalVariableWriteCheck(MethodModel method, int slot, TypeKind type) { + public CheckGenerator getLocalVariableWriteCheck(MethodModel method, int slot, TypeKind type) { if (type != TypeKind.REFERENCE) return null; List annos = getLocalVariableAnnotations(method, slot); - return resolveVerifier(annos); + return resolveGenerator(annos); } @Override - public RuntimeVerifier getArrayStoreCheck(TypeKind componentType) { + public CheckGenerator getArrayStoreCheck(TypeKind componentType) { if (componentType == TypeKind.REFERENCE) { TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { @@ -99,7 +92,7 @@ public RuntimeVerifier getArrayStoreCheck(TypeKind componentType) { } @Override - public RuntimeVerifier getArrayLoadCheck(TypeKind componentType) { + public CheckGenerator getArrayLoadCheck(TypeKind componentType) { if (componentType == TypeKind.REFERENCE) { TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) { @@ -110,7 +103,7 @@ public RuntimeVerifier getArrayLoadCheck(TypeKind componentType) { } @Override - public RuntimeVerifier getBoundaryCallCheck(String owner, MethodTypeDesc desc) { + public CheckGenerator getBoundaryCallCheck(String owner, MethodTypeDesc desc) { boolean isUnchecked = !safetyFilter.test(new ClassInfo(owner, null, null)); TypeKind returnType = TypeKind.from(desc.returnType()); @@ -124,7 +117,7 @@ public RuntimeVerifier getBoundaryCallCheck(String owner, MethodTypeDesc desc) { } @Override - public RuntimeVerifier getBoundaryFieldReadCheck(String owner, String fieldName, TypeKind type) { + public CheckGenerator getBoundaryFieldReadCheck(String owner, String fieldName, TypeKind type) { boolean isUnchecked = !safetyFilter.test(new ClassInfo(owner, null, null)); if (isUnchecked && type == TypeKind.REFERENCE) { TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); @@ -135,17 +128,13 @@ public RuntimeVerifier getBoundaryFieldReadCheck(String owner, String fieldName, return null; } - // --- 3. Inheritance Logic --- - @Override public boolean shouldGenerateBridge(ParentMethod parentMethod) { if (parentMethod.owner().thisClass().asInternalName().equals("java/lang/Object")) return false; MethodModel method = parentMethod.method(); - // MethodTypeDesc param parsing var paramTypes = method.methodTypeSymbol().parameterList(); - // 1. Check Parameters for (int i = 0; i < paramTypes.size(); i++) { boolean explicitNoop = false; boolean explicitEnforce = false; @@ -163,7 +152,6 @@ public boolean shouldGenerateBridge(ParentMethod parentMethod) { if (explicitEnforce) return true; - // If no explicit decision, check default if it's a reference type TypeKind pType = TypeKind.from(paramTypes.get(i)); if (pType == TypeKind.REFERENCE && !explicitNoop) { TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault(); @@ -173,7 +161,6 @@ public boolean shouldGenerateBridge(ParentMethod parentMethod) { } } - // 2. Check Return Type TypeKind returnType = TypeKind.from(method.methodTypeSymbol().returnType()); if (returnType == TypeKind.REFERENCE) { boolean explicitNoop = false; @@ -204,21 +191,18 @@ public boolean shouldGenerateBridge(ParentMethod parentMethod) { } @Override - public RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int paramIndex) { + public CheckGenerator getBridgeParameterCheck(ParentMethod parentMethod, int paramIndex) { MethodModel method = parentMethod.method(); List annos = getMethodParamAnnotations(method, paramIndex); - RuntimeVerifier verifier = resolveVerifier(annos); - if (verifier != null) return verifier.withAttribution(AttributionKind.CALLER); + CheckGenerator generator = resolveGenerator(annos); + if (generator != null) return generator.withAttribution(AttributionKind.CALLER); // 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 = @@ -237,15 +221,15 @@ public RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int pa } @Override - public RuntimeVerifier getBridgeReturnCheck(ParentMethod parentMethod) { + public CheckGenerator getBridgeReturnCheck(ParentMethod parentMethod) { MethodModel method = parentMethod.method(); TypeKind returnType = TypeKind.from(method.methodTypeSymbol().returnType()); if (returnType != TypeKind.REFERENCE) return null; List annos = getMethodReturnAnnotations(method); - RuntimeVerifier verifier = resolveVerifier(annos); - if (verifier != null) return verifier.withAttribution(AttributionKind.CALLER); + CheckGenerator generator = resolveGenerator(annos); + if (generator != null) return generator.withAttribution(AttributionKind.CALLER); boolean isExplicitNoop = false; for (Annotation a : annos) { @@ -263,7 +247,6 @@ public RuntimeVerifier getBridgeReturnCheck(ParentMethod parentMethod) { return null; } - // --- Parsing Helpers --- private List getMethodParamAnnotations(MethodModel method, int paramIndex) { List result = new ArrayList<>(); method diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/policy/EnforcementPolicy.java b/framework/src/main/java/io/github/eisop/runtimeframework/policy/InstrumentationStrategy.java similarity index 58% rename from framework/src/main/java/io/github/eisop/runtimeframework/policy/EnforcementPolicy.java rename to framework/src/main/java/io/github/eisop/runtimeframework/policy/InstrumentationStrategy.java index da9fdc5..0794653 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/policy/EnforcementPolicy.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/policy/InstrumentationStrategy.java @@ -1,6 +1,6 @@ package io.github.eisop.runtimeframework.policy; -import io.github.eisop.runtimeframework.core.RuntimeVerifier; +import io.github.eisop.runtimeframework.core.CheckGenerator; import io.github.eisop.runtimeframework.resolution.ParentMethod; import java.lang.classfile.ClassModel; import java.lang.classfile.FieldModel; @@ -9,57 +9,56 @@ import java.lang.constant.MethodTypeDesc; /** Defines the rules for WHEN to inject a runtime check. */ -public interface EnforcementPolicy { +public interface InstrumentationStrategy { /** Should we check this specific parameter at method entry? */ - RuntimeVerifier getParameterCheck(MethodModel method, int paramIndex, TypeKind type); + CheckGenerator getParameterCheck(MethodModel method, int paramIndex, TypeKind type); /** Should we check a write to this field? */ - RuntimeVerifier getFieldWriteCheck(FieldModel field, TypeKind type); + CheckGenerator getFieldWriteCheck(FieldModel field, TypeKind type); /** Should we check a read from this field? */ - RuntimeVerifier getFieldReadCheck(FieldModel field, TypeKind type); + CheckGenerator getFieldReadCheck(FieldModel field, TypeKind type); /** Should we check this return value? */ - RuntimeVerifier getReturnCheck(MethodModel method); + CheckGenerator getReturnCheck(MethodModel method); /** * Should we check a write to a field in an EXTERNAL class? (Used when Unchecked code writes to * Checked code). */ - default RuntimeVerifier getBoundaryFieldWriteCheck( - String owner, String fieldName, TypeKind type) { + default CheckGenerator getBoundaryFieldWriteCheck(String owner, String fieldName, TypeKind type) { return null; } /** We are calling a method on 'owner'. Should we check the result? */ - RuntimeVerifier getBoundaryCallCheck(String owner, MethodTypeDesc desc); + CheckGenerator getBoundaryCallCheck(String owner, MethodTypeDesc desc); /** We are reading field from an EXTERNAL class. Should we check the value? */ - RuntimeVerifier getBoundaryFieldReadCheck(String owner, String fieldName, TypeKind type); + CheckGenerator getBoundaryFieldReadCheck(String owner, String fieldName, TypeKind type); /** Should we generate a bridge for this inherited method? */ boolean shouldGenerateBridge(ParentMethod parentMethod); /** For a bridge we are generating, what check applies to this parameter? */ - RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int paramIndex); + CheckGenerator getBridgeParameterCheck(ParentMethod parentMethod, int paramIndex); /** For a bridge we are generating, what check applies to the return value? */ - default RuntimeVerifier getBridgeReturnCheck(ParentMethod parentMethod) { + default CheckGenerator getBridgeReturnCheck(ParentMethod parentMethod) { return null; } /** Should we check an value being stored into an array? */ - RuntimeVerifier getArrayStoreCheck(TypeKind componentType); + CheckGenerator getArrayStoreCheck(TypeKind componentType); /** Should we check a value being read from an array? */ - RuntimeVerifier getArrayLoadCheck(TypeKind componentType); + CheckGenerator getArrayLoadCheck(TypeKind componentType); /** Should we check a value being stored in a variable? */ - RuntimeVerifier getLocalVariableWriteCheck(MethodModel method, int slot, TypeKind type); + CheckGenerator getLocalVariableWriteCheck(MethodModel method, int slot, TypeKind type); /** Should we check the return of an unchecked override? */ - default RuntimeVerifier getUncheckedOverrideReturnCheck( + default CheckGenerator getUncheckedOverrideReturnCheck( ClassModel classModel, MethodModel method, ClassLoader loader) { return null; } diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/policy/GlobalEnforcementPolicy.java b/framework/src/main/java/io/github/eisop/runtimeframework/policy/StrictBoundaryStrategy.java similarity index 85% rename from framework/src/main/java/io/github/eisop/runtimeframework/policy/GlobalEnforcementPolicy.java rename to framework/src/main/java/io/github/eisop/runtimeframework/policy/StrictBoundaryStrategy.java index 771eeed..64cc9f9 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/policy/GlobalEnforcementPolicy.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/policy/StrictBoundaryStrategy.java @@ -1,25 +1,27 @@ package io.github.eisop.runtimeframework.policy; -import io.github.eisop.runtimeframework.core.RuntimeVerifier; +import io.github.eisop.runtimeframework.core.CheckGenerator; import io.github.eisop.runtimeframework.core.TypeSystemConfiguration; import io.github.eisop.runtimeframework.core.ValidationKind; import io.github.eisop.runtimeframework.filter.ClassInfo; import io.github.eisop.runtimeframework.filter.Filter; +import java.lang.annotation.Annotation; import java.lang.classfile.ClassModel; import java.lang.classfile.MethodModel; import java.lang.classfile.TypeKind; import java.lang.constant.ClassDesc; +import java.lang.reflect.Field; import java.lang.reflect.Method; -public class GlobalEnforcementPolicy extends StandardEnforcementPolicy { +public class StrictBoundaryStrategy extends BoundaryStrategy { - public GlobalEnforcementPolicy( + public StrictBoundaryStrategy( TypeSystemConfiguration configuration, Filter safetyFilter) { super(configuration, safetyFilter); } @Override - public RuntimeVerifier getBoundaryFieldWriteCheck(String owner, String fieldName, TypeKind type) { + public CheckGenerator getBoundaryFieldWriteCheck(String owner, String fieldName, TypeKind type) { if (isClassChecked(owner)) { if (type == TypeKind.REFERENCE) { if (isFieldOptOut(owner, fieldName)) { @@ -35,7 +37,7 @@ public RuntimeVerifier getBoundaryFieldWriteCheck(String owner, String fieldName } @Override - public RuntimeVerifier getUncheckedOverrideReturnCheck( + public CheckGenerator getUncheckedOverrideReturnCheck( ClassModel classModel, MethodModel method, ClassLoader loader) { String superName = classModel.superclass().map(sc -> sc.asInternalName().replace('/', '.')).orElse(null); @@ -53,14 +55,13 @@ public RuntimeVerifier getUncheckedOverrideReturnCheck( String parentDesc = getMethodDescriptor(m); if (methodDesc.equals(parentDesc)) { // Check parent method annotations - for (java.lang.annotation.Annotation anno : m.getAnnotations()) { + for (Annotation anno : m.getAnnotations()) { String desc = "L" + anno.annotationType().getName().replace('.', '/') + ";"; TypeSystemConfiguration.ConfigEntry entry = configuration.find(desc); if (entry != null && entry.kind() == ValidationKind.NOOP) return null; } // Check return type annotations - for (java.lang.annotation.Annotation anno : - m.getAnnotatedReturnType().getAnnotations()) { + for (Annotation anno : m.getAnnotatedReturnType().getAnnotations()) { String desc = "L" + anno.annotationType().getName().replace('.', '/') + ";"; TypeSystemConfiguration.ConfigEntry entry = configuration.find(desc); if (entry != null && entry.kind() == ValidationKind.NOOP) return null; @@ -94,7 +95,7 @@ private boolean isClassChecked(String internalName) { String className = internalName.replace('/', '.'); ClassLoader cl = Thread.currentThread().getContextClassLoader(); Class clazz = Class.forName(className, false, cl); - for (java.lang.annotation.Annotation anno : clazz.getAnnotations()) { + for (Annotation anno : clazz.getAnnotations()) { if (anno.annotationType() .getName() .equals("io.github.eisop.runtimeframework.qual.AnnotatedFor")) { @@ -112,14 +113,14 @@ private boolean isFieldOptOut(String owner, String fieldName) { Class clazz = Class.forName( owner.replace('/', '.'), false, Thread.currentThread().getContextClassLoader()); - java.lang.reflect.Field field = clazz.getDeclaredField(fieldName); + Field field = clazz.getDeclaredField(fieldName); - for (java.lang.annotation.Annotation anno : field.getAnnotations()) { + for (Annotation anno : field.getAnnotations()) { String desc = "L" + anno.annotationType().getName().replace('.', '/') + ";"; TypeSystemConfiguration.ConfigEntry entry = configuration.find(desc); if (entry != null && entry.kind() == ValidationKind.NOOP) return true; } - for (java.lang.annotation.Annotation anno : field.getAnnotatedType().getAnnotations()) { + for (Annotation anno : field.getAnnotatedType().getAnnotations()) { String desc = "L" + anno.annotationType().getName().replace('.', '/') + ";"; TypeSystemConfiguration.ConfigEntry entry = configuration.find(desc); if (entry != null && entry.kind() == ValidationKind.NOOP) return true; diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/util/SysOutInstrumenter.java b/framework/src/main/java/io/github/eisop/runtimeframework/util/SysOutInstrumenter.java index 8b256a8..453b9da 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/util/SysOutInstrumenter.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/util/SysOutInstrumenter.java @@ -4,8 +4,9 @@ import java.lang.classfile.ClassBuilder; import java.lang.classfile.ClassModel; import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeTransform; import java.lang.classfile.MethodModel; -import java.lang.classfile.TypeKind; import java.lang.classfile.instruction.ArrayLoadInstruction; import java.lang.classfile.instruction.ArrayStoreInstruction; import java.lang.classfile.instruction.FieldInstruction; @@ -27,78 +28,62 @@ public SysOutInstrumenter() { super(info -> true); } - private void print(CodeBuilder b, String msg) { - b.getstatic(SYSOUT_SYSTEM, "out", SYSOUT_STREAM); - b.ldc(msg); - b.invokevirtual(SYSOUT_STREAM, "println", SYSOUT_PRINTLN); - } - - @Override - protected void generateParameterCheck( - CodeBuilder b, int slotIndex, TypeKind type, MethodModel method, int paramIndex) { - print(b, " [Param Check] Verifying argument at slot " + slotIndex); - } - - @Override - protected void generateFieldWriteCheck( - CodeBuilder b, FieldInstruction field, ClassModel classModel) { - print(b, " [Field Write] About to write to: " + field.name().stringValue()); - } - - @Override - protected void generateFieldReadCheck( - CodeBuilder b, FieldInstruction field, ClassModel classModel) { - print(b, " [Field Read] Just read from: " + field.name().stringValue()); - } - @Override - protected void generateReturnCheck(CodeBuilder b, ReturnInstruction ret, MethodModel method) { - print(b, " [Return Check] Returning from method via opcode: " + ret.opcode().name()); - } - - @Override - protected void generateUncheckedReturnCheck( - CodeBuilder b, - ReturnInstruction ret, - MethodModel method, - ClassModel classModel, - ClassLoader loader) { - print( - b, - " [Unchecked Override Return Check] Checking return of overridden method: " - + method.methodName().stringValue()); - } - - @Override - protected void generateMethodCallCheck(CodeBuilder b, InvokeInstruction invoke) { - print( - b, - " [Call Site] Just called: " - + invoke.owner().asInternalName() - + "." - + invoke.name().stringValue()); + protected CodeTransform createCodeTransform( + ClassModel classModel, MethodModel methodModel, boolean isCheckedScope, ClassLoader loader) { + return new SysOutTransform(); } @Override protected void generateBridgeMethods(ClassBuilder builder, ClassModel model, ClassLoader loader) { - // Debug instrumenter does not generate bridges, but we can log that the hook was hit - // System.out.println("[SysOutInstrumenter] Bridge hook triggered for: " + - // model.thisClass().asInternalName()); + // Debug instrumenter does not generate bridges } - @Override - protected void generateArrayLoadCheck(CodeBuilder b, ArrayLoadInstruction instruction) { - print(b, " [Array Load] Reading from array"); + private static void print(CodeBuilder b, String msg) { + b.getstatic(SYSOUT_SYSTEM, "out", SYSOUT_STREAM); + b.ldc(msg); + b.invokevirtual(SYSOUT_STREAM, "println", SYSOUT_PRINTLN); } - @Override - protected void generateArrayStoreCheck(CodeBuilder b, ArrayStoreInstruction instruction) { - print(b, " [Array Store] Writing to array"); - } + private static class SysOutTransform implements CodeTransform { - @Override - protected void generateStoreCheck( - CodeBuilder b, StoreInstruction instruction, MethodModel method) { - print(b, " [Local Store] Writing to slot " + instruction.slot()); + @Override + public void accept(CodeBuilder b, CodeElement element) { + if (element instanceof FieldInstruction f) { + if (isWrite(f)) { + print(b, " [Field Write] About to write to: " + f.name().stringValue()); + b.with(element); + } else { + b.with(element); + print(b, " [Field Read] Just read from: " + f.name().stringValue()); + } + } else if (element instanceof ReturnInstruction r) { + print(b, " [Return Check] Returning from method via opcode: " + r.opcode().name()); + b.with(element); + } else if (element instanceof InvokeInstruction i) { + b.with(element); + print( + b, + " [Call Site] Just called: " + + i.owner().asInternalName() + + "." + + i.name().stringValue()); + } else if (element instanceof ArrayLoadInstruction) { + b.with(element); + print(b, " [Array Load] Reading from array"); + } else if (element instanceof ArrayStoreInstruction) { + print(b, " [Array Store] Writing to array"); + b.with(element); + } else if (element instanceof StoreInstruction s) { + print(b, " [Local Store] Writing to slot " + s.slot()); + b.with(element); + } else { + b.with(element); + } + } + + private boolean isWrite(FieldInstruction f) { + return f.opcode().name().startsWith("PUT"); + } } } From 5b3ab27aa4c61e2a78af9326fe9cd44af1936740 Mon Sep 17 00:00:00 2001 From: Alex Cook Date: Mon, 2 Feb 2026 13:42:46 -0500 Subject: [PATCH 6/6] chore: cleanup --- .../eisop/runtimeframework/core/RuntimeInstrumenter.java | 2 +- .../eisop/runtimeframework/policy/BoundaryStrategy.java | 2 +- .../runtimeframework/policy/InstrumentationStrategy.java | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java b/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java index 4139979..df6f56c 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/core/RuntimeInstrumenter.java @@ -60,4 +60,4 @@ protected abstract CodeTransform createCodeTransform( protected abstract void generateBridgeMethods( ClassBuilder builder, ClassModel model, ClassLoader loader); -} \ No newline at end of file +} diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/policy/BoundaryStrategy.java b/framework/src/main/java/io/github/eisop/runtimeframework/policy/BoundaryStrategy.java index aa17a75..63e0b59 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/policy/BoundaryStrategy.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/policy/BoundaryStrategy.java @@ -327,4 +327,4 @@ private List getLocalVariableAnnotations(MethodModel method, int slo }); return result; } -} \ No newline at end of file +} diff --git a/framework/src/main/java/io/github/eisop/runtimeframework/policy/InstrumentationStrategy.java b/framework/src/main/java/io/github/eisop/runtimeframework/policy/InstrumentationStrategy.java index 65f89fd..0794653 100644 --- a/framework/src/main/java/io/github/eisop/runtimeframework/policy/InstrumentationStrategy.java +++ b/framework/src/main/java/io/github/eisop/runtimeframework/policy/InstrumentationStrategy.java @@ -48,11 +48,6 @@ default CheckGenerator getBridgeReturnCheck(ParentMethod parentMethod) { return null; } - /** For a bridge we are generating, what check applies to the return value? */ - default RuntimeVerifier getBridgeReturnCheck(ParentMethod parentMethod) { - return null; - } - /** Should we check an value being stored into an array? */ CheckGenerator getArrayStoreCheck(TypeKind componentType);