Skip to content

Commit 8e8c590

Browse files
Merge branch 'policy-refactor'
2 parents aefacd7 + 81c02d9 commit 8e8c590

20 files changed

Lines changed: 503 additions & 326 deletions

File tree

checker/src/main/java/io/github/eisop/runtimeframework/checker/nullness/NullnessRuntimeChecker.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import io.github.eisop.runtimeframework.core.RuntimeChecker;
44
import io.github.eisop.runtimeframework.core.TypeSystemConfiguration;
55
import io.github.eisop.runtimeframework.core.ValidationKind;
6-
import io.github.eisop.runtimeframework.filter.ClassInfo;
7-
import io.github.eisop.runtimeframework.filter.Filter;
86
import io.github.eisop.runtimeframework.instrumentation.EnforcementInstrumenter;
97
import io.github.eisop.runtimeframework.instrumentation.RuntimeInstrumenter;
8+
import io.github.eisop.runtimeframework.policy.RuntimePolicy;
109
import io.github.eisop.runtimeframework.resolution.BytecodeHierarchyResolver;
1110
import io.github.eisop.runtimeframework.resolution.HierarchyResolver;
1211
import io.github.eisop.runtimeframework.strategy.InstrumentationStrategy;
@@ -21,7 +20,7 @@ public String getName() {
2120
}
2221

