{"record":{"id":"f380e76b95076569","repo":"oracle/graal","slug":"cannot-signal-lock-that-is-not-held","errorCode":null,"errorMessage":"Cannot signal lock that is not held","messagePattern":"Cannot signal lock that is not held","errorType":"exception","errorClass":"IllegalMonitorStateException","httpStatus":null,"severity":"error","filePath":"espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/blocking/EspressoLock.java","lineNumber":369,"sourceCode":"        enterWaitInterruptible(interruptible);\n        return interruptible.getResult();\n    }\n\n    @Override\n    public boolean awaitUntil(Date deadline) throws GuestInterruptedException {\n        if (!isHeldByCurrentThread()) {\n            throw new IllegalMonitorStateException();\n        }\n        WaitUntilInterruptible interruptible = new WaitUntilInterruptible(deadline);\n        enterWaitInterruptible(interruptible);\n        return interruptible.getResult();\n    }\n\n    @Override\n    @TruffleBoundary\n    public void signal() {\n        if (!isHeldByCurrentThread()) {\n            throw new IllegalMonitorStateException(\"Cannot signal lock that is not held\");\n        }\n        ensureWaitLockInitialized();\n        waitLock.lock();\n        try {\n            signals = Math.min(signals + 1, waiters);\n            waitCondition.signal();\n        } finally {\n            waitLock.unlock();\n        }\n    }\n\n    @Override\n    @TruffleBoundary\n    public void signalAll() {\n        if (!isHeldByCurrentThread()) {\n            throw new IllegalMonitorStateException(\"Cannot signal lock that is not held\");\n        }\n        ensureWaitLockInitialized();","sourceCodeStart":351,"sourceCodeEnd":387,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/blocking/EspressoLock.java#L351-L387","documentation":"EspressoLock.signal() (the guest-side Condition.signal / Object.notify path) verifies the lock is held by the current thread before signalling waiters; if not, it throws IllegalMonitorStateException with the message 'Cannot signal lock that is not held'.","triggerScenarios":"Calling signal()/notify() without holding the corresponding EspressoLock - e.g. notify() outside synchronized, or signal() after lock release.","commonSituations":"Producer/consumer code where notify is in a different method than the synchronized block; refactoring that split lock scope; callback invoked from another thread that never acquired the lock.","solutions":["Acquire the lock (synchronized block or lock.lock()) before signal/notify.","Keep notify/signal inside the same critical section that mutates the waited-upon state.","Use @TruffleBoundary-aware host code paths rather than ad-hoc signalling from arbitrary threads."],"exampleFix":"// before\nsharedState = value;\nobj.notify();\n\n// after\n synchronized (obj) {\n     sharedState = value;\n     obj.notify();\n }","handlingStrategy":"validation","validationCode":"if (lock.isHeldByCurrentThread()) {\n    lock.signal();\n}","typeGuard":null,"tryCatchPattern":"synchronized (obj) {\n    try { obj.notify(); }\n    catch (IllegalMonitorStateException e) { throw e; /* scope bug */ }\n}","preventionTips":["Signal only from within the critical section that changed the state.","Check isHeldByCurrentThread() in debug builds before signal paths."],"tags":["espresso","graalvm","concurrency","locks","notify"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}