Ignore:
Timestamp:
Jan 10, 2020, 10:49:51 AM (5 years ago)
Author:
[email protected]
Message:

ObjectAllocationSinkingPhase doesn't model pointers to allocations in control flow properly
https://bugs.webkit.org/show_bug.cgi?id=204738
<rdar://problem/57553238>

Reviewed by Yusuke Suzuki.

JSTests:

  • stress/allocation-sinking-must-model-allocation-pointers-properly-2.js: Added.

(assert):
(v9):

  • stress/allocation-sinking-must-model-allocation-pointers-properly-3.js: Added.

(assert):
(v9):

  • stress/allocation-sinking-must-model-allocation-pointers-properly-4.js: Added.

(bool):
(effects):
(escape):
(bar):

  • stress/allocation-sinking-must-model-allocation-pointers-properly.js: Added.

(alwaysFalse):
(sometimesZero):
(assert):
(v9):

Source/JavaScriptCore:

Allocation sinking phase conducts a points to analysis. It uses this
information for programs like:

`
1: NewObject
2: NewObject
3: PutByOffset(@2, @1, "x")
4: GetByOffset(@2, "x")
`

It solves the points to problem knowing @4 points to @1.

It tracks this data in the LocalHeap data structure. This is used to track
the heap across blocks, and it includes a merge function to handle control
flow merges. However, this merge function would not always merge the pointer
sets together. It sometimes would merge them together, since it had a fast
path check inside merge, which would just copy the contents of the block to be
merged with itself if it were this block's first time merging. This fast path happened
to hide the bug in general case merge code. If we didn't take this fast path,
we would just never transfer pointer sets from predecessor to successor. This
could lead to all kinds of issues, including using the incorrect phantom node
in IR instead of its materialized version. It could also lead to the phase not
sinking objects it is capable of sinking.

This patch makes it so that we merge together the pointer sets. We always add
new pointers to the set. So in pointer A->B, if the set has yet to see A, we
add it. If the set already contains pointer A->B, and we encounter a new
pointer A->C, or if we encounter a merge without any A->* pointer, we mark
the A pointer as top, marking it A->TOP. We do this to ensure that we fixpoint.
We're guaranteed that m_pointers is monotonically increasing (module liveness
pruning, which is a constant). And once something is TOP, it never becomes
anything else. (Instead of marking a pointer top, we used to just remove it
from the set, but this has issues, as it could lead to us ping-ponging in
our fixpoint analysis, add, remove, add, remove, etc.)

So the merge rules are:
{A->B} merge {A->B} => {A->B}
{A->B} merge {A->C} => {A->TOP}
{A->B} merge {A->TOP} => {A->TOP}
{A->B} merge {} => {A->TOP}


