diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/PrivilegedThreadFactory.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/PrivilegedThreadFactory.java new file mode 100644 index 00000000000..a4f53ad92e8 --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/PrivilegedThreadFactory.java @@ -0,0 +1,51 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util.thread; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.function.Supplier; + +/** + * Convenience class to ensure that a new Thread is created + * inside a privileged block. + * + * This prevents the Thread constructor + * from pinning the caller's context classloader. This happens + * when the Thread constructor takes a snapshot of the current + * calling context - which contains ProtectionDomains that may + * reference the context classloader - and remembers it for the + * lifetime of the Thread. + */ +class PrivilegedThreadFactory +{ + /** + * Use a Supplier to make a new thread, calling it within + * a privileged block to prevent classloader pinning. + * + * @param newThreadSupplier a Supplier to create a fresh thread + * @return a new thread, protected from classloader pinning. + */ + static T newThread(Supplier newThreadSupplier) + { + return AccessController.doPrivileged(new PrivilegedAction() + { + @Override + public T run() + { + return newThreadSupplier.get(); + } + }); + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java index 0cfafaa586c..3b2e225a13d 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java @@ -15,6 +15,8 @@ package org.eclipse.jetty.util.thread; import java.io.Closeable; import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -697,11 +699,15 @@ public class QueuedThreadPool extends ContainerLifeCycle implements ThreadFactor @Override public Thread newThread(Runnable runnable) { - Thread thread = new Thread(_threadGroup, runnable); - thread.setDaemon(isDaemon()); - thread.setPriority(getThreadsPriority()); - thread.setName(_name + "-" + thread.getId()); - return thread; + return PrivilegedThreadFactory.newThread(() -> + { + Thread thread = new Thread(_threadGroup, runnable); + thread.setDaemon(isDaemon()); + thread.setPriority(getThreadsPriority()); + thread.setName(_name + "-" + thread.getId()); + thread.setContextClassLoader(getClass().getClassLoader()); + return thread; + }); } protected void removeThread(Thread thread) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java index da322b226d5..1d16dac3f13 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java @@ -13,6 +13,8 @@ package org.eclipse.jetty.util.thread; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -31,7 +33,10 @@ import org.slf4j.LoggerFactory; public class ShutdownThread extends Thread { private static final Logger LOG = LoggerFactory.getLogger(ShutdownThread.class); - private static final ShutdownThread _thread = new ShutdownThread(); + private static final ShutdownThread _thread = PrivilegedThreadFactory.newThread(() -> + { + return new ShutdownThread(); + }); private final AutoLock _lock = new AutoLock(); private boolean _hooked; diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java index fe6abc240f9..f1ed28b2fd5 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java @@ -14,6 +14,8 @@ package org.eclipse.jetty.util.thread; import java.io.Closeable; +import java.net.URL; +import java.net.URLClassLoader; import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -22,6 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -849,6 +852,28 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest assertThat(count(dump, "QueuedThreadPoolTest.lambda$testDump$"), is(1)); } + @Test + public void testContextClassLoader() throws Exception + { + QueuedThreadPool tp = new QueuedThreadPool(); + try (StacklessLogging stackless = new StacklessLogging(QueuedThreadPool.class)) + { + //change the current thread's classloader to something else + Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[] {})); + + //create a new thread + Thread t = tp.newThread(() -> + { + //the executing thread should be still set to the classloader of the QueuedThreadPool, + //not that of the thread that created this thread. + assertThat(Thread.currentThread().getContextClassLoader(), Matchers.equalTo(QueuedThreadPool.class.getClassLoader())); + }); + + //new thread should be set to the classloader of the QueuedThreadPool + assertThat(t.getContextClassLoader(), Matchers.equalTo(QueuedThreadPool.class.getClassLoader())); + } + } + private int count(String s, String p) { int c = 0;