Skip to content

Commit 679e5df

Browse files
authored
Make ContextHandler catch and ignore exceptions thrown by Thread.setContextClassLoader() (#13677)
Ignore SecurityException thrown by setContextClassLoader() so JDK25's InnocuousThread can call into a context's request and response. Signed-off-by: Ludovic Orban <lorban@bitronix.be>
1 parent e4a0673 commit 679e5df

File tree

2 files changed

+183
-19
lines changed

2 files changed

+183
-19
lines changed

jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
public class ContextHandler extends Handler.Wrapper implements Attributes, AliasCheck, Deployable
7878
{
7979
private static final Logger LOG = LoggerFactory.getLogger(ContextHandler.class);
80-
private static final ThreadLocal<Context> __context = new ThreadLocal<>();
80+
private static final ThreadLocal<Context> CURRENT_CONTEXT = new ThreadLocal<>();
8181

8282
public static final String MANAGED_ATTRIBUTES = "org.eclipse.jetty.server.context.ManagedAttributes";
8383

@@ -95,7 +95,7 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
9595
*/
9696
public static Context getCurrentContext()
9797
{
98-
return __context.get();
98+
return CURRENT_CONTEXT.get();
9999
}
100100

101101
/**
@@ -106,7 +106,7 @@ public static Context getCurrentContext()
106106
*/
107107
public static Context getCurrentContext(Server server)
108108
{
109-
Context context = __context.get();
109+
Context context = CURRENT_CONTEXT.get();
110110
return context == null ? (server == null ? null : server.getContext()) : context;
111111
}
112112

@@ -513,6 +513,11 @@ public ClassLoader getClassLoader()
513513
return _classLoader;
514514
}
515515

516+
/**
517+
* The {@link ClassLoader} to set as the {@link Thread#setContextClassLoader(ClassLoader) thread's context classloader}
518+
* when the thread executing this handler enters the scope of this context.
519+
* @param contextLoader the {@link ClassLoader} or {@code null} to avoid changing the thread's context classloader.
520+
*/
516521
public void setClassLoader(ClassLoader contextLoader)
517522
{
518523
if (isStarted())
@@ -655,8 +660,8 @@ public boolean addEventListener(EventListener listener)
655660
if (listener instanceof ContextScopeListener contextScopeListener)
656661
{
657662
_contextListeners.add(contextScopeListener);
658-
if (__context.get() != null)
659-
contextScopeListener.enterScope(__context.get(), null);
663+
if (CURRENT_CONTEXT.get() != null)
664+
contextScopeListener.enterScope(CURRENT_CONTEXT.get(), null);
660665
}
661666
return true;
662667
}
@@ -671,8 +676,8 @@ public boolean removeEventListener(EventListener listener)
671676
if (listener instanceof ContextScopeListener contextScopeListener)
672677
{
673678
_contextListeners.remove(contextScopeListener);
674-
if (__context.get() != null)
675-
contextScopeListener.exitScope(__context.get(), null);
679+
if (CURRENT_CONTEXT.get() != null)
680+
contextScopeListener.exitScope(CURRENT_CONTEXT.get(), null);
676681
}
677682
return true;
678683
}
@@ -728,12 +733,31 @@ protected void initializeDefaultsComplete()
728733
{
729734
}
730735

736+
/**
737+
* <p>Enters the scope of the {@link Context}.</p>
738+
* <p> Attempts to set the {@link Thread#setContextClassLoader(ClassLoader) thread's context classloader}
739+
* to the {@link #getClassLoader() configured one} if a non-null one was set.</p>
740+
*
741+
* @param contextRequest the context's {@link Request}
742+
* @return the previous {@link Thread#getContextClassLoader() thread's context classloader}
743+
*/
731744
protected ClassLoader enterScope(Request contextRequest)
732745
{
746+
CURRENT_CONTEXT.set(_context);
747+
733748
ClassLoader lastLoader = Thread.currentThread().getContextClassLoader();
734-
__context.set(_context);
735-
if (_classLoader != null)
736-
Thread.currentThread().setContextClassLoader(_classLoader);
749+
if (_classLoader != null && _classLoader != lastLoader)
750+
{
751+
try
752+
{
753+
Thread.currentThread().setContextClassLoader(_classLoader);
754+
}
755+
catch (Throwable x)
756+
{
757+
if (LOG.isDebugEnabled())
758+
LOG.debug("error setting a context classloader on thread {}", Thread.currentThread(), x);
759+
}
760+
}
737761
notifyEnterScope(contextRequest);
738762
return lastLoader;
739763
}
@@ -756,11 +780,20 @@ protected void notifyEnterScope(Request request)
756780
}
757781
}
758782

