3838 * <h3>Phase 1 -- Bytecode analysis (seed propagation)</h3>
3939 * <p>
4040 * Scans all reachable classes and builds a method-level call graph. Starting from seed
41- * methods ({@code ClassLoader.loadClass(String)}, {@code Class.forName(String)}, and
42- * {@code Class.forName(String, boolean, ClassLoader)}), propagates backwards through the
41+ * methods ({@code ClassLoader.loadClass(String)}, {@code Class.forName(String)},
42+ * {@code Class.forName(String, boolean, ClassLoader)}, and
43+ * {@code MethodHandles.Lookup.findClass(String)}), propagates backwards through the
4344 * call graph using a fixed-point algorithm to identify every application method that
4445 * transitively calls a class-loading seed. JDK and infrastructure classes
4546 * ({@code java/}, {@code javax/}, {@code jakarta/}, {@code sun/}, {@code org/objectweb/})
@@ -96,9 +97,12 @@ class ClassLoadingChainAnalyzer {
9697
9798 private static final Logger log = Logger .getLogger (ClassLoadingChainAnalyzer .class .getName ());
9899
99- private static final String CLASSLOADER_LOAD_CLASS = "java/lang/ClassLoader.loadClass(Ljava/lang/String;)Ljava/lang/Class;" ;
100- private static final String CLASS_FOR_NAME_1 = "java/lang/Class.forName(Ljava/lang/String;)Ljava/lang/Class;" ;
101- private static final String CLASS_FOR_NAME_3 = "java/lang/Class.forName(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;" ;
100+ /** JDK methods that load classes by name — the seeds for call chain propagation. */
101+ private static final Set <String > SEED_METHODS = Set .of (
102+ "java/lang/ClassLoader.loadClass(Ljava/lang/String;)Ljava/lang/Class;" ,
103+ "java/lang/Class.forName(Ljava/lang/String;)Ljava/lang/Class;" ,
104+ "java/lang/Class.forName(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;" ,
105+ "java/lang/invoke/MethodHandles$Lookup.findClass(Ljava/lang/String;)Ljava/lang/Class;" );
102106
103107 private static final int MAX_CALLER_DEPTH = 5 ;
104108
@@ -121,16 +125,14 @@ static Set<String> analyze(
121125 Map <String , Set <String >> callerIndex = new HashMap <>();
122126 Set <String > classLoadingMethods = findClassLoadingMethods (reachableClasses , allBytecode , callerIndex );
123127 if (classLoadingMethods .isEmpty ()) {
124- log .debug ("No class-loading methods found in reachable classes" );
125- return new HashSet <>();
128+ return Set .of ();
126129 }
127130 log .debugf ("Found %d class-loading methods" , classLoadingMethods .size ());
128131
129132 // Phase 2: Find entry point classes
130133 Set <String > entryPointClasses = findEntryPointClasses (classLoadingMethods , callerIndex );
131134 if (entryPointClasses .isEmpty ()) {
132- log .debug ("No entry point classes found for class-loading chains" );
133- return new HashSet <>();
135+ return Set .of ();
134136 }
135137 log .debugf ("Found %d entry point classes for class-loading chains" , entryPointClasses .size ());
136138
@@ -154,14 +156,22 @@ private static Set<String> findClassLoadingMethods(
154156 Map <String , Supplier <byte []>> allBytecode ,
155157 Map <String , Set <String >> callerIndex ) {
156158
157- // Seed set
158- Set <String > classLoadingMethods = new HashSet <>();
159- classLoadingMethods .add (CLASSLOADER_LOAD_CLASS );
160- classLoadingMethods .add (CLASS_FOR_NAME_1 );
161- classLoadingMethods .add (CLASS_FOR_NAME_3 );
159+ Map <String , Set <String >> methodCallees = buildCallGraph (reachableClasses , allBytecode , callerIndex );
160+ return propagateFromSeeds (methodCallees );
161+ }
162+
163+ /**
164+ * Scans reachable bytecode and builds two maps in a single pass:
165+ * <ul>
166+ * <li>{@code methodCallees}: method → set of methods it calls</li>
167+ * <li>{@code callerIndex}: method → set of methods that call it (reverse index)</li>
168+ * </ul>
169+ */
170+ private static Map <String , Set <String >> buildCallGraph (
171+ Set <String > reachableClasses ,
172+ Map <String , Supplier <byte []>> allBytecode ,
173+ Map <String , Set <String >> callerIndex ) {
162174
163- // Build method call graph and caller index in one pass
164- // methodCallees: method -> set of methods it calls
165175 Map <String , Set <String >> methodCallees = new HashMap <>();
166176
167177 for (String className : reachableClasses ) {
@@ -195,8 +205,17 @@ public void visitMethodInsn(int opcode, String owner, String mname,
195205 }, ClassReader .SKIP_FRAMES | ClassReader .SKIP_DEBUG );
196206 }
197207
198- // Propagate to fixed point: if method A calls method B which is a class-loading method
199- // (and B is not a JDK/infra class), then A is also a class-loading method
208+ return methodCallees ;
209+ }
210+
211+ /**
212+ * Starting from {@link #SEED_METHODS}, propagates through the call graph to find all
213+ * application methods that transitively call a class-loading seed.
214+ * JDK/infrastructure methods are excluded from propagation.
215+ */
216+ private static Set <String > propagateFromSeeds (Map <String , Set <String >> methodCallees ) {
217+ Set <String > classLoadingMethods = new HashSet <>(SEED_METHODS );
218+
200219 boolean changed = true ;
201220 while (changed ) {
202221 changed = false ;
@@ -217,11 +236,7 @@ public void visitMethodInsn(int opcode, String owner, String mname,
217236 }
218237 }
219238
220- // Remove the seed JDK methods
221- classLoadingMethods .remove (CLASSLOADER_LOAD_CLASS );
222- classLoadingMethods .remove (CLASS_FOR_NAME_1 );
223- classLoadingMethods .remove (CLASS_FOR_NAME_3 );
224-
239+ classLoadingMethods .removeAll (SEED_METHODS );
225240 return classLoadingMethods ;
226241 }
227242
@@ -280,33 +295,26 @@ private static Set<String> findEntryPointClasses(
280295 }
281296
282297 /**
283- * Extracts the method name from a method key like "com/example/Foo.methodName(Ljava/lang/String;)V".
298+ * Finds the dot+paren boundary in a method key like "com/example/Foo.methodName(...)V".
299+ * Returns the dot index, or -1 if the key is malformed.
284300 */
285- private static String extractMethodName (String methodKey ) {
301+ private static int findMethodSeparator (String methodKey ) {
286302 int parenIdx = methodKey .indexOf ('(' );
287- if (parenIdx < 0 ) {
288- return null ;
289- }
290- int dotIdx = methodKey .lastIndexOf ('.' , parenIdx );
291- if (dotIdx < 0 ) {
292- return null ;
293- }
294- return methodKey .substring (dotIdx + 1 , parenIdx );
303+ return parenIdx < 0 ? -1 : methodKey .lastIndexOf ('.' , parenIdx );
304+ }
305+
306+ /** Extracts the method name from a key like {@code "com/example/Foo.bar(I)V"} → {@code "bar"}. */
307+ private static String extractMethodName (String methodKey ) {
308+ int dotIdx = findMethodSeparator (methodKey );
309+ return dotIdx < 0 ? null : methodKey .substring (dotIdx + 1 , methodKey .indexOf ('(' ));
295310 }
296311
297312 /**
298- * Extracts the class name (internal form) from a method key.
313+ * Extracts the class name (internal form) from a key like {@code "com/example/Foo.bar(I)V"} → {@code "com/example/Foo"} .
299314 */
300315 private static String extractClassName (String methodKey ) {
301- int parenIdx = methodKey .indexOf ('(' );
302- if (parenIdx < 0 ) {
303- return null ;
304- }
305- int dotIdx = methodKey .lastIndexOf ('.' , parenIdx );
306- if (dotIdx < 0 ) {
307- return null ;
308- }
309- return methodKey .substring (0 , dotIdx );
316+ int dotIdx = findMethodSeparator (methodKey );
317+ return dotIdx < 0 ? null : methodKey .substring (0 , dotIdx );
310318 }
311319
312320 /**
@@ -318,9 +326,29 @@ private static Set<String> executeWithRecordingClassLoader(
318326 Map <String , Supplier <byte []>> allBytecode ,
319327 Set <String > allKnownClasses ) {
320328
329+ Map <String , byte []> bytecodeMap = resolveBytecodeMap (allBytecode );
321330 Set <String > allDiscovered = new HashSet <>();
322331
323- // Resolve bytecode once for all entry points
332+ // Suppress stdout/stderr: loaded classes may print warnings or stack traces
333+ // during their static initialization. These are expected and harmless.
334+ java .io .PrintStream originalOut = System .out ;
335+ java .io .PrintStream originalErr = System .err ;
336+ try {
337+ System .setOut (new java .io .PrintStream (java .io .OutputStream .nullOutputStream ()));
338+ System .setErr (new java .io .PrintStream (java .io .OutputStream .nullOutputStream ()));
339+ for (String entryPoint : entryPointClasses ) {
340+ executeEntryPoint (entryPoint , bytecodeMap , allKnownClasses , allDiscovered );
341+ }
342+ } finally {
343+ System .setOut (originalOut );
344+ System .setErr (originalErr );
345+ }
346+
347+ return allDiscovered ;
348+ }
349+
350+ /** Resolves all bytecode suppliers into a concrete map for use by the RecordingClassLoader. */
351+ private static Map <String , byte []> resolveBytecodeMap (Map <String , Supplier <byte []>> allBytecode ) {
324352 Map <String , byte []> bytecodeMap = new HashMap <>();
325353 for (Map .Entry <String , Supplier <byte []>> entry : allBytecode .entrySet ()) {
326354 try {
@@ -329,57 +357,51 @@ private static Set<String> executeWithRecordingClassLoader(
329357 // skip unreadable classes
330358 }
331359 }
360+ return bytecodeMap ;
361+ }
332362
333- // Suppress stdout/stderr during recording: loaded classes (BouncyCastle, SLF4J, etc.)
334- // may print warnings, stack traces, or status messages during their static initialization.
335- // These are expected and harmless — redirect to a null stream to keep the build log clean.
336- java .io .PrintStream originalOut = System .out ;
337- java .io .PrintStream originalErr = System .err ;
338- java .io .PrintStream nullStream = new java .io .PrintStream (java .io .OutputStream .nullOutputStream ());
339- try {
340- System .setOut (nullStream );
341- System .setErr (nullStream );
342- for (String entryPoint : entryPointClasses ) {
343- log .debugf ("Executing entry point class: %s" , entryPoint );
344-
345- RecordingClassLoader loader = new RecordingClassLoader (bytecodeMap );
346-
347- try {
348- Class <?> clazz = Class .forName (entryPoint , true , loader );
349- try {
350- Object instance = clazz .getConstructor ().newInstance ();
351- // If the instance is a Map (like BouncyCastle's Provider which extends Properties),
352- // iterate values and record strings that match known classes
353- if (instance instanceof java .util .Map ) {
354- java .util .Map <?, ?> map = (java .util .Map <?, ?>) instance ;
355- for (Object value : map .values ()) {
356- if (value instanceof String ) {
357- String strValue = (String ) value ;
358- if (allKnownClasses .contains (strValue )) {
359- allDiscovered .add (strValue );
360- }
361- }
362- }
363- }
364- } catch (Exception e ) {
365- log .debugf ("Could not instantiate entry point %s: %s" , entryPoint , e .getMessage ());
366- } catch (LinkageError e ) {
367- log .debugf ("LinkageError instantiating entry point %s: %s" , entryPoint , e .getMessage ());
368- }
369- } catch (Exception e ) {
370- log .debugf ("Could not load entry point %s: %s" , entryPoint , e .getMessage ());
371- } catch (LinkageError e ) {
372- log .debugf ("LinkageError loading entry point %s: %s" , entryPoint , e .getMessage ());
373- }
363+ /**
364+ * Loads and instantiates a single entry point class in a fresh RecordingClassLoader.
365+ * Records all class load attempts, plus class name strings from Map values.
366+ */
367+ private static void executeEntryPoint (String entryPoint , Map <String , byte []> bytecodeMap ,
368+ Set <String > allKnownClasses , Set <String > discovered ) {
369+ log .debugf ("Executing entry point class: %s" , entryPoint );
370+ RecordingClassLoader loader = new RecordingClassLoader (bytecodeMap );
374371
375- allDiscovered .addAll (loader .getLoadedClassNames ());
372+ try {
373+ Class <?> clazz = Class .forName (entryPoint , true , loader );
374+ try {
375+ Object instance = clazz .getConstructor ().newInstance ();
376+ collectClassNamesFromMapValues (instance , allKnownClasses , discovered );
377+ } catch (Exception e ) {
378+ log .debugf ("Could not instantiate entry point %s: %s" , entryPoint , e .getMessage ());
379+ } catch (LinkageError e ) {
380+ log .debugf ("LinkageError instantiating entry point %s: %s" , entryPoint , e .getMessage ());
376381 }
377- } finally {
378- System .setOut (originalOut );
379- System .setErr (originalErr );
382+ } catch (Exception e ) {
383+ log .debugf ("Could not load entry point %s: %s" , entryPoint , e .getMessage ());
384+ } catch (LinkageError e ) {
385+ log .debugf ("LinkageError loading entry point %s: %s" , entryPoint , e .getMessage ());
380386 }
381387
382- return allDiscovered ;
388+ discovered .addAll (loader .getLoadedClassNames ());
389+ }
390+
391+ /**
392+ * If the object is a Map (e.g., BouncyCastle's Provider extends Properties),
393+ * extracts String values that match known class names. These are class names stored
394+ * via methods like {@code addAlgorithm(key, className)} for deferred loading.
395+ */
396+ private static void collectClassNamesFromMapValues (Object instance ,
397+ Set <String > allKnownClasses , Set <String > discovered ) {
398+ if (instance instanceof java .util .Map <?, ?> map ) {
399+ for (Object value : map .values ()) {
400+ if (value instanceof String strValue && allKnownClasses .contains (strValue )) {
401+ discovered .add (strValue );
402+ }
403+ }
404+ }
383405 }
384406
385407 /**
0 commit comments