Questions w.r.t. plans to restrict the FFM API
Maurizio Cimadamore
maurizio.cimadamore at oracle.com
Thu Apr 29 15:52:38 UTC 2021
Hi Mike,
a disclaimer first - while I understand that JEP 411 (Deprecate the
Security Manager for Removal) and JEP 412 (Foreign Function & Memory API
(Incubator)) partially _do_ overlap, I'd like to point out that the
approach proposed in JEP 412 has evolved rather independently from JEP
411 - and is, instead, an evolution of the command line option that has
been, in Java 15 and 16 required anyway when interacting with low lever
memory access features, or the foreign linker (this flag used to be
-Dforeign.restricted=permit).
So, I will refrain from commenting on security manager here, and I will
focus on the specifics of the FFM needs.
On 29/04/2021 15:48, Mike Hearn wrote:
> 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.
Here you are making an assumption: the kind of safety we want to enforce
here is not too important. JEP 412 goes in quite a bit of lengths in
showing exactly the opposite: having unrestricted access to native code
in the way that JNI allows today is a permanent hole in the safety of
the Java platform. This has ramifications not only in the terms of
crashes that users will get when interacting (badly) with such native
code (with potential to e.g. overwrite JVM's own data structure) but
also in terms of limiting or inhibiting certain optimizations: if JNI
code can set final fields, the optimizing compiler cannot trust them. So
that's the price you pay for having a platform which is effectively
unsafe by default (since JNI is enabled by default). "Putting an airbag
in a car" is just not the right analogy here, I'm afraid.
From here, the principle: access to potentially unsafe features should
be disabled _by default_ (unlike JNI). We can quibble over which
mechanism is used in order to opt-in (maybe a command line flag is not
the way, maybe it is) but we do need an opt-in, as opposed to just make
it unsafe by default.
>
> 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.
Again I see a certain tendency of viewing safety as just a synonym for
"better error recovery". As explain above, that's not it.
>
> Is a CLI flag the best way to ensure developers know they're risking the
> nice debugging experience?
Ok, this is a fair question - no objections here.
>
> 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.
I believe you are placing the burden of opting-in in the wrong place.
The places where opt-in will occur will be the "applications" which
package one or more libraries using FFM. These applications will already
come with launchers, or will be generated via tools like `jpackage`. I
don't think adding an extra flag "in the application launcher"
represents that much of an issue. For instance, I look at IntelliJ which
is installed on my system; it comes with a pretty sizeable "sh" launcher
which fires up the VM with the "right" configuration. All I'm saying is
that applications are already providing launchers which add required
options to the `java` launcher, according to their needs. What JEP 412
proposes doesn't seem to leave these use cases particularly worse off.
>
> 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.
Three things here:
* an application which supports plugins might not know, at startup,
which modules will require native access. This is an issue, I agree. But
it's just that we didn't go very far e.g. in the reflective support.
Maybe it will be possible, for a module that is "inside the bubble" to
grant (reflectively) native access to other modules.
* you don't see the flipside; consider now an application which supports
plugins but which, for whatever reason, _doesn't want_ plugins to access
native code. The proposed solution gives you a way to do that (if required).
* "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" - maybe, maybe not, who knows? I always have problem with
such general statements - what evidence do we have to back that claim
up? Does that change if, at some point, the same flag will be required
for JNI access too (which is something JEP 412 alludes to). One of the
reasons as to why this API is still incubating [1] is exactly that - to
allow for more feedback from real world use cases, and re-adjust based
on concrete feedback.
>
> 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.
Again, support for the mechanism described in JEP 412 is still raw, but
(i) the check doesn't cost much (optimized by C2 as part of
@CallerSensitive machinery), and (ii) we have other building blocks in
the pipeline: at some point we envision that it should be possible to
mark modules requiring restricted methods as "native" (this might be
done implicitly, by javac, or explicitly, in the module-info). When such
tagging of modules occurs, we are now in a position to "fail fast": if a
module is native, but is not listed in the command line flag we fail as
soon as the application starts (as opposed to when the first restricted
call occurs). Of course for stuff in the classpath there's not much we
can do.
>
> 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.
True, this is a sore point - the mechanism we have now is not very
friendly w.r.t. shuffling code between modules. That said, again, this
is mostly something that the application packager will have to think
about - and the actions to fix these kind of mismatches should be
trivial enough. Not to completely dismiss your point, but I think we
have to be honest about whose usability are we concerned about.
>
> 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:
Again, crash safety is NOT the only thing we're after here - JEP 412 is
pretty clear about that. And arguing against/for SM removal is not for
this mailing list (as I explained JEP-412 and JEP-411 evolved rather
independently).
>
> 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.
Yes, this is how JNI works, and is "as broken" as JNI. I've explained
above why it is much better (maybe not for you specifically, but for the
Java platform as a whole) to have potentially unsafe behavior disabled
by default.
>
> 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.
I will read on ;-)
>
> 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.
I believe this is the real deal in your email; let's leave aside
considerations about crash-safety (it's not about that), or speculation
about how many clients will migrate from JNI to the FFM API (we simply
have no data to back any such claims at this point), or whether SM
should be removed or not (which is outside the scope of this mailing
list and JEP). The question is, _given we want an opt in mechanism_ is a
command line flag the best way to provide such an opt in?
Your proposal of putting the FFM API into a separate module is a
credible alternative: let's consider the FFM a low level API, and stash
it in a different module. This module, like incubating modules, will be
disabled by default. When added to the module graph, you will get some
warning at runtime.
Does this meet the criteria? Yes and no. Let me expand on that.
* unsafe API are disabled by default - that's good
* as you say, it relies on the concept of modules not resolved by
default, which we already have
but
* the entire FFM API (even its safe bits - basically the entire memory
access API) are now disabled by default
* by putting things in a separate module will prevent us from e.g.
adding more awareness to the new API points from class in in java.base -
e.g. it would be nice to have j.l.Integer.LAYOUT -> MemoryLayout - but
if MemoryLayout is defined elsewhere we can't do that (there are at
least another 5-6 examples like that, and there will be more, I'm sure,
as soon as the FFM API will land somewhere in java.base).
* if the module is not added to the graph, you get a generic sounding
error like ClassNotFoundException - I think we'd like something more
precise, like "native access X in class Y cannot occur because FFM
module is disabled". But maybe this is just implementation.
You might be tempted to say (to avoid some of these problems): let's
split the safe FFM API from the unsafe FFM API, and put the unsafe FFM
API in a different module. This has been considered (and actually done
before!). The problem with this approach is that now users have no clue
that e.g. from a MemoryAddress they can "unsafely" obtain a
MemorySegment because the unsafe method that lets them do that is not on
MemoryAddress, it's somewhere else. API discoverability, IDE completion
and the likes will suffer a great deal by such splitting.
I'm not completely closing the door to this idea (which has been
considered, internally) - I'm merely pointing out the fact that it's no
silver bullet; if we go down the path I think it will have to be an all
or nothing approach (e.g. either the entire FFM API goes in a separate
module, or none will), and moving the entire API into a separate module
will have consequences on potential evolution of API in the Java SE API.
For instance, we could provide support for closing a byte buffer - by
associating a direct ByteBuffer with a ResourceScope, on creation. But
if ResourceScope is in a different module... we can't do that (or we
will have to add such a method somewhere else - where users will be a
lot less likely to find it).
Maurizio
[1] - https://mail.openjdk.java.net/pipermail/jdk-dev/2021-April/005383.html
> 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