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

Fb jpms - fixes for illegal access to private information in java.* packages. #1328

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

struberg
Copy link
Member

@struberg struberg commented Dec 4, 2024

please review and give feedback. Can push it later myself or you just apply it - txs!

Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

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

Hi @struberg
I have a few comments scattered throughout.

pom.xml Outdated Show resolved Hide resolved
Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

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

Hi @struberg

See my comments and @ppkarwasz's.

@struberg struberg requested a review from garydgregory December 5, 2024 13:42
asfgit pushed a commit that referenced this pull request Dec 5, 2024
asfgit pushed a commit that referenced this pull request Dec 5, 2024
Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

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

Hi @struberg

We cannot test that an object implements a stock interface like TemporalAccessor or CharSequence because this won't work with custom implementations (TemporalAmount is also not accounted for, and others I am sure).

See the new test I added in git master: EqualsBuilderReflectJreImplementationTest

I think we can only test for an actual class or the the package that class lives in.

TY

struberg and others added 3 commits December 5, 2024 20:30
Not every CharSequence might implement equals,
not every TemporalAccessor might be a java.* class.
@struberg struberg requested a review from garydgregory December 5, 2024 22:13
Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

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

Hello @struberg

Checking against an interface in isJavaInternalClass() is not going to work.

I updated EqualsBuilderReflectJreImplementationTest in git master.

@struberg
Copy link
Member Author

struberg commented Dec 6, 2024

Hello @struberg

Checking against an interface in isJavaInternalClass() is not going to work.

I updated EqualsBuilderReflectJreImplementationTest in git master.

Gary, this has already long been resolved in f20639a and ec4150b

I've even merged your changes (and removed that new --add-opens again...) from main to this very branch.

