AspectJ versus Instrumentation
The teacher has two ways of catching a pupil in the act, and they are not equally good.
One covers every place in the room where the forbidden thing could be asked for. The other covers the thing itself. The second catches more, because there is only one of it and there may be a hundred places that ask, and you can only cover the ones you can see. This page is the honest list of what the first one misses.
Ares enforces a security policy on untrusted student code through two interchangeable engines:
- Instrumentation (
api.aop.java.instrumentation): a ByteBuddy Java agent that rewrites the bytecode of the Java Development Kit (JDK) target classes themselves and injects advice into the intercepted method/constructor. - AspectJ (
api.aop.java.aspectj): aspects woven into the compiled student and project classes, withbefore()advice attached tocall(...)pointcuts.
Both engines are intended to mirror the same policy decisions, but they do so
through separate AspectJ and Byte Buddy implementations that read the same
JavaAOPTestCaseSettings fields. For operations covered by both interception
lists, their verdicts agree only while that duplicated logic is kept in sync.
The differences below are not bugs that can be fixed
in code; they are structural consequences of how each engine attaches to the
program. They exist so that policy authors and reviewers understand that
AspectJ mode is strictly weaker than Instrumentation mode, and so that
ASPECTJ-mode policies are written with these limits in mind.
1. AspectJ uses caller-side weaving (call()), not target rewriting
All Ares AspectJ pointcuts are call(...) pointcuts. A call() join point is
woven into the call site, i.e. into the code that makes the call, not into
the method being called. AspectJ can therefore only intercept a forbidden
operation if the call to it is made from code that AspectJ wove
(student code and project code).
Instrumentation rewrites the target JDK class, so it intercepts the operation no matter who calls it.
Consequence: a forbidden operation invoked from any code AspectJ did not weave is not seen by AspectJ but is still caught by Instrumentation.
2. JDK-internal call sites cannot be woven
Ares does not weave the JDK. Calls that originate inside the JDK are
therefore invisible to AspectJ. The most important example is the NIO network
stack: a high-level connect can bottom out in sun.nio.ch.Net.connect* or in
the various sun.nio.ch.*ChannelImpl classes, and the actual OS-level call is
made from inside those JDK classes.
Instrumentation explicitly hooks sun.nio.ch.Net, SocketChannelImpl,
DatagramChannelImpl, the asynchronous channel implementations and
NetMulticastSocket precisely as a backstop for this dispatch. AspectJ has no
equivalent and cannot acquire one, because it cannot weave call sites that live
inside the JDK.
Consequence: raw NIO and multicast network operations are not reliably enforced under ASPECTJ. ASPECTJ-mode network policies should be treated as covering the high-level, directly-called socket APIs only.
3. Indirect, reflective and library-mediated calls escape
Only woven call sites are intercepted, so an operation reached through a layer AspectJ did not weave escapes enforcement:
- A third-party library on the classpath that performs the forbidden operation is not woven, so its internal call to the JDK is not seen.
- A reflective invocation (
Method.invoke) made from inside the JDK reflection machinery is a JDK-internal call site and is not woven. - Any helper that the build does not run through the AspectJ weaver is a blind spot.
Instrumentation is unaffected because it rewrites the target, so the caller being unwoven, reflective or third-party does not matter.
4. Coverage is an enumerated list of call sites, not of targets
Both engines enumerate the classes and methods they care about, but the meaning
differs. For Instrumentation the enumeration selects target types to
rewrite (including subtypes and overrides via hasSuperType /
isOverriddenFrom), so every caller of those targets is covered. For AspectJ
the enumeration selects call signatures to weave at the call site, and the
+ subtype form only helps where the call site itself is woven.
Consequence: keeping the two engines in parity requires deliberate, ongoing effort. New JDK APIs, renamed internal classes, or new dispatch paths can widen the AspectJ gap silently. A cross-engine parity test (run one policy through both engines and assert identical verdicts) is the recommended guard.
5. Net effect on the threat model
Under the assumption that the student controls call sites, timing and object shapes:
- Instrumentation is the fully-trusted enforcement mode: it sees every caller of a rewritten target.
- AspectJ is best-effort for anything that can be reached without a woven call site, and structurally cannot cover JDK-internal dispatch (most importantly raw NIO and multicast networking).
Recommendation: prefer Instrumentation where the platform allows it. Where only AspectJ is available (e.g. environments where attaching a Java agent is not possible), document that raw NIO and reflective/library-mediated forbidden operations are not guaranteed to be blocked, and constrain the policy accordingly.
6. The one inversion: inherited final Object methods on Thread
The general ordering above (Instrumentation strictly stronger) inverts for the
thread-monitor methods Object.notify(), Object.notifyAll() and the
Object.wait(...) overloads when the receiver is a Thread. The architecture
engines, ArchUnit and the T. J. Watson Libraries for Analysis (WALA), list
java.lang.Thread.notify() (WALA and ArchUnit) and
java.lang.Object.wait(long) (ArchUnit) as forbidden thread-manipulation
methods, so a policy that forbids thread manipulation expects these to be blocked.
- Instrumentation cannot enforce them. These methods are
finaland declared byjava.lang.Object;Threadneither declares nor can override them. Byte Buddy rewrites the declaring type, and the thread matcher isnamed/hasSuperType(java.lang.Thread) and isDeclaredBy(that hierarchy), which never includesObject. There is therefore noThread.notify()method to rewrite; the only reachable target isObject.notify()itself, which would instrument every object in the Java Virtual Machine (JVM) and is unacceptable. - AspectJ can enforce them, caller-side.
call(* java.lang.Object+.notify()) && target(java.lang.Thread+)weaves the call site and thetarget(Thread+)runtime guard confines advice execution toThreadreceivers. (The narrowercall(* java.lang.Thread+.notify())does not weave: AspectJ resolves the declaring type tojava.lang.Object.) The advice treats a manipulation as a membership-only check against the thread-creation allow-list: a thread whose class may be created may be notified as well, without consuming the creation quota.
Consequence and current scope: AspectJ enforces notify/notifyAll/wait on
Thread receivers at runtime. It records the effective task class of each admitted
Thread in a weak identity map and revalidates that class against the active policy
on every manipulation. This survives Thread.exit() clearing the task field without
mistaking Thread.currentThread() or another unrelated pre-existing thread for an
allowed receiver, and without retaining permission across policy changes.
Byte Buddy cannot add method-entry advice to these inherited final methods by
transforming Thread. The instrumentation agent instead rewrites application call
sites for all five Object monitor overloads (notify, notifyAll, and the three
wait forms) to wrappers which perform the same receiver-based policy check before
invoking the original operation. The wrappers preserve ordinary Object-monitor
behaviour: the thread policy runs only when the runtime receiver is a Thread.
Instrumentation uses the same weak instance tracking and membership-only
manipulation semantics as AspectJ, without consuming the creation quota.