Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JRuby adds Java proxy classes to the Java module even if they are not from JRubyClassLoader #8156

Closed
barrkel opened this issue Mar 19, 2024 · 7 comments · Fixed by #8208
Closed
Milestone

Comments

@barrkel
Copy link

barrkel commented Mar 19, 2024

JRuby version 9.3.13.0
Platform 6.5.13-1rodete2

Expected behavior:

  • There is a class org.example.C which is in a jar which is not on the class path, call this C_jar
  • There is a class org.example.C which is in a jar which is on the class path, call this C_classpath
  • C_jar is loaded via URLClassLoader and instantiated in Java code. This is done to isolate the different versions of C.
  • Use Ruby to evaluate an object expression which results in an instance of an object with the class C_jar
  • Use the Java module to reference the class C, expecting to get C_classpath (i.e. Java::OrgExample::C)

Actual behavior:

  • Using the Java module syntax to access C results in C_jar

Workaround:

  • Use the Java module syntax to access C before any Ruby expression which evaluates to an object of class C_jar

In Java.java, generateClassProxy calls addToJavaPackageModule without checking the class loader of the class it generated a proxy for. Effectively, manipulating values from non-JRubyClassLoader classes pollutes the Java module with incompatible classes.

@headius
Copy link
Member

headius commented Mar 26, 2024

Interesting! Do you think you can come up with a small reproduction for us?

@headius headius added this to the JRuby 9.4.7.0 milestone Mar 26, 2024
@barrkel
Copy link
Author

barrkel commented Mar 26, 2024

java-module-pollution.zip

Steps:

  1. Unzip the attached zip and enter the directory.
  2. Run ./build.sh to compile the Java classes and build the jar; javac and jar need to be on the path.
  3. Run ./repro.rb broken with jruby on the path and observe:
Expecting 'isolated in jar': isolated in jar
Expecting 'packaged with app': isolated in jar
  1. Run ./repro.rb workaround and observe:
Expecting 'isolated in jar': isolated in jar
Expecting 'packaged with app': packaged with app

@barrkel
Copy link
Author

barrkel commented Mar 26, 2024

Having thought about it a little bit more:

  • There's a potential backward compatibility issue; it's possible some people have relied on Java:: access to classes they loaded from a jar similar to how the repro works. This may have worked OK if they didn't have duplicate classes on the app class path.
  • There may be subtleties to how class loader parent/child delegation works. The scenario I present is specifically problematic when the parent of the jar's class loader is the bootstrap class loader (bootstrap is parent of the system class loader). This is required to actually get the jar's version of the class and not accidentally load the app's class. I don't know how other configurations of class loader hierarchy may interfere, and I'm not sure of the right predicate with which to gate addition of proxy classes to the Java module - if it needs a simple class loader comparison, or if it needs to follow chains, or if it needs to be configurable.

@headius
Copy link
Member

headius commented Apr 24, 2024

After looking at your example I think I understand the problem.

When a Java object enters Ruby with a class we haven't seen before, we need to set up the "proxy" class under our own Java module hierarchy. If multiple classes of the same name but different effective classes (e.g. from different classloaders), whichever gets set up first will "win" and become the entry in Java::. That's why your workaround fixes it: the classloader version gets set up first.

There's no hard reason why we immediately add the constant entry for the class under Java:: other than that step being a standard part of setting up the proxy. If we didn't do that, the "side loaded" jar version of the class would not be assigned a constant entry and you'd get the expected behavior.

Avoiding setting the constant also would not really affect users, since any subsequent access of the constant name would lazily pick up the class, and only from the classloader hierarchy JRuby uses for Java integration.

An alternative, less aggressive change would be to only automatically add the constant entry when we know it's from our Java integration classloader.

@kares @enebo What are your thoughts here? It's probably not something we can change in 9.4.x but for Java 10 we could consider not always setting up the Java:: constant entries until they are explicitly requested.

headius added a commit to headius/jruby that referenced this issue Apr 24, 2024
Previously whenever we see a new type of Java object, we both set
up the proxy class AND set the constant. If that object's class
is not from the typical Java integration classloader hierarchy, it
will pollute the Java:: module with a normally inaccessible entry.

This change only sets up the constant when it is accessed through
the normal package reference, rather than when first seen.

Fixes jruby#8156
@headius
Copy link
Member

headius commented Apr 24, 2024

I pushed #8208 as an experiment in delaying when we set up the Java:: constants, and my first attempt appears to be passing all internal tests. Instead of immediately adding those Java:: entries when we encounter a new class, we only add them when they are explicitly accessed (as Java::C in your code) and load them only from our internal Java integration classloader hierarchy. Objects with different classes of the same name would never be accessible through that syntax, but would continue to work fine in JRuby.

@barrkel If you are able to test that branch with your application, I'd appreciate it.

@headius
Copy link
Member

headius commented Apr 24, 2024

Oh and I should temper expectations a bit... this may be too major of a change to do in a point release of JRuby 9.4.x, so it would either be guarded behind a configuration property (defaulting to non-lazy in 9.4.7.0) or only merged into JRuby 10 for release later this year.

headius added a commit to headius/jruby that referenced this issue Apr 25, 2024
In order to avoid polluting the Java:: namespace with classes from
other classloader hierarchies, we are modifying the proxy class
setup process to not eagerly set a constant for newly-encountered
classes. This property controls the behavior, defaulting to true
for JRuby 9.4.x but likely to switch to false in a future release
(probably JRuby 10).

The constants will still be defined if accessed directly, as
before. We just won't define them when we happen to see an object
with an unfamiliar type.

See jruby#8208 and the bug it fixed jruby#8156.
@headius
Copy link
Member

headius commented Apr 25, 2024

@barrkel I have a PR settling now that would allow to turn off the "eager" constant definition, so that only constants accessed directly (and loaded out of our classloader hierarchy) will be defined in the Java:: namespace. The property will be jruby.ji.eager.constants=false for JVM or -Xjit.eager.constants=false passed to JRuby's launcher, and it defaults to true in JRuby 9.4.7.0. We'll switch it to false for JRuby 10.

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 a pull request may close this issue.

2 participants