This branch clearly improves the situation with standard jpms setup for a lot of common cases. Of course, this does not yet fix ALL the problems (albeit it's a good start). But if people come across another InaccessibleObjectException then they can still --add-opens as needed. So the current version is clearly an improvement. If you agree, then we should merge it into main instead of yet more parallel work in different branches...

@struberg struberg requested a review from garydgregory December 6, 2024 06:30
@garydgregory
Copy link
Member

Hello @struberg
Checking against an interface in isJavaInternalClass() is not going to work.
I updated EqualsBuilderReflectJreImplementationTest in git master.

Gary, this has already long been resolved in f20639a and ec4150b

I've even merged your changes (and removed that new --add-opens again...) from main to this very branch.

This branch clearly improves the situation with standard jpms setup for a lot of common cases. Of course, this does not yet fix ALL the problems (albeit it's a good start). But if people come across another InaccessibleObjectException then they can still --add-opens as needed. So the current version is clearly an improvement. If you agree, then we should merge it into main instead of yet more parallel work in different branches...

Hello @struberg

The issue is not that this PR is a wants to start improving something, the issue is that this PR breaks existing behavior.

You cannot check for interfaces to short-circuit logic in Reflection.isJavaInternalClass(). This will break existing applications.

The branch is not up to date with master. This fails:

git clone https://github.com/struberg/commons-lang.git 
cd commons-lang
git checkout fb_jpms
git remote add upstream https://github.com/apache/commons-lang.git
git pull upstream master
mvn clean test -Dtest=EqualsBuilderReflectJreImplementationTest

@struberg
Copy link
Member Author

struberg commented Dec 6, 2024

You cannot check for interfaces to short-circuit logic in Reflection.isJavaInternalClass(). This will break existing applications.

Which of those is an interface?
https://github.com/struberg/commons-lang/blob/ec4150bc13cfd2705e5d672f47706930af578c39/src/main/java/org/apache/commons/lang3/builder/Reflection.java#L62

The thing you are talking about IS ALREADY RESOLVED since yesterday.

And one other thing: commons-lang3 ReflectionToString ReflectionEquals etc was BROKEN on anything Java9++ - period!
The build is ONLY green because you just disabled all the modularity - which is something not everyone can do in real projects!

My spare time to work on this stuff is very limited - as is yours I bet. So please use our both time productively. As I said: not everything will fully work from the beginning. You just added java.time.Period, fine, good catch. But why on earth don't we just take that PR and commit it and then improve it TOGETHER? I'm not willing to do more empty rounds getting sticks thrown at me at will. I really appreciate the review feedback as it indeed did highly improve the code. But now is the point to decide whether the direction is right or not - and then improve things from here TOGETHER.

@struberg
Copy link
Member Author

struberg commented Dec 6, 2024

Btw, I'm looking for another method name for isJavaInternalClass(). It should rather express that the given clazz cannot be introspected via reflection. Maybe something like isNonIntrospectibleClass()?

Further down the road we need some way to tell whether a class is open or not. And all that in a way which also works on Java8 still...

@garydgregory
Copy link
Member

You cannot check for interfaces to short-circuit logic in Reflection.isJavaInternalClass(). This will break existing applications.

Which of those is an interface? https://github.com/struberg/commons-lang/blob/ec4150bc13cfd2705e5d672f47706930af578c39/src/main/java/org/apache/commons/lang3/builder/Reflection.java#L62

The thing you are talking about IS ALREADY RESOLVED since yesterday.

I guess you could be bothered to look it up? https://docs.oracle.com/javase/8/docs/api/java/time/temporal/Temporal.html

@struberg
Copy link
Member Author

struberg commented Dec 6, 2024

I guess you could be bothered to look it up

Indeed, had in the back of my mind that Temporal is an abstract class.
Latest version uses reflection to figure that a class is not accessible. This works, but maybe we should try a mixture of the old approach + the new one?

Thought about having a static Map<Class, Boolean> KNOWN_INACCESSIBLE_CLASSES which contains the concrete classes like LocalDate, Boolean, Character, Integer, etc.
And then in a second step we do the reflection check. But as this might also have unexpected results (people might forget that they closed the jars) we should probably log it out?

struberg and others added 3 commits December 7, 2024 14:44
* cache for known inaccessible classes
* add documentation for that cache.
txs to @erans for the name suggestion!
@struberg struberg dismissed garydgregory’s stale review December 8, 2024 10:59

point was valid in the past, but has already been resolved before the review

Float.class,
Double.class,
java.util.Date.class,
java.sql.Date.class,
Copy link
Member

Choose a reason for hiding this comment

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

This is not specific to this PR: I don't think we should introduce a new JPMS module dependency, here on java.sql. Lang does not use any classes from java.sql so this would drag in a new JPMS module. For 4.0, we've mentioned dropping our dependency on java.desktop and only depending on java.base.

Copy link
Member Author

Choose a reason for hiding this comment

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

yup true

f.setAccessible(true);
}

nonIntrospectibleClasses.put(clazz, false);
Copy link
Member

Choose a reason for hiding this comment

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

This looks problematic since the map will grow uncontrollably, possibly until every class on your classpath gets referenced, there's no way to know really.

Copy link
Member Author

Choose a reason for hiding this comment

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

this is a trade-off vs performance. it will only cache those classes which got hit in a reflection equals, toString, etc. Thus in practice it is a very limited set of classes I'd say. Can you think of a possible place where we can hold such a cache at least during a single invocation? Maybe inside the builder and handing the Map as another parameter to this method?

}

