/* 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 "builtin/TestingFunctions.h" #include "js/ArrayBuffer.h" // JS::{IsArrayBufferObject,GetArrayBufferLengthAndData,NewExternalArrayBuffer} #include "js/GlobalObject.h" // JS_NewGlobalObject #include "js/PropertyAndElement.h" // JS_GetProperty, JS_SetProperty #include "js/StructuredClone.h" #include "jsapi-tests/tests.h" using namespace js; #ifdef DEBUG // Skip test, since it will abort with an assert in buf->Init(7). #else BEGIN_TEST(testStructuredClone_invalidLength) { auto buf = js::MakeUnique( JS::StructuredCloneScope::DifferentProcess); CHECK(buf); CHECK(buf->Init(7)); RootedValue clone(cx); JS::CloneDataPolicy policy; CHECK(!JS_ReadStructuredClone(cx, *buf, JS_STRUCTURED_CLONE_VERSION, JS::StructuredCloneScope::DifferentProcess, &clone, policy, nullptr, nullptr)); return true; } END_TEST(testStructuredClone_invalidLength) #endif BEGIN_TEST(testStructuredClone_object) { JS::RootedObject g1(cx, createGlobal()); JS::RootedObject g2(cx, createGlobal()); CHECK(g1); CHECK(g2); JS::RootedValue v1(cx); { JSAutoRealm ar(cx, g1); JS::RootedValue prop(cx, JS::Int32Value(1337)); JS::RootedObject obj(cx, JS_NewPlainObject(cx)); v1 = JS::ObjectOrNullValue(obj); CHECK(v1.isObject()); CHECK(JS_SetProperty(cx, obj, "prop", prop)); } { JSAutoRealm ar(cx, g2); JS::RootedValue v2(cx); CHECK(JS_StructuredClone(cx, v1, &v2, nullptr, nullptr)); CHECK(v2.isObject()); JS::RootedObject obj(cx, &v2.toObject()); JS::RootedValue prop(cx); CHECK(JS_GetProperty(cx, obj, "prop", &prop)); CHECK(prop.isInt32()); CHECK(&v1.toObject() != obj); CHECK_EQUAL(prop.toInt32(), 1337); } return true; } END_TEST(testStructuredClone_object) BEGIN_TEST(testStructuredClone_string) { JS::RootedObject g1(cx, createGlobal()); JS::RootedObject g2(cx, createGlobal()); CHECK(g1); CHECK(g2); JS::RootedValue v1(cx); { JSAutoRealm ar(cx, g1); JS::RootedValue prop(cx, JS::Int32Value(1337)); v1 = JS::StringValue(JS_NewStringCopyZ(cx, "Hello World!")); CHECK(v1.isString()); CHECK(v1.toString()); } { JSAutoRealm ar(cx, g2); JS::RootedValue v2(cx); CHECK(JS_StructuredClone(cx, v1, &v2, nullptr, nullptr)); CHECK(v2.isString()); CHECK(v2.toString()); JS::RootedValue expected( cx, JS::StringValue(JS_NewStringCopyZ(cx, "Hello World!"))); CHECK_SAME(v2, expected); } return true; } END_TEST(testStructuredClone_string) BEGIN_TEST(testStructuredClone_externalArrayBuffer) { ExternalData data("One two three four"); JS::RootedObject g1(cx, createGlobal()); JS::RootedObject g2(cx, createGlobal()); CHECK(g1); CHECK(g2); JS::RootedValue v1(cx); { JSAutoRealm ar(cx, g1); JS::RootedObject obj( cx, JS::NewExternalArrayBuffer(cx, data.len(), data.contents(), &ExternalData::freeCallback, &data)); CHECK(!data.wasFreed()); v1 = JS::ObjectOrNullValue(obj); CHECK(v1.isObject()); } { JSAutoRealm ar(cx, g2); JS::RootedValue v2(cx); CHECK(JS_StructuredClone(cx, v1, &v2, nullptr, nullptr)); CHECK(v2.isObject()); JS::RootedObject obj(cx, &v2.toObject()); CHECK(&v1.toObject() != obj); size_t len; bool isShared; uint8_t* clonedData; JS::GetArrayBufferLengthAndData(obj, &len, &isShared, &clonedData); // The contents of the two array buffers should be equal, but not the // same pointer. CHECK_EQUAL(len, data.len()); CHECK(clonedData != data.contents()); CHECK(strcmp(reinterpret_cast(clonedData), data.asString()) == 0); CHECK(!data.wasFreed()); } // GC the array buffer before data goes out of scope v1.setNull(); JS_GC(cx); JS_GC(cx); // Trigger another to wait for background finalization to end CHECK(data.wasFreed()); return true; } END_TEST(testStructuredClone_externalArrayBuffer) BEGIN_TEST(testStructuredClone_externalArrayBufferDifferentThreadOrProcess) { CHECK(testStructuredCloneCopy(JS::StructuredCloneScope::SameProcess)); CHECK(testStructuredCloneCopy(JS::StructuredCloneScope::DifferentProcess)); return true; } bool testStructuredCloneCopy(JS::StructuredCloneScope scope) { ExternalData data("One two three four"); JS::RootedObject buffer( cx, JS::NewExternalArrayBuffer(cx, data.len(), data.contents(), &ExternalData::freeCallback, &data)); CHECK(buffer); CHECK(!data.wasFreed()); JS::RootedValue v1(cx, JS::ObjectValue(*buffer)); JS::RootedValue v2(cx); CHECK(clone(scope, v1, &v2)); JS::RootedObject bufferOut(cx, v2.toObjectOrNull()); CHECK(bufferOut); CHECK(JS::IsArrayBufferObject(bufferOut)); size_t len; bool isShared; uint8_t* clonedData; JS::GetArrayBufferLengthAndData(bufferOut, &len, &isShared, &clonedData); // Cloning should copy the data, so the contents of the two array buffers // should be equal, but not the same pointer. CHECK_EQUAL(len, data.len()); CHECK(clonedData != data.contents()); CHECK(strcmp(reinterpret_cast(clonedData), data.asString()) == 0); CHECK(!data.wasFreed()); buffer = nullptr; bufferOut = nullptr; v1.setNull(); v2.setNull(); JS_GC(cx); JS_GC(cx); CHECK(data.wasFreed()); return true; } bool clone(JS::StructuredCloneScope scope, JS::HandleValue v1, JS::MutableHandleValue v2) { JSAutoStructuredCloneBuffer clonedBuffer(scope, nullptr, nullptr); CHECK(clonedBuffer.write(cx, v1)); CHECK(clonedBuffer.read(cx, v2)); return true; } END_TEST(testStructuredClone_externalArrayBufferDifferentThreadOrProcess) struct StructuredCloneTestPrincipals final : public JSPrincipals { uint32_t rank; explicit StructuredCloneTestPrincipals(uint32_t rank, int32_t rc = 1) : rank(rank) { this->refcount = rc; } bool write(JSContext* cx, JSStructuredCloneWriter* writer) override { return JS_WriteUint32Pair(writer, rank, 0); } bool isSystemOrAddonPrincipal() override { return true; } static bool read(JSContext* cx, JSStructuredCloneReader* reader, JSPrincipals** outPrincipals) { uint32_t rank; uint32_t unused; if (!JS_ReadUint32Pair(reader, &rank, &unused)) { return false; } *outPrincipals = new StructuredCloneTestPrincipals(rank); return !!*outPrincipals; } static void destroy(JSPrincipals* p) { auto p1 = static_cast(p); delete p1; } static uint32_t getRank(JSPrincipals* p) { if (!p) { return 0; } return static_cast(p)->rank; } static bool subsumes(JSPrincipals* a, JSPrincipals* b) { return getRank(a) > getRank(b); } static JSSecurityCallbacks securityCallbacks; static StructuredCloneTestPrincipals testPrincipals; }; JSSecurityCallbacks StructuredCloneTestPrincipals::securityCallbacks = { nullptr, // contentSecurityPolicyAllows subsumes}; BEGIN_TEST(testStructuredClone_SavedFrame) { JS_SetSecurityCallbacks(cx, &StructuredCloneTestPrincipals::securityCallbacks); JS_InitDestroyPrincipalsCallback(cx, StructuredCloneTestPrincipals::destroy); JS_InitReadPrincipalsCallback(cx, StructuredCloneTestPrincipals::read); auto testPrincipals = new StructuredCloneTestPrincipals(42, 0); CHECK(testPrincipals); auto DONE = (JSPrincipals*)0xDEADBEEF; struct { const char* name; JSPrincipals* principals; } principalsToTest[] = { {"IsSystem", &js::ReconstructedSavedFramePrincipals::IsSystem}, {"IsNotSystem", &js::ReconstructedSavedFramePrincipals::IsNotSystem}, {"testPrincipals", testPrincipals}, {"nullptr principals", nullptr}, {"DONE", DONE}}; const char* FILENAME = "filename.js"; for (auto* pp = principalsToTest; pp->principals != DONE; pp++) { fprintf(stderr, "Testing with principals '%s'\n", pp->name); JS::RealmOptions options; JS::RootedObject g(cx, JS_NewGlobalObject(cx, getGlobalClass(), pp->principals, JS::FireOnNewGlobalHook, options)); CHECK(g); JSAutoRealm ar(cx, g); CHECK(js::DefineTestingFunctions(cx, g, false, false)); JS::RootedValue srcVal(cx); CHECK( evaluate("(function one() { \n" // 1 " return (function two() { \n" // 2 " return (function three() { \n" // 3 " return saveStack(); \n" // 4 " }()); \n" // 5 " }()); \n" // 6 "}()); \n", // 7 FILENAME, 1, &srcVal)); CHECK(srcVal.isObject()); JS::RootedObject srcObj(cx, &srcVal.toObject()); CHECK(srcObj->is()); JS::Rooted srcFrame(cx, &srcObj->as()); CHECK(srcFrame->getPrincipals() == pp->principals); JS::RootedValue destVal(cx); CHECK(JS_StructuredClone(cx, srcVal, &destVal, nullptr, nullptr)); CHECK(destVal.isObject()); JS::RootedObject destObj(cx, &destVal.toObject()); CHECK(destObj->is()); JS::Handle destFrame = destObj.as(); size_t framesCopied = 0; for (JS::Handle f : js::SavedFrame::RootedRange(cx, destFrame)) { framesCopied++; CHECK(f != srcFrame); if (pp->principals == testPrincipals) { // We shouldn't get a pointer to the same // StructuredCloneTestPrincipals instance since we should have // serialized and then deserialized it into a new instance. CHECK(f->getPrincipals() != pp->principals); // But it should certainly have the same rank. CHECK(StructuredCloneTestPrincipals::getRank(f->getPrincipals()) == StructuredCloneTestPrincipals::getRank(pp->principals)); } else { // For our singleton principals, we should always get the same // pointer back. CHECK(js::ReconstructedSavedFramePrincipals::is(pp->principals) || pp->principals == nullptr); CHECK(f->getPrincipals() == pp->principals); } CHECK(EqualStrings(f->getSource(), srcFrame->getSource())); CHECK(f->getLine() == srcFrame->getLine()); CHECK(f->getColumn() == srcFrame->getColumn()); CHECK(EqualStrings(f->getFunctionDisplayName(), srcFrame->getFunctionDisplayName())); srcFrame = srcFrame->getParent(); } // Four function frames + one global frame. CHECK(framesCopied == 4); } return true; } END_TEST(testStructuredClone_SavedFrame)