Skip to content

Commit f8453ac

Browse files
authored
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 105bc0e commit f8453ac

File tree

10 files changed

+181
-111
lines changed

10 files changed

+181
-111
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
@@ -48,8 +48,10 @@
4848
import java.util.ArrayList;
4949
import java.util.Collections;
5050
import java.util.HashMap;
51+
import java.util.LinkedHashSet;
5152
import java.util.List;
5253
import java.util.Map;
54+
import java.util.Set;
5355

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

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

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

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

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

461460
// read existing bundles. this does some checks on the installation too.
462461
PluginsService.getPluginBundles(pluginsDir);
463462

464463
// add plugin jars to the list
465464
Path pluginJars[] = FileSystemUtils.files(candidate, "*.jar");
466465
for (Path jar : pluginJars) {
467-
jars.add(jar.toUri().toURL());
466+
if (jars.add(jar.toUri().toURL()) == false) {
467+
throw new IllegalStateException("jar hell! duplicate plugin jar: " + jar);
468+
}
468469
}
469470
// TODO: no jars should be an error
470471
// TODO: verify the classname exists in one of the jars!
471472

472473
// check combined (current classpath + new jars to-be-added)
473-
JarHell.checkJarHell(jars.toArray(new URL[jars.size()]));
474+
JarHell.checkJarHell(jars);
474475
}
475476

476477
/**

0 commit comments

Comments
 (0)