/**
* Check whether the given class internas can be accessed. This might return false in Java9
Copy link
Member

Choose a reason for hiding this comment

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

"internas" spelling

Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

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

Hi All,

So far, I'm still against this change.

These features are designed to use reflection, and this PR tries to turn some of that off.

It seems to me like it will cause unintended consequences beyond all of the ones I pointed out through the new unit tests I wrote which showed the initial implementation would break existing apps.

Now, we have a different implementation, which brings in a new set of problems:

  • It depends on a new JPMS module java.sql
  • it grows a map without any bounds (a CVE waiting to happen IMO)
  • you can't turn it off
  • The behavior is completely hidden from the user

@ppkarwasz
Copy link

ppkarwasz commented Dec 9, 2024

So far, I'm still against this change.

These features are designed to use reflection, and this PR tries to turn some of that off.

I agree. All the reflectionX methods have a warning:

Fields can be private, thus AccessibleObject.setAccessible is used to bypass normal access control checks. This will fail under a security manager unless the appropriate permissions are set.

The warning might require an update, but it seems clear to me that it also applies to the JPMS access checks. So it is up to caller to ensure that the class is open to org.apache.commons.lang3. If the caller uses testRecursive, he must also ensure that the appropriate permissions are set for all the field types.

@struberg
Copy link
Member Author

struberg commented Dec 9, 2024

I agree. All the reflectionX methods have a warning:

Fields can be private, thus AccessibleObject.setAccessible is used to bypass normal access control checks.

why is this disussion again NOT happening on any ASF list but here where it does not get reflected to any ASF list afaict?

@garydgregory
Copy link
Member

This discussion is mirrored on the apache list here:
https://lists.apache.org/thread/gwljc3ylz3cjvbg9m7sv9dtj139obprz

Copy link
Member

@garydgregory garydgregory left a comment

Choose a reason for hiding this comment

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

Note that this PR is worse than I thought because we already have a feature to bypass reflection! Use EqualsBuilder.setBypassReflectionClasses(List<Class<?>>).

@struberg
Copy link
Member Author

struberg commented Dec 9, 2024

further comments over at the commons dev list.

@struberg
Copy link
Member Author

Note that this PR is worse than I thought because we already have a feature to bypass reflection! Use EqualsBuilder.setBypassReflectionClasses(List<Class<?>>).

Are you sure that you really understand that code?

If you would setBypassReflectionClasses(ArrayList.class) or HashSet.class or HashMap.class etc, then you would totally break the EqualsBuilder. Because this would also stop looking down into the equality of the elements contained in that List/Set/Map.

Funnily you yourself did add hand-tinkered bypasses for those classes in selected places - just not for every use case. So you yourself added code to NOT use reflection for some of the cases.

So this rather sounds like a strawman argument to me.

@struberg
Copy link
Member Author

struberg commented Dec 13, 2024

I agree. All the reflectionX methods have a warning:

Well this is way more subtly weird right now.

Imagine the following code:

        Map<String, String> a = new HashMap<>(20);
        a.put("A", "A");

        Map<String, String> b = new HashMap<>();
        b.put("A", "A");

Here we have the case that a.equals(b) returns true. Because that's the contract of Maps!
But when you use new EqualsBuilder().setTestRecursive(true).append(a, b).isEquals() then you'll get false.
That's because by giving a different initial array size, we'll get a different internal (!) structure (bigger internal arrays, etc).

But that means that right now the EqualsBuilder (even in Java8) does not even adheres to the contract of the Map interface. But those internal details should not matter! Even if I would not have set an initial size - what if I add 30 elements to one of the 2 maps, but then remove everything but the "A" entry again? It will be equals according to the Map contract, but not for the EqualsBuilder. Looks rather broken to me.

One might argue that that's exactly how all the reflection stuff in commons-lang was designed from the beginning.
Otoh there are already a few places where Maps, Collections, etc get a special treatment. So why not also here?

@struberg
Copy link
Member Author

Note that this PR is worse than I thought because we already have a feature to bypass reflection! Use EqualsBuilder.setBypassReflectionClasses(List<Class<?>>).

This is just plain wrong. You cannot use setBypassReflectionClasses for e.g. Map without loosing the reflexive comparison of it's entries.

@struberg struberg dismissed garydgregory’s stale review December 28, 2024 18:12

Because the reason given is just plain wrong. See my last 2 comments. Either we adhere the Map, Collection, etc equals contracts (as indicated in the JavaDoc of the ReflectionEquals and others commons lang classes) then we need this handling. Or not, then we need to to remove all the references to Effective Java etc. But right now we have rather broken behaviour

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.

3 participants