Exclude Java agent jars from class path of launching class loader

ExecutableArchiveLauncher creates a ClassLoader that is used by the
Launcher to load an application’s classes. During the creation of this
ClassLoader URLs from another ClassLoader are copied over. This was
resulting in Java agents that are added to the system class loader
via the -javaagent launch option being available on both the system
class loader and the created class loader. Java agents are intended to
always be loaded by the system class loader. Making them available on
another class loader breaks this model.

This commit updates ExecutableArchiveLauncher so that it skips the URLs
of any Java agents (found by examining the JVM’s input arguments) when
copying URLs over to the new classloader, thereby ensuring that Java
agents are only ever loaded by the system class loader.

Fixes #863
pull/931/head
Andy Wilkinson 11 years ago
parent 146a337b53
commit ee08667e81

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2013 the original author or authors. * Copyright 2012-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,20 +30,28 @@ import org.springframework.boot.loader.archive.Archive.EntryFilter;
/** /**
* Base class for executable archive {@link Launcher}s. * Base class for executable archive {@link Launcher}s.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
*/ */
public abstract class ExecutableArchiveLauncher extends Launcher { public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive; private final Archive archive;
private final JavaAgentDetector javaAgentDetector;
public ExecutableArchiveLauncher() { public ExecutableArchiveLauncher() {
this(new InputArgumentsJavaAgentDetector());
}
public ExecutableArchiveLauncher(JavaAgentDetector javaAgentDetector) {
try { try {
this.archive = createArchive(); this.archive = createArchive();
} }
catch (Exception ex) { catch (Exception ex) {
throw new IllegalStateException(ex); throw new IllegalStateException(ex);
} }
this.javaAgentDetector = javaAgentDetector;
} }
protected final Archive getArchive() { protected final Archive getArchive() {
@ -74,7 +82,9 @@ public abstract class ExecutableArchiveLauncher extends Launcher {
ClassLoader loader = getDefaultClassLoader(); ClassLoader loader = getDefaultClassLoader();
if (loader instanceof URLClassLoader) { if (loader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) loader).getURLs()) { for (URL url : ((URLClassLoader) loader).getURLs()) {
copy.add(url); if (!this.javaAgentDetector.isJavaAgentJar(url)) {
copy.add(url);
}
} }
} }
for (URL url : urls) { for (URL url : urls) {

@ -0,0 +1,101 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A {@link JavaAgentDetector} that detects jars supplied via the {@code -javaagent} JVM
* input argument.
*
* @author Andy Wilkinson
* @since 1.1.0
*/
public class InputArgumentsJavaAgentDetector implements JavaAgentDetector {
private static final String JAVA_AGENT_PREFIX = "-javaagent:";
private final Set<URL> javaAgentJars;
public InputArgumentsJavaAgentDetector() {
this(getInputArguments());
}
InputArgumentsJavaAgentDetector(List<String> inputArguments) {
this.javaAgentJars = getJavaAgentJars(inputArguments);
}
private static List<String> getInputArguments() {
try {
return AccessController.doPrivileged(new PrivilegedAction<List<String>>() {
@Override
public List<String> run() {
return ManagementFactory.getRuntimeMXBean().getInputArguments();
}
});
}
catch (Exception ex) {
return Collections.<String> emptyList();
}
}
private Set<URL> getJavaAgentJars(List<String> inputArguments) {
Set<URL> javaAgentJars = new HashSet<URL>();
for (String argument : inputArguments) {
String path = getJavaAgentJarPath(argument);
if (path != null) {
try {
javaAgentJars.add(new File(path).getCanonicalFile().toURI().toURL());
}
catch (IOException ex) {
throw new IllegalStateException(
"Failed to determine canonical path of Java agent at path '"
+ path + "'");
}
}
}
return javaAgentJars;
}
private String getJavaAgentJarPath(String arg) {
if (arg.startsWith(JAVA_AGENT_PREFIX)) {
String path = arg.substring(JAVA_AGENT_PREFIX.length());
int equalsIndex = path.indexOf('=');
if (equalsIndex > -1) {
path = path.substring(0, equalsIndex);
}
return path;
}
return null;
}
@Override
public boolean isJavaAgentJar(URL url) {
return this.javaAgentJars.contains(url);
}
}

@ -0,0 +1,37 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader;
import java.net.URL;
/**
* A strategy for detecting Java agents
*
* @author Andy Wilkinson
*
* @since 1.1
*/
public interface JavaAgentDetector {
/**
* Returns {@code true} if {@code url} points to a Java agent jar file, otherwise
* {@code false} is returned.
*
* @param url The url to examine
*/
public boolean isJavaAgentJar(URL url);
}

@ -0,0 +1,118 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.archive.Archive.Entry;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
/**
* Tests for {@link ExecutableArchiveLauncher}
*
* @author Andy Wilkinson
*/
public class ExecutableArchiveLauncherTests {
@Mock
private JavaAgentDetector javaAgentDetector;
private ExecutableArchiveLauncher launcher;
@Before
public void setupMocks() {
MockitoAnnotations.initMocks(this);
this.launcher = new UnitTestExecutableArchiveLauncher(this.javaAgentDetector);
}
@Test
public void createdClassLoaderContainsUrlsFromThreadContextClassLoader()
throws Exception {
final URL[] urls = new URL[] { new URL("file:one"), new URL("file:two") };
doWithTccl(new URLClassLoader(urls), new Callable<Void>() {
@Override
public Void call() throws Exception {
ClassLoader classLoader = ExecutableArchiveLauncherTests.this.launcher
.createClassLoader(new URL[0]);
assertClassLoaderUrls(classLoader, urls);
return null;
}
});
}
@Test
public void javaAgentJarsAreExcludedFromClasspath() throws Exception {
URL javaAgent = new File("my-agent.jar").getCanonicalFile().toURI().toURL();
final URL one = new URL("file:one");
when(this.javaAgentDetector.isJavaAgentJar(javaAgent)).thenReturn(true);
doWithTccl(new URLClassLoader(new URL[] { javaAgent, one }), new Callable<Void>() {
@Override
public Void call() throws Exception {
ClassLoader classLoader = ExecutableArchiveLauncherTests.this.launcher
.createClassLoader(new URL[0]);
assertClassLoaderUrls(classLoader, new URL[] { one });
return null;
}
});
}
private void assertClassLoaderUrls(ClassLoader classLoader, URL[] urls) {
assertTrue(classLoader instanceof URLClassLoader);
assertArrayEquals(urls, ((URLClassLoader) classLoader).getURLs());
}
private static final class UnitTestExecutableArchiveLauncher extends
ExecutableArchiveLauncher {
public UnitTestExecutableArchiveLauncher(JavaAgentDetector javaAgentDetector) {
super(javaAgentDetector);
}
@Override
protected boolean isNestedArchive(Entry entry) {
return false;
}
}
private void doWithTccl(ClassLoader classLoader, Callable<?> action) throws Exception {
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(classLoader);
action.call();
}
finally {
Thread.currentThread().setContextClassLoader(old);
}
}
}

@ -0,0 +1,71 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link InputArgumentsJavaAgentDetector}
*
* @author Andy Wilkinson
*/
public class InputArgumentsJavaAgentDetectorTests {
@Test
public void nonAgentJarsDoNotProduceFalsePositives() throws MalformedURLException,
IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar"));
assertFalse(detector.isJavaAgentJar(new File("something-else.jar")
.getCanonicalFile().toURI().toURL()));
}
@Test
public void singleJavaAgent() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar"));
assertTrue(detector.isJavaAgentJar(new File("my-agent.jar").getCanonicalFile()
.toURI().toURL()));
}
@Test
public void singleJavaAgentWithOptions() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar=a=alpha,b=bravo"));
assertTrue(detector.isJavaAgentJar(new File("my-agent.jar").getCanonicalFile()
.toURI().toURL()));
}
@Test
public void multipleJavaAgents() throws MalformedURLException, IOException {
InputArgumentsJavaAgentDetector detector = new InputArgumentsJavaAgentDetector(
Arrays.asList("-javaagent:my-agent.jar", "-javaagent:my-other-agent.jar"));
assertTrue(detector.isJavaAgentJar(new File("my-agent.jar").getCanonicalFile()
.toURI().toURL()));
assertTrue(detector.isJavaAgentJar(new File("my-other-agent.jar")
.getCanonicalFile().toURI().toURL()));
}
}
Loading…
Cancel
Save