null hygiene of Q-types: are we safe yet?

Frederic Parain frederic.parain at oracle.com
Wed Apr 21 12:40:48 UTC 2021



> On Apr 20, 2021, at 1:17 PM, John Rose <john.r.rose at oracle.com> wrote:
> 
> The current Valhalla JVM prototype allows `CONSTANT_Class[QFoo;]` as
> well as `CONSTANT_Class{Foo]`.
> 
> It consults the Q-vs-non-Q distinction by means of a bit-field in the
> CP, called `JVM_CONSTANT_QDescBit` (0x80), which is adjoined bitwise
> to the `CONSTANT_Class` tag value (0x07).
> 
> The same information is redundantly encoded in the CP by always
> recording the original symbol of a `CONSTANT_Class` item even after
> resolution; if it is of the form “Q…;” then that provides the same
> information as the QDescBit.

The QDescBit is only used by the interpreter, to avoid having to do 
navigate through the CP, retrieving the class name entry, then the
Symbol and then to parse the first character of the char array inside.
Testing a bit in the tag is way simpler when writing assembly code.

The rest of the VM is is using is_Q_signature() instead.


> 
> One use of the QDescBit is to gate the selection of ref-mirror
> vs. val-mirror (the Q-mirror is the val-mirror).

Not really. 
First of all, by “mirror” I assume you’re speaking about the VM
metadata (InstanceKlass/InlineKlass) because the VM doesn’t
use the Java mirrors.

The current model is LPoint$ref;/QPoint$val; so there’s no need to
look at the Q marker. Point$ref has its own metadata (an InstanceKlass
instance) and Point$val has also its own metadata (an InlineKlass
instance). Most of the VM just passes a Klass* or an InstanceKlass*
to indicate which projection is being used.

Before this model, we already had a L/Q model with two signatures but
a single metadata for both projection. The QDescBit dates was implemented
at this time, when we had to add side channels to carry the L/Q distinction
alongside the Klass*. When model has been changed to LPoint$ref;/QPoint$val;
this code was still working and was kept in place.

With the return if the L/Q model, we are already in the process of
adding back the side channels. Being lazy on the implementation
of the interpreter means the QDescBit will retrieve its original purpose.


> 
> The “Q bit” is also used in `StackMapReader::parse_verification_type`,
> to decide which “flavor” of class type to hand to the verifier.  (It
> consults CP::klass_name_at and then Symbol::is_Q_signature, not the
> QDescBit.  Perhaps this should be cleaned up one way or the other, to
> use a common convention for detecting Q-ness.)
> 
> The `checkcast` bytecode excludes `null` when it sees `QDescBit` set,
> whether the CP entry is resolved or not.  This preserves null-hygiene
> within Q-types.
> 
> (I do have some quibbles with the exact semantics of the bytecode,
> mainly related to future-proofing.  Specifically, I think a `null`
> query should be specified to perform class loading, even though,
> today, the answer is no longer in question at that point.  Should
> `instanceof` do a similar trick?  No, `null` could be a valid inline
> value some day, but you still cannot tell which class of `null` it is:
> all `nulls` look the same.)

The current semantic of a Q-descriptor in the VM is “null free primitive class”,
and according to the extended semantic of checkcast, if the ToS is the
null reference, an exception will be thrown for any checkcast which has a
Q-signature in argument, so there’s currently no need to load the class.

With the change in the semantic of the Q-descriptor, class loading will
become necessary (but this is not the current model yet).

> 
> The verifier makes a distinction between Q-types and non-Q-types,
> loading Q-descriptors with a special verification type
> (`VT::inline_type`) in `SMR::parse_verification_type` as noted above.
> The verifier generally keeps references and inlines separate.  This
> has the effect of keeping `null`-dirty L-types from contaminating
> Q-types during verification.
> 
> `VT::is_ref_assignable_from_inline_type` allows a Q-type to promote to
> its own regular L-type (ref-type) or those of any of its supers
> (including Object).  There is no implicit “demotion” from a regular
> L-type down to a Q-type, in the verifier; this must always be done (as
> with `null`) using `checkcast QFoo;`.
> 
> We want null-safety of Q-types to be strong enough so that when the
> interpreter runs Q-values through Q-descriptors of method arguments,
> nulls have already been excluded before (or during) method entry.  By
> the time the JIT kicks in (either C1 or C2) we need Q-types to
> participate in scalarized calling conventions (for
> compiled-to-compiled code).  

Just a note here: C1 doesn’t use scalarized arguments, only C2 does.
The only place where C1, and the interpreter, scalarize a primitive object
is when returning a non-null primitive object, because they don’t know
if the caller was a C2 compiled method or not and there’s no adapters
on return. So they both conservatively scalarize the return value and
pass it in registers alongside with a reference to the heap allocated
version. 

> This doesn’t work well if there are
> wandering nulls that show up for Q-values at method entry.  In
> particular, it’s not good enough to catch nulls _later than method
> entry_ in the interpreter, but _during method entry_ when the code
> compiles.  And the above scheme does seem satisfy these goals.
> 
> For bytecode behaviors that are simpler than method argument passing
> we might assist the verifier (as needed) by adding implicit `null`
> checks where Q-descriptors appear, for storing into flattened fields,
> or returning a Q-value from a method.
> 
> (We will need to revisit the problem of applying extra checks to
> method arguments when we do specialized generics, because any of the
> arguments to a generic method, and/or a method of a generic class or
> interface, might be contextually specialized.  So we haven’t
> completely escaped from the complexity of per-argument checks
> in the interpreter.  I do think these per-arguments checks can
> be safely done on method entry, in most cases, which localizes
> the complexity somewhat.)

This is another problem to be addressed for generic specialization.
We might have options to consider like generating small entry point
stubs at specialization time that the interpreter could use before
jumping to its regular entry point. Those stubs would have all the
required checks applied to the right arguments on stack, avoiding
the complex logic to retrieve each individual check and the position
of the corresponding argument on the stack.

> 
> I think all of the above is pretty null-safe.  So, where are the
> remaining “cracks in the armor”?


We had a robust null-safe environment thanks to the verifier and the
semantic of the Q-descriptor meaning null-free primitive class.

We were concerned with the modification of the semantic, not meaning
null-free anymore, but load-and-look (with no details about what to look
for or what would be the outcome of doing it). Loosing this verifiable null-free
property would have forced the VM to do costly null-checks on many
locations, including invocation points.

Your Monday’s proposal that the Q-descriptor, when applied to primitive classes,
would still mean null-free restores this verifiable property. The verifier would not
directly look for null pollution, but would ensure that any conversion to a 
Q-marked-primitive-class will go through a checkcast which will prevent any null
reference to leak into the null-free space we are relying on. Harold and I discussed
this model and we are confident the verifier will be able to continue to guarantee
null-safety for primitive classes.

Fred





More information about the valhalla-dev mailing list