testObjectSwap.cpp (13328B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 */ 4 /* This Source Code Form is subject to the terms of the Mozilla Public 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 7 8 /* 9 * Test JSObject::swap. 10 * 11 * This test creates objects from a description of their configuration. Objects 12 * are given unique property names and values. A list of configurations is 13 * created and the result of swapping every combination checked. 14 */ 15 16 #include "mozilla/Sprintf.h" 17 18 #include "js/AllocPolicy.h" 19 #include "js/Vector.h" 20 #include "jsapi-tests/tests.h" 21 #include "vm/PlainObject.h" 22 23 #include "gc/StableCellHasher-inl.h" 24 #include "vm/JSContext-inl.h" 25 #include "vm/JSObject-inl.h" 26 27 using namespace js; 28 29 struct NativeConfig { 30 uint32_t propCount; 31 uint32_t elementCount; 32 bool inDictionaryMode; 33 }; 34 35 struct ProxyConfig { 36 bool inlineValues; 37 }; 38 39 struct ObjectConfig { 40 const JSClass* clasp; 41 bool isNative; 42 bool nurseryAllocated; 43 bool hasUniqueId; 44 union { 45 NativeConfig native; 46 ProxyConfig proxy; 47 }; 48 }; 49 50 using ObjectConfigVector = Vector<ObjectConfig, 0, SystemAllocPolicy>; 51 52 static const JSClass TestProxyClasses[] = { 53 PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(1 /* Min */)), 54 PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(2)), 55 PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(7)), 56 PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(8)), 57 PROXY_CLASS_DEF("TestProxy", JSCLASS_HAS_RESERVED_SLOTS(14 /* Max */)), 58 }; 59 60 static void TestDOMObject_finalize(JS::GCContext* gcx, JSObject* obj) { 61 // Dummy finalize method so we can swap with background finalized object. 62 } 63 64 static const JSClassOps TestDOMObjectClassOps = { 65 nullptr, // addProperty 66 nullptr, // delProperty 67 nullptr, // enumerate 68 nullptr, // newEnumerate 69 nullptr, // resolve 70 nullptr, // mayResolve 71 TestDOMObject_finalize, 72 nullptr, // call 73 nullptr, // construct 74 nullptr, 75 }; 76 77 #define DEFINE_TEST_DOM_CLASS(Slots) \ 78 {"TestDOMObject", \ 79 JSCLASS_IS_DOMJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(Slots) | \ 80 JSCLASS_BACKGROUND_FINALIZE | JSCLASS_SKIP_NURSERY_FINALIZE, \ 81 &TestDOMObjectClassOps} 82 83 static const JSClass TestDOMClasses[] = { 84 DEFINE_TEST_DOM_CLASS(0), DEFINE_TEST_DOM_CLASS(1), 85 DEFINE_TEST_DOM_CLASS(2), DEFINE_TEST_DOM_CLASS(7), 86 DEFINE_TEST_DOM_CLASS(8), DEFINE_TEST_DOM_CLASS(20)}; 87 88 #undef DEFINE_TEST_DOM_CLASS 89 90 static const uint32_t TestPropertyCounts[] = {0, 1, 2, 7, 8, 20}; 91 92 static const uint32_t TestElementCounts[] = {0, 20}; 93 94 static bool Verbose = false; 95 96 class TenuredProxyHandler final : public Wrapper { 97 public: 98 static const TenuredProxyHandler singleton; 99 constexpr TenuredProxyHandler() : Wrapper(0) {} 100 bool canNurseryAllocate() const override { return false; } 101 }; 102 103 const TenuredProxyHandler TenuredProxyHandler::singleton; 104 105 class NurseryProxyHandler final : public Wrapper { 106 public: 107 static const NurseryProxyHandler singleton; 108 constexpr NurseryProxyHandler() : Wrapper(0) {} 109 bool canNurseryAllocate() const override { return true; } 110 }; 111 112 const NurseryProxyHandler NurseryProxyHandler::singleton; 113 114 BEGIN_TEST(testObjectSwap) { 115 AutoLeaveZeal noZeal(cx); 116 117 ObjectConfigVector objectConfigs = CreateObjectConfigs(); 118 119 for (const ObjectConfig& config1 : objectConfigs) { 120 for (const ObjectConfig& config2 : objectConfigs) { 121 { 122 uint32_t id1; 123 RootedObject obj1(cx, CreateObject(config1, &id1)); 124 CHECK(obj1); 125 126 uint32_t id2; 127 RootedObject obj2(cx, CreateObject(config2, &id2)); 128 CHECK(obj2); 129 130 if (Verbose) { 131 fprintf(stderr, "Swap %p (%s) and %p (%s)\n", obj1.get(), 132 GetLocation(obj1), obj2.get(), GetLocation(obj2)); 133 } 134 135 uint64_t uid1 = 0; 136 if (config1.hasUniqueId) { 137 uid1 = gc::GetUniqueIdInfallible(obj1); 138 } 139 uint64_t uid2 = 0; 140 if (config2.hasUniqueId) { 141 uid2 = gc::GetUniqueIdInfallible(obj2); 142 } 143 144 { 145 AutoEnterOOMUnsafeRegion oomUnsafe; 146 JSObject::swap(cx, obj1, obj2, oomUnsafe); 147 } 148 149 CHECK(CheckObject(obj1, config2, id2)); 150 CHECK(CheckObject(obj2, config1, id1)); 151 152 CHECK(CheckUniqueIds(obj1, config1.hasUniqueId, uid1, obj2, 153 config2.hasUniqueId, uid2)); 154 155 // Check we can promote swapped nursery objects. 156 cx->minorGC(JS::GCReason::API); 157 } 158 159 if (Verbose) { 160 fprintf(stderr, "\n"); 161 } 162 } 163 164 // JSObject::swap can suppress GC so ensure we clean up occasionally. 165 JS_GC(cx); 166 } 167 168 return true; 169 } 170 171 ObjectConfigVector CreateObjectConfigs() { 172 ObjectConfigVector configs; 173 174 ObjectConfig config; 175 176 for (bool nurseryAllocated : {false, true}) { 177 config.nurseryAllocated = nurseryAllocated; 178 179 for (bool hasUniqueId : {false, true}) { 180 config.hasUniqueId = hasUniqueId; 181 182 config.isNative = true; 183 config.native = NativeConfig{0, false}; 184 185 for (const JSClass& jsClass : TestDOMClasses) { 186 config.clasp = &jsClass; 187 188 for (uint32_t propCount : TestPropertyCounts) { 189 config.native.propCount = propCount; 190 191 for (uint32_t elementCount : TestElementCounts) { 192 config.native.elementCount = elementCount; 193 194 for (bool inDictionaryMode : {false, true}) { 195 if (inDictionaryMode && propCount == 0) { 196 continue; 197 } 198 199 config.native.inDictionaryMode = inDictionaryMode; 200 MOZ_RELEASE_ASSERT(configs.append(config)); 201 } 202 } 203 } 204 } 205 206 config.isNative = false; 207 config.proxy = ProxyConfig{false}; 208 209 for (const JSClass& jsClass : TestProxyClasses) { 210 config.clasp = &jsClass; 211 212 for (bool inlineValues : {true, false}) { 213 config.proxy.inlineValues = inlineValues; 214 MOZ_RELEASE_ASSERT(configs.append(config)); 215 } 216 } 217 } 218 } 219 220 return configs; 221 } 222 223 const char* GetLocation(JSObject* obj) { 224 return obj->isTenured() ? "tenured heap" : "nursery"; 225 } 226 227 // Counter used to give slots and property names unique values. 228 uint32_t nextId = 0; 229 230 JSObject* CreateObject(const ObjectConfig& config, uint32_t* idOut) { 231 *idOut = nextId; 232 JSObject* obj = 233 config.isNative ? CreateNativeObject(config) : CreateProxy(config); 234 235 if (config.hasUniqueId) { 236 uint64_t unused; 237 if (!gc::GetOrCreateUniqueId(obj, &unused)) { 238 return nullptr; 239 } 240 } 241 242 return obj; 243 } 244 245 JSObject* CreateNativeObject(const ObjectConfig& config) { 246 MOZ_ASSERT(config.isNative); 247 248 NewObjectKind kind = config.nurseryAllocated ? GenericObject : TenuredObject; 249 Rooted<NativeObject*> obj(cx, 250 NewBuiltinClassInstance(cx, config.clasp, kind)); 251 if (!obj) { 252 return nullptr; 253 } 254 255 MOZ_RELEASE_ASSERT(IsInsideNursery(obj) == config.nurseryAllocated); 256 257 for (uint32_t i = 0; i < JSCLASS_RESERVED_SLOTS(config.clasp); i++) { 258 JS::SetReservedSlot(obj, i, Int32Value(nextId++)); 259 } 260 261 if (config.native.inDictionaryMode) { 262 // Put object in dictionary mode by defining a non-last property and 263 // deleting it later. 264 MOZ_RELEASE_ASSERT(config.native.propCount != 0); 265 if (!JS_DefineProperty(cx, obj, "dummy", 0, JSPROP_ENUMERATE)) { 266 return nullptr; 267 } 268 } 269 270 for (uint32_t i = 0; i < config.native.propCount; i++) { 271 RootedId name(cx, CreatePropName(nextId++)); 272 if (name.isVoid()) { 273 return nullptr; 274 } 275 276 if (!JS_DefinePropertyById(cx, obj, name, nextId++, JSPROP_ENUMERATE)) { 277 return nullptr; 278 } 279 } 280 281 if (config.native.elementCount) { 282 if (!obj->ensureElements(cx, config.native.elementCount)) { 283 return nullptr; 284 } 285 for (uint32_t i = 0; i < config.native.elementCount; i++) { 286 obj->setDenseInitializedLength(i + 1); 287 obj->initDenseElement(i, Int32Value(nextId++)); 288 } 289 MOZ_ASSERT(obj->hasDynamicElements()); 290 } 291 292 if (config.native.inDictionaryMode) { 293 JS::ObjectOpResult result; 294 JS_DeleteProperty(cx, obj, "dummy", result); 295 MOZ_RELEASE_ASSERT(result.ok()); 296 } 297 298 MOZ_RELEASE_ASSERT(obj->inDictionaryMode() == config.native.inDictionaryMode); 299 300 return obj; 301 } 302 303 JSObject* CreateProxy(const ObjectConfig& config) { 304 RootedValue priv(cx, Int32Value(nextId++)); 305 306 RootedObject expando(cx, NewPlainObject(cx)); 307 RootedValue expandoId(cx, Int32Value(nextId++)); 308 if (!expando || !JS_SetProperty(cx, expando, "id", expandoId)) { 309 return nullptr; 310 } 311 312 ProxyOptions options; 313 options.setClass(config.clasp); 314 options.setLazyProto(true); 315 316 const Wrapper* handler; 317 if (config.nurseryAllocated) { 318 handler = &NurseryProxyHandler::singleton; 319 } else { 320 handler = &TenuredProxyHandler::singleton; 321 } 322 323 RootedObject obj(cx, NewProxyObject(cx, handler, priv, nullptr, options)); 324 if (!obj) { 325 return nullptr; 326 } 327 328 Rooted<ProxyObject*> proxy(cx, &obj->as<ProxyObject>()); 329 proxy->setExpando(expando); 330 331 for (uint32_t i = 0; i < JSCLASS_RESERVED_SLOTS(config.clasp); i++) { 332 JS::SetReservedSlot(proxy, i, Int32Value(nextId++)); 333 } 334 335 if (!config.proxy.inlineValues) { 336 // To create a proxy with non-inline values we must swap the proxy with an 337 // object with a different size. 338 NewObjectKind kind = 339 config.nurseryAllocated ? GenericObject : TenuredObject; 340 RootedObject dummy(cx, 341 NewBuiltinClassInstance(cx, &TestDOMClasses[0], kind)); 342 if (!dummy) { 343 return nullptr; 344 } 345 346 AutoEnterOOMUnsafeRegion oomUnsafe; 347 JSObject::swap(cx, obj, dummy, oomUnsafe); 348 proxy = &dummy->as<ProxyObject>(); 349 } 350 351 MOZ_RELEASE_ASSERT(IsInsideNursery(proxy) == config.nurseryAllocated); 352 MOZ_RELEASE_ASSERT(proxy->usingInlineValueArray() == 353 config.proxy.inlineValues); 354 355 return proxy; 356 } 357 358 bool CheckObject(HandleObject obj, const ObjectConfig& config, uint32_t id) { 359 CHECK(obj->is<NativeObject>() == config.isNative); 360 CHECK(obj->getClass() == config.clasp); 361 362 uint32_t reservedSlots = JSCLASS_RESERVED_SLOTS(config.clasp); 363 364 if (Verbose) { 365 fprintf(stderr, "Check %p is a %s object with %u reserved slots", obj.get(), 366 config.isNative ? "native" : "proxy", reservedSlots); 367 if (config.isNative) { 368 fprintf(stderr, 369 ", %u properties, %u elements and %s in dictionary mode\n", 370 config.native.propCount, config.native.elementCount, 371 config.native.inDictionaryMode ? "is" : "is not"); 372 } else { 373 fprintf(stderr, " with %s values\n", 374 config.proxy.inlineValues ? "inline" : "out-of-line"); 375 } 376 } 377 378 if (!config.isNative) { 379 CHECK(GetProxyPrivate(obj) == Int32Value(id++)); 380 381 Value expandoValue = GetProxyExpando(obj); 382 CHECK(expandoValue.isObject()); 383 384 RootedObject expando(cx, &expandoValue.toObject()); 385 RootedValue expandoId(cx); 386 JS_GetProperty(cx, expando, "id", &expandoId); 387 CHECK(expandoId == Int32Value(id++)); 388 } 389 390 for (uint32_t i = 0; i < reservedSlots; i++) { 391 CHECK(JS::GetReservedSlot(obj, i) == Int32Value(id++)); 392 } 393 394 if (config.isNative) { 395 Rooted<NativeObject*> nobj(cx, &obj->as<NativeObject>()); 396 uint32_t propCount = GetPropertyCount(nobj); 397 398 CHECK(propCount == config.native.propCount); 399 400 for (uint32_t i = 0; i < config.native.propCount; i++) { 401 RootedId name(cx, CreatePropName(id++)); 402 CHECK(!name.isVoid()); 403 404 RootedValue value(cx); 405 CHECK(JS_GetPropertyById(cx, obj, name, &value)); 406 CHECK(value == Int32Value(id++)); 407 } 408 409 CHECK(nobj->getDenseInitializedLength() == config.native.elementCount); 410 for (uint32_t i = 0; i < config.native.elementCount; i++) { 411 Value value = nobj->getDenseElement(i); 412 CHECK(value == Int32Value(id++)); 413 } 414 } 415 416 return true; 417 } 418 419 bool CheckUniqueIds(HandleObject obj1, bool hasUniqueId1, uint64_t uid1, 420 HandleObject obj2, bool hasUniqueId2, uint64_t uid2) { 421 if (uid1 && uid2) { 422 MOZ_RELEASE_ASSERT(uid1 != uid2); 423 } 424 425 // Check unique IDs are NOT swapped. 426 CHECK(CheckUniqueId(obj1, hasUniqueId1, uid1)); 427 CHECK(CheckUniqueId(obj2, hasUniqueId2, uid2)); 428 429 // Check unique IDs are different if present. 430 if (gc::HasUniqueId(obj1) && gc::HasUniqueId(obj2)) { 431 CHECK(gc::GetUniqueIdInfallible(obj1) != gc::GetUniqueIdInfallible(obj2)); 432 } 433 434 return true; 435 } 436 437 bool CheckUniqueId(HandleObject obj, bool hasUniqueId, uint64_t uid) { 438 if (hasUniqueId) { 439 CHECK(gc::HasUniqueId(obj)); 440 CHECK(gc::GetUniqueIdInfallible(obj) == uid); 441 } else { 442 // Swap may add a unique ID to an object. 443 } 444 445 if (obj->is<NativeObject>()) { 446 CHECK(!obj->zone()->uniqueIds().has(obj)); 447 } 448 return true; 449 } 450 451 uint32_t GetPropertyCount(NativeObject* obj) { 452 uint32_t count = 0; 453 for (ShapePropertyIter<NoGC> iter(obj->shape()); !iter.done(); iter++) { 454 count++; 455 } 456 457 return count; 458 } 459 460 jsid CreatePropName(uint32_t id) { 461 char buffer[32]; 462 SprintfLiteral(buffer, "prop%u", id); 463 464 RootedString atom(cx, JS_AtomizeString(cx, buffer)); 465 if (!atom) { 466 return jsid::Void(); 467 } 468 469 return jsid::NonIntAtom(atom); 470 } 471 472 END_TEST(testObjectSwap)