-
Notifications
You must be signed in to change notification settings - Fork 354
Guard rethrowIfBlockingException call against unexpected failures #12240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
209973c
53c967f
c1f1295
2261d3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,24 +49,43 @@ public Size apply(final MethodVisitor mv, final Implementation.Context context) | |
| // | ||
| // Emits the following Java-equivalent code when exitOnFailure is false: | ||
| // | ||
| // BlockingExceptionHandler.rethrowIfBlockingException(t); | ||
| // try { | ||
| // BlockingExceptionHandler.rethrowIfBlockingException(t); | ||
| // InstrumentationErrors.recordError(); | ||
| // org.slf4j.LoggerFactory.getLogger((Class) ExceptionLogger.class) | ||
| // .debug("Failed to handle exception in instrumentation for <type> (" + adviceName + | ||
| // ")", t); | ||
| // } catch (Throwable t2) { | ||
| // if (t2.getClass().getName().equals("datadog.appsec.api.blocking.BlockingException")) | ||
| // { | ||
| // throw t2; | ||
| // } | ||
| // } | ||
| // | ||
| // and the same with .error(...) followed by System.exit(1) when exitOnFailure is true. | ||
| // | ||
| // rethrowIfBlockingException is inside the try/catch (rather than called bare, as | ||
| // before) so that an unexpected failure resolving/invoking it - e.g. a | ||
| // NoClassDefFoundError from a classloader that can't see the appsec module - is | ||
| // swallowed like any other instrumentation error instead of replacing the original | ||
| // exception and escaping into the instrumented method's caller. The catch handler | ||
| // re-throws only when the caught exception really is the BlockingException the call | ||
| // was meant to propagate. It compares by class name rather than using `instanceof` | ||
| // because `instanceof` would itself need to resolve BlockingException via the | ||
| // instrumented class's own classloader - the same NoClassDefFoundError risk this whole | ||
| // try/catch exists to guard against. | ||
| final Label logStart = new Label(); | ||
| final Label logEnd = new Label(); | ||
| final Label eatException = new Label(); | ||
| final Label notBlocking = new Label(); | ||
| final Label handlerExit = new Label(); | ||
|
|
||
| // Frames are only meaningful for class files in version 6 or later. | ||
| final boolean frames = context.getClassFileVersion().isAtLeast(ClassFileVersion.JAVA_V6); | ||
|
|
||
| mv.visitTryCatchBlock(logStart, logEnd, eatException, "java/lang/Throwable"); | ||
| mv.visitLabel(logStart); | ||
|
|
||
| if (appSecEnabled) { | ||
| // Need throwable on top for rethrowIfBlockingException. | ||
| // stack: (top) adviceName, throwable -> top throwable | ||
|
|
@@ -81,8 +100,6 @@ public Size apply(final MethodVisitor mv, final Implementation.Context context) | |
| mv.visitInsn(Opcodes.SWAP); | ||
| } | ||
|
|
||
| mv.visitTryCatchBlock(logStart, logEnd, eatException, "java/lang/Throwable"); | ||
| mv.visitLabel(logStart); | ||
| // record instrumentation error | ||
| if (detailedErrors) { | ||
| // recordError(Throwable) needs throwable on top, then we restore. | ||
|
|
@@ -148,12 +165,42 @@ public Size apply(final MethodVisitor mv, final Implementation.Context context) | |
| mv.visitLabel(logEnd); | ||
| mv.visitJumpInsn(Opcodes.GOTO, handlerExit); | ||
|
|
||
| // if the runtime can't reach our ExceptionHandler or logger, | ||
| // silently eat the exception | ||
| // If the runtime can't reach our ExceptionHandler or logger, or | ||
| // rethrowIfBlockingException itself failed unexpectedly, silently eat the exception - | ||
| // unless it's the BlockingException rethrowIfBlockingException was meant to propagate, | ||
| // in which case let it through. | ||
| mv.visitLabel(eatException); | ||
| if (frames) { | ||
| mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"}); | ||
| } | ||
| if (appSecEnabled) { | ||
| // Compare by class name instead of `instanceof`: `instanceof` would resolve | ||
| // BlockingException via the instrumented class's own classloader, which can throw | ||
| // NoClassDefFoundError right here - uncaught - on a classloader that can't see the | ||
| // appsec module. A name comparison never triggers that resolution. | ||
| mv.visitInsn(Opcodes.DUP); | ||
| mv.visitMethodInsn( | ||
| Opcodes.INVOKEVIRTUAL, | ||
| "java/lang/Object", | ||
| "getClass", | ||
| "()Ljava/lang/Class;", | ||
| false); | ||
| mv.visitMethodInsn( | ||
| Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false); | ||
| mv.visitLdcInsn("datadog.appsec.api.blocking.BlockingException"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
An AppSec or RASP block that uses a BlockingException subclass can fail, so the request can continue. Assertion details
Was this helpful? React 👍 or 👎 |
||
| mv.visitMethodInsn( | ||
| Opcodes.INVOKEVIRTUAL, | ||
| "java/lang/String", | ||
| "equals", | ||
| "(Ljava/lang/Object;)Z", | ||
| false); | ||
| mv.visitJumpInsn(Opcodes.IFEQ, notBlocking); | ||
| mv.visitInsn(Opcodes.ATHROW); | ||
| mv.visitLabel(notBlocking); | ||
| if (frames) { | ||
| mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"}); | ||
| } | ||
| } | ||
| mv.visitInsn(Opcodes.POP); | ||
| // mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Throwable", | ||
| // "printStackTrace", "()V", false); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package datadog.trace.agent.test; | ||
|
|
||
| import datadog.appsec.api.blocking.BlockingException; | ||
| import java.net.URL; | ||
| import java.net.URLClassLoader; | ||
|
|
||
| /** | ||
| * A {@link URLClassLoader} that delegates to the given parent for everything - including {@code | ||
| * datadog.trace.bootstrap.*} and slf4j, which in production are visible from any classloader - | ||
| * except {@link BlockingException} (which in production is only visible if the appsec module is | ||
| * present) and the given target class name, which this loader defines locally so it gets | ||
| * instrumented as if loaded by an isolated (e.g. plugin/OSGi-style) classloader. | ||
| */ | ||
| final class AppSecInvisibleClassLoader extends URLClassLoader { | ||
| private final String isolatedClassName; | ||
|
|
||
| AppSecInvisibleClassLoader(URL[] classpath, ClassLoader parent, String isolatedClassName) { | ||
| super(classpath, parent); | ||
| this.isolatedClassName = isolatedClassName; | ||
| } | ||
|
|
||
| @Override | ||
| protected synchronized Class<?> loadClass(String name, boolean resolve) | ||
| throws ClassNotFoundException { | ||
| Class<?> found = findLoadedClass(name); | ||
| if (found == null) { | ||
| if (name.equals(BlockingException.class.getName())) { | ||
| throw new ClassNotFoundException(name); | ||
| } | ||
| found = name.equals(isolatedClassName) ? findClass(name) : super.loadClass(name, resolve); | ||
| } | ||
| if (resolve) { | ||
| resolveClass(found); | ||
| } | ||
| return found; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When advice throws a subclass of the public, non-final
BlockingException,BlockingExceptionHandler.rethrowIfBlockingExceptionrecognizes it viainstanceofand throws the same object, but this exact runtime-name comparison returns false and the subsequentPOPswallows it. The instrumented operation then continues instead of enforcing the AppSec block; preserve the helper's subtype semantics without resolvingBlockingExceptionthrough the instrumented classloader, for example by inspecting superclass names.Useful? React with 👍 / 👎.