Thanks to Samuel Groß of Google Project Zero for identifying this bug.

  • dfg/DFGObjectAllocationSinkingPhase.cpp:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/JavaScriptCore/dfg/DFGObjectAllocationSinkingPhase.cpp

    r254252 r254349  
    11/*
    2  * Copyright (C) 2015-2019 Apple Inc. All rights reserved.
     2 * Copyright (C) 2015-2020 Apple Inc. All rights reserved.
    33 *
    44 * Redistribution and use in source and binary forms, with or without
     
    403403    {
    404404        auto iter = m_pointers.find(node);
    405         ASSERT(iter == m_pointers.end() || m_allocations.contains(iter->value));
     405        ASSERT(iter == m_pointers.end() || (!iter->value || m_allocations.contains(iter->value)));
    406406        return iter == m_pointers.end() ? nullptr : iter->value;
    407407    }
     
    500500        }
    501501
    502         mergePointerSets(m_pointers, other.m_pointers, toEscape);
     502        {
     503            // This works because we won't collect all pointers until all of our predecessors
     504            // merge their pointer sets with ours. That allows us to see the full state of the
     505            // world during our fixpoint analysis. Once we have the full set of pointers, we
     506            // only mark pointers to TOP, so we will eventually converge.
     507            for (auto entry : other.m_pointers) {
     508                auto addResult = m_pointers.add(entry.key, entry.value);
     509                if (addResult.iterator->value != entry.value) {
     510                    if (addResult.iterator->value) {
     511                        toEscape.addVoid(addResult.iterator->value);
     512                        addResult.iterator->value = nullptr;
     513                    }
     514                    if (entry.value)
     515                        toEscape.addVoid(entry.value);
     516                }
     517            }
     518            // This allows us to rule out pointers for graphs like this:
     519            // bb#0
     520            // branch #1, #2
     521            // #1:
     522            // x = pointer A
     523            // jump #3
     524            // #2:
     525            // y = pointer B
     526            // jump #3
     527            // #3:
     528            // ...
     529            //
     530            // When we merge state at #3, we'll very likely prune away the x and y pointer,
     531            // since they're not live. But if they do happen to make it to this merge function, when
     532            // #3 merges with #2 and #1, it'll eventually rule out x and y as not existing
     533            // in the other, and therefore not existing in #3, which is the desired behavior.
     534            //
     535            // This also is necessary for a graph like this:
     536            // #0
     537            // o = {}
     538            // o2 = {}
     539            // jump #1
     540            //
     541            // #1
     542            // o.f = o2
     543            // effects()
     544            // x = o.f
     545            // escape(o)
     546            // branch #2, #1
     547            //
     548            // #2
     549            // x cannot be o2 here, it has to be TOP
     550            // ...
     551            //
     552            // On the first fixpoint iteration, we might think that x is o2 at the head
     553            // of #2. However, when we fixpoint our analysis, we determine that o gets
     554            // escaped. This means that when we fixpoint, x will eventually not be a pointer.
     555            // When we merge again here, we'll notice and mark o2 as escaped.
     556            for (auto& entry : m_pointers) {
     557                if (!other.m_pointers.contains(entry.key)) {
     558                    if (entry.value) {
     559                        toEscape.addVoid(entry.value);
     560                        entry.value = nullptr;
     561                        ASSERT(!m_pointers.find(entry.key)->value);
     562                    }
     563                }
     564            }
     565        }
    503566
    504567        for (Node* identifier : toEscape)
     
    535598        // Pointers should point to an actual allocation
    536599        for (const auto& entry : m_pointers) {
    537             ASSERT_UNUSED(entry, entry.value);
    538             ASSERT(m_allocations.contains(entry.value));
     600            if (entry.value)
     601                ASSERT(m_allocations.contains(entry.value));
    539602        }
    540603
     
    566629    }
    567630
    568     const HashMap<Node*, Node*>& pointers() const
    569     {
    570         return m_pointers;
    571     }
    572 
    573631    void dump(PrintStream& out) const
    574632    {
     
    577635            out.print("    #", entry.key, ": ", entry.value, "\n");
    578636        out.print("  Pointers:\n");
    579         for (const auto& entry : m_pointers)
    580             out.print("    ", entry.key, " => #", entry.value, "\n");
     637        for (const auto& entry : m_pointers) {
     638            out.print("    ", entry.key, " => #");
     639            if (entry.value)
     640                out.print(entry.value, "\n");
     641            else
     642                out.print("TOP\n");
     643        }
    581644    }
    582645
     
    672735    {
    673736        NodeSet reachable;
    674         for (const auto& entry : m_pointers)
    675             reachable.addVoid(entry.value);
     737        for (const auto& entry : m_pointers) {
     738            if (entry.value)
     739                reachable.addVoid(entry.value);
     740        }
    676741
    677742        // Repeatedly mark as reachable allocations in fields of other
     
    811876                // a live allocation.
    812877                //
    813                 // This means we can accidentaly leak non-dominating
     878                // This means we can accidentally leak non-dominating
    814879                // nodes into the successor. However, due to the
    815880                // non-dominance property, we are guaranteed that the
     
    820885                m_heap.pruneByLiveness(m_combinedLiveness.liveAtTail[block]);
    821886
    822                 for (BasicBlock* successorBlock : block->successors())
     887                for (BasicBlock* successorBlock : block->successors()) {
     888                    // FIXME: Maybe we should:
     889                    // 1. Store the liveness pruned heap as part of m_heapAtTail
     890                    // 2. Move this code above where we make block merge with
     891                    // its predecessors before walking the block forward.
     892                    // https://bugs.webkit.org/show_bug.cgi?id=206041
     893                    LocalHeap heap = m_heapAtHead[successorBlock];
    823894                    m_heapAtHead[successorBlock].merge(m_heap);
     895                    if (heap != m_heapAtHead[successorBlock])
     896                        changed = true;
     897                }
    824898            }
    825899        } while (changed);
Note: See TracChangeset for help on using the changeset viewer.