Skip to content

Commit 12bdf4d

Browse files
christianhaeublsynecdoche
authored andcommitted
Verify that recurring callbacks are allocation free.
(cherry picked from commit 291172a)
1 parent 2ea2f81 commit 12bdf4d

File tree

4 files changed

+94
-98
lines changed

4 files changed

+94
-98
lines changed

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/SubstrateJVM.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
*/
2525
package com.oracle.svm.core.jfr;
2626

27-
import java.lang.reflect.Field;
2827
import java.util.List;
2928

3029
import org.graalvm.compiler.api.replacements.Fold;
@@ -187,16 +186,6 @@ public static JfrLogging getJfrLogging() {
187186
return get().jfrLogging;
188187
}
189188

190-
public static Object getHandler(Class<? extends jdk.internal.event.Event> eventClass) {
191-
try {
192-
Field f = eventClass.getDeclaredField("eventHandler");
193-
f.setAccessible(true);
194-
return f.get(null);
195-
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
196-
throw new InternalError("Could not access event handler");
197-
}
198-
}
199-
200189
@Uninterruptible(reason = "Prevent races with VM operations that start/stop recording.", callerMustBe = true)
201190
protected boolean isRecording() {
202191
return recording;

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Safepoint.java

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,10 @@
6565
import com.oracle.svm.core.nodes.SafepointCheckNode;
6666
import com.oracle.svm.core.option.HostedOptionKey;
6767
import com.oracle.svm.core.option.RuntimeOptionKey;
68-
import com.oracle.svm.core.snippets.KnownIntrinsics;
6968
import com.oracle.svm.core.snippets.SnippetRuntime;
7069
import com.oracle.svm.core.snippets.SnippetRuntime.SubstrateForeignCallDescriptor;
7170
import com.oracle.svm.core.snippets.SubstrateForeignCallTarget;
7271
import com.oracle.svm.core.stack.JavaFrameAnchors;
73-
import com.oracle.svm.core.thread.VMThreads.ActionOnExitSafepointSupport;
7472
import com.oracle.svm.core.thread.VMThreads.ActionOnTransitionToJavaSupport;
7573
import com.oracle.svm.core.thread.VMThreads.SafepointBehavior;
7674
import com.oracle.svm.core.thread.VMThreads.StatusSupport;
@@ -180,16 +178,23 @@ private static long getSafepointPromptnessFailureNanos() {
180178
return Options.SafepointPromptnessFailureNanos.getValue().longValue();
181179
}
182180

183-
/**
184-
* Used to wrap exceptions that are explicitly thrown by recurring callbacks.
185-
*/
186-
static class SafepointException extends RuntimeException {
187-
private static final long serialVersionUID = 1L;
188-
189-
final Throwable inner;
190-
191-
SafepointException(Throwable inner) {
192-
this.inner = inner;
181+
@Uninterruptible(reason = "Must not contain safepoint checks.")
182+
private static void slowPathSafepointCheck(int newStatus, boolean callerHasJavaFrameAnchor, boolean popFrameAnchor) throws Throwable {
183+
try {
184+
slowPathSafepointCheck0(newStatus, callerHasJavaFrameAnchor, popFrameAnchor);
185+
} catch (ThreadingSupportImpl.SafepointException e) {
186+
/* This exception is intended to be thrown from safepoint checks, at one's own risk */
187+
throw ThreadingSupportImpl.RecurringCallbackTimer.getAndClearPendingException();
188+
} catch (Throwable ex) {
189+
/*
190+
* The foreign call from snippets to this method does not have an exception edge. So we
191+
* could miss an exception handler if we unwind an exception from this method.
192+
*
193+
* Any exception coming out of a safepoint would be surprising to users. There is a good
194+
* reason why Thread.stop() has been deprecated a long time ago (we do not support it on
195+
* Substrate VM).
196+
*/
197+
VMError.shouldNotReachHere(ex);
193198
}
194199
}
195200

@@ -202,7 +207,7 @@ static class SafepointException extends RuntimeException {
202207
* </ul>
203208
**/
204209
@Uninterruptible(reason = "Must not contain safepoint checks.")
205-
private static void slowPathSafepointCheck(int newStatus, boolean callerHasJavaFrameAnchor, boolean popFrameAnchor) {
210+
private static void slowPathSafepointCheck0(int newStatus, boolean callerHasJavaFrameAnchor, boolean popFrameAnchor) {
206211
final IsolateThread myself = CurrentIsolate.getCurrentThread();
207212

208213
SafepointListenerSupport.singleton().beforeSlowpathSafepointCheck();
@@ -412,44 +417,9 @@ public static void slowPathSafepointCheck() throws Throwable {
412417
Safepoint.setSafepointRequested(THREAD_REQUEST_RESET);
413418
return;
414419
}
415-
VMError.guarantee(StatusSupport.isStatusJava(), "Attempting to do a safepoint check when not in Java mode");
416-
417-
try {
418-
/*
419-
* Block on mutex held by thread that requested safepoint, i.e., transition to native
420-
* code.
421-
*/
422-
slowPathSafepointCheck(StatusSupport.STATUS_IN_JAVA, false, false);
423-
424-
} catch (SafepointException se) {
425-
/* This exception is intended to be thrown from safepoint checks, at one's own risk */
426-
throw se.inner;
427-
428-
} catch (Throwable ex) {
429-
/*
430-
* The foreign call from snippets to this method does not have an exception edge. So we
431-
* could miss an exception handler if we unwind an exception from this method.
432-
*
433-
* Any exception coming out of a safepoint would be surprising to users. There is a good
434-
* reason why Thread.stop() has been deprecated a long time ago (we do not support it on
435-
* Substrate VM).
436-
*/
437-
VMError.shouldNotReachHere(ex);
438-
}
439420

440-
exitSlowPathCheck();
441-
}
442-
443-
@Uninterruptible(reason = "Must not contain safepoint checks")
444-
private static void exitSlowPathCheck() {
445-
if (ActionOnExitSafepointSupport.isActionPending()) {
446-
if (LoomSupport.isEnabled() && ActionOnExitSafepointSupport.isSwitchStackPending()) {
447-
ActionOnExitSafepointSupport.clearActions();
448-
KnownIntrinsics.farReturn(0, ActionOnExitSafepointSupport.getSwitchStackSP(), ActionOnExitSafepointSupport.getSwitchStackIP(), false);
449-
} else {
450-
assert false : "Unexpected action pending.";
451-
}
452-
}
421+
VMError.guarantee(StatusSupport.isStatusJava(), "Attempting to do a safepoint check when not in Java mode");
422+
slowPathSafepointCheck(StatusSupport.STATUS_IN_JAVA, false, false);
453423
}
454424

455425
/**
@@ -544,7 +514,7 @@ public static void transitionVMToNative() {
544514
*/
545515
@SubstrateForeignCallTarget(stubCallingConvention = false)
546516
@Uninterruptible(reason = "Must not contain safepoint checks")
547-
private static void enterSlowPathTransitionFromNativeToNewStatus(int newStatus, boolean popFrameAnchor) {
517+
private static void enterSlowPathTransitionFromNativeToNewStatus(int newStatus, boolean popFrameAnchor) throws Throwable {
548518
VMError.guarantee(StatusSupport.isStatusNativeOrSafepoint(), "Must either be at a safepoint or in native mode");
549519
VMError.guarantee(!SafepointBehavior.ignoresSafepoints(),
550520
"The safepoint handling doesn't change the status of threads that ignore safepoints. So, the fast path transition must succeed and this slow path must not be called");

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadingSupportImpl.java

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
package com.oracle.svm.core.thread;
2626

2727
import static com.oracle.svm.core.SubstrateOptions.MultiThreaded;
28+
import static com.oracle.svm.core.heap.RestrictHeapAccess.Access.NO_ALLOCATION;
2829
import static com.oracle.svm.core.thread.ThreadingSupportImpl.Options.SupportRecurringCallback;
2930

31+
import java.io.Serial;
3032
import java.util.concurrent.TimeUnit;
3133

3234
import org.graalvm.compiler.api.replacements.Fold;
@@ -42,7 +44,6 @@
4244
import com.oracle.svm.core.heap.RestrictHeapAccess;
4345
import com.oracle.svm.core.option.HostedOptionKey;
4446
import com.oracle.svm.core.option.SubstrateOptionsParser;
45-
import com.oracle.svm.core.thread.Safepoint.SafepointException;
4647
import com.oracle.svm.core.thread.VMThreads.ActionOnTransitionToJavaSupport;
4748
import com.oracle.svm.core.thread.VMThreads.StatusSupport;
4849
import com.oracle.svm.core.threadlocal.FastThreadLocalFactory;
@@ -69,12 +70,8 @@ public static class Options {
6970
* adapt to a changing frequency of safepoint checks in the code that the thread executes.
7071
*/
7172
public static class RecurringCallbackTimer {
72-
private static final RecurringCallbackAccess CALLBACK_ACCESS = new RecurringCallbackAccess() {
73-
@Override
74-
public void throwException(Throwable t) {
75-
throw new SafepointException(t);
76-
}
77-
};
73+
private static final FastThreadLocalObject<Throwable> EXCEPTION_TL = FastThreadLocalFactory.createObject(Throwable.class, "RecurringCallbackTimer.exception");
74+
private static final RecurringCallbackAccess CALLBACK_ACCESS = new RecurringCallbackAccessImpl();
7875

7976
/**
8077
* Weight of the newest sample in {@link #ewmaChecksPerNano}. Older samples have a total
@@ -107,6 +104,14 @@ public void throwException(Throwable t) {
107104
this.requestedChecks = INITIAL_CHECKS;
108105
}
109106

107+
@Uninterruptible(reason = "Prevent recurring callback execution.", callerMustBe = true)
108+
public static Throwable getAndClearPendingException() {
109+
Throwable t = EXCEPTION_TL.get();
110+
VMError.guarantee(t != null, "There must be a recurring callback exception pending.");
111+
EXCEPTION_TL.set(null);
112+
return t;
113+
}
114+
110115
@Uninterruptible(reason = "Must not contain safepoint checks.")
111116
void evaluate() {
112117
updateStatistics();
@@ -208,24 +213,42 @@ private boolean isCallbackDisabled() {
208213
}
209214

210215
/**
211-
* Separate method to invoke {@link #callback} so that {@link #evaluate()} can be strictly
212-
* {@link Uninterruptible} and allocation-free.
216+
* Recurring callbacks may be executed in any method that contains a safepoint check. This
217+
* includes methods that need to be allocation free. Therefore, recurring callbacks must not
218+
* allocate any Java heap memory.
213219
*/
214220
@Uninterruptible(reason = "Required by caller, but does not apply to callee.", calleeMustBe = false)
215-
@RestrictHeapAccess(reason = "Callee may allocate", access = RestrictHeapAccess.Access.UNRESTRICTED)
221+
@RestrictHeapAccess(reason = "Recurring callbacks must not allocate.", access = NO_ALLOCATION)
216222
private void invokeCallback() {
217223
try {
218224
callback.run(CALLBACK_ACCESS);
219-
} catch (SafepointException se) {
220-
throw se;
225+
} catch (SafepointException e) {
226+
throw e;
221227
} catch (Throwable t) {
222228
/*
223-
* Recurring callbacks are specified to ignore all exceptions. We cannot even log
224-
* the exception because that could lead to a StackOverflowError (especially when
225-
* the recurring callback failed with a StackOverflowError).
229+
* Recurring callbacks are specified to ignore exceptions (except if the exception
230+
* is thrown via RecurringCallbackAccess.throwException(), which is handled above).
231+
* We cannot even log the exception because that could lead to a StackOverflowError
232+
* (especially when the recurring callback failed with a StackOverflowError).
226233
*/
227234
}
228235
}
236+
237+
/**
238+
* We need to distinguish between arbitrary exceptions (must be swallowed) and exceptions
239+
* that are thrown via {@link RecurringCallbackAccess#throwException} (must be forwarded to
240+
* the application). When a recurring callback uses
241+
* {@link RecurringCallbackAccess#throwException}, we store the exception in a thread local
242+
* (to avoid allocations) and throw a pre-allocated marker exception instead. We catch the
243+
* marker exception internally, accesses the thread local, and rethrow that exception.
244+
*/
245+
private static final class RecurringCallbackAccessImpl implements RecurringCallbackAccess {
246+
@Override
247+
public void throwException(Throwable t) {
248+
EXCEPTION_TL.set(t);
249+
throw SafepointException.SINGLETON;
250+
}
251+
}
229252
}
230253

231254
private static final FastThreadLocalObject<RecurringCallbackTimer> activeTimer = FastThreadLocalFactory.createObject(RecurringCallbackTimer.class, "ThreadingSupportImpl.activeTimer");
@@ -332,9 +355,9 @@ static boolean needsNativeToJavaSlowpath() {
332355
}
333356

334357
/**
335-
* Recurring callbacks execute arbitrary code and can throw {@link SafepointException}s. In some
336-
* code parts (e.g., when executing VM operations), we can't deal with arbitrary code execution
337-
* and therefore need to pause the execution of recurring callbacks.
358+
* Recurring callbacks execute arbitrary code and may throw exceptions. In some code parts
359+
* (e.g., when executing VM operations), we can't deal with arbitrary code execution and
360+
* therefore need to pause the execution of recurring callbacks.
338361
*/
339362
@Uninterruptible(reason = "Must not contain safepoint checks.")
340363
public static void pauseRecurringCallback(@SuppressWarnings("unused") String reason) {
@@ -370,11 +393,17 @@ public static void resumeRecurringCallbackAtNextSafepoint() {
370393
*/
371394
public static void resumeRecurringCallback() {
372395
if (resumeCallbackExecution()) {
373-
try {
374-
onSafepointCheckSlowpath();
375-
} catch (SafepointException e) {
376-
throwUnchecked(e.inner); // needed: callers cannot declare `throws Throwable`
377-
}
396+
maybeExecuteRecurringCallback();
397+
}
398+
}
399+
400+
@Uninterruptible(reason = "Prevent safepoints.")
401+
private static void maybeExecuteRecurringCallback() {
402+
try {
403+
onSafepointCheckSlowpath();
404+
} catch (SafepointException e) {
405+
/* Callers cannot declare `throws Throwable`. */
406+
throwUnchecked(RecurringCallbackTimer.getAndClearPendingException());
378407
}
379408
}
380409

@@ -407,7 +436,18 @@ public static boolean isRecurringCallbackSupported() {
407436
}
408437

409438
@SuppressWarnings("unchecked")
439+
@Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
410440
private static <T extends Throwable> void throwUnchecked(Throwable exception) throws T {
411441
throw (T) exception; // T is inferred as RuntimeException, but doesn't have to be
412442
}
443+
444+
static final class SafepointException extends RuntimeException {
445+
public static final SafepointException SINGLETON = new SafepointException();
446+
447+
@Serial //
448+
private static final long serialVersionUID = 1L;
449+
450+
private SafepointException() {
451+
}
452+
}
413453
}

substratevm/src/org.graalvm.polyglot.nativeapi/src/org/graalvm/polyglot/nativeapi/PolyglotNativeAPI.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2413,20 +2413,17 @@ private static PolyglotStatus doRegisterRecurringCallback0(long intervalNanos, P
24132413
PolyglotCallbackInfoInternal info = new PolyglotCallbackInfoInternal(new ObjectHandle[0], data);
24142414
var infoHandle = (PolyglotCallbackInfo) objectHandles.create(info);
24152415
var handles = getHandles();
2416-
RecurringCallback recurringCallback = new RecurringCallback() {
2417-
@Override
2418-
public void run(Threading.RecurringCallbackAccess access) {
2419-
handles.freezeHandleCreation();
2420-
try {
2421-
callback.invoke((PolyglotIsolateThread) CurrentIsolate.getCurrentThread(), infoHandle);
2422-
CallbackException ce = threadLocals.get().callbackException;
2423-
if (ce != null) {
2424-
access.throwException(ce);
2425-
}
2426-
} finally {
2427-
threadLocals.get().callbackException = null;
2428-
handles.unfreezeHandleCreation();
2416+
RecurringCallback recurringCallback = access -> {
2417+
handles.freezeHandleCreation();
2418+
try {
2419+
callback.invoke((PolyglotIsolateThread) CurrentIsolate.getCurrentThread(), infoHandle);
2420+
CallbackException ce = threadLocals.get().callbackException;
2421+
if (ce != null) {
2422+
access.throwException(ce);
24292423
}
2424+
} finally {
2425+
threadLocals.get().callbackException = null;
2426+
handles.unfreezeHandleCreation();
24302427
}
24312428
};
24322429
try {

0 commit comments

Comments
 (0)