Skip to content

Commit 1649f72

Browse files
fix(aop): add JCE crypto-policy read exemption to the AspectJ backend
1 parent 48e06bb commit 1649f72

2 files changed

Lines changed: 122 additions & 28 deletions

File tree

src/main/java/de/tum/cit/ase/ares/api/aop/java/aspectj/adviceandpointcut/JavaAspectJAbstractAdviceDefinitions.aj

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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
/**

src/main/java/de/tum/cit/ase/ares/api/aop/java/instrumentation/advice/JavaInstrumentationAdviceAbstractToolbox.java

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ public abstract class JavaInstrumentationAdviceAbstractToolbox {
4141
@Nonnull
4242
private static final StackWalker STACK_WALKER = StackWalker.getInstance();
4343

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

369416
/**

0 commit comments

Comments
 (0)