| // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "ppapi/tests/test_instance_deprecated.h" | |
| #include <assert.h> | |
| #include "ppapi/c/ppb_var.h" | |
| #include "ppapi/cpp/module.h" | |
| #include "ppapi/tests/testing_instance.h" | |
| static const char kSetValueFunction[] = "SetValue"; | |
| static const char kSetExceptionFunction[] = "SetException"; | |
| static const char kReturnValueFunction[] = "ReturnValue"; | |
| InstanceSO::InstanceSO(TestInstance* i) | |
| : test_instance_(i), | |
| testing_interface_(i->testing_interface()) { | |
| } | |
| InstanceSO::~InstanceSO() { | |
| if (test_instance_) | |
| test_instance_->clear_instance_so(); | |
| } | |
| bool InstanceSO::HasMethod(const pp::Var& name, pp::Var* exception) { | |
| if (!name.is_string()) | |
| return false; | |
| return name.AsString() == kSetValueFunction || | |
| name.AsString() == kSetExceptionFunction || | |
| name.AsString() == kReturnValueFunction; | |
| } | |
| pp::Var InstanceSO::Call(const pp::Var& method_name, | |
| const std::vector<pp::Var>& args, | |
| pp::Var* exception) { | |
| if (!method_name.is_string()) | |
| return false; | |
| std::string name = method_name.AsString(); | |
| if (name == kSetValueFunction) { | |
| if (args.size() != 1 || !args[0].is_string()) | |
| *exception = pp::Var("Bad argument to SetValue(<string>)"); | |
| else if (test_instance_) | |
| test_instance_->set_string(args[0].AsString()); | |
| } else if (name == kSetExceptionFunction) { | |
| if (args.size() != 1 || !args[0].is_string()) | |
| *exception = pp::Var("Bad argument to SetException(<string>)"); | |
| else | |
| *exception = args[0]; | |
| } else if (name == kReturnValueFunction) { | |
| if (args.size() != 1) | |
| *exception = pp::Var("Need single arg to call ReturnValue"); | |
| else | |
| return args[0]; | |
| } else { | |
| *exception = pp::Var("Bad function call"); | |
| } | |
| return pp::Var(); | |
| } | |
| REGISTER_TEST_CASE(Instance); | |
| TestInstance::TestInstance(TestingInstance* instance) | |
| : TestCase(instance), | |
| instance_so_(NULL) {} | |
| bool TestInstance::Init() { | |
| return true; | |
| } | |
| TestInstance::~TestInstance() { | |
| ResetTestObject(); | |
| if (testing_interface_->IsOutOfProcess() == PP_FALSE) { | |
| // This should cause the instance object's destructor to be called. | |
| testing_interface_->RunV8GC(instance_->pp_instance()); | |
| // Test a post-condition which ensures the instance objects destructor is | |
| // called. This only works reliably in-process. Out-of-process, it only | |
| // can work when the renderer stays alive a short while after the plugin | |
| // instance is destroyed. If the renderer is being shut down, too much | |
| // happens asynchronously for the out-of-process case to work reliably. In | |
| // particular: | |
| // - The Var ReleaseObject message is asynchronous. | |
| // - The PPB_Var_Deprecated host-side proxy posts a task to actually | |
| // release the object when the ReleaseObject message is received. | |
| // - The PPP_Class Deallocate message is asynchronous. | |
| // At time of writing this comment, if you modify the code so that the above | |
| // happens synchronously, and you remove the restriction that the plugin | |
| // can't be unblocked by a sync message, then this check actually passes | |
| // reliably for out-of-process. But we don't want to make any of those | |
| // changes so we just skip the check. | |
| PP_DCHECK(!instance_so_); | |
| } else { | |
| // Out-of-process, this destructor might not actually get invoked. Clear | |
| // the InstanceSOs reference to the instance so there is no UAF. | |
| if (instance_so_) | |
| instance_so_->clear_test_instance(); | |
| } | |
| // Save the fact that we were destroyed in sessionStorage. This tests that | |
| // we can ExecuteScript at instance destruction without crashing. It also | |
| // allows us to check that ExecuteScript will run and succeed in certain | |
| // cases. In particular, when the instance is destroyed by normal DOM | |
| // deletion, ExecuteScript will actually work. See | |
| // TestExecuteScriptInInstanceShutdown for that test. Note, however, that | |
| // ExecuteScript will *not* have an effect when the instance is destroyed | |
| // because the renderer was shut down. | |
| pp::Var ret = instance()->ExecuteScript( | |
| "sessionStorage.setItem('instance_destroyed', 'true');"); | |
| } | |
| void TestInstance::RunTests(const std::string& filter) { | |
| RUN_TEST(ExecuteScript, filter); | |
| RUN_TEST(RecursiveObjects, filter); | |
| RUN_TEST(LeakedObjectDestructors, filter); | |
| RUN_TEST(SetupExecuteScriptAtInstanceShutdown, filter); | |
| RUN_TEST(ExecuteScriptAtInstanceShutdown, filter); | |
| } | |
| void TestInstance::LeakReferenceAndIgnore(const pp::Var& leaked) { | |
| static const PPB_Var* var_interface = static_cast<const PPB_Var*>( | |
| pp::Module::Get()->GetBrowserInterface(PPB_VAR_INTERFACE)); | |
| var_interface->AddRef(leaked.pp_var()); | |
| IgnoreLeakedVar(leaked.pp_var().value.as_id); | |
| } | |
| pp::deprecated::ScriptableObject* TestInstance::CreateTestObject() { | |
| if (!instance_so_) | |
| instance_so_ = new InstanceSO(this); | |
| return instance_so_; | |
| } | |
| std::string TestInstance::TestExecuteScript() { | |
| // Simple call back into the plugin. | |
| pp::Var exception; | |
| pp::Var ret = instance_->ExecuteScript( | |
| "document.getElementById('plugin').SetValue('hello, world');", | |
| &exception); | |
| ASSERT_TRUE(ret.is_undefined()); | |
| ASSERT_TRUE(exception.is_undefined()); | |
| ASSERT_TRUE(string_ == "hello, world"); | |
| // Return values from the plugin should be returned. | |
| ret = instance_->ExecuteScript( | |
| "document.getElementById('plugin').ReturnValue('return value');", | |
| &exception); | |
| ASSERT_TRUE(ret.is_string() && ret.AsString() == "return value"); | |
| ASSERT_TRUE(exception.is_undefined()); | |
| // Exception thrown by the plugin should be caught. | |
| ret = instance_->ExecuteScript( | |
| "document.getElementById('plugin').SetException('plugin exception');", | |
| &exception); | |
| ASSERT_TRUE(ret.is_undefined()); | |
| ASSERT_TRUE(exception.is_string()); | |
| // Due to a limitation in the implementation of TryCatch, it doesn't actually | |
| // pass the strings up. Since this is a trusted only interface, we've decided | |
| // not to bother fixing this for now. | |
| // Exception caused by string evaluation should be caught. | |
| exception = pp::Var(); | |
| ret = instance_->ExecuteScript("document.doesntExist()", &exception); | |
| ASSERT_TRUE(ret.is_undefined()); | |
| ASSERT_TRUE(exception.is_string()); // Don't know exactly what it will say. | |
| PASS(); | |
| } | |
| // A scriptable object that contains other scriptable objects recursively. This | |
| // is used to help verify that our scriptable object clean-up code works | |
| // properly. | |
| class ObjectWithChildren : public pp::deprecated::ScriptableObject { | |
| public: | |
| ObjectWithChildren(TestInstance* i, int num_descendents) { | |
| if (num_descendents > 0) { | |
| child_ = pp::VarPrivate(i->instance(), | |
| new ObjectWithChildren(i, num_descendents - 1)); | |
| } | |
| } | |
| struct IgnoreLeaks {}; | |
| ObjectWithChildren(TestInstance* i, int num_descendents, IgnoreLeaks) { | |
| if (num_descendents > 0) { | |
| child_ = pp::VarPrivate(i->instance(), | |
| new ObjectWithChildren(i, num_descendents - 1, | |
| IgnoreLeaks())); | |
| i->IgnoreLeakedVar(child_.pp_var().value.as_id); | |
| } | |
| } | |
| private: | |
| pp::VarPrivate child_; | |
| }; | |
| std::string TestInstance::TestRecursiveObjects() { | |
| const int kNumChildren = 20; | |
| { | |
| // These should be deleted when we exit scope, so should not leak. | |
| pp::VarPrivate not_leaked(instance(), new ObjectWithChildren(this, | |
| kNumChildren)); | |
| } | |
| // We need to run the GC multiple times until all of the vars are released. | |
| // Each GC invocation will result in releasing a var, which will result in its | |
| // children not having any references, allowing them also to be collected. | |
| for (int i = 0; i < kNumChildren; ++i) | |
| testing_interface_->RunV8GC(instance_->pp_instance()); | |
| // Leak some, but tell TestCase to ignore the leaks. This test is run and then | |
| // reloaded (see ppapi_uitest.cc). If these aren't cleaned up when the first | |
| // run is torn down, they will show up as leaks in the second run. | |
| // NOTE: The ScriptableObjects are actually leaked, but they should be removed | |
| // from the tracker. See below for a test that verifies that the | |
| // destructor is not run. | |
| pp::VarPrivate leaked( | |
| instance(), | |
| new ObjectWithChildren(this, kNumChildren, | |
| ObjectWithChildren::IgnoreLeaks())); | |
| // Now leak a reference to the root object. This should force the root and | |
| // all its descendents to stay in the tracker. | |
| LeakReferenceAndIgnore(leaked); | |
| PASS(); | |
| } | |
| // A scriptable object that should cause a crash if its destructor is run. We | |
| // don't run the destructor for objects which the plugin leaks. This is to | |
| // prevent them doing dangerous things at cleanup time, such as executing script | |
| // or creating new objects. | |
| class BadDestructorObject : public pp::deprecated::ScriptableObject { | |
| public: | |
| BadDestructorObject() {} | |
| ~BadDestructorObject() { | |
| assert(false); | |
| } | |
| }; | |
| std::string TestInstance::TestLeakedObjectDestructors() { | |
| pp::VarPrivate leaked(instance(), new BadDestructorObject()); | |
| // Leak a reference so it gets deleted on instance shutdown. | |
| LeakReferenceAndIgnore(leaked); | |
| PASS(); | |
| } | |
| std::string TestInstance::TestSetupExecuteScriptAtInstanceShutdown() { | |
| // This test only exists so that it can be run before | |
| // TestExecuteScriptAtInstanceShutdown. See the comment for that test. | |
| pp::Var exception; | |
| pp::Var result = instance()->ExecuteScript( | |
| "sessionStorage.removeItem('instance_destroyed');", &exception); | |
| ASSERT_TRUE(exception.is_undefined()); | |
| ASSERT_TRUE(result.is_undefined()); | |
| PASS(); | |
| } | |
| std::string TestInstance::TestExecuteScriptAtInstanceShutdown() { | |
| // This test relies on the previous test being run in the same browser | |
| // session, but in such a way that the instance is destroyed. See | |
| // chrome/test/ppapi/ppapi_browsertest.cc for how the navigation happens. | |
| // | |
| // Given those constraints, ~TestInstance should have been invoked to set | |
| // instance_destroyed in sessionStorage. So all we have to do is make sure | |
| // that it was set as expected. | |
| pp::Var result = instance()->ExecuteScript( | |
| "sessionStorage.getItem('instance_destroyed');"); | |
| ASSERT_TRUE(result.is_string()); | |
| ASSERT_EQ(std::string("true"), result.AsString()); | |
| instance()->ExecuteScript("sessionStorage.removeItem('instance_destroyed');"); | |
| PASS(); | |
| } | |