diff options
Diffstat (limited to 'js/src/devtools/rootAnalysis/t')
-rw-r--r-- | js/src/devtools/rootAnalysis/t/exceptions/source.cpp | 57 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/exceptions/test.py | 21 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/graph/source.cpp | 90 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/graph/test.py | 54 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/hazards/source.cpp | 566 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/hazards/test.py | 121 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp | 76 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/sixgill-tree/test.py | 63 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/sixgill.py | 70 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/suppression/source.cpp | 72 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/suppression/test.py | 21 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/testlib.py | 231 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/types/source.cpp | 120 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/types/test.py | 16 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/virtual/source.cpp | 292 | ||||
-rw-r--r-- | js/src/devtools/rootAnalysis/t/virtual/test.py | 91 |
16 files changed, 1961 insertions, 0 deletions
diff --git a/js/src/devtools/rootAnalysis/t/exceptions/source.cpp b/js/src/devtools/rootAnalysis/t/exceptions/source.cpp new file mode 100644 index 0000000000..8d38a790a1 --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/exceptions/source.cpp @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Simply including <exception> was enough to crash sixgill at one point. +#include <exception> + +#define ANNOTATE(property) __attribute__((annotate(property))) + +struct Cell { + int f; +} ANNOTATE("GC Thing"); + +extern void GC() ANNOTATE("GC Call"); + +void GC() { + // If the implementation is too trivial, the function body won't be emitted at + // all. + asm(""); +} + +class RAII_GC { + public: + RAII_GC() {} + ~RAII_GC() { GC(); } +}; + +// ~AutoSomething calls GC because of the RAII_GC field. The constructor, +// though, should *not* GC -- unless it throws an exception. Which is not +// possible when compiled with -fno-exceptions. This test will try it both +// ways. +class AutoSomething { + RAII_GC gc; + + public: + AutoSomething() : gc() { + asm(""); // Ooh, scary, this might throw an exception + } + ~AutoSomething() { asm(""); } +}; + +extern Cell* getcell(); + +extern void usevar(Cell* cell); + +void f() { + Cell* thing = getcell(); // Live range starts here + + // When compiling with -fexceptions, there should be a hazard below. With + // -fno-exceptions, there should not be one. We will check both. + { + AutoSomething smth; // Constructor can GC only if exceptions are enabled + usevar(thing); // Live range ends here + } // In particular, 'thing' is dead at the destructor, so no hazard +} diff --git a/js/src/devtools/rootAnalysis/t/exceptions/test.py b/js/src/devtools/rootAnalysis/t/exceptions/test.py new file mode 100644 index 0000000000..a40753d87a --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/exceptions/test.py @@ -0,0 +1,21 @@ +# flake8: noqa: F821 + +test.compile("source.cpp", "-fno-exceptions") +test.run_analysis_script("gcTypes") + +hazards = test.load_hazards() +assert len(hazards) == 0 + +# If we compile with exceptions, then there *should* be a hazard because +# AutoSomething::AutoSomething might throw an exception, which would cause the +# partially-constructed value to be torn down, which will call ~RAII_GC. + +test.compile("source.cpp", "-fexceptions") +test.run_analysis_script("gcTypes") + +hazards = test.load_hazards() +assert len(hazards) == 1 +hazard = hazards[0] +assert hazard.function == "void f()" +assert hazard.variable == "thing" +assert "AutoSomething::AutoSomething" in hazard.GCFunction diff --git a/js/src/devtools/rootAnalysis/t/graph/source.cpp b/js/src/devtools/rootAnalysis/t/graph/source.cpp new file mode 100644 index 0000000000..0adff8d532 --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/graph/source.cpp @@ -0,0 +1,90 @@ +#define ANNOTATE(property) __attribute__((annotate(property))) + +extern void GC() ANNOTATE("GC Call"); + +void GC() { + // If the implementation is too trivial, the function body won't be emitted at + // all. + asm(""); +} + +extern void g(int x); +extern void h(int x); + +void f(int x) { + if (x % 3) { + GC(); + g(x); + } + h(x); +} + +void g(int x) { + if (x % 2) f(x); + h(x); +} + +void h(int x) { + if (x) { + f(x - 1); + g(x - 1); + } +} + +void leaf() { asm(""); } + +void nonrecursive_root() { + leaf(); + leaf(); + GC(); +} + +void self_recursive(int x) { + if (x) self_recursive(x - 1); +} + +// Set up the graph +// +// n1 <--> n2 n4 <--> n5 +// \ / +// --> n3 <--------- +// \ +// ---> n6 --> n7 <---> n8 --> n9 +// +// So recursive roots are one of (n1, n2) plus one of (n4, n5). +extern void n1(int x); +extern void n2(int x); +extern void n3(int x); +extern void n4(int x); +extern void n5(int x); +extern void n6(int x); +extern void n7(int x); +extern void n8(int x); +extern void n9(int x); + +void n1(int x) { n2(x); } + +void n2(int x) { + if (x) n1(x - 1); + n3(x); +} + +void n4(int x) { n5(x); } + +void n5(int x) { + if (x) n4(x - 1); + n3(x); +} + +void n3(int x) { n6(x); } + +void n6(int x) { n7(x); } + +void n7(int x) { n8(x); } + +void n8(int x) { + if (x) n7(x - 1); + n9(x); +} + +void n9(int x) { asm(""); } diff --git a/js/src/devtools/rootAnalysis/t/graph/test.py b/js/src/devtools/rootAnalysis/t/graph/test.py new file mode 100644 index 0000000000..f78500f200 --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/graph/test.py @@ -0,0 +1,54 @@ +# 'test' is provided by the calling script. +# flake8: noqa: F821 + +test.compile("source.cpp") +test.run_analysis_script("gcTypes") + +info = test.load_typeInfo() + +gcFunctions = test.load_gcFunctions() + +f = "void f(int32)" +g = "void g(int32)" +h = "void h(int32)" + +assert f in gcFunctions +assert g in gcFunctions +assert h in gcFunctions +assert "void leaf()" not in gcFunctions +assert "void nonrecursive_root()" in gcFunctions + +callgraph = test.load_callgraph() +assert callgraph.calleeGraph[f][g] +assert callgraph.calleeGraph[f][h] +assert callgraph.calleeGraph[g][f] +assert callgraph.calleeGraph[g][h] + +node = ["void n{}(int32)".format(i) for i in range(10)] +mnode = [callgraph.unmangledToMangled.get(f) for f in node] +for src, dst in [ + (1, 2), + (2, 1), + (4, 5), + (5, 4), + (2, 3), + (5, 3), + (3, 6), + (6, 7), + (7, 8), + (8, 7), + (8, 9), +]: + assert callgraph.calleeGraph[node[src]][node[dst]] + +funcInfo = test.load_funcInfo() +rroots = set( + [ + callgraph.mangledToUnmangled[f] + for f in funcInfo + if funcInfo[f].get("recursive_root") + ] +) +assert len(set([node[1], node[2]]) & rroots) == 1 +assert len(set([node[4], node[5]]) & rroots) == 1 +assert len(rroots) == 4, "rroots = {}".format(rroots) # n1, n4, f, self_recursive diff --git a/js/src/devtools/rootAnalysis/t/hazards/source.cpp b/js/src/devtools/rootAnalysis/t/hazards/source.cpp new file mode 100644 index 0000000000..fe991653af --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/hazards/source.cpp @@ -0,0 +1,566 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <utility> + +#define ANNOTATE(property) __attribute__((annotate(property))) + +// MarkVariableAsGCSafe is a magic function name used as an +// explicit annotation. + +namespace JS { +namespace detail { +template <typename T> +static void MarkVariableAsGCSafe(T&) { + asm(""); +} +} // namespace detail +} // namespace JS + +#define JS_HAZ_VARIABLE_IS_GC_SAFE(var) JS::detail::MarkVariableAsGCSafe(var) + +struct Cell { + int f; +} ANNOTATE("GC Thing"); + +template <typename T, typename U> +struct UntypedContainer { + char data[sizeof(T) + sizeof(U)]; +} ANNOTATE("moz_inherit_type_annotations_from_template_args"); + +struct RootedCell { + RootedCell(Cell*) {} +} ANNOTATE("Rooted Pointer"); + +class AutoSuppressGC_Base { + public: + AutoSuppressGC_Base() {} + ~AutoSuppressGC_Base() {} +} ANNOTATE("Suppress GC"); + +class AutoSuppressGC_Child : public AutoSuppressGC_Base { + public: + AutoSuppressGC_Child() : AutoSuppressGC_Base() {} +}; + +class AutoSuppressGC { + AutoSuppressGC_Child helpImBeingSuppressed; + + public: + AutoSuppressGC() {} +}; + +class AutoCheckCannotGC { + public: + AutoCheckCannotGC() {} + ~AutoCheckCannotGC() { asm(""); } +} ANNOTATE("Invalidated by GC"); + +extern void GC() ANNOTATE("GC Call"); +extern void invisible(); + +void GC() { + // If the implementation is too trivial, the function body won't be emitted at + // all. + asm(""); + invisible(); +} + +extern void usecell(Cell*); + +extern bool flipcoin(); + +void suppressedFunction() { + GC(); // Calls GC, but is always called within AutoSuppressGC +} + +void halfSuppressedFunction() { + GC(); // Calls GC, but is sometimes called within AutoSuppressGC +} + +void unsuppressedFunction() { + GC(); // Calls GC, never within AutoSuppressGC +} + +class IDL_Interface { + public: + ANNOTATE("Can run script") virtual void canScriptThis() {} + virtual void cannotScriptThis() {} + ANNOTATE("Can run script") virtual void overridden_canScriptThis() = 0; + virtual void overridden_cannotScriptThis() = 0; +}; + +class IDL_Subclass : public IDL_Interface { + ANNOTATE("Can run script") void overridden_canScriptThis() override {} + void overridden_cannotScriptThis() override {} +}; + +volatile static int x = 3; +volatile static int* xp = &x; +struct GCInDestructor { + ~GCInDestructor() { + invisible(); + asm(""); + *xp = 4; + GC(); + } +}; + +template <typename T> +void usecontainer(T* value) { + if (value) asm(""); +} + +Cell* cell() { + static Cell c; + return &c; +} + +Cell* f() { + GCInDestructor kaboom; + + Cell* cell1 = cell(); + Cell* cell2 = cell(); + Cell* cell3 = cell(); + Cell* cell4 = cell(); + { + AutoSuppressGC nogc; + suppressedFunction(); + halfSuppressedFunction(); + } + usecell(cell1); + halfSuppressedFunction(); + usecell(cell2); + unsuppressedFunction(); + { + // Old bug: it would look from the first AutoSuppressGC constructor it + // found to the last destructor. This statement *should* have no effect. + AutoSuppressGC nogc; + } + usecell(cell3); + Cell* cell5 = cell(); + usecell(cell5); + + { + // Templatized container that inherits attributes from Cell*, should + // report a hazard. + UntypedContainer<int, Cell*> container1; + usecontainer(&container1); + GC(); + usecontainer(&container1); + } + + { + // As above, but with a non-GC type. + UntypedContainer<int, double> container2; + usecontainer(&container2); + GC(); + usecontainer(&container2); + } + + // Hazard in return value due to ~GCInDestructor + Cell* cell6 = cell(); + return cell6; +} + +Cell* copy_and_gc(Cell* src) { + GC(); + return reinterpret_cast<Cell*>(88); +} + +void use(Cell* cell) { + static int x = 0; + if (cell) x++; +} + +struct CellContainer { + Cell* cell; + CellContainer() { asm(""); } +}; + +void loopy() { + Cell cell; + + // No hazard: haz1 is not live during call to copy_and_gc. + Cell* haz1; + for (int i = 0; i < 10; i++) { + haz1 = copy_and_gc(haz1); + } + + // No hazard: haz2 is live up to just before the GC, and starting at the + // next statement after it, but not across the GC. + Cell* haz2 = &cell; + for (int j = 0; j < 10; j++) { + use(haz2); + GC(); + haz2 = &cell; + } + + // Hazard: haz3 is live from the final statement in one iteration, across + // the GC in the next, to the use in the 2nd statement. + Cell* haz3; + for (int k = 0; k < 10; k++) { + GC(); + use(haz3); + haz3 = &cell; + } + + // Hazard: haz4 is live across a GC hidden in a loop. + Cell* haz4 = &cell; + for (int i2 = 0; i2 < 10; i2++) { + GC(); + } + use(haz4); + + // Hazard: haz5 is live from within a loop across a GC. + Cell* haz5; + for (int i3 = 0; i3 < 10; i3++) { + haz5 = &cell; + } + GC(); + use(haz5); + + // No hazard: similar to the haz3 case, but verifying that we do not get + // into an infinite loop. + Cell* haz6; + for (int i4 = 0; i4 < 10; i4++) { + GC(); + haz6 = &cell; + } + + // No hazard: haz7 is constructed within the body, so it can't make a + // hazard across iterations. Note that this requires CellContainer to have + // a constructor, because otherwise the analysis doesn't see where + // variables are declared. (With the constructor, it knows that + // construction of haz7 obliterates any previous value it might have had. + // Not that that's possible given its scope, but the analysis doesn't get + // that information.) + for (int i5 = 0; i5 < 10; i5++) { + GC(); + CellContainer haz7; + use(haz7.cell); + haz7.cell = &cell; + } + + // Hazard: make sure we *can* see hazards across iterations involving + // CellContainer; + CellContainer haz8; + for (int i6 = 0; i6 < 10; i6++) { + GC(); + use(haz8.cell); + haz8.cell = &cell; + } +} + +namespace mozilla { +template <typename T> +class UniquePtr { + T* val; + + public: + UniquePtr() : val(nullptr) { asm(""); } + UniquePtr(T* p) : val(p) {} + UniquePtr(UniquePtr<T>&& u) : val(u.val) { u.val = nullptr; } + ~UniquePtr() { use(val); } + T* get() { return val; } + void reset() { val = nullptr; } +} ANNOTATE("moz_inherit_type_annotations_from_template_args"); +} // namespace mozilla + +extern void consume(mozilla::UniquePtr<Cell> uptr); + +void safevals() { + Cell cell; + + // Simple hazard. + Cell* unsafe1 = &cell; + GC(); + use(unsafe1); + + // Safe because it's known to be nullptr. + Cell* safe2 = &cell; + safe2 = nullptr; + GC(); + use(safe2); + + // Unsafe because it may not be nullptr. + Cell* unsafe3 = &cell; + if (reinterpret_cast<long>(&cell) & 0x100) { + unsafe3 = nullptr; + } + GC(); + use(unsafe3); + + // Unsafe because it's not nullptr anymore. + Cell* unsafe3b = &cell; + unsafe3b = nullptr; + unsafe3b = &cell; + GC(); + use(unsafe3b); + + // Hazard involving UniquePtr. + { + mozilla::UniquePtr<Cell> unsafe4(&cell); + GC(); + // Destructor uses unsafe4. + } + + // reset() to safe value before the GC. + { + mozilla::UniquePtr<Cell> safe5(&cell); + safe5.reset(); + GC(); + } + + // reset() to safe value after the GC. + { + mozilla::UniquePtr<Cell> safe6(&cell); + GC(); + safe6.reset(); + } + + // reset() to safe value after the GC -- but we've already used it, so it's + // too late. + { + mozilla::UniquePtr<Cell> unsafe7(&cell); + GC(); + use(unsafe7.get()); + unsafe7.reset(); + } + + // initialized to safe value. + { + mozilla::UniquePtr<Cell> safe8; + GC(); + } + + // passed to a function that takes ownership before GC. + { + mozilla::UniquePtr<Cell> safe9(&cell); + consume(std::move(safe9)); + GC(); + } + + // passed to a function that takes ownership after GC. + { + mozilla::UniquePtr<Cell> unsafe10(&cell); + GC(); + consume(std::move(unsafe10)); + } + + // annotated to be safe before the GC. (This doesn't make + // a lot of sense here; the annotation is for when some + // type is known to only contain safe values, eg it is + // initialized as empty, or it is a union and we know + // that the GC pointer variants are not in use.) + { + mozilla::UniquePtr<Cell> safe11(&cell); + JS_HAZ_VARIABLE_IS_GC_SAFE(safe11); + GC(); + } + + // annotate as safe value after the GC -- since nothing else + // has touched the variable, that means it was already safe + // during the GC. + { + mozilla::UniquePtr<Cell> safe12(&cell); + GC(); + JS_HAZ_VARIABLE_IS_GC_SAFE(safe12); + } + + // annotate as safe after the GC -- but we've already used it, so it's + // too late. + { + mozilla::UniquePtr<Cell> unsafe13(&cell); + GC(); + use(unsafe13.get()); + JS_HAZ_VARIABLE_IS_GC_SAFE(unsafe13); + } + + // Check JS_HAZ_CAN_RUN_SCRIPT annotation handling. + IDL_Subclass sub; + IDL_Subclass* subp = ⊂ + IDL_Interface* base = ⊂ + { + Cell* unsafe14 = &cell; + base->canScriptThis(); + use(unsafe14); + } + { + Cell* unsafe15 = &cell; + subp->canScriptThis(); + use(unsafe15); + } + { + // Almost the same as the last one, except call using the actual object, not + // a pointer. The type is known, so there is no danger of the actual type + // being a subclass that has overridden the method with an implementation + // that calls script. + Cell* safe16 = &cell; + sub.canScriptThis(); + use(safe16); + } + { + Cell* safe17 = &cell; + base->cannotScriptThis(); + use(safe17); + } + { + Cell* safe18 = &cell; + subp->cannotScriptThis(); + use(safe18); + } + { + // A use after a GC, but not before. (This does not initialize safe19 by + // setting it to a value, because assignment would start its live range, and + // this test is to see if a variable with no known live range start requires + // a use before the GC or not. It should.) + Cell* safe19; + GC(); + extern void initCellPtr(Cell**); + initCellPtr(&safe19); + } +} + +// Make sure `this` is live at the beginning of a function. +class Subcell : public Cell { + int method() { + GC(); + return f; // this->f + } +}; + +template <typename T> +struct RefPtr { + ~RefPtr() { GC(); } + bool forget() { return true; } + bool use() { return true; } + void assign_with_AddRef(T* aRawPtr) { asm(""); } +}; + +extern bool flipcoin(); + +Cell* refptr_test1() { + static Cell cell; + RefPtr<float> v1; + Cell* ref_unsafe1 = &cell; + return ref_unsafe1; +} + +Cell* refptr_test2() { + static Cell cell; + RefPtr<float> v2; + Cell* ref_safe2 = &cell; + v2.forget(); + return ref_safe2; +} + +Cell* refptr_test3() { + static Cell cell; + RefPtr<float> v3; + Cell* ref_unsafe3 = &cell; + if (x) { + v3.forget(); + } + return ref_unsafe3; +} + +Cell* refptr_test4() { + static Cell cell; + RefPtr<int> r; + return &cell; // hazard in return value +} + +Cell* refptr_test5() { + static Cell cell; + RefPtr<int> r; + return nullptr; // returning immobile value, so no hazard +} + +float somefloat = 1.2; + +Cell* refptr_test6() { + static Cell cell; + RefPtr<float> v6; + Cell* ref_unsafe6 = &cell; + // v6 can be used without an intervening forget() before the end of the + // function, even though forget() will be called at least once. + v6.forget(); + if (x) { + v6.forget(); + v6.assign_with_AddRef(&somefloat); + } + return ref_unsafe6; +} + +Cell* refptr_test7() { + static Cell cell; + RefPtr<float> v7; + Cell* ref_unsafe7 = &cell; + // Similar to above, but with a loop. + while (flipcoin()) { + v7.forget(); + v7.assign_with_AddRef(&somefloat); + } + return ref_unsafe7; +} + +Cell* refptr_test8() { + static Cell cell; + RefPtr<float> v8; + Cell* ref_unsafe8 = &cell; + // If the loop is traversed, forget() will be called. But that doesn't + // matter, because even on the last iteration v8.use() will have been called + // (and potentially dropped the refcount or whatever.) + while (v8.use()) { + v8.forget(); + } + return ref_unsafe8; +} + +Cell* refptr_test9() { + static Cell cell; + RefPtr<float> v9; + Cell* ref_safe9 = &cell; + // Even when not going through the loop, forget() will be called and so the + // dtor will not Release. + while (v9.forget()) { + v9.assign_with_AddRef(&somefloat); + } + return ref_safe9; +} + +Cell* refptr_test10() { + static Cell cell; + RefPtr<float> v10; + Cell* ref_unsafe10 = &cell; + // The destructor has a backwards path that skips the loop body. + v10.assign_with_AddRef(&somefloat); + while (flipcoin()) { + v10.forget(); + } + return ref_unsafe10; +} + +std::pair<bool, AutoCheckCannotGC> pair_returning_function() { + return std::make_pair(true, AutoCheckCannotGC()); +} + +void aggr_init_unsafe() { + // nogc will be live after the call, so across the GC. + auto [ok, nogc] = pair_returning_function(); + GC(); +} + +void aggr_init_safe() { + // The analysis should be able to tell that nogc is only live after the call, + // not before. (This is to check for a problem where the return value was + // getting stored into a different temporary than the local nogc variable, + // and so its initialization was never seen and so it was assumed to be live + // throughout the function.) + GC(); + auto [ok, nogc] = pair_returning_function(); +} diff --git a/js/src/devtools/rootAnalysis/t/hazards/test.py b/js/src/devtools/rootAnalysis/t/hazards/test.py new file mode 100644 index 0000000000..c4e9549305 --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/hazards/test.py @@ -0,0 +1,121 @@ +# flake8: noqa: F821 + +from collections import defaultdict + +test.compile("source.cpp") +test.run_analysis_script("gcTypes") + +# gcFunctions should be the inverse, but we get to rely on unmangled names here. +gcFunctions = test.load_gcFunctions() +assert "void GC()" in gcFunctions +assert "void suppressedFunction()" not in gcFunctions +assert "void halfSuppressedFunction()" in gcFunctions +assert "void unsuppressedFunction()" in gcFunctions +assert "int32 Subcell::method()" in gcFunctions +assert "Cell* f()" in gcFunctions + +hazards = test.load_hazards() +hazmap = {haz.variable: haz for haz in hazards} +assert "cell1" not in hazmap +assert "cell2" in hazmap +assert "cell3" in hazmap +assert "cell4" not in hazmap +assert "cell5" not in hazmap +assert "cell6" not in hazmap +assert "<returnvalue>" in hazmap +assert "this" in hazmap + +assert hazmap["cell2"].function == "Cell* f()" + +# Check that the correct GC call is reported for each hazard. (cell3 has a +# hazard from two different GC calls; it doesn't really matter which is +# reported.) +assert hazmap["cell2"].GCFunction == "void halfSuppressedFunction()" +assert hazmap["cell3"].GCFunction in ( + "void halfSuppressedFunction()", + "void unsuppressedFunction()", +) +returnval_hazards = set( + haz.function for haz in hazards if haz.variable == "<returnvalue>" +) +assert "Cell* f()" in returnval_hazards +assert "Cell* refptr_test1()" in returnval_hazards +assert "Cell* refptr_test2()" not in returnval_hazards +assert "Cell* refptr_test3()" in returnval_hazards +assert "Cell* refptr_test4()" in returnval_hazards +assert "Cell* refptr_test5()" not in returnval_hazards +assert "Cell* refptr_test6()" in returnval_hazards +assert "Cell* refptr_test7()" in returnval_hazards +assert "Cell* refptr_test8()" in returnval_hazards +assert "Cell* refptr_test9()" not in returnval_hazards + +assert "container1" in hazmap +assert "container2" not in hazmap + +# Type names are handy to have in the report. +assert hazmap["cell2"].type == "Cell*" +assert hazmap["<returnvalue>"].type == "Cell*" +assert hazmap["this"].type == "Subcell*" + +# loopy hazards. See comments in source. +assert "haz1" not in hazmap +assert "haz2" not in hazmap +assert "haz3" in hazmap +assert "haz4" in hazmap +assert "haz5" in hazmap +assert "haz6" not in hazmap +assert "haz7" not in hazmap +assert "haz8" in hazmap + +# safevals hazards. See comments in source. +assert "unsafe1" in hazmap +assert "safe2" not in hazmap +assert "unsafe3" in hazmap +assert "unsafe3b" in hazmap +assert "unsafe4" in hazmap +assert "safe5" not in hazmap +assert "safe6" not in hazmap +assert "unsafe7" in hazmap +assert "safe8" not in hazmap +assert "safe9" not in hazmap +assert "safe10" not in hazmap +assert "safe11" not in hazmap +assert "safe12" not in hazmap +assert "unsafe13" in hazmap +assert "unsafe14" in hazmap +assert "unsafe15" in hazmap +assert "safe16" not in hazmap +assert "safe17" not in hazmap +assert "safe18" not in hazmap +assert "safe19" not in hazmap + +# method hazard. + +byfunc = defaultdict(lambda: defaultdict(dict)) +for haz in hazards: + byfunc[haz.function][haz.variable] = haz + +methhaz = byfunc["int32 Subcell::method()"] +assert "this" in methhaz +assert methhaz["this"].type == "Subcell*" + +haz_functions = set(haz.function for haz in hazards) + +# RefPtr<T> tests. + +haz_functions = set(haz.function for haz in hazards) +assert "Cell* refptr_test1()" in haz_functions +assert "Cell* refptr_test2()" not in haz_functions +assert "Cell* refptr_test3()" in haz_functions +assert "Cell* refptr_test4()" in haz_functions +assert "Cell* refptr_test5()" not in haz_functions +assert "Cell* refptr_test6()" in haz_functions +assert "Cell* refptr_test7()" in haz_functions +assert "Cell* refptr_test8()" in haz_functions +assert "Cell* refptr_test9()" not in haz_functions +assert "Cell* refptr_test10()" in haz_functions + +# aggr_init tests. + +assert "void aggr_init_safe()" not in haz_functions +assert "void aggr_init_unsafe()" in haz_functions diff --git a/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp b/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp new file mode 100644 index 0000000000..149d77b03a --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#define ANNOTATE(property) __attribute__((annotate(property))) + +namespace js { +namespace gc { +struct Cell { + int f; +} ANNOTATE("GC Thing"); +} // namespace gc +} // namespace js + +struct Bogon {}; + +struct JustACell : public js::gc::Cell { + bool iHaveNoDataMembers() { return true; } +}; + +struct JSObject : public js::gc::Cell, public Bogon { + int g; +}; + +struct SpecialObject : public JSObject { + int z; +}; + +struct ErrorResult { + bool hasObj; + JSObject* obj; + void trace() {} +} ANNOTATE("Suppressed GC Pointer"); + +struct OkContainer { + ErrorResult res; + bool happy; +}; + +struct UnrootedPointer { + JSObject* obj; +}; + +template <typename T> +class Rooted { + T data; +} ANNOTATE("Rooted Pointer"); + +extern void js_GC() ANNOTATE("GC Call") ANNOTATE("Slow"); + +void js_GC() {} + +void root_arg(JSObject* obj, JSObject* random) { + // Use all these types so they get included in the output. + SpecialObject so; + UnrootedPointer up; + Bogon b; + OkContainer okc; + Rooted<JSObject*> ro; + Rooted<SpecialObject*> rso; + + obj = random; + + JSObject* other1 = obj; + js_GC(); + + float MARKER1 = 0; + JSObject* other2 = obj; + other1->f = 1; + other2->f = -1; + + unsigned int u1 = 1; + unsigned int u2 = -1; +} diff --git a/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py b/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py new file mode 100644 index 0000000000..5e99fff908 --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py @@ -0,0 +1,63 @@ +# flake8: noqa: F821 +import re + +test.compile("source.cpp") +test.computeGCTypes() +body = test.process_body(test.load_db_entry("src_body", re.compile(r"root_arg"))[0]) + +# Rendering positive and negative integers +marker1 = body.assignment_line("MARKER1") +equal(body.edge_from_line(marker1 + 2)["Exp"][1]["String"], "1") +equal(body.edge_from_line(marker1 + 3)["Exp"][1]["String"], "-1") + +equal(body.edge_from_point(body.assignment_point("u1"))["Exp"][1]["String"], "1") +equal( + body.edge_from_point(body.assignment_point("u2"))["Exp"][1]["String"], "4294967295" +) + +assert "obj" in body["Variables"] +assert "random" in body["Variables"] +assert "other1" in body["Variables"] +assert "other2" in body["Variables"] + +# Test function annotations +js_GC = test.process_body(test.load_db_entry("src_body", re.compile(r"js_GC"))[0]) +annotations = js_GC["Variables"]["void js_GC()"]["Annotation"] +assert annotations +found_call_annotate = False +for annotation in annotations: + (annType, value) = annotation["Name"] + if annType == "annotate" and value == "GC Call": + found_call_annotate = True +assert found_call_annotate + +# Test type annotations + +# js::gc::Cell first +cell = test.load_db_entry("src_comp", "js::gc::Cell")[0] +assert cell["Kind"] == "Struct" +annotations = cell["Annotation"] +assert len(annotations) == 1 +(tag, value) = annotations[0]["Name"] +assert tag == "annotate" +assert value == "GC Thing" + +# Check JSObject inheritance. +JSObject = test.load_db_entry("src_comp", "JSObject")[0] +bases = [b["Base"] for b in JSObject["CSUBaseClass"]] +assert "js::gc::Cell" in bases +assert "Bogon" in bases +assert len(bases) == 2 + +# Check type analysis +gctypes = test.load_gcTypes() +assert "js::gc::Cell" in gctypes["GCThings"] +assert "JustACell" in gctypes["GCThings"] +assert "JSObject" in gctypes["GCThings"] +assert "SpecialObject" in gctypes["GCThings"] +assert "UnrootedPointer" in gctypes["GCPointers"] +assert "Bogon" not in gctypes["GCThings"] +assert "Bogon" not in gctypes["GCPointers"] +assert "ErrorResult" not in gctypes["GCPointers"] +assert "OkContainer" not in gctypes["GCPointers"] +assert "class Rooted<JSObject*>" not in gctypes["GCPointers"] diff --git a/js/src/devtools/rootAnalysis/t/sixgill.py b/js/src/devtools/rootAnalysis/t/sixgill.py new file mode 100644 index 0000000000..0b8c2c7073 --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/sixgill.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from collections import defaultdict + +# Simplified version of the body info. + + +class Body(dict): + def __init__(self, body): + self["BlockIdKind"] = body["BlockId"]["Kind"] + if "Variable" in body["BlockId"]: + self["BlockName"] = body["BlockId"]["Variable"]["Name"][0].split("$")[-1] + loc = body["Location"] + self["LineRange"] = (loc[0]["Line"], loc[1]["Line"]) + self["Filename"] = loc[0]["CacheString"] + self["Edges"] = body.get("PEdge", []) + self["Points"] = { + i: p["Location"]["Line"] for i, p in enumerate(body["PPoint"], 1) + } + self["Index"] = body["Index"] + self["Variables"] = { + x["Variable"]["Name"][0].split("$")[-1]: x["Type"] + for x in body["DefineVariable"] + } + + # Indexes + self["Line2Points"] = defaultdict(list) + for point, line in self["Points"].items(): + self["Line2Points"][line].append(point) + self["SrcPoint2Edges"] = defaultdict(list) + for edge in self["Edges"]: + src, dst = edge["Index"] + self["SrcPoint2Edges"][src].append(edge) + self["Line2Edges"] = defaultdict(list) + for (src, edges) in self["SrcPoint2Edges"].items(): + line = self["Points"][src] + self["Line2Edges"][line].extend(edges) + + def edges_from_line(self, line): + return self["Line2Edges"][line] + + def edge_from_line(self, line): + edges = self.edges_from_line(line) + assert len(edges) == 1 + return edges[0] + + def edges_from_point(self, point): + return self["SrcPoint2Edges"][point] + + def edge_from_point(self, point): + edges = self.edges_from_point(point) + assert len(edges) == 1 + return edges[0] + + def assignment_point(self, varname): + for edge in self["Edges"]: + if edge["Kind"] != "Assign": + continue + dst = edge["Exp"][0] + if dst["Kind"] != "Var": + continue + if dst["Variable"]["Name"][0] == varname: + return edge["Index"][0] + raise Exception("assignment to variable %s not found" % varname) + + def assignment_line(self, varname): + return self["Points"][self.assignment_point(varname)] diff --git a/js/src/devtools/rootAnalysis/t/suppression/source.cpp b/js/src/devtools/rootAnalysis/t/suppression/source.cpp new file mode 100644 index 0000000000..56e458bdaa --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/suppression/source.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#define ANNOTATE(property) __attribute__((annotate(property))) + +struct Cell { + int f; +} ANNOTATE("GC Thing"); + +class AutoSuppressGC_Base { + public: + AutoSuppressGC_Base() {} + ~AutoSuppressGC_Base() {} +} ANNOTATE("Suppress GC"); + +class AutoSuppressGC_Child : public AutoSuppressGC_Base { + public: + AutoSuppressGC_Child() : AutoSuppressGC_Base() {} +}; + +class AutoSuppressGC { + AutoSuppressGC_Child helpImBeingSuppressed; + + public: + AutoSuppressGC() {} +}; + +extern void GC() ANNOTATE("GC Call"); + +void GC() { + // If the implementation is too trivial, the function body won't be emitted at + // all. + asm(""); +} + +extern void foo(Cell*); + +void suppressedFunction() { + GC(); // Calls GC, but is always called within AutoSuppressGC +} + +void halfSuppressedFunction() { + GC(); // Calls GC, but is sometimes called within AutoSuppressGC +} + +void unsuppressedFunction() { + GC(); // Calls GC, never within AutoSuppressGC +} + +void f() { + Cell* cell1 = nullptr; + Cell* cell2 = nullptr; + Cell* cell3 = nullptr; + { + AutoSuppressGC nogc; + suppressedFunction(); + halfSuppressedFunction(); + } + foo(cell1); + halfSuppressedFunction(); + foo(cell2); + unsuppressedFunction(); + { + // Old bug: it would look from the first AutoSuppressGC constructor it + // found to the last destructor. This statement *should* have no effect. + AutoSuppressGC nogc; + } + foo(cell3); +} diff --git a/js/src/devtools/rootAnalysis/t/suppression/test.py b/js/src/devtools/rootAnalysis/t/suppression/test.py new file mode 100644 index 0000000000..118ae422ab --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/suppression/test.py @@ -0,0 +1,21 @@ +# flake8: noqa: F821 +test.compile("source.cpp") +test.run_analysis_script("gcTypes", upto="gcFunctions") + +# The suppressions file uses mangled names. +info = test.load_funcInfo() +suppressed = [f for f, v in info.items() if v.get("limits", 0) | 1] + +# Only one of these is fully suppressed (ie, *always* called within the scope +# of an AutoSuppressGC). +assert len(list(filter(lambda f: "suppressedFunction" in f, suppressed))) == 1 +assert len(list(filter(lambda f: "halfSuppressedFunction" in f, suppressed))) == 0 +assert len(list(filter(lambda f: "unsuppressedFunction" in f, suppressed))) == 0 + +# gcFunctions should be the inverse, but we get to rely on unmangled names here. +gcFunctions = test.load_gcFunctions() +assert "void GC()" in gcFunctions +assert "void suppressedFunction()" not in gcFunctions +assert "void halfSuppressedFunction()" in gcFunctions +assert "void unsuppressedFunction()" in gcFunctions +assert "void f()" in gcFunctions diff --git a/js/src/devtools/rootAnalysis/t/testlib.py b/js/src/devtools/rootAnalysis/t/testlib.py new file mode 100644 index 0000000000..a7187395c6 --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/testlib.py @@ -0,0 +1,231 @@ +import json +import os +import re +import subprocess +import sys +from collections import defaultdict, namedtuple + +from sixgill import Body + +scriptdir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + +HazardSummary = namedtuple( + "HazardSummary", ["function", "variable", "type", "GCFunction", "location"] +) + +Callgraph = namedtuple( + "Callgraph", + [ + "functionNames", + "nameToId", + "mangledToUnmangled", + "unmangledToMangled", + "calleesOf", + "callersOf", + "tags", + "calleeGraph", + "callerGraph", + ], +) + + +def equal(got, expected): + if got != expected: + print("Got '%s', expected '%s'" % (got, expected)) + + +def extract_unmangled(func): + return func.split("$")[-1] + + +class Test(object): + def __init__(self, indir, outdir, cfg, verbose=0): + self.indir = indir + self.outdir = outdir + self.cfg = cfg + self.verbose = verbose + + def infile(self, path): + return os.path.join(self.indir, path) + + def binpath(self, prog): + return os.path.join(self.cfg.sixgill_bin, prog) + + def compile(self, source, options=""): + env = os.environ + env["CCACHE_DISABLE"] = "1" + if "-fexceptions" not in options and "-fno-exceptions" not in options: + options += " -fno-exceptions" + cmd = "{CXX} -c {source} -O3 -std=c++17 -fplugin={sixgill} -fplugin-arg-xgill-mangle=1 {options}".format( # NOQA: E501 + source=self.infile(source), + CXX=self.cfg.cxx, + sixgill=self.cfg.sixgill_plugin, + options=options, + ) + if self.cfg.verbose > 0: + print("Running %s" % cmd) + subprocess.check_call(["sh", "-c", cmd]) + + def load_db_entry(self, dbname, pattern): + """Look up an entry from an XDB database file, 'pattern' may be an exact + matching string, or an re pattern object matching a single entry.""" + + if hasattr(pattern, "match"): + output = subprocess.check_output( + [self.binpath("xdbkeys"), dbname + ".xdb"], universal_newlines=True + ) + matches = list(filter(lambda _: re.search(pattern, _), output.splitlines())) + if len(matches) == 0: + raise Exception("entry not found") + if len(matches) > 1: + raise Exception("multiple entries found") + pattern = matches[0] + + output = subprocess.check_output( + [self.binpath("xdbfind"), "-json", dbname + ".xdb", pattern], + universal_newlines=True, + ) + return json.loads(output) + + def run_analysis_script(self, startPhase="gcTypes", upto=None): + open("defaults.py", "w").write( + """\ +analysis_scriptdir = '{scriptdir}' +sixgill_bin = '{bindir}' +""".format( + scriptdir=scriptdir, bindir=self.cfg.sixgill_bin + ) + ) + cmd = [ + sys.executable, + os.path.join(scriptdir, "analyze.py"), + ["-q", "", "-v"][min(self.verbose, 2)], + ] + cmd += ["--first", startPhase] + if upto: + cmd += ["--last", upto] + cmd.append("--source=%s" % self.indir) + cmd.append("--js=%s" % self.cfg.js) + if self.cfg.verbose: + print("Running " + " ".join(cmd)) + subprocess.check_call(cmd) + + def computeGCTypes(self): + self.run_analysis_script("gcTypes", upto="gcTypes") + + def computeHazards(self): + self.run_analysis_script("gcTypes") + + def load_text_file(self, filename, extract=lambda l: l): + fullpath = os.path.join(self.outdir, filename) + values = (extract(line.strip()) for line in open(fullpath, "r")) + return list(filter(lambda _: _ is not None, values)) + + def load_json_file(self, filename, reviver=None): + fullpath = os.path.join(self.outdir, filename) + with open(fullpath) as fh: + return json.load(fh, object_hook=reviver) + + def load_gcTypes(self): + def grab_type(line): + m = re.match(r"^(GC\w+): (.*)", line) + if m: + return (m.group(1) + "s", m.group(2)) + return None + + gctypes = defaultdict(list) + for collection, typename in self.load_text_file( + "gcTypes.txt", extract=grab_type + ): + gctypes[collection].append(typename) + return gctypes + + def load_typeInfo(self, filename="typeInfo.txt"): + return self.load_json_file(filename) + + def load_funcInfo(self, filename="limitedFunctions.lst"): + return self.load_json_file(filename) + + def load_gcFunctions(self): + return self.load_text_file("gcFunctions.lst", extract=extract_unmangled) + + def load_callgraph(self): + data = Callgraph( + functionNames=["dummy"], + nameToId={}, + mangledToUnmangled={}, + unmangledToMangled={}, + calleesOf=defaultdict(list), + callersOf=defaultdict(list), + tags=defaultdict(set), + calleeGraph=defaultdict(dict), + callerGraph=defaultdict(dict), + ) + + def lookup(id): + mangled = data.functionNames[int(id)] + return data.mangledToUnmangled.get(mangled, mangled) + + def add_call(caller, callee, limit): + data.calleesOf[caller].append(callee) + data.callersOf[callee].append(caller) + data.calleeGraph[caller][callee] = True + data.callerGraph[callee][caller] = True + + def process(line): + if line.startswith("#"): + name = line.split(" ", 1)[1] + data.nameToId[name] = len(data.functionNames) + data.functionNames.append(name) + return + + if line.startswith("="): + m = re.match(r"^= (\d+) (.*)", line) + mangled = data.functionNames[int(m.group(1))] + unmangled = m.group(2) + data.nameToId[unmangled] = id + data.mangledToUnmangled[mangled] = unmangled + data.unmangledToMangled[unmangled] = mangled + return + + limit = 0 + m = re.match(r"^\w (?:/(\d+))? ", line) + if m: + limit = int(m[1]) + + tokens = line.split(" ") + if tokens[0] in ("D", "R"): + _, caller, callee = tokens + add_call(lookup(caller), lookup(callee), limit) + elif tokens[0] == "T": + data.tags[tokens[1]].add(line.split(" ", 2)[2]) + elif tokens[0] in ("F", "V"): + pass + + elif tokens[0] == "I": + m = re.match(r"^I (\d+) VARIABLE ([^\,]*)", line) + pass + + self.load_text_file("callgraph.txt", extract=process) + return data + + def load_hazards(self): + def grab_hazard(line): + m = re.match( + r"Function '(.*?)' has unrooted '(.*?)' of type '(.*?)' live across GC call '(.*?)' at (.*)", # NOQA: E501 + line, + ) + if m: + info = list(m.groups()) + info[0] = info[0].split("$")[-1] + info[3] = info[3].split("$")[-1] + return HazardSummary(*info) + return None + + return self.load_text_file("hazards.txt", extract=grab_hazard) + + def process_body(self, body): + return Body(body) + + def process_bodies(self, bodies): + return [self.process_body(b) for b in bodies] diff --git a/js/src/devtools/rootAnalysis/t/types/source.cpp b/js/src/devtools/rootAnalysis/t/types/source.cpp new file mode 100644 index 0000000000..e823f0339b --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/types/source.cpp @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <memory> +#include <utility> + +#define ANNOTATE(property) __attribute__((annotate(property))) + +struct Cell { + int f; +} ANNOTATE("GC Thing"); + +namespace World { +namespace NS { +struct Unsafe { + int g; + ~Unsafe() { asm(""); } +} ANNOTATE("Invalidated by GC") ANNOTATE("GC Pointer or Reference"); +} // namespace NS +} // namespace World + +extern void GC() ANNOTATE("GC Call"); +extern void invisible(); + +void GC() { + // If the implementation is too trivial, the function body won't be emitted at + // all. + asm(""); + invisible(); +} + +struct GCOnDestruction { + ~GCOnDestruction() { GC(); } +}; + +struct NoGCOnDestruction { + ~NoGCOnDestruction() { asm(""); } +}; + +extern void usecell(Cell*); + +Cell* cell() { + static Cell c; + return &c; +} + +template <typename T, typename U> +struct SimpleTemplate { + int member; +}; + +template <typename T, typename U> +class ANNOTATE("moz_inherit_type_annotations_from_template_args") Container { + public: + template <typename V, typename W> + void foo(V& v, W& w) { + class InnerClass {}; + InnerClass xxx; + return; + } +}; + +Cell* f() { + Container<int, double> c1; + Container<SimpleTemplate<int, int>, SimpleTemplate<double, double>> c2; + Container<Container<int, double>, Container<void, void>> c3; + Container<Container<SimpleTemplate<int, int>, void>, + Container<void, SimpleTemplate<char, char>>> + c4; + + return nullptr; +}; + +void rvalue_ref(World::NS::Unsafe&& arg1) { GC(); } + +void ref(const World::NS::Unsafe& arg2) { + GC(); + static int use = arg2.g; +} + +// A function that consumes a parameter, but only if passed by rvalue reference. +extern void eat(World::NS::Unsafe&&); +extern void eat(World::NS::Unsafe&); + +void rvalue_ref_ok() { + World::NS::Unsafe unsafe1; + eat(std::move(unsafe1)); + GC(); +} + +void rvalue_ref_not_ok() { + World::NS::Unsafe unsafe2; + eat(unsafe2); + GC(); +} + +void rvalue_ref_arg_ok(World::NS::Unsafe&& unsafe3) { + eat(std::move(unsafe3)); + GC(); +} + +void rvalue_ref_arg_not_ok(World::NS::Unsafe&& unsafe4) { + eat(unsafe4); + GC(); +} + +void shared_ptr_hazard() { + Cell* unsafe5 = f(); + { auto p = std::make_shared<GCOnDestruction>(); } + usecell(unsafe5); +} + +void shared_ptr_no_hazard() { + Cell* safe6 = f(); + { auto p = std::make_shared<NoGCOnDestruction>(); } + usecell(safe6); +} diff --git a/js/src/devtools/rootAnalysis/t/types/test.py b/js/src/devtools/rootAnalysis/t/types/test.py new file mode 100644 index 0000000000..4a2b985abf --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/types/test.py @@ -0,0 +1,16 @@ +# flake8: noqa: F821 + +from collections import defaultdict + +test.compile("source.cpp") +test.run_analysis_script() +hazards = test.load_hazards() +hazmap = {haz.variable: haz for haz in hazards} +assert "arg1" in hazmap +assert "arg2" in hazmap +assert "unsafe1" not in hazmap +assert "unsafe2" in hazmap +assert "unsafe3" not in hazmap +assert "unsafe4" in hazmap +assert "unsafe5" in hazmap +assert "safe6" not in hazmap diff --git a/js/src/devtools/rootAnalysis/t/virtual/source.cpp b/js/src/devtools/rootAnalysis/t/virtual/source.cpp new file mode 100644 index 0000000000..83633a3436 --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/virtual/source.cpp @@ -0,0 +1,292 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#define ANNOTATE(property) __attribute__((annotate(property))) + +extern void GC() ANNOTATE("GC Call"); + +void GC() { + // If the implementation is too trivial, the function body won't be emitted at + // all. + asm(""); +} + +// Special-cased function -- code that can run JS has an artificial edge to +// js::RunScript. +namespace js { +void RunScript() { GC(); } +} // namespace js + +struct Cell { + int f; +} ANNOTATE("GC Thing"); + +extern void foo(); + +void bar() { GC(); } + +typedef void (*func_t)(); + +class Base { + public: + int ANNOTATE("field annotation") dummy; + virtual void someGC() ANNOTATE("Base pure virtual method") = 0; + virtual void someGC(int) ANNOTATE("overloaded Base pure virtual method") = 0; + virtual void sibGC() = 0; + virtual void onBase() { bar(); } + func_t functionField; + + // For now, this is just to verify that the plugin doesn't crash. The + // analysis code does not yet look at this annotation or output it anywhere + // (though it *is* being recorded.) + static float testAnnotations() ANNOTATE("static func"); + + // Similar, though sixgill currently completely ignores parameter annotations. + static double testParamAnnotations(Cell& ANNOTATE("param annotation") + ANNOTATE("second param annot") cell) + ANNOTATE("static func") ANNOTATE("second func"); +}; + +float Base::testAnnotations() { + asm(""); + return 1.1; +} + +double Base::testParamAnnotations(Cell& cell) { + asm(""); + return 1.2; +} + +class Super : public Base { + public: + virtual void ANNOTATE("Super pure virtual") noneGC() = 0; + virtual void allGC() = 0; + virtual void onSuper() { asm(""); } + void nonVirtualFunc() { asm(""); } +}; + +class Sub1 : public Super { + public: + void noneGC() override { foo(); } + void someGC() override ANNOTATE("Sub1 override") ANNOTATE("second attr") { + foo(); + } + void someGC(int) override ANNOTATE("Sub1 override for int overload") { + foo(); + } + void allGC() override { + foo(); + bar(); + } + void sibGC() override { foo(); } + void onBase() override { foo(); } +} ANNOTATE("CSU1") ANNOTATE("CSU2"); + +class Sub2 : public Super { + public: + void noneGC() override { foo(); } + void someGC() override { + foo(); + bar(); + } + void someGC(int) override { + foo(); + bar(); + } + void allGC() override { + foo(); + bar(); + } + void sibGC() override { foo(); } +}; + +class Sibling : public Base { + public: + virtual void noneGC() { foo(); } + void someGC() override { + foo(); + bar(); + } + void someGC(int) override { + foo(); + bar(); + } + virtual void allGC() { + foo(); + bar(); + } + void sibGC() override { bar(); } +}; + +class AutoSuppressGC { + public: + AutoSuppressGC() {} + ~AutoSuppressGC() {} +} ANNOTATE("Suppress GC"); + +void use(Cell*) { asm(""); } + +class nsISupports { + public: + virtual ANNOTATE("Can run script") void danger() { asm(""); } + + virtual ~nsISupports() = 0; +}; + +class nsIPrincipal : public nsISupports { + public: + ~nsIPrincipal() override{}; +}; + +struct JSPrincipals { + int debugToken; + JSPrincipals() = default; + virtual ~JSPrincipals() { GC(); } +}; + +class nsJSPrincipals : public nsIPrincipal, public JSPrincipals { + public: + void Release() { delete this; } +}; + +class SafePrincipals : public nsIPrincipal { + public: + ~SafePrincipals() { foo(); } +}; + +void f() { + Sub1 s1; + Sub2 s2; + + static Cell cell; + { + Cell* c1 = &cell; + s1.noneGC(); + use(c1); + } + { + Cell* c2 = &cell; + s2.someGC(); + use(c2); + } + { + Cell* c3 = &cell; + s1.allGC(); + use(c3); + } + { + Cell* c4 = &cell; + s2.noneGC(); + use(c4); + } + { + Cell* c5 = &cell; + s2.someGC(); + use(c5); + } + { + Cell* c6 = &cell; + s2.allGC(); + use(c6); + } + + Super* super = &s2; + { + Cell* c7 = &cell; + super->noneGC(); + use(c7); + } + { + Cell* c8 = &cell; + super->someGC(); + use(c8); + } + { + Cell* c9 = &cell; + super->allGC(); + use(c9); + } + + { + Cell* c10 = &cell; + s1.functionField(); + use(c10); + } + { + Cell* c11 = &cell; + super->functionField(); + use(c11); + } + { + Cell* c12 = &cell; + super->sibGC(); + use(c12); + } + + Base* base = &s2; + { + Cell* c13 = &cell; + base->sibGC(); + use(c13); + } + + nsJSPrincipals pals; + { + Cell* c14 = &cell; + nsISupports* p = &pals; + p->danger(); + use(c14); + } + + // Base defines, Sub1 overrides, static Super can call either. + { + Cell* c15 = &cell; + super->onBase(); + use(c15); + } + + { + Cell* c16 = &cell; + s2.someGC(7); + use(c16); + } + + { + Cell* c17 = &cell; + super->someGC(7); + use(c17); + } + + { + nsJSPrincipals* princ = new nsJSPrincipals(); + Cell* c18 = &cell; + delete princ; // Can GC + use(c18); + } + + { + nsJSPrincipals* princ = new nsJSPrincipals(); + nsISupports* supp = static_cast<nsISupports*>(princ); + Cell* c19 = &cell; + delete supp; // Can GC + use(c19); + } + + { + auto* safe = new SafePrincipals(); + Cell* c20 = &cell; + delete safe; // Cannot GC + use(c20); + } + + { + auto* safe = new SafePrincipals(); + nsISupports* supp = static_cast<nsISupports*>(safe); + Cell* c21 = &cell; + delete supp; // Compiler thinks destructor can GC. + use(c21); + } +} diff --git a/js/src/devtools/rootAnalysis/t/virtual/test.py b/js/src/devtools/rootAnalysis/t/virtual/test.py new file mode 100644 index 0000000000..e8474ae28b --- /dev/null +++ b/js/src/devtools/rootAnalysis/t/virtual/test.py @@ -0,0 +1,91 @@ +# 'test' is provided by the calling script. +# flake8: noqa: F821 + +test.compile("source.cpp") +test.run_analysis_script("gcTypes") + +info = test.load_typeInfo() + +assert "Sub1" in info["OtherCSUTags"] +assert ["CSU1", "CSU2"] == sorted(info["OtherCSUTags"]["Sub1"]) +assert "Base" in info["OtherFieldTags"] +assert "someGC" in info["OtherFieldTags"]["Base"] +assert "Sub1" in info["OtherFieldTags"] +assert "someGC" in info["OtherFieldTags"]["Sub1"] + +# For now, fields with the same name (eg overloaded virtual methods) just +# accumulate attributes. +assert ["Sub1 override", "Sub1 override for int overload", "second attr"] == sorted( + info["OtherFieldTags"]["Sub1"]["someGC"] +) + +gcFunctions = test.load_gcFunctions() + +assert "void Sub1::noneGC()" not in gcFunctions +assert "void Sub1::someGC()" not in gcFunctions +assert "void Sub1::someGC(int32)" not in gcFunctions +assert "void Sub1::allGC()" in gcFunctions +assert "void Sub2::noneGC()" not in gcFunctions +assert "void Sub2::someGC()" in gcFunctions +assert "void Sub2::someGC(int32)" in gcFunctions +assert "void Sub2::allGC()" in gcFunctions + +callgraph = test.load_callgraph() + +assert callgraph.calleeGraph["void f()"]["Super.noneGC:0"] +assert callgraph.calleeGraph["Super.noneGC:0"]["Sub1.noneGC:0"] +assert callgraph.calleeGraph["Super.noneGC:0"]["Sub2.noneGC:0"] +assert callgraph.calleeGraph["Sub1.noneGC:0"]["void Sub1::noneGC()"] +assert callgraph.calleeGraph["Sub2.noneGC:0"]["void Sub2::noneGC()"] +assert "void Sibling::noneGC()" not in callgraph.calleeGraph["Super.noneGC:0"] +assert callgraph.calleeGraph["Super.onBase:0"]["Sub1.onBase:0"] +assert callgraph.calleeGraph["Sub1.onBase:0"]["void Sub1::onBase()"] +assert callgraph.calleeGraph["Super.onBase:0"]["void Base::onBase()"] +assert "void Sibling::onBase()" not in callgraph.calleeGraph["Super.onBase:0"] + +hazards = test.load_hazards() +hazmap = {haz.variable: haz for haz in hazards} + +assert "c1" not in hazmap +assert "c2" in hazmap +assert "c3" in hazmap +assert "c4" not in hazmap +assert "c5" in hazmap +assert "c6" in hazmap +assert "c7" not in hazmap +assert "c8" in hazmap +assert "c9" in hazmap +assert "c10" in hazmap +assert "c11" in hazmap + +# Virtual resolution should take the static type into account: the only method +# implementations considered should be those of descendants, even if the +# virtual method is inherited and not overridden in the static class. (Base +# defines sibGC() as pure virtual, Super inherits it without overriding, +# Sibling and Sub2 both implement it.) + +# Call Base.sibGC on a Super pointer: can only call Sub2.sibGC(), which does not GC. +# In particular, PEdgeCallInstance.Exp.Field.FieldCSU.Type = {Kind: "CSU", Name="Super"} +assert "c12" not in hazmap +# Call Base.sibGC on a Base pointer; can call Sibling.sibGC(), which GCs. +assert "c13" in hazmap + +# Call nsISupports.danger() which is annotated to be overridable and hence can GC. +assert "c14" in hazmap + +# someGC(int) overload +assert "c16" in hazmap +assert "c17" in hazmap + +# Super.onBase() could call the GC'ing Base::onBase(). +assert "c15" in hazmap + +# virtual ~nsJSPrincipals calls ~JSPrincipals calls GC. +assert "c18" in hazmap +assert "c19" in hazmap + +# ~SafePrincipals does not GC. +assert "c20" not in hazmap + +# ...but when cast to a nsISupports*, the compiler can't tell that it won't. +assert "c21" in hazmap |