[foreign-abi] minimizing the surface of restricted native access

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Wed Nov 27 15:19:20 UTC 2019


Hi,
over the last few weeks we had many discussions on the fact that some of 
the uses of the memory access API, especially when they intersect 
foreign function access, are inherently unsafe. Examples of these include:

* obtaining a null pointer
* forge constant pointers (e.g. a MemoryAddress whose value is 42)
* have memory access VarHandles which deal with MemoryAddress (e.g. to 
read and write addresses)
* interact with pointers which come from native code

In this email I'd like to focus on which strategies we could adopt to 
try and make the above operations as _safe_ as possible.

Let's start with nulls; every bit of code working with some native 
libraries will, at some point, need to utter NULL, right? Unfortunately, 
the memory access API does not (even under the system-abi branch) offer 
safe access to the null MemoryAddress. The reasoning behind it is that, 
since the address is NULL, if you tried to dereference it, you could 
crash the VM; granted we could special case it so that we threw a NPE 
instead. But then, what happens if you offset the NPE? Would the 
offsetted address still be covered by the NPE guarantee?

I think a more stable way to get there is to resort to the notion of a 
Nothing memory segment (this is not a new idea - in fact, even before I 
joined the project, the Panana/foreign work had a similar concept of 
Nothing region). What is the Nothing segment? It is a 'root' native 
segment (read: it cannot be closed), whose base address is 0 and whose 
length is also 0. Why is this segment interesting? Two reasons:

* the baseAddress() of the Nothing segment is... the NULL pointer
* every address derived from the Nothing segment is, by definition, out 
of bounds

The latter observation is key to provide safety: any attempt to 
dereference _any_ address (including NULL itself) derived from the 
Nothing segment will result in a IOOB exception! But wait - this means 
that we can also support use cases where the user wants to 'forge' an 
address with a given constant value. For instance:

Nothing.baseAddress().offset(42) // creates a constant address with value 42

This is pretty straightforward. Now, as long as the user does not 
dereference these addresses, everything is fine - that is, a NULL or a 
'forged' address can be passed to a native method handle. So far so good 
- we have covered two important use cases w/o the need of any extra 
restricted native operations.

But we can pull this string some more; let's consider the case of having 
a VarHandle which:

- (read) turned long values into MemoryAddress
- (write) turned MemoryAddress into a long

Such an abstraction would be immensely helpful to define e.g. native 
bindings. But, again, so far we have decided _against_ providing such a 
capability because, if we provided that, it would then be possible to, 
indirectly, forge pointers - e.g. by writing a long value into a memory 
location and then reading that same value as a MemoryAddress.

But, thanks to the Nothing region, we now have a way out: the 
dereference operation can be made safe, if the MemoryAddress generated 
by the VarHandle (upon read) is backed by the special Nothing region. 
This means that the user won't be able to dereference such an address, 
but this might be ok in cases where the address just needs to be written 
somewhere else, or passed to some native library.

In fact, more generally, we can use the same trick to model _all_ 
MemoryAddress(es) that are generated in the ABI layer (by native method 
handles). Granted, this will add some restrictions (a client cannot 
dereference or close them), but it will also help making the client code 
_safer_. In most cases, these limitations will end up being sensible 
ones, as most C libraries tend to fall into one of the following two 
categories:

1) caller is responsible for allocation; API is typically called with 
some 'out' parameter (e.g. fill_array(&buf) )
2) callee is responsible for allocation and deallocation; that is API 
provide symmetric points for allocate/deallocate (e.g. 
create_foo/destroy_foo)

In (1), the Java code calling the native API will create the 
MemorySegment itself; as such it can fully manage its spatial/temporal 
bounds.
In (2), the Java code deals with _opaque_ addresses, but such APIs are 
typically providing all required entry points so that the client never 
has to dereference or free directly.

So, I'm confident that the tricks explained so far will, in practice, 
handle the vast majority of the use cases _without any need for 
restricted native operations_. There are however cases where the caller 
knows _exactly_ what the spatial/temporal bounds of a returned address 
are; consider the following classic example:

char *strcat(char *dest, const char *src);

Here, the client is required to allocate a 'dest' buffer that is _big_ 
enough to hold the concatenated string. A pointer to the same buffer is 
then returned to the caller, as the documentation states:

>
> The  strcat() function appends the src string to the dest string, 
> over‐writing the terminating null byte ('\0') at the end of dest,  
> and  then
> adds  a  terminating  null  byte.  The strings may not overlap, and 
> the dest string must have enough space for the  result. [...]
> The strcat() [...] function return a pointer to the  resulting string 
> dest.


So here we have a problem: the Java code calling strcat has a fully 
managed segment for the buffer; but then strcat is called, and a _new_ 
address is returned; since the new address is generated by native code, 
it has to be modeled as an address backed by the special Nothing segment 
(see above). This means that the result of strcat is *less* powerful 
than its input (even though they are effectively the same address!).

A similar problem would occur when storing pointers inside a struct; 
let's say a client creates a struct and saves a pointer to a known 
segment B inside that struct. If the pointer is later retrieved, the var 
handle used to dereference the pointer struct field will return an 
address backed by the Nothing segment; so, again, we are in an 
asymmetric situation where writing then reading gives us an address that 
is not the same as the one we started with.

These asymmetries can, to some extent, still be (safely!) cured. Let's 
assume we add the the following API point to MemoryAddress:

MemoryAddress rebase(MemorySegment)

The semantics of this method is simple: if the address is contained in 
the supplied segment, this method returns a _new_ address that is 
re-interpreted as an offset into the supplied segment.

It is easy to see how rebase() can be used to fix all the asymmetries 
discussed above; for instance, when calling strcat, the client already 
knows the segment associated with the 'dest' parameter (and hence with 
the return value of that function). So, rebasing the returned address to 
the original segment will make the address fully trusted again (a 
similar approach can be made to work when accessing struct pointer 
fields, we leave that as an exercise to the reader).

But what if a client really wants to dereference a _random_ pointer 
generated by a library? In this case, the client has no well-known 
segment which can be used as a rebase target, so, what can be done? This 
is where we need to veer into restricted native access territory, and 
provide some escape hatch by which an untrusted address can be made 
trusted again. There are many ways to do that; perhaps the simplest is 
to add a new method on MemoryAddress:

MemoryAddress asUnchecked()

which returns a new memory address which point to same location as the 
original address, but whose access is neither spatially, nor temporally 
checked. Another way to get there would be to expose the dual of the 
Nothing region - e.g. the Everything region, in which no address can 
ever be out of bounds (obtaining such a segment would, again, be a 
restricted native operation which requires higher privileges). Then, the 
untrusted address can be made trusted by simply rebasing the untrusted 
address against the Everything segment.

This model has a pleasing property: access to all memory addresses 
(whether managed or coming from native code) is always _safe_ by 
default, and we have _safe_ way to express most of the idioms which 
frequently occur when working with native libraries. This has few 
advantages: first, it minimizes uses of restricted native operations 
(thus minimizing the needs of e.g. extra runtime flags). Secondly, it 
makes transitions between safe and unsafe _explicit_ and thus deadly 
simple to spot (for instance, a client could easily search for all 
usages of the 'MemoryAddress::asUnchecked()' method to narrow down 
potential issues).

There is, of course a limitation: this model assumes that a client will 
never need/want to 'free' a random pointer obtained by calling a native 
library: since the Nothing segment is always alive (by definition), it 
cannot be closed. While this restriction will be ok in most cases, there 
will be some APIs requiring this - examples are strdup, vasprintf, which 
require the client to specifically 'free' the given address. That said, 
this problem doesn't seem too difficult to solve: for instance a client 
can request a native MethodHandle which points to the 'free' stdlib 
function, and then can call it directly on the required address! I think 
such an approach would actually be preferable to an approach where 
MemorySegment::close() secretly implies calling free() on some address - 
which might be the case now, but might not be later on (e.g. if we 
replace the allocator used internally by the memory access API).

Thoughts?

Maurizio















More information about the panama-dev mailing list