Questions w.r.t. plans to restrict the FFM API
Mike Hearn
mike at plan99.net
Thu Apr 29 14:48:34 UTC 2021
In the newest JEP there is an extended discussion of making the current
restriction mechanism permanent. The proximity to the JEP related to
deprecating the SecurityManager confused me somewhat, because whilst the
need to restrict native code and memory access is clear when sandboxing,
when there's no sandbox the benefits are less clear. I'm trying to figure
out what the concrete end result of restricting FFM or JNI will really be
and if another command line flag is the best way to achieve it.
The word "safe" has had a variety of meanings in the past but with the
demise of the SM "safe" is only really means robust against developer
mistakes. It's safety in the sense of rounded corners or airbags in cars
and not in the sense of locked doors. And ultimately that is really about
the quality of the debugging experience, or perhaps robustness in the field
against unexpected bugs that can be caught and ignored e.g. IntelliJ is
quite good at recovering from exceptions in plugins.
So the --enable-native-access flag is about signalling to the VM that I'm
willing to make the crash experience worse in order to use a particular
module. It can't actually *stop* anything because the JVM is giving up on
restricting malicious code, so the flag only has meaning if people are
playing along. Without the SM a malicious module can subvert the JVM in
many ways e.g. by overwriting the application's own JAR file, or loading an
agent, or attaching to the VM. So the flag is a form of confirmation, "are
you sure? yes/no" type dialog but in the CLI context. If someone wants to
use a module that needs native code then they're always going to set it.
Is a CLI flag the best way to ensure developers know they're risking the
nice debugging experience?
Pros:
1. It's an explicit action that the user must take to opt-in.
Cons:
1. Existing module metadata formats (POMs) don't support requesting command
line flags. Therefore, library authors must communicate this via their
documentation, and that need must be propagated up through the
documentation of every library at every level of the dependency hierarchy.
This is a lot of work and opens up many opportunities to forget it or
overlook it, especially if you bring in a new version via a routine upgrade
and don't re-check the upstream README. Due to the freeform nature of
documentation there's no way to improve this situation with tooling.
2. Dynamically loaded plugins cannot alter the JVM command line flags.
Thus, there is no way to make plugins that use native code with this
approach. That's a major loss of functionality. A significant part of the
Java ecosystem will *never* abandon JNI if the replacement requires the
user to manually change JVM flags to use a new plugin. Consider for
example, how painful this would be for Gradle or Maven, where almost all
functionality is in the form of plugins. If a build system plugin is 1 line
of code to install when using JNI, or an extended operation involving
reconfiguring global settings files for every developer on the project when
using FFM, plugin authors will just continue using the old JNI API if they
care about their user experience.
3. It's (presumably?) not statically enforced. The check that a piece of
code has access to a restricted API would happen at the time it's called,
using reflection. This means either the API should be used carefully by
users to avoid hitting too many access control checks, e.g. by not
constantly creating new objects but rather by using a capability-oriented
approach.
4. If a module is split/refactored the users will all break even if a
"requires transitive" module is left in place under the old name, because
the --enable-native-access will apply to the empty transitive module rather
than the new module where the FFM API is used.
5. That could be fixed by using stack walks but .... hmm, wait a minute.
There's a feature in the JVM already that does this type of thing and is
explicitly designed for making code safe in every sense of the word.
It's hard to escape the feeling that this flag would end up as a sort of
half-baked reinvention of the SecurityManager but less useful, because with
the SM we can actually sandbox our libraries and protect ourselves from
increasingly common supply chain attacks. It gives safety in *all* senses
of the word, not just crash-safety. I won't try and argue for a stay of
execution for the SM here, even though it seems more relevant now than in
the past. But it does seem worth re-considering this flag. A couple of
alternatives might be:
1. No flag. If a library wants to use the unsafe FFM API then it just uses
it and if my JVM segfaults, I realise that library is using buggy native
code so switch to a competing library that's more robust. This seems fine.
It's the same as how every other language/runtime does it, it's the same as
how JNI works and frankly it's never really been a problem in my own work.
2. Don't deprecate the SM. Instead upgrade it to fix its problems and then
put the unsafe FFM API under a permission. Random libraries off of Maven
Central can be treated as genuinely "unsafe", which would make CISOs happy.
But of course, it's also a lot of work.
3. Putting the unsafe FFM API into a separate module so usage of it can be
statically detected using JPMS metadata. Then it's up to the build system
or IDE to show a warning if I add a dependency on a library that requires
unsafe FFM API. This is better because the build system controls my
dependencies, so it's helpful if it's also the place to ban dependencies
for various reasons. Services like Maven/JetBrains Package Search can show
unsafe modules in the UI this way too. Most importantly it solves the
plugin problem. Now plugin loaders can study the module graph and (if they
want to) alert the user if the module might be "unsafe". In practice I
suspect nobody would, because - again due to the lack of the SM - the
warning isn't actually actionable and the true meaning of "unsafe" would be
hard to communicate. Arguably this type of safety doesn't matter to end
users at all, because they won't be the ones debugging crashes. But there
is at least an option to do so.
If the functionality does stay behind a flag then I suspect the result will
be that many developers just stick with JNI, as despite the far inferior
API it will provide a superior experience for users. Alternatively people
will just stay on the classpath, seeing this as one more reason to avoid
the JPMS, because everyone will just end up with
--enable-native-access=ALL-UNNAMED in their "canned" set of JVM flags.
Attempts to restrict JNI will all face the same problems.
More information about the panama-dev
mailing list