First, not to sound ungrateful, I appreciate everything over at OpenJDK does and thank them for being excellent stewards of the ecosystem. Their fair and balanced approach, while putting backwards compatibility at the forefront, is something very rare these days.
> Legacy bugs have been fixed so it is exceptionally rare to need to break encapsulation to work around them.
I do think their line of thinking is quite optimistic. Take this famous bug: https://bugs.openjdk.org/browse/JDK-8207840 which was set to Resolved, Won't Fix and instead the recommend a newer API. Here's the thing, a lot of third party code still uses the older API. A major use case is attempting to call the SalesForce API using HTTPUrlConnection or a library that wraps this class... Good luck. Despite SalesForce being extraordinarily common, you simply can't call it using the JDK's standard classes as the SF API required HTTP PATCH.
The only workaround we can find, save building/patching an entire JDK, is at Runtime, to do some dirty things to the JDK with the Reflection and/or Unsafe APIs. We'd love to migrate every app to the latest JDK where HTTP PATCH might be supported, but RuntimePatching allows us to not have to cross that bridge.
As a matter of fact, we have a JVM Agent that intercepts class loads and can patch 3rd Party Libraries on the fly. This allows us to immediately deploy fixes to prod when things like Spring4Shell appear.
As the maintainer of a large piece of Java software that interacts with legacy code in over 50% of the codebase, this was a very scary read. It sounds like the entire basis for my software which tens of thousands of people rely on every day is soon to be "restricted". I cannot prepare my users for the fact that my software _will not work with no recourse_ in the future. I understand the JVM's need for safety, but this is personally destroying my livelihood. It's along the lines of telling Ruby users one day that monkey patching will be phased out in the next release.
I suppose the next layer down the chain is to start patching code with ASM. Ugh.
The old JVMs will still be available and you so will the Docker containers with them.
That said the writing has been on the wall since Java 9's project Jigsaw. If you have a product that depends on it, I'm surprised you were caught unaware. With more control over what is exported, the amount of compiler optimisations possible greatly increases.
Well new JVM rules comes with new JVM. So something gotta give. Leaving Java be potentially unsafe just because few customer relied on it would be irresponsible behavior on JDK maintainers part.
I'm not sure how current JVMs are "unsafe" though. If you go messing with the internals of third party classes, seems like any consequences should be obvious.
> If you go messing with the internals of third party classes, seems like any consequences should be obvious.
The point made in the article is that it's often third-party libraries that do this, breaking invariants in your code. And, with older JVMs, it won't be immediately obvious to you as a user of that library that this is happening.
Try loading a library that uses com.sun.* through reflection in a program running on JVM 9 or higher with default settings and you'll see an example. Maybe it hasn't happened to code that you personally wrote, but it has certainly happened this way to code that someone at OpenJDK wrote.
But that third party code may depend on some JVM internal through unsafe, which latter might change (and one really should not depend on internals) and break your code.
> To balance the need for integrity with both the circumstantial, convenience uses of JDK internals and the essential uses, Java gives the user – the application's owner (typically its author, maintainer, or deployer) – the final say on which strong encapsulation boundaries are in place and which should be ignored. This freedom is offered under the guiding principle that the ability of one component to encroach on the boundaries of another must be explicitly granted by the application. Libraries cannot choose to obtain encapsulation-busting "superpowers" without the knowledge and consent of the application's owner.
No, the CLI `--add-opens` CLI option will stay. In other words, applications will need to consciously enable the encapsulation-breaking stuff. Is that bad? Modern software moved to public APIs quite a bit ago. That said, if old applications want to use new JDKs, they will require quite some developement, yes.
One of Java's strong points has always been the amount of care to source and binary compatibility (Java 8 classes work fine on Java 20, source code only need minor modifications).
I welcome the new restrictions since they make Java more secure and future-proof, but they have a price. There's lots of (framework) code that makes use of those loopholes and many Java projects already have issues with maintenance since it's not the 'hot stuff' anymore.
Won't Project Panama solve this? One of the goals in JEP 424 is to provide performance competitive with sun.misc.Unsafe so that (it says later in the document) it can eventually be removed.
The fix is to go through the front door, not the back door.
Whatever deserialisation library/technique you use, it should have to use constructors just like everyone else, because that's where you can put your logic to make sure all your invariants hold.
The problem is Java's Serializable interface, which is a core part of the language. To the extent that it is implemented with VM black magic, and has an entire keyword (transient) in the languages syntax to support it.
It is possible to provide your own (de)serialization logic, and so you could, in principle, enforce your invariant there. Unfortunately, to provide custom logic you implement:
Notably, the deserialization method is not a constructor, which causes problems if you have final fields.
You can use the defaultReadObject method to invoke the JVM black magic, then perform your own invariant checks, but you need to know to do this. When the alternative of adding 'implements serializable' you will likely have developers forget that they need to add the checks.
On the glass half full side, at least classes need to opt in to serializeability. There is another universe, where classes are serializable by default.
Having said all of this, Java's serializable is a massive security hole that should never be given untrusted input.
The solution is code generation, which is more performant and less brittle than maintaining runtime reflection machinery to break encapsulation. Moshi is a good example of a library that does this.
29 comments
[ 3.5 ms ] story [ 86.2 ms ] thread> Legacy bugs have been fixed so it is exceptionally rare to need to break encapsulation to work around them.
I do think their line of thinking is quite optimistic. Take this famous bug: https://bugs.openjdk.org/browse/JDK-8207840 which was set to Resolved, Won't Fix and instead the recommend a newer API. Here's the thing, a lot of third party code still uses the older API. A major use case is attempting to call the SalesForce API using HTTPUrlConnection or a library that wraps this class... Good luck. Despite SalesForce being extraordinarily common, you simply can't call it using the JDK's standard classes as the SF API required HTTP PATCH.
The only workaround we can find, save building/patching an entire JDK, is at Runtime, to do some dirty things to the JDK with the Reflection and/or Unsafe APIs. We'd love to migrate every app to the latest JDK where HTTP PATCH might be supported, but RuntimePatching allows us to not have to cross that bridge.
As a matter of fact, we have a JVM Agent that intercepts class loads and can patch 3rd Party Libraries on the fly. This allows us to immediately deploy fixes to prod when things like Spring4Shell appear.
I suppose the next layer down the chain is to start patching code with ASM. Ugh.
That said the writing has been on the wall since Java 9's project Jigsaw. If you have a product that depends on it, I'm surprised you were caught unaware. With more control over what is exported, the amount of compiler optimisations possible greatly increases.
The point made in the article is that it's often third-party libraries that do this, breaking invariants in your code. And, with older JVMs, it won't be immediately obvious to you as a user of that library that this is happening.
> To balance the need for integrity with both the circumstantial, convenience uses of JDK internals and the essential uses, Java gives the user – the application's owner (typically its author, maintainer, or deployer) – the final say on which strong encapsulation boundaries are in place and which should be ignored. This freedom is offered under the guiding principle that the ability of one component to encroach on the boundaries of another must be explicitly granted by the application. Libraries cannot choose to obtain encapsulation-busting "superpowers" without the knowledge and consent of the application's owner.
No, the CLI `--add-opens` CLI option will stay. In other words, applications will need to consciously enable the encapsulation-breaking stuff. Is that bad? Modern software moved to public APIs quite a bit ago. That said, if old applications want to use new JDKs, they will require quite some developement, yes.
which may be forever
which might be an acceptable tradeoff if you're shovelling JSON to a websocket
not so good if you're a smart order router trying to hit a level that will be gone in under a microsecond
I suppose it's to avoid sneaky libraries that dynamically starts an agent to crack open the code of the JDK.
[1] https://bugs.openjdk.org/browse/JDK-8306275
He mentioned the Even class, but there didn't seem to be anything that looked to fix the lack of validation when reading from JSON?
If it's out of scope, why bring it up without mentioning further work (or am I missing something)?
The fix is to go through the front door, not the back door.
Whatever deserialisation library/technique you use, it should have to use constructors just like everyone else, because that's where you can put your logic to make sure all your invariants hold.
It is possible to provide your own (de)serialization logic, and so you could, in principle, enforce your invariant there. Unfortunately, to provide custom logic you implement:
Notably, the deserialization method is not a constructor, which causes problems if you have final fields.You can use the defaultReadObject method to invoke the JVM black magic, then perform your own invariant checks, but you need to know to do this. When the alternative of adding 'implements serializable' you will likely have developers forget that they need to add the checks.
On the glass half full side, at least classes need to opt in to serializeability. There is another universe, where classes are serializable by default.
Having said all of this, Java's serializable is a massive security hole that should never be given untrusted input.