759-
protected void exitScope(Request request, Context lastContext, ClassLoader lastLoader)
783+
/**
784+
* <p>Exits the scope of the {@link Context}.</p>
785+
*
786+
* @param contextRequest the context's {@link Request}
787+
* @param lastContext the previous context to restore as the current one.
788+
* @param lastLoader the previous {@link Thread#getContextClassLoader() thread's context classloader} to restore
789+
*/
790+
protected void exitScope(Request contextRequest, Context lastContext, ClassLoader lastLoader)
760791
{
761-
notifyExitScope(request);
762-
__context.set(lastContext);
763-
Thread.currentThread().setContextClassLoader(lastLoader);
792+
CURRENT_CONTEXT.set(lastContext);
793+
794+
notifyExitScope(contextRequest);
795+
if (Thread.currentThread().getContextClassLoader() != lastLoader)
796+
Thread.currentThread().setContextClassLoader(lastLoader);
764797
}
765798

766799
/**
@@ -1588,7 +1621,7 @@ public List<String> getVirtualHosts()
15881621

15891622
public void call(Invocable.Callable callable, Request request) throws Exception
15901623
{
1591-
Context lastContext = __context.get();
1624+
Context lastContext = CURRENT_CONTEXT.get();
15921625
if (lastContext == this)
15931626
callable.call();
15941627
else
@@ -1607,7 +1640,7 @@ public void call(Invocable.Callable callable, Request request) throws Exception
16071640

16081641
public <T> boolean test(Predicate<T> predicate, T t, Request request)
16091642
{
1610-
Context lastContext = __context.get();
1643+
Context lastContext = CURRENT_CONTEXT.get();
16111644
if (lastContext == this)
16121645
return predicate.test(t);
16131646

@@ -1624,7 +1657,7 @@ public <T> boolean test(Predicate<T> predicate, T t, Request request)
16241657

16251658
public void accept(Consumer<Throwable> consumer, Throwable t, Request request)
16261659
{
1627-
Context lastContext = __context.get();
1660+
Context lastContext = CURRENT_CONTEXT.get();
16281661
if (lastContext == this)
16291662
consumer.accept(t);
16301663
else
@@ -1649,7 +1682,7 @@ public void run(Runnable runnable)
16491682

16501683
public void run(Runnable runnable, Request request)
16511684
{
1652-
Context lastContext = __context.get();
1685+
Context lastContext = CURRENT_CONTEXT.get();
16531686
if (lastContext == this)
16541687
runnable.run();
16551688
else

jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
import java.net.URL;
2020
import java.net.URLClassLoader;
2121
import java.nio.ByteBuffer;
22+
import java.nio.channels.AsynchronousFileChannel;
23+
import java.nio.channels.CompletionHandler;
24+
import java.nio.file.Path;
25+
import java.nio.file.StandardOpenOption;
2226
import java.util.ArrayList;
2327
import java.util.Arrays;
2428
import java.util.List;
@@ -66,6 +70,7 @@
6670
import org.eclipse.jetty.server.internal.HttpChannelState;
6771
import org.eclipse.jetty.toolchain.test.FS;
6872
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
73+
import org.eclipse.jetty.util.Blocker;
6974
import org.eclipse.jetty.util.BufferUtil;
7075
import org.eclipse.jetty.util.Callback;
7176
import org.eclipse.jetty.util.IO;
@@ -107,7 +112,7 @@ public class ContextHandlerTest
107112
AtomicBoolean _inContext;
108113

109114
@BeforeEach
110-
public void beforeEach() throws Exception
115+
public void beforeEach()
111116
{
112117
_server = new Server();
113118
_loader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
@@ -170,6 +175,132 @@ public void testMiss() throws Exception
170175
assertThat(stream.getResponse().getStatus(), equalTo(404));
171176
}
172177

178+
@ParameterizedTest
179+
@ValueSource(booleans = {true, false})
180+
public void testThreadRefusingContextClassLoader(boolean withClassLoader) throws Exception
181+
{
182+
LocalConnector connector = new LocalConnector(_server);
183+
_server.addConnector(connector);
184+
_contextHandler.setClassLoader(withClassLoader ? _loader : null);
185+
_contextHandler.setHandler(new Handler.Abstract()
186+
{
187+
@Override
188+
public boolean handle(Request request, Response response, Callback callback) throws Exception
189+
{
190+
CountDownLatch latch = new CountDownLatch(1);
191+
var t = new Thread()
192+
{
193+
@Override
194+
public void run()
195+
{
196+
try (Blocker.Callback cb = Blocker.callback())
197+
{
198+
// When a classloader is configured, Response.write() tries to set it as the context classloader.
199+
response.write(true, ByteBuffer.allocate(32), cb);
200+
cb.block();
201+
}
202+
catch (IOException e)
203+
{
204+
throw new RuntimeException(e);
205+
}
206+
finally
207+
{
208+
latch.countDown();
209+
}
210+
}
211+
212+
@Override
213+
public void setContextClassLoader(ClassLoader cl)
214+
{
215+
throw new ArithmeticException();
216+
}
217+
};
218+
t.start();
219+
assertTrue(latch.await(5, TimeUnit.SECONDS));
220+
221+
callback.succeeded();
222+
return true;
223+
}
224+
});
225+
_server.start();
226+
227+
String rawRequest = """
228+
GET /ctx/ HTTP/1.1
229+
Host: local
230+
Connection: close
231+
232+
""";
233+
234+
String rawResponse = connector.getResponse(rawRequest);
235+
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
236+
assertThat(response.getStatus(), is(200));
237+
}
238+
239+
@ParameterizedTest
240+
@ValueSource(booleans = {true, false})
241+
public void testAIOAndClassLoader(boolean withClassLoader) throws Exception
242+
{
243+
LocalConnector connector = new LocalConnector(_server);
244+
_server.addConnector(connector);
245+
_contextHandler.setClassLoader(withClassLoader ? _loader : null);
246+
_contextHandler.setHandler(new Handler.Abstract()
247+
{
248+
@Override
249+
public boolean handle(Request request, Response response, Callback callback) throws Exception
250+
{
251+
Path tempFile = request.getContext().getTempDirectory().toPath().resolve("file.bin");
252+
try (AsynchronousFileChannel afc = AsynchronousFileChannel.open(tempFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE))
253+
{
254+
CountDownLatch latch = new CountDownLatch(1);
255+
// In JDK 25, The CompletionHandler gets called by an InnocuousThread that throws when trying to set the context classloader.
256+
afc.write(ByteBuffer.allocate(32), 0L, null, new CompletionHandler<>()
257+
{
258+
@Override
259+
public void completed(Integer result, Object attachment)
260+
{
261+
try (Blocker.Callback cb = Blocker.callback())
262+
{
263+
// When a classloader is configured, Response.write() tries to set it as the context classloader.
264+
response.write(true, ByteBuffer.allocate(32), cb);
265+
cb.block();
266+
}
267+
catch (IOException e)
268+
{
269+
throw new RuntimeException(e);
270+
}
271+
finally
272+
{
273+
latch.countDown();
274+
}
275+
}
276+
277+
@Override
278+
public void failed(Throwable exc, Object attachment)
279+
{
280+
latch.countDown();
281+
}
282+
});
283+
assertTrue(latch.await(5, TimeUnit.SECONDS));
284+
}
285+
286+
callback.succeeded();
287+
return true;
288+
}
289+
});
290+
_server.start();
291+
292+
String rawRequest = """
293+
GET /ctx/ HTTP/1.1
294+
Host: local
295+
Connection: close
296+
297+
""";
298+
299+
String rawResponse = connector.getResponse(rawRequest);
300+
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
301+
assertThat(response.getStatus(), is(200));
302+
}
303+
173304
@Test
174305
public void testSimpleGET() throws Exception
175306
{

0 commit comments

Comments
 (0)