2322
@Override
24-
public RuntimeInstrumenter getInstrumenter(Filter<ClassInfo> filter) {
23+
public RuntimeInstrumenter getInstrumenter(RuntimePolicy policy) {
2524
NullnessCheckGenerator verifier = new NullnessCheckGenerator();
2625

2726
TypeSystemConfiguration config =
@@ -30,12 +29,10 @@ public RuntimeInstrumenter getInstrumenter(Filter<ClassInfo> filter) {
3029
.onNoop(Nullable.class)
3130
.withDefault(ValidationKind.ENFORCE, verifier);
3231

33-
InstrumentationStrategy strategy = createStrategy(config, filter);
32+
InstrumentationStrategy strategy = createStrategy(config, policy);
3433

35-
HierarchyResolver resolver =
36-
new BytecodeHierarchyResolver(
37-
className -> filter.test(new ClassInfo(className.replace('.', '/'), null, null)));
34+
HierarchyResolver resolver = new BytecodeHierarchyResolver(info -> policy.isChecked(info));
3835

39-
return new EnforcementInstrumenter(strategy, resolver, filter);
36+
return new EnforcementInstrumenter(strategy, resolver);
4037
}
4138
}

framework/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ plugins {
22
id 'java'
33
}
44

5+
dependencies {
6+
testImplementation platform('org.junit:junit-bom:6.0.2')
7+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
8+
testImplementation 'org.junit.jupiter:junit-jupiter'
9+
}
10+
511
jar {
612
manifest {
713
attributes(

framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeAgent.java

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import io.github.eisop.runtimeframework.filter.ClassListFilter;
66
import io.github.eisop.runtimeframework.filter.Filter;
77
import io.github.eisop.runtimeframework.filter.FrameworkSafetyFilter;
8+
import io.github.eisop.runtimeframework.policy.DefaultRuntimePolicy;
9+
import io.github.eisop.runtimeframework.policy.RuntimePolicy;
810
import io.github.eisop.runtimeframework.runtime.RuntimeVerifier;
911
import io.github.eisop.runtimeframework.runtime.ViolationHandler;
1012
import java.lang.instrument.Instrumentation;
@@ -14,42 +16,14 @@ public final class RuntimeAgent {
1416

1517
public static void premain(String args, Instrumentation inst) {
1618
Filter<ClassInfo> safeFilter = new FrameworkSafetyFilter();
17-
Filter<ClassInfo> strategyFilter = safeFilter;
1819

1920
String checkedClasses = System.getProperty("runtime.classes");
2021
boolean isGlobalMode = Boolean.getBoolean("runtime.global");
2122
boolean trustAnnotatedFor = Boolean.getBoolean("runtime.trustAnnotatedFor");
22-
23-
if (checkedClasses != null && !checkedClasses.isBlank()) {
24-
System.out.println("[RuntimeAgent] Checked Scope restricted to: " + checkedClasses);
25-
Filter<ClassInfo> listFilter = new ClassListFilter(Arrays.asList(checkedClasses.split(",")));
26-
strategyFilter = info -> safeFilter.test(info) && listFilter.test(info);
27-
} else if (trustAnnotatedFor) {
28-
strategyFilter = info -> false;
29-
}
30-
31-
Filter<ClassInfo> scanFilter = strategyFilter;
32-
boolean scanAll = false;
33-
34-
if (trustAnnotatedFor) {
35-
System.out.println(
36-
"[RuntimeAgent] Auto-Discovery Enabled. Scanning all safe classes for annotations.");
37-
scanAll = true;
38-
}
39-
40-
if (isGlobalMode) {
41-
System.out.println(
42-
"[RuntimeAgent] Global Mode ENABLED. Scanning all safe classes for external writes.");
43-
scanAll = true;
44-
}
45-
46-
if (checkedClasses == null && !trustAnnotatedFor) {
47-
scanAll = true;
48-
}
49-
50-
if (scanAll) {
51-
scanFilter = safeFilter;
52-
}
23+
Filter<ClassInfo> checkedScopeFilter =
24+
(checkedClasses != null && !checkedClasses.isBlank())
25+
? new ClassListFilter(Arrays.asList(checkedClasses.split(",")))
26+
: Filter.rejectAll();
5327

5428
// 3. Configure Violation Handler
5529
String handlerClassName = System.getProperty("runtime.handler");
@@ -83,9 +57,18 @@ public static void premain(String args, Instrumentation inst) {
8357
return;
8458
}
8559

86-
inst.addTransformer(
87-
new RuntimeTransformer(
88-
scanFilter, strategyFilter, checker, trustAnnotatedFor, isGlobalMode),
89-
false);
60+
RuntimePolicy policy =
61+
new DefaultRuntimePolicy(
62+
safeFilter, checkedScopeFilter, isGlobalMode, trustAnnotatedFor, checker.getName());
63+
64+
System.out.println("[RuntimeAgent] Policy mode: " + (isGlobalMode ? "GLOBAL" : "STANDARD"));
65+
if (checkedClasses != null && !checkedClasses.isBlank()) {
66+
System.out.println("[RuntimeAgent] Checked scope list: " + checkedClasses);
67+
}
68+
if (trustAnnotatedFor) {
69+
System.out.println("[RuntimeAgent] Checked scope includes @AnnotatedFor classes.");
70+
}
71+
72+
inst.addTransformer(new RuntimeTransformer(policy, checker), false);
9073
}
9174
}

framework/src/main/java/io/github/eisop/runtimeframework/agent/RuntimeTransformer.java

Lines changed: 13 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,23 @@
11
package io.github.eisop.runtimeframework.agent;
22

33
import io.github.eisop.runtimeframework.core.RuntimeChecker;
4-
import io.github.eisop.runtimeframework.filter.AnnotatedForFilter;
54
import io.github.eisop.runtimeframework.filter.ClassInfo;
6-
import io.github.eisop.runtimeframework.filter.Filter;
75
import io.github.eisop.runtimeframework.instrumentation.RuntimeInstrumenter;
6+
import io.github.eisop.runtimeframework.policy.ClassClassification;
7+
import io.github.eisop.runtimeframework.policy.RuntimePolicy;
88
import java.lang.classfile.ClassFile;
99
import java.lang.classfile.ClassModel;
1010
import java.lang.instrument.ClassFileTransformer;
1111
import java.security.ProtectionDomain;
1212

1313
public class RuntimeTransformer implements ClassFileTransformer {
1414

15-
private final Filter<ClassInfo> scanFilter;
16-
private final Filter<ClassInfo> strategyFilter;
17-
private final RuntimeChecker checker;
18-
private final boolean trustAnnotatedFor;
19-
private final boolean isGlobalMode;
20-
private final AnnotatedForFilter annotatedForFilter;
15+
private final RuntimePolicy policy;
16+
private final RuntimeInstrumenter instrumenter;
2117

22-
public RuntimeTransformer(
23-
Filter<ClassInfo> scanFilter,
24-
Filter<ClassInfo> strategyFilter,
25-
RuntimeChecker checker,
26-
boolean trustAnnotatedFor,
27-
boolean isGlobalMode) {
28-
this.scanFilter = scanFilter;
29-
this.strategyFilter = strategyFilter;
30-
this.checker = checker;
31-
this.trustAnnotatedFor = trustAnnotatedFor;
32-
this.isGlobalMode = isGlobalMode;
33-
this.annotatedForFilter = trustAnnotatedFor ? new AnnotatedForFilter(checker.getName()) : null;
18+
public RuntimeTransformer(RuntimePolicy policy, RuntimeChecker checker) {
19+
this.policy = policy;
20+
this.instrumenter = checker.getInstrumenter(policy);
3421
}
3522

3623
@Override
@@ -42,63 +29,24 @@ public byte[] transform(
4229
ProtectionDomain protectionDomain,
4330
byte[] classfileBuffer) {
4431

45-
if (className != null
46-
&& (className.startsWith("java/")
47-
|| className.startsWith("sun/")
48-
|| className.startsWith("jdk/")
49-
|| className.startsWith("org/gradle"))) {
32+
if (className == null) {
5033
return null;
5134
}
5235

5336
ClassInfo info = new ClassInfo(className, loader, module);
5437

55-
if (!scanFilter.test(info)) {
56-
return null;
57-
}
58-
59-
System.out.println("[RuntimeFramework] Processing: " + className);
60-
6138
try {
6239
ClassFile cf = ClassFile.of();
6340
ClassModel classModel = cf.parse(classfileBuffer);
41+
ClassClassification classification = policy.classify(info, classModel);
6442

65-
boolean isChecked = strategyFilter.test(info);
66-
67-
if (!isChecked && trustAnnotatedFor && annotatedForFilter != null) {
68-
if (annotatedForFilter.test(classModel, loader)) {
69-
System.out.println(
70-
"[RuntimeFramework] Auto-detected Checked Class/Package: " + className);
71-
isChecked = true;
72-
}
73-
}
74-
75-
if (!isChecked && !isGlobalMode) {
43+
if (classification == ClassClassification.SKIP) {
7644
return null;
7745
}
7846

79-
boolean finalIsChecked = isChecked;
80-
Filter<ClassInfo> dynamicFilter =
81-
ctx -> {
82-
ClassLoader effectiveLoader = ctx.loader();
83-
if (effectiveLoader == null) {
84-
effectiveLoader = loader;
85-
}
86-
ClassInfo effectiveCtx =
87-
new ClassInfo(ctx.internalName(), effectiveLoader, ctx.module());
88-
89-
if (effectiveCtx.internalName().equals(className)) {
90-
return finalIsChecked;
91-
}
92-
if (trustAnnotatedFor
93-
&& annotatedForFilter != null
94-
&& annotatedForFilter.test(effectiveCtx)) {
95-
return true;
96-
}
97-
return strategyFilter.test(effectiveCtx);
98-
};
99-
100-
RuntimeInstrumenter instrumenter = checker.getInstrumenter(dynamicFilter);
101-
return cf.transformClass(classModel, instrumenter.asClassTransform(classModel, loader));
47+
boolean isCheckedScope = classification == ClassClassification.CHECKED;
48+
return cf.transformClass(
49+
classModel, instrumenter.asClassTransform(classModel, loader, isCheckedScope));
10250

10351
} catch (Throwable t) {
10452
System.err.println("[RuntimeFramework] CRASH transforming: " + className);
Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package io.github.eisop.runtimeframework.core;
22

3-
import io.github.eisop.runtimeframework.filter.ClassInfo;
4-
import io.github.eisop.runtimeframework.filter.Filter;
53
import io.github.eisop.runtimeframework.instrumentation.RuntimeInstrumenter;
4+
import io.github.eisop.runtimeframework.policy.RuntimePolicy;
65
import io.github.eisop.runtimeframework.strategy.BoundaryStrategy;
76
import io.github.eisop.runtimeframework.strategy.InstrumentationStrategy;
8-
import io.github.eisop.runtimeframework.strategy.StrictBoundaryStrategy;
97

108
/**
119
* Represents a specific type system or check to be enforced (e.g., Nullness, Immutability). This
@@ -19,29 +17,19 @@ public abstract class RuntimeChecker {
1917
/**
2018
* Creates or returns the instrumenter that injects this checker's logic.
2119
*
22-
* @param filter The safety filter currently active in the Agent. The instrumenter should use this
23-
* to determine boundary checks (Checked vs Unchecked).
20+
* @param policy The active runtime policy that classifies checked/unchecked scope.
2421
*/
25-
public abstract RuntimeInstrumenter getInstrumenter(Filter<ClassInfo> filter);
22+
public abstract RuntimeInstrumenter getInstrumenter(RuntimePolicy policy);
2623

2724
/**
28-
* Helper method to create the appropriate InstrumentationStrategy based on the framework's
29-
* configuration (e.g., -Druntime.global=true).
30-
*
31-
* <p>Subclasses should use this instead of manually checking system properties.
25+
* Helper method to create the instrumentation strategy based on the active policy.
3226
*
3327
* @param config The TypeSystemConfiguration for this checker.
34-
* @param filter The filter defining the boundary between Checked and Unchecked code.
35-
* @return A configured InstrumentationStrategy (Standard or Global).
28+
* @param policy The active runtime policy.
29+
* @return A configured InstrumentationStrategy.
3630
*/
3731
protected InstrumentationStrategy createStrategy(
38-
TypeSystemConfiguration config, Filter<ClassInfo> filter) {
39-
40-
boolean isGlobalMode = Boolean.getBoolean("runtime.global");
41-
if (isGlobalMode) {
42-
return new StrictBoundaryStrategy(config, filter);
43-
} else {
44-
return new BoundaryStrategy(config, filter);
45-
}
32+
TypeSystemConfiguration config, RuntimePolicy policy) {
33+
return new BoundaryStrategy(config, policy);
4634
}
4735
}

framework/src/main/java/io/github/eisop/runtimeframework/filter/AnnotatedForFilter.java

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
public class AnnotatedForFilter implements Filter<ClassInfo> {
2323

2424
private final String targetSystem;
25-
private final Map<String, Boolean> cache = new ConcurrentHashMap<>();
25+
private final Map<CacheKey, Boolean> cache = new ConcurrentHashMap<>();
2626
private static final String ANNOTATED_FOR_DESC = AnnotatedFor.class.descriptorString();
2727

2828
public AnnotatedForFilter(String targetSystem) {
@@ -39,27 +39,29 @@ public AnnotatedForFilter(String targetSystem) {
3939
*/
4040
public boolean test(ClassModel model, ClassLoader loader) {
4141
String className = model.thisClass().asInternalName();
42+
CacheKey cacheKey = new CacheKey(className, loader);
4243

43-
if (cache.containsKey(className)) {
44-
return cache.get(className);
44+
if (cache.containsKey(cacheKey)) {
45+
return cache.get(cacheKey);
4546
}
4647

4748
boolean result = hasAnnotatedFor(model);
4849
if (!result) {
4950
result = hasPackageLevelAnnotation(className, loader);
5051
}
5152

52-
cache.put(className, result);
53+
cache.put(cacheKey, result);
5354
return result;
5455
}
5556

5657
@Override
5758
public boolean test(ClassInfo info) {
5859
String className = info.internalName();
5960
if (className == null) return false;
61+
CacheKey cacheKey = new CacheKey(className, info.loader());
6062

61-
if (cache.containsKey(className)) {
62-
return cache.get(className);
63+
if (cache.containsKey(cacheKey)) {
64+
return cache.get(cacheKey);
6365
}
6466

6567
boolean result = false;
@@ -78,7 +80,7 @@ public boolean test(ClassInfo info) {
7880
System.err.println("[AnnotatedForFilter] Failed to load bytecode for: " + className);
7981
}
8082

81-
cache.put(className, result);
83+
cache.put(cacheKey, result);
8284
return result;
8385
}
8486

@@ -117,4 +119,30 @@ private boolean hasAnnotatedFor(ClassModel model) {
117119
})
118120
.orElse(false);
119121
}
122+
123+
private static final class CacheKey {
124+
private final String className;
125+
private final ClassLoader loader;
126+
127+
private CacheKey(String className, ClassLoader loader) {
128+
this.className = className;
129+
this.loader = loader;
130+
}
131+
132+
@Override
133+
public boolean equals(Object obj) {
134+
if (this == obj) {
135+
return true;
136+
}
137+
if (!(obj instanceof CacheKey other)) {
138+
return false;
139+
}
140+
return className.equals(other.className) && loader == other.loader;
141+
}
142+
143+
@Override
144+
public int hashCode() {
145+
return 31 * className.hashCode() + System.identityHashCode(loader);
146+
}
147+
}
120148
}

framework/src/main/java/io/github/eisop/runtimeframework/filter/FrameworkSafetyFilter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public boolean test(ClassInfo info) {
1616
return false;
1717
}
1818

19-
// 2. Skip the runtime framework itself
20-
if (name.startsWith("io/github/eisop/")) {
19+
// 2. Skip the runtime framework internals
20+
if (name.startsWith("io/github/eisop/runtimeframework/")) {
2121
return false;
2222
}
2323

0 commit comments

Comments
 (0)