Skip to content

Pipe: Reject plugin jars that conflict with parent ClassLoader bytecode - #18410

Open
luoluoyuyu wants to merge 5 commits into
apache:masterfrom
luoluoyuyu:fix/pipe-plugin-classloader-bytecode-conflict
Open

Pipe: Reject plugin jars that conflict with parent ClassLoader bytecode#18410
luoluoyuyu wants to merge 5 commits into
apache:masterfrom
luoluoyuyu:fix/pipe-plugin-classloader-bytecode-conflict

Conversation

@luoluoyuyu

Copy link
Copy Markdown
Member

Description

Restore standard parent-delegation for PipePluginClassLoader and fail fast when a plugin ships the same class name with different bytecode, without defining classes into the parent during the check.


This PR has:

  • been self-reviewed.
    • concurrent read
    • concurrent write
    • concurrent read and write
  • added documentation for new or modified features or behaviors.
  • added Javadocs for most classes and all non-trivial methods.
  • added or updated version, license, or notice information
  • added comments explaining the "why" and the intent of the code wherever would not be obvious
    for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold
    for code coverage.
  • added integration tests.
  • been tested in a test IoTDB cluster.

Key changed/added classes (or packages if there are too many classes) in this PR

Restore standard parent-delegation for PipePluginClassLoader and fail
fast when a plugin ships the same class name with different bytecode,
without defining classes into the parent during the check.
closeIfPossible();
private static void collectJarConflicts(Path jarPath, ClassLoader parent, List<String> conflicts)
throws IOException {
try (JarFile jarFile = new JarFile(jarPath.toFile())) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Resolve Multi-Release JAR entries using the runtime version before comparing bytes

ew JarFile(jarPath.toFile()) opens the JAR with the base (Java 8) view, while parent.getResourceAsStream(entry.getName()) is runtime-aware and resolves pkg/Foo.class to (for example) META-INF/versions/17/pkg/Foo.class on JDK 17. Consequently, even the exact same MR-JAR on the parent and plugin classpaths can be reported as conflicting: with jackson-core-2.16.2.jar, the base and Java 17 FastDoubleSwar.class entries have different bytes, so this check rejects an otherwise identical dependency.

Please compare the runtime-selected plugin bytes as well (for example, open JarFile with Runtime.version()), and skip/deduplicate physical META-INF/versions/ entries. A regression test with a Multi-Release JAR would help prevent this false positive.

@Caideyipi Caideyipi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found several correctness issues in the new class-loading validation. Please address the inline findings before merging.

if (resolve) {
resolveClass(loadedClass);
final byte[] parentBytes = readAllBytes(parentIn);
if (!Arrays.equals(parentBytes, pluginBytes)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Identical bytes do not make parent delegation safe for package-private members. For example, if the plugin contains p.Entry and a byte-identical package-private p.Helper, while the parent only has p.Helper, this check passes. The default parent-first loader then defines Entry in the plugin loader and Helper in the parent loader; JVM runtime packages include the defining loader, so Entry calling Helper throws IllegalAccessError. I reproduced this with identical Helper.class bytes. Please keep package ownership coherent (for example, child-first for plugin-owned packages or reject split-package duplicates) and add a regression test.


for (Path path : pluginFiles) {
final String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
if (fileName.endsWith(JAR_SUFFIX)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] The scan inventory is narrower than the URLs that are actually loaded. addUrls adds every regular file, and URLClassLoader loads a valid JAR renamed to .zip, but this branch only scans names ending in .jar (and .class). The SQL path preserves arbitrary URI extensions, so a .zip/renamed JAR containing a conflicting class bypasses the check and is then parent-delegated. Please detect archives by opening them (or enforce the artifact type) and add a regression test.

}

validateNoConflictingClassesWithParent(rootPath, pluginFiles, parent);
addUrls(pluginFiles);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Manifest Class-Path dependencies are not included in pluginFiles. URLClassLoader follows a JAR manifest's Class-Path entries and can load classes from sibling or out-of-root JARs after addURL, but validation only scans files returned by Files.walk(rootPath). A conflicting class in such a dependency therefore bypasses this check. Please resolve and scan the effective URL class path (including manifest dependencies), or explicitly reject/disable manifest Class-Path entries.

new PipePluginClassLoader(childJar.toString(), parentClassLoader)) {
final URL resourceUrl = pluginClassLoader.getResource("config.properties");
Assert.assertNotNull(resourceUrl);
try (InputStream inputStream = resourceUrl.openStream()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This test currently fails on Windows CI. The latest PR check reports FileSystemException: parent.jar ... being used by another process during cleanup in testPluginResourceIsolation (https://github.com/apache/iotdb/actions/runs/31354401714/job/93351225887). resourceUrl.openStream() opens a cached JarURLConnection; closing the classloaders does not release that cached JarFile. Use getResourceAsStream, set useCaches(false) before opening, or otherwise avoid deleting while the cached connection is alive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants