From 9624bfc3eee6a21465dfebefb669f806363370c7 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Thu, 10 Sep 2026 11:42:53 -0400 Subject: [PATCH] Fix NPE from activating a null span in ContinuableScopeManager A null span passed to activateSpan() (e.g. from TracingSendHandler when the websocket span was concurrently cleared by HandlerContext.reset()) was silently pushed as a scope with a null Context. The corrupted scope only surfaced as an NPE on the *next* activation on that thread, when `top.context.with(span)` dereferenced the null context. The existing `assert span != null` never caught this since assertions are never enabled (-ea) in production. Co-Authored-By: Claude Sonnet 5 --- .../websocket/jsr256/TracingSendHandler.java | 2 +- .../scopemanager/ContinuableScopeManager.java | 7 ++++-- .../scopemanager/ScopeManagerForkedTest.java | 22 +++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/instrumentation/websocket/javax-websocket-1.0/src/main/java/datadog/trace/instrumentation/websocket/jsr256/TracingSendHandler.java b/dd-java-agent/instrumentation/websocket/javax-websocket-1.0/src/main/java/datadog/trace/instrumentation/websocket/jsr256/TracingSendHandler.java index c5a582859a3..62712b15592 100644 --- a/dd-java-agent/instrumentation/websocket/javax-websocket-1.0/src/main/java/datadog/trace/instrumentation/websocket/jsr256/TracingSendHandler.java +++ b/dd-java-agent/instrumentation/websocket/javax-websocket-1.0/src/main/java/datadog/trace/instrumentation/websocket/jsr256/TracingSendHandler.java @@ -21,7 +21,7 @@ public TracingSendHandler(SendHandler delegate, HandlerContext handlerContext) { @Override public void onResult(SendResult sendResult) { final AgentSpan wsSpan = handlerContext.getWebsocketSpan(); - try (final ContextScope ignored = activateSpan(wsSpan)) { + try (final ContextScope ignored = wsSpan != null ? activateSpan(wsSpan) : null) { delegate.onResult(sendResult); } finally { if (sendResult.getException() != null) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/scopemanager/ContinuableScopeManager.java b/dd-trace-core/src/main/java/datadog/trace/core/scopemanager/ContinuableScopeManager.java index 33e6580574e..56d36a180af 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/scopemanager/ContinuableScopeManager.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/scopemanager/ContinuableScopeManager.java @@ -113,6 +113,11 @@ private AgentScope activate( final byte source, final boolean overrideAsyncPropagation, final boolean isAsyncPropagating) { + if (span == null) { + log.debug(SEND_TELEMETRY, "Attempted to activate a null span. Returning NoopScope."); + return INVALID_SCOPE; + } + ScopeStack scopeStack = scopeStack(); final ContinuableScope top = scopeStack.top; @@ -131,8 +136,6 @@ private AgentScope activate( } } - assert span != null; - // Inherit the async propagation from the active scope unless the value is overridden boolean asyncPropagation = overrideAsyncPropagation diff --git a/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerForkedTest.java b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerForkedTest.java index dc683942549..f3c4a101dce 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerForkedTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/scopemanager/ScopeManagerForkedTest.java @@ -36,6 +36,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.NoopScope; import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; import datadog.trace.common.writer.ListWriter; import datadog.trace.core.CoreTracer; @@ -124,6 +125,27 @@ void nonDdspanActivationResultsInAContinuableScope() { assertNull(scopeManager.active()); } + @Test + void activatingNullSpanReturnsNoopScopeAndDoesNotCorruptStack() { + AgentScope nullScope = scopeManager.activateSpan(null); + + assertInstanceOf(NoopScope.class, nullScope); + assertNull(scopeManager.active()); + + nullScope.close(); + + // a subsequent activation on the same thread must not NPE, even though the noop + // activation above never pushed a scope with a null context onto the stack + AgentSpan span = tracer.buildSpan("test", "test").start(); + AgentScope scope = tracer.activateSpan(span); + + assertSame(scope, scopeManager.active()); + assertSame(span, scope.span()); + + scope.close(); + span.finish(); + } + @Test void noScopeIsActiveBeforeActivation() throws Exception { tracer.buildSpan("test", "test").start();