Skip to content

Commit a8b78e7

Browse files
committed
Packaging: Remove classpath ordering hack (#23596)
After the removal of the joda time hack we used to have, we can cleanup the codebase handling in security, jarhell and plugins to be more picky about uniqueness. This was originally in #18959 which was never merged. closes #18959
1 parent ccaebd1 commit a8b78e7

File tree

11 files changed

+190
-113
lines changed

11 files changed

+190
-113
lines changed

core/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.security.Permissions;
3232
import java.security.Policy;
3333
import java.security.ProtectionDomain;
34+
import java.util.Collections;
3435
import java.util.Map;
3536
import java.util.function.Predicate;
3637

@@ -50,7 +51,7 @@ final class ESPolicy extends Policy {
5051

5152
ESPolicy(PermissionCollection dynamic, Map<String,Policy> plugins, boolean filterBadDefaults) {
5253
this.template = Security.readPolicy(getClass().getResource(POLICY_RESOURCE), JarHell.parseClassPath());
53-
this.untrusted = Security.readPolicy(getClass().getResource(UNTRUSTED_RESOURCE), new URL[0]);
54+
this.untrusted = Security.readPolicy(getClass().getResource(UNTRUSTED_RESOURCE), Collections.emptySet());
5455
if (filterBadDefaults) {
5556
this.system = new SystemPolicy(Policy.getPolicy());
5657
} else {

core/src/main/java/org/elasticsearch/bootstrap/JarHell.java

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@
3636
import java.nio.file.SimpleFileVisitor;
3737
import java.nio.file.attribute.BasicFileAttributes;
3838
import java.util.Arrays;
39+
import java.util.Collections;
3940
import java.util.Enumeration;
4041
import java.util.HashMap;
4142
import java.util.HashSet;
43+
import java.util.LinkedHashSet;
4244
import java.util.Locale;
4345
import java.util.Map;
4446
import java.util.Set;
@@ -93,7 +95,7 @@ public static void checkJarHell() throws IOException, URISyntaxException {
9395
* @return array of URLs
9496
* @throws IllegalStateException if the classpath contains empty elements
9597
*/
96-
public static URL[] parseClassPath() {
98+
public static Set<URL> parseClassPath() {
9799
return parseClassPath(System.getProperty("java.class.path"));
98100
}
99101

@@ -104,13 +106,12 @@ public static URL[] parseClassPath() {
104106
* @throws IllegalStateException if the classpath contains empty elements
105107
*/
106108
@SuppressForbidden(reason = "resolves against CWD because that is how classpaths work")
107-
static URL[] parseClassPath(String classPath) {
109+
static Set<URL> parseClassPath(String classPath) {
108110
String pathSeparator = System.getProperty("path.separator");
109111
String fileSeparator = System.getProperty("file.separator");
110112
String elements[] = classPath.split(pathSeparator);
111-
URL urlElements[] = new URL[elements.length];
112-
for (int i = 0; i < elements.length; i++) {
113-
String element = elements[i];
113+
Set<URL> urlElements = new LinkedHashSet<>(); // order is already lost, but some filesystems have it
114+
for (String element : elements) {
114115
// Technically empty classpath element behaves like CWD.
115116
// So below is the "correct" code, however in practice with ES, this is usually just a misconfiguration,
116117
// from old shell scripts left behind or something:
@@ -136,21 +137,25 @@ static URL[] parseClassPath(String classPath) {
136137
}
137138
// now just parse as ordinary file
138139
try {
139-
urlElements[i] = PathUtils.get(element).toUri().toURL();
140+
URL url = PathUtils.get(element).toUri().toURL();
141+
if (urlElements.add(url) == false) {
142+
throw new IllegalStateException("jar hell!" + System.lineSeparator() +
143+
"duplicate jar on classpath: " + classPath);
144+
}
140145
} catch (MalformedURLException e) {
141146
// should not happen, as we use the filesystem API
142147
throw new RuntimeException(e);
143148
}
144149
}
145-
return urlElements;
150+
return Collections.unmodifiableSet(urlElements);
146151
}
147152

148153
/**
149154
* Checks the set of URLs for duplicate classes
150155
* @throws IllegalStateException if jar hell was found
151156
*/
152157
@SuppressForbidden(reason = "needs JarFile for speed, just reading entries")
153-
public static void checkJarHell(URL urls[]) throws URISyntaxException, IOException {
158+
public static void checkJarHell(Set<URL> urls) throws URISyntaxException, IOException {
154159
Logger logger = Loggers.getLogger(JarHell.class);
155160
// we don't try to be sneaky and use deprecated/internal/not portable stuff
156161
// like sun.boot.class.path, and with jigsaw we don't yet have a way to get
@@ -168,8 +173,8 @@ public static void checkJarHell(URL urls[]) throws URISyntaxException, IOExcepti
168173
}
169174
if (path.toString().endsWith(".jar")) {
170175
if (!seenJars.add(path)) {
171-
logger.debug("excluding duplicate classpath element: {}", path);
172-
continue;
176+
throw new IllegalStateException("jar hell!" + System.lineSeparator() +
177+
"duplicate jar on classpath: " + path);
173178
}
174179
logger.debug("examining jar: {}", path);
175180
try (JarFile file = new JarFile(path.toString())) {
@@ -198,8 +203,8 @@ public static void checkJarHell(URL urls[]) throws URISyntaxException, IOExcepti
198203
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
199204
String entry = root.relativize(file).toString();
200205
if (entry.endsWith(".class")) {
201-
// normalize with the os separator
202-
entry = entry.replace(sep, ".").substring(0, entry.length() - 6);
206+
// normalize with the os separator, remove '.class'
207+
entry = entry.replace(sep, ".").substring(0, entry.length() - ".class".length());
203208
checkClass(clazzes, entry, path);
204209
}
205210
return super.visitFile(file, attrs);

core/src/main/java/org/elasticsearch/bootstrap/Security.java

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@
4949
import java.util.ArrayList;
5050
import java.util.Collections;
5151
import java.util.HashMap;
52+
import java.util.LinkedHashSet;
5253
import java.util.List;
5354
import java.util.Map;
55+
import java.util.Set;
5456

5557
/**
5658
* Initializes SecurityManager with necessary permissions.
@@ -128,19 +130,23 @@ static void configure(Environment environment, boolean filterBadDefaults) throws
128130
@SuppressForbidden(reason = "proper use of URL")
129131
static Map<String,Policy> getPluginPermissions(Environment environment) throws IOException, NoSuchAlgorithmException {
130132
Map<String,Policy> map = new HashMap<>();
131-
// collect up lists of plugins and modules
132-
List<Path> pluginsAndModules = new ArrayList<>();
133+
// collect up set of plugins and modules by listing directories.
134+
Set<Path> pluginsAndModules = new LinkedHashSet<>(); // order is already lost, but some filesystems have it
133135
if (Files.exists(environment.pluginsFile())) {
134136
try (DirectoryStream<Path> stream = Files.newDirectoryStream(environment.pluginsFile())) {
135137
for (Path plugin : stream) {
136-
pluginsAndModules.add(plugin);
138+
if (pluginsAndModules.add(plugin) == false) {
139+
throw new IllegalStateException("duplicate plugin: " + plugin);
140+
}
137141
}
138142
}
139143
}
140144
if (Files.exists(environment.modulesFile())) {
141145
try (DirectoryStream<Path> stream = Files.newDirectoryStream(environment.modulesFile())) {
142-
for (Path plugin : stream) {
143-
pluginsAndModules.add(plugin);
146+
for (Path module : stream) {
147+
if (pluginsAndModules.add(module) == false) {
148+
throw new IllegalStateException("duplicate module: " + module);
149+
}
144150
}
145151
}
146152
}
@@ -150,15 +156,18 @@ static Map<String,Policy> getPluginPermissions(Environment environment) throws I
150156
if (Files.exists(policyFile)) {
151157
// first get a list of URLs for the plugins' jars:
152158
// we resolve symlinks so map is keyed on the normalize codebase name
153-
List<URL> codebases = new ArrayList<>();
159+
Set<URL> codebases = new LinkedHashSet<>(); // order is already lost, but some filesystems have it
154160
try (DirectoryStream<Path> jarStream = Files.newDirectoryStream(plugin, "*.jar")) {
155161
for (Path jar : jarStream) {
156-
codebases.add(jar.toRealPath().toUri().toURL());
162+
URL url = jar.toRealPath().toUri().toURL();
163+
if (codebases.add(url) == false) {
164+
throw new IllegalStateException("duplicate module/plugin: " + url);
165+
}
157166
}
158167
}
159168

160169
// parse the plugin's policy file into a set of permissions
161-
Policy policy = readPolicy(policyFile.toUri().toURL(), codebases.toArray(new URL[codebases.size()]));
170+
Policy policy = readPolicy(policyFile.toUri().toURL(), codebases);
162171

163172
// consult this policy for each of the plugin's jars:
164173
for (URL url : codebases) {
@@ -176,24 +185,33 @@ static Map<String,Policy> getPluginPermissions(Environment environment) throws I
176185
/**
177186
* Reads and returns the specified {@code policyFile}.
178187
* <p>
179-
* Resources (e.g. jar files and directories) listed in {@code codebases} location
180-
* will be provided to the policy file via a system property of the short name:
181-
* e.g. <code>${codebase.joda-convert-1.2.jar}</code> would map to full URL.
188+
* Jar files listed in {@code codebases} location will be provided to the policy file via
189+
* a system property of the short name: e.g. <code>${codebase.joda-convert-1.2.jar}</code>
190+
* would map to full URL.
182191
*/
183192
@SuppressForbidden(reason = "accesses fully qualified URLs to configure security")
184-
static Policy readPolicy(URL policyFile, URL codebases[]) {
193+
static Policy readPolicy(URL policyFile, Set<URL> codebases) {
185194
try {
186195
try {
187196
// set codebase properties
188197
for (URL url : codebases) {
189198
String shortName = PathUtils.get(url.toURI()).getFileName().toString();
190-
System.setProperty("codebase." + shortName, url.toString());
199+
if (shortName.endsWith(".jar") == false) {
200+
continue; // tests :(
201+
}
202+
String previous = System.setProperty("codebase." + shortName, url.toString());
203+
if (previous != null) {
204+
throw new IllegalStateException("codebase property already set: " + shortName + "->" + previous);
205+
}
191206
}
192207
return Policy.getInstance("JavaPolicy", new URIParameter(policyFile.toURI()));
193208
} finally {
194209
// clear codebase properties
195210
for (URL url : codebases) {
196211
String shortName = PathUtils.get(url.toURI()).getFileName().toString();
212+
if (shortName.endsWith(".jar") == false) {
213+
continue; // tests :(
214+
}
197215
System.clearProperty("codebase." + shortName);
198216
}
199217
}

core/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -452,22 +452,23 @@ private PluginInfo verify(Terminal terminal, Path pluginRoot, boolean isBatch, E
452452
/** check a candidate plugin for jar hell before installing it */
453453
void jarHellCheck(Path candidate, Path pluginsDir) throws Exception {
454454
// create list of current jars in classpath
455-
final List<URL> jars = new ArrayList<>();
456-
jars.addAll(Arrays.asList(JarHell.parseClassPath()));
455+
final Set<URL> jars = new HashSet<>(JarHell.parseClassPath());
457456

458457
// read existing bundles. this does some checks on the installation too.
459458
PluginsService.getPluginBundles(pluginsDir);
460459

461460
// add plugin jars to the list
462461
Path pluginJars[] = FileSystemUtils.files(candidate, "*.jar");
463462
for (Path jar : pluginJars) {
464-
jars.add(jar.toUri().toURL());
463+
if (jars.add(jar.toUri().toURL()) == false) {
464+
throw new IllegalStateException("jar hell! duplicate plugin jar: " + jar);
465+
}
465466
}
466467
// TODO: no jars should be an error
467468
// TODO: verify the classname exists in one of the jars!
468469

469470
// check combined (current classpath + new jars to-be-added)
470-
JarHell.checkJarHell(jars.toArray(new URL[jars.size()]));
471+
JarHell.checkJarHell(jars);
471472
}
472473

473474
/**

0 commit comments

Comments
 (0)