RFR: 8303737: C2: Load can bypass subtype check that enforces it's from the right object type

Christian Hagedorn chagedorn at openjdk.org
Thu Sep 14 11:06:46 UTC 2023


On Wed, 6 Sep 2023 14:55:01 GMT, Roland Westrelin <roland at openjdk.org> wrote:

> This patch has 3 test cases:
> 
> TestAddPChainMismatchedBase.java
> TestAddPChainMismatchedBase2.java
> 
> Both fail with a "Base pointers must match" assert
> 
> TestLoadBypassesClassCast.java
> 
> fails with: assert(Universe::is_in_heap(result)) failed: object not in heap
> 
> All failures are related to 8139771 (Eliminating CastPP nodes at Phis
> when they all come from a unique input may cause crash). The problem
> there was that if a Phi merges say:
> 
> 
> (Phi (CastPP in) in)
> 
> 
> it was transformed to `in`. Doing that would cause the control
> dependency from the `Phi` on the null check that guards the `CastPP`
> to be dropped. As demonstrated by the test case of 8139771, a load
> could then float above the null check that should have guaranteed the
> object being read from was non null.
> 
> The fix for 8139771 was to transform:
> 
> 
> (Phi (CastPP in) in)
> 
> 
> to:
> 
> 
> (CastPP in)
> 
> 
> That `CastPP` inherits the type of the `Phi` and is flagged as a
> special kind of cast node so it's not eliminated if the `CastPP`
> doesn't narrow the type of its input. As a result some depending load
> cannot float above the cast.
> 
> Because that was found to be too conservative, code was added to try
> to eliminate cast node added as replacement of a `Phi` by looking for
> a dominating cast with the same input and a narrower type that the
> cast. The idea was that if there was some dominating null check then
> we would not need the cast. As TestLoadBypassesClassCast.java shows,
> this is flawed.
> 
> TestLoadBypassesClassCast.java demonstrates that what used to happen
> with a null check (a load that bypasses a null check) can still happen
> with a type check: in the case of the test, the load of `objectField`
> from `o` happens before `o` is checked to be of type `A` (the holder
> of the field). When `o` is not of type `A`, the result of the load is
> never used by compiled code but if the gc runs while the result of the
> load is live (as it does in the test case), gc code sees something
> that's not a valid oop and crashes.
> 
> In the test case, the same logic is duplicated in 2 branches of
> ifs. There are 2 loads of `objectField`, one in each copy of the
> logic. After transformation, the `objectField` loads lose their
> dependency on type checks for `A` and common above a safepoint causing
> a load from something that may not be a `A` to be live across a
> safepoint.
> 
> Now looking at one copy of the logic:
> 
> - right above the `objectField` load, a `Phi` (that merges the 2
>   banches of `if (flag) {` and has type non null Object) is tested to
>   be a subtype of `A`....

Nice analysis! Took me some time to grasp it but after thinking about it some more after our offline discussion, I think it's a good way to fix this problem. 

Apart from some small code style comments, it looks good to me!

src/hotspot/share/opto/castnode.cpp line 112:

> 110:     return false;
> 111:   }
> 112:   if (((ConstraintCastNode&) n)._dependency != _dependency) {

Maybe you can put `((ConstraintCastNode&) n)._dependency` into a variable as you are using it a couple of times.

src/hotspot/share/opto/castnode.hpp line 58:

> 56:   public:
> 57:   ConstraintCastNode(Node* n, const Type* t, ConstraintCastNode::DependencyType dependency,
> 58:                      const TypeTuple* types)

I suggest to rename `types` to `extra_types` to match the field name. Same in the other `make*` methods.

src/hotspot/share/opto/castnode.hpp line 107:

> 105:   bool higher_equal_types(PhaseGVN* phase, const Node* other) const;
> 106: 
> 107:   int nb_extra_types() const {

Maybe we could rename this to `extra_types_count()` to make it more clear.

src/hotspot/share/opto/castnode.hpp line 109:

> 107:   int nb_extra_types() const {
> 108:     return _extra_types == nullptr ? 0 : _extra_types->cnt();
> 109:   }

New line:
Suggestion:

  }

src/hotspot/share/opto/cfgnode.cpp line 2136:

> 2134:       // going away
> 2135:       Node* cast = nullptr;
> 2136:       const TypeTuple* extra_types = collect_types(phase, r, phi_type);

Completely optional but you could just directly use `bottom_type()` and `in(0)` inside `collect_types` instead of passing it with `r` and `phi_type`.

src/hotspot/share/opto/cfgnode.cpp line 2575:

> 2573: // Sort the types using an arbitrary order so a list of some types always hashes to the same TypeTuple (and TypeTuple
> 2574: // pointer comparison is enough to tell if 2 list of types are the same or not)
> 2575: const TypeTuple* PhiNode::collect_types(PhaseGVN* phase, const Node* r, const Type* phi_type) const {

I suggest to rename `r` -> `region`.

-------------

Marked as reviewed by chagedorn (Reviewer).

PR Review: https://git.openjdk.org/jdk/pull/15595#pullrequestreview-1626554813
PR Review Comment: https://git.openjdk.org/jdk/pull/15595#discussion_r1325769268
PR Review Comment: https://git.openjdk.org/jdk/pull/15595#discussion_r1325757382
PR Review Comment: https://git.openjdk.org/jdk/pull/15595#discussion_r1325741552
PR Review Comment: https://git.openjdk.org/jdk/pull/15595#discussion_r1325741930
PR Review Comment: https://git.openjdk.org/jdk/pull/15595#discussion_r1325774629
PR Review Comment: https://git.openjdk.org/jdk/pull/15595#discussion_r1325737586


More information about the hotspot-compiler-dev mailing list