@@ -40,6 +40,18 @@ public abstract aspect JavaAspectJAbstractAdviceDefinitions {
4040 @Nonnull
4141 private static final StackWalker STACK_WALKER = StackWalker.getInstance ();
4242
43+ /**
44+ * Second StackWalker, retaining {@code Class } references, used only by
45+ * {@link #isJceCryptoPolicyScanInProgress()}. That check is on a cold path (it
46+ * fires only for the rare {@code javax.crypto.JceSecurity } jurisdiction-policy
47+ * scan), so the extra per-frame cost of {@link StackWalker.Option#RETAIN_CLASS_REFERENCE}
48+ * is acceptable there and is not paid by the hot-path inspectors above, which
49+ * keep using {@link #STACK_WALKER}.
50+ */
51+ @Nonnull
52+ private static final StackWalker CLASS_RETAINING_STACK_WALKER = StackWalker
53+ .getInstance (StackWalker .Option .RETAIN_CLASS_REFERENCE );
54+
4355 /**
4456 * Lazily resolved Class< ; ?> ; reference for the AOP settings holder, cached
4557 * so the reflective lookup runs once per JVM rather than on every advice call.
@@ -357,24 +369,59 @@ public abstract aspect JavaAspectJAbstractAdviceDefinitions {
357369 /**
358370 * Returns {@code true } when the current call stack is inside
359371 * {@code javax.crypto.JceSecurity }'s own JCE jurisdiction-policy scan (its
360- * static initialiser calls {@code Files.newDirectoryStream } with the fixed
372+ * private {@code setupJurisdictionPolicies() } method, invoked from its static
373+ * initialiser, calls {@code Files.newDirectoryStream } with the fixed
361374 * {@code {default,exempt }_*.policy} glob against the real {@code java.home }
362- * policy directory). The signal is precise and cannot be spoofed: a student
363- * calling the same JDK file-system APIs directly, even with an identical file
364- * name or glob argument, has no {@code JceSecurity } frame between their own
365- * code and the read, so such an access stays blocked. Because
366- * {@code JceSecurity } never takes a student-influenceable path/glob argument,
367- * this frame check alone precisely identifies the genuine JVM-triggered scan —
368- * no additional location check on the intercepted argument is needed or
369- * possible, since the bare glob argument resolves against the working
370- * directory, not {@code java.home }, when converted to a path.
375+ * policy directory). Matching is deliberately narrow so the signal cannot be
376+ * spoofed:
377+ * <ul >
378+ * <li >the frame's class name must equal {@code javax.crypto.JceSecurity }
379+ * exactly (not merely start with it), so an unrelated {@code JceSecurity* }
380+ * type cannot pass;</li>
381+ * <li >the frame's method name must equal {@code setupJurisdictionPolicies }
382+ * exactly, so a call routed through e.g. {@code JceSecurity.getInstance }
383+ * — which student-controlled provider SPI code can execute beneath, per
384+ * {@code Provider.getService() } — does not satisfy this check merely because
385+ * some {@code JceSecurity } frame happens to be on the stack;</li>
386+ * <li >the frame's declaring class must be loaded by the bootstrap or
387+ * platform class loader (mirroring {@link #requireTrustedRuntimeType(Object)}'s
388+ * trust check), so student code cannot spoof the signal by defining its own
389+ * class literally named {@code javax.crypto.JceSecurity } via a custom class
390+ * loader and calling a same-named method on it.</li>
391+ * </ul>
392+ * A student calling the same JDK file-system APIs directly, even with an
393+ * identical file name or glob argument, satisfies none of the above, so such
394+ * an access stays blocked. Because {@code JceSecurity } never takes a
395+ * student-influenceable path/glob argument, this call-stack-context check
396+ * alone precisely identifies the genuine JVM-triggered scan — no additional
397+ * location check on the intercepted argument is needed or possible, since the
398+ * bare glob argument resolves against the working directory, not
399+ * {@code java.home }, when converted to a path.
400+ * <p >
401+ * Resolving {@link StackWalker.StackFrame#getDeclaringClass()} requires
402+ * {@link StackWalker.Option#RETAIN_CLASS_REFERENCE}; any {@link LinkageError}
403+ * that resolution triggers (e.g. a {@link ClassCircularityError} during class
404+ * loading) is caught and treated as a failure to establish identity, so the
405+ * check fails closed (returns {@code false }, denying the exemption) rather
406+ * than propagating. The outer advice's {@link #enterAdvice()} re-entrancy
407+ * guard is already held while this runs, so any advice re-entered by that
408+ * resolution is a no-op, not unbounded recursion.
371409 *
372- * @return {@code true } if a {@code javax.crypto.JceSecurity } frame is present
373- * on the current stack
410+ * @return {@code true } if a genuine {@code javax.crypto.JceSecurity }
411+ * jurisdiction-policy-scan frame is present on the current stack
374412 */
375413 static boolean isJceCryptoPolicyScanInProgress () {
376- return STACK_WALKER . walk(
377- frames - > frames. map(StackWalker . StackFrame :: getClassName). anyMatch(className - > className. startsWith(" javax.crypto.JceSecurity" )));
414+ try {
415+ return CLASS_RETAINING_STACK_WALKER . walk(frames - > frames
416+ .filter(frame - > " setupJurisdictionPolicies" . equals(frame. getMethodName())
417+ && " javax.crypto.JceSecurity" . equals(frame. getClassName()))
418+ .anyMatch(frame - > {
419+ ClassLoader loader = frame. getDeclaringClass(). getClassLoader();
420+ return loader == null || loader == ClassLoader . getPlatformClassLoader();
421+ }));
422+ } catch (LinkageError error) {
423+ return false ;
424+ }
378425 }
379426
380427 /**
0 commit comments