File
Blob: src/workerd/jsg/jsg-test.c++
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #include "jsg-test.h" |
| 6 | |
| 7 | namespace workerd::jsg::test { |
| 8 | namespace { |
| 9 | |
| 10 | // Non-JSG types aren't GC-visitable. |
| 11 | static_assert(!isGcVisitable<int>()); |
| 12 | static_assert(!isGcVisitable<kj::String>()); |
| 13 | |
| 14 | // Various reference types are. |
| 15 | static_assert(isGcVisitable<Ref<Object>>()); |
| 16 | static_assert(isGcVisitable<kj::Maybe<Ref<Object>>>()); |
| 17 | static_assert(isGcVisitable<Data>()); |
| 18 | static_assert(isGcVisitable<V8Ref<v8::Object>>()); |
| 19 | |
| 20 | // Resource types are not directly visitable. Their visitForGc() is private. You should be visiting |
| 21 | // a Ref<T> pointing at them instead. |
| 22 | static_assert(!isGcVisitable<Object>()); |
| 23 | static_assert(!isGcVisitable<NumberBox>()); |
| 24 | static_assert(!isGcVisitable<BoxBox>()); |
| 25 | |
| 26 | // Any type that defines a public visitForGc() is visitable. |
| 27 | static_assert(isGcVisitable<TestStruct>()); |
| 28 | static_assert(isGcVisitable<kj::Maybe<TestStruct>>()); |
| 29 | |
| 30 | // jsg::Lock is not acceptable as a coroutine param |
| 31 | static_assert(kj::_::isDisallowedInCoroutine<Lock>()); |
| 32 | static_assert(kj::_::isDisallowedInCoroutine<Lock&>()); |
| 33 | static_assert(kj::_::isDisallowedInCoroutine<Lock*>()); |
| 34 | |
| 35 | // ======================================================================================== |
| 36 | |
| 37 | V8System v8System; |
| 38 | class ContextGlobalObject: public Object, public ContextGlobal {}; |
| 39 | |
| 40 | struct TestContext: public ContextGlobalObject { |
| 41 | JSG_RESOURCE_TYPE(TestContext) {} |
| 42 | }; |
| 43 | JSG_DECLARE_ISOLATE_TYPE(TestIsolate, TestContext); |
| 44 | |
| 45 | KJ_TEST("hello world") { |
| 46 | Evaluator<TestContext, TestIsolate> e(v8System); |
| 47 | e.expectEval("'Hello' + ', World!'", "string", "Hello, World!"); |
| 48 | } |
| 49 | |
| 50 | KJ_TEST("throw") { |
| 51 | Evaluator<TestContext, TestIsolate> e(v8System); |
| 52 | e.expectEval("throw new Error('some error message')", "throws", "Error: some error message"); |
| 53 | } |
| 54 | |
| 55 | KJ_TEST("context type is exposed in the global scope") { |
| 56 | Evaluator<TestContext, TestIsolate> e(v8System); |
| 57 | e.expectEval("this instanceof TestContext", "boolean", "true"); |
| 58 | } |
| 59 | |
| 60 | // ======================================================================================== |
| 61 | |
| 62 | struct InheritContext: public ContextGlobalObject { |
| 63 | struct Other: public Object { |
| 64 | static jsg::Ref<Other> constructor(jsg::Lock& js) { |
| 65 | return js.alloc<Other>(); |
| 66 | } |
| 67 | JSG_RESOURCE_TYPE(Other) {} |
| 68 | }; |
| 69 | |
| 70 | Ref<NumberBox> newExtendedAsBase(jsg::Lock& js, double value, kj::String text) { |
| 71 | return ExtendedNumberBox::constructor(js, value, kj::mv(text)); |
| 72 | } |
| 73 | |
| 74 | JSG_RESOURCE_TYPE(InheritContext) { |
| 75 | JSG_NESTED_TYPE(NumberBox); |
| 76 | JSG_NESTED_TYPE(Other); |
| 77 | JSG_NESTED_TYPE(ExtendedNumberBox); |
| 78 | |
| 79 | JSG_METHOD(newExtendedAsBase); |
| 80 | } |
| 81 | }; |
| 82 | JSG_DECLARE_ISOLATE_TYPE( |
| 83 | InheritIsolate, InheritContext, NumberBox, InheritContext::Other, ExtendedNumberBox); |
| 84 | |
| 85 | KJ_TEST("inheritance") { |
| 86 | Evaluator<InheritContext, InheritIsolate> e(v8System); |
| 87 | e.expectEval("var n = new ExtendedNumberBox(123, 'foo');\n" |
| 88 | "n.increment();\n" |
| 89 | "n.getValue()", |
| 90 | "number", "124"); |
| 91 | |
| 92 | e.expectEval("var n = new ExtendedNumberBox(123, 'foo');\n" |
| 93 | "n.increment();\n" |
| 94 | "n.value", |
| 95 | "number", "124"); |
| 96 | |
| 97 | e.expectEval("new ExtendedNumberBox(123, 'foo').getText()", "string", "foo"); |
| 98 | |
| 99 | e.expectEval("var n = new ExtendedNumberBox(123, 'foo');\n" |
| 100 | "n.setText('bar');\n" |
| 101 | "n.text", |
| 102 | "string", "bar"); |
| 103 | |
| 104 | e.expectEval("var n = new ExtendedNumberBox(123, 'foo');\n" |
| 105 | "n.text = 'bar';\n" |
| 106 | "n.getText()", |
| 107 | "string", "bar"); |
| 108 | |
| 109 | e.expectEval("new ExtendedNumberBox(123, 'foo') instanceof NumberBox", "boolean", "true"); |
| 110 | |
| 111 | e.expectEval("new ExtendedNumberBox(123, 'foo') instanceof ExtendedNumberBox", "boolean", "true"); |
| 112 | |
| 113 | e.expectEval("new ExtendedNumberBox(123, 'foo') instanceof Other", "boolean", "false"); |
| 114 | |
| 115 | e.expectEval("newExtendedAsBase(123, 'foo') instanceof NumberBox", "boolean", "true"); |
| 116 | |
| 117 | e.expectEval("newExtendedAsBase(123, 'foo') instanceof ExtendedNumberBox", "boolean", "true"); |
| 118 | } |
| 119 | |
| 120 | // ======================================================================================== |
| 121 | |
| 122 | struct Utf8Context: public ContextGlobalObject { |
| 123 | bool callWithBmpUnicode(Lock& js, jsg::Function<bool(kj::StringPtr)> function) { |
| 124 | return function(js, "中国网络"); |
| 125 | } |
| 126 | bool callWithEmojiUnicode(Lock& js, jsg::Function<bool(kj::StringPtr)> function) { |
| 127 | return function(js, "😺☁️☄️🐵"); |
| 128 | } |
| 129 | JSG_RESOURCE_TYPE(Utf8Context) { |
| 130 | JSG_METHOD(callWithBmpUnicode); |
| 131 | JSG_METHOD(callWithEmojiUnicode); |
| 132 | } |
| 133 | }; |
| 134 | JSG_DECLARE_ISOLATE_TYPE(Utf8Isolate, Utf8Context); |
| 135 | |
| 136 | KJ_TEST("utf-8 scripts") { |
| 137 | Evaluator<Utf8Context, Utf8Isolate> e(v8System); |
| 138 | |
| 139 | // BMP unicode. |
| 140 | e.expectEval("'中国网络'", "string", "中国网络"); |
| 141 | |
| 142 | // Emoji unicode (including non-BMP characters). |
| 143 | e.expectEval("'😺☁️☄️🐵'", "string", "😺☁️☄️🐵"); |
| 144 | |
| 145 | // Go the other way. |
| 146 | e.expectEval("callWithBmpUnicode(str => str == '中国网络')", "boolean", "true"); |
| 147 | e.expectEval("callWithEmojiUnicode(str => str == '😺☁️☄️🐵')", "boolean", "true"); |
| 148 | } |
| 149 | |
| 150 | // ======================================================================================== |
| 151 | |
| 152 | struct RefContext: public ContextGlobalObject { |
| 153 | Ref<NumberBox> addAndReturnCopy(jsg::Lock& js, NumberBox& box, double value) { |
| 154 | auto copy = js.alloc<NumberBox>(box.value); |
| 155 | copy->value += value; |
| 156 | return copy; |
| 157 | } |
| 158 | Ref<NumberBox> addAndReturnOwn(Ref<NumberBox> box, double value) { |
| 159 | box->value += value; |
| 160 | return box; |
| 161 | } |
| 162 | |
| 163 | JSG_RESOURCE_TYPE(RefContext) { |
| 164 | JSG_NESTED_TYPE(NumberBox); |
| 165 | JSG_METHOD(addAndReturnCopy); |
| 166 | JSG_METHOD(addAndReturnOwn); |
| 167 | } |
| 168 | }; |
| 169 | JSG_DECLARE_ISOLATE_TYPE(RefIsolate, RefContext, NumberBox); |
| 170 | |
| 171 | KJ_TEST("Ref") { |
| 172 | Evaluator<RefContext, RefIsolate> e(v8System); |
| 173 | // addAndReturnCopy() creates a new object and returns it. |
| 174 | e.expectEval("var orig = new NumberBox(123);\n" |
| 175 | "var result = addAndReturnCopy(orig, 321);\n" |
| 176 | "[orig.value, result.value, orig == result].join(', ')", |
| 177 | "string", "123, 444, false"); |
| 178 | |
| 179 | // addAndReturnOwn() modifies the original object and returns it by identity. |
| 180 | e.expectEval("var orig = new NumberBox(123);\n" |
| 181 | "var result = addAndReturnOwn(orig, 321);\n" |
| 182 | "[orig.value, result.value, orig == result].join(', ')", |
| 183 | "string", "444, 444, true"); |
| 184 | } |
| 185 | |
| 186 | // ======================================================================================== |
| 187 | |
| 188 | struct ProtoContext: public ContextGlobalObject { |
| 189 | ProtoContext(): contextProperty(kj::str("default-context-property-value")) {} |
| 190 | |
| 191 | kj::StringPtr getContextProperty() { |
| 192 | return contextProperty; |
| 193 | } |
| 194 | void setContextProperty(kj::String s) { |
| 195 | contextProperty = kj::mv(s); |
| 196 | } |
| 197 | |
| 198 | JSG_RESOURCE_TYPE(ProtoContext) { |
| 199 | JSG_NESTED_TYPE(NumberBox); |
| 200 | JSG_NESTED_TYPE(BoxBox); |
| 201 | JSG_NESTED_TYPE(ExtendedNumberBox); |
| 202 | JSG_METHOD(getContextProperty); |
| 203 | JSG_METHOD(setContextProperty); |
| 204 | JSG_INSTANCE_PROPERTY(contextProperty, getContextProperty, setContextProperty); |
| 205 | } |
| 206 | |
| 207 | private: |
| 208 | kj::String contextProperty; |
| 209 | }; |
| 210 | JSG_DECLARE_ISOLATE_TYPE(ProtoIsolate, ProtoContext, NumberBox, BoxBox, ExtendedNumberBox); |
| 211 | |
| 212 | const auto kIllegalInvocation = |
| 213 | "TypeError: Illegal invocation: function called with incorrect `this` reference. " |
| 214 | "See https://developers.cloudflare.com/workers/observability/errors/#illegal-invocation-errors for details."_kj; |
| 215 | |
| 216 | KJ_TEST("can't invoke builtin methods with alternative 'this'") { |
| 217 | Evaluator<ProtoContext, ProtoIsolate> e(v8System); |
| 218 | e.expectEval("NumberBox.prototype.getValue.call(123)", "throws", kIllegalInvocation); |
| 219 | e.expectEval("NumberBox.prototype.getValue.call(new BoxBox(new NumberBox(123), 123))", "throws", |
| 220 | kIllegalInvocation); |
| 221 | e.expectEval("getContextProperty.call(new NumberBox(123))", "throws", kIllegalInvocation); |
| 222 | } |
| 223 | |
| 224 | KJ_TEST("can't use builtin as prototype") { |
| 225 | Evaluator<ProtoContext, ProtoIsolate> e(v8System); |
| 226 | e.expectEval("function JsType() {}\n" |
| 227 | "JsType.prototype = new NumberBox(123);\n" |
| 228 | "new JsType().getValue()", |
| 229 | "throws", kIllegalInvocation); |
| 230 | e.expectEval("function JsType() {}\n" |
| 231 | "JsType.prototype = new ExtendedNumberBox(123, 'foo');\n" |
| 232 | "new JsType().getValue()", |
| 233 | "throws", kIllegalInvocation); |
| 234 | e.expectEval("function JsType() {}\n" |
| 235 | "JsType.prototype = new NumberBox(123);\n" |
| 236 | "new JsType().value", |
| 237 | "number", "123"); |
| 238 | e.expectEval("function JsType() {}\n" |
| 239 | "JsType.prototype = new NumberBox(123);\n" |
| 240 | "let t = new JsType();\n" |
| 241 | "Reflect.get(JsType.prototype, 'value', t)\n", |
| 242 | "number", "123"); |
| 243 | e.expectEval("function JsType() {}\n" |
| 244 | "JsType.prototype = new ExtendedNumberBox(123, 'foo');\n" |
| 245 | "new JsType().value", |
| 246 | "number", "123"); |
| 247 | e.expectEval("function JsType() {}\n" |
| 248 | "JsType.prototype = this;\n" |
| 249 | "new JsType().getContextProperty()", |
| 250 | "throws", kIllegalInvocation); |
| 251 | |
| 252 | // For historical reasons, we allow using the global object as a prototype and accessing |
| 253 | // properties through a derived object. Our accessor implementations for global object properties |
| 254 | // ignore `this` and go directly to the singleton context object, so it doesn't matter. |
| 255 | // |
| 256 | // (Once upon a time, V8 supported a thing called an "AccessorSignature" which would handle the |
| 257 | // type checking, but it didn't work correctly for the global object. V8 later removed |
| 258 | // AccessorSignature entirely, forcing us to implement manual type checking. We could totally |
| 259 | // make our manual type checking work correctly for global properties, but, again, it doesn't |
| 260 | // really matter, and I'd rather not inadvertently break someone.) |
| 261 | e.expectEval("function JsType() {}\n" |
| 262 | "JsType.prototype = this;\n" |
| 263 | "new JsType().contextProperty", |
| 264 | "string", "default-context-property-value"); |
| 265 | } |
| 266 | |
| 267 | // ======================================================================================== |
| 268 | |
| 269 | struct IcuContext: public ContextGlobalObject { |
| 270 | JSG_RESOURCE_TYPE(IcuContext) {} |
| 271 | }; |
| 272 | JSG_DECLARE_ISOLATE_TYPE(IcuIsolate, IcuContext); |
| 273 | |
| 274 | KJ_TEST("ICU is properly initialized") { |
| 275 | Evaluator<IcuContext, IcuIsolate> e(v8System); |
| 276 | e.expectEval("function charCodes(str) {" |
| 277 | " let result = [];\n" |
| 278 | " for (let i = 0; i < str.length; i++) {\n" |
| 279 | " result.push(str.charCodeAt(i));\n" |
| 280 | " }\n" |
| 281 | " return result.join(',');\n" |
| 282 | "}" |
| 283 | "[ charCodes('\u1E9B\u0323'),\n" |
| 284 | " charCodes('\u1E9B\u0323'.normalize('NFC')),\n" |
| 285 | " charCodes('\u1E9B\u0323'.normalize('NFD')),\n" |
| 286 | " charCodes('\u1E9B\u0323'.normalize('NFKD')),\n" |
| 287 | " charCodes('\u1E9B\u0323'.normalize('NFKC')) ].join(' ')", |
| 288 | |
| 289 | "string", "7835,803 7835,803 383,803,775 115,803,775 7785"); |
| 290 | } |
| 291 | |
| 292 | // ======================================================================================== |
| 293 | |
| 294 | KJ_TEST("Uncaught JsExceptionThrown reports stack") { |
| 295 | auto exception = |
| 296 | KJ_ASSERT_NONNULL(kj::runCatchingExceptions([&]() { throw JsExceptionThrown(); })); |
| 297 | KJ_ASSERT( |
| 298 | exception.getDescription().startsWith("std::exception: Uncaught JsExceptionThrown\nstack: "), |
| 299 | exception.getDescription()); |
| 300 | } |
| 301 | |
| 302 | // TODO(test): Find some way to verify that C++ objects get garbage-collected as expected (hard to |
| 303 | // test since GC technically does not guarantee that it will collect everything). |
| 304 | |
| 305 | // ======================================================================================== |
| 306 | |
| 307 | struct LockLogContext: public ContextGlobalObject { |
| 308 | JSG_RESOURCE_TYPE(LockLogContext) {} |
| 309 | }; |
| 310 | JSG_DECLARE_ISOLATE_TYPE(LockLogIsolate, LockLogContext); |
| 311 | |
| 312 | KJ_TEST("jsg::Lock logWarning") { |
| 313 | LockLogIsolate isolate(v8System, kj::heap<IsolateObserver>()); |
| 314 | bool called = false; |
| 315 | isolate.runInLockScope([&](LockLogIsolate::Lock& lock) { |
| 316 | lock.setLoggerCallback([&called](jsg::Lock& js, auto message) { |
| 317 | KJ_ASSERT(message == "Yes that happened"_kj); |
| 318 | called = true; |
| 319 | }); |
| 320 | lock.logWarning("Yes that happened"_kj); |
| 321 | KJ_ASSERT(called); |
| 322 | }); |
| 323 | } |
| 324 | |
| 325 | // ======================================================================================== |
| 326 | // JSG_CALLABLE Test |
| 327 | struct CallableContext: public ContextGlobalObject { |
| 328 | struct MyCallable: public Object { |
| 329 | public: |
| 330 | static Ref<MyCallable> constructor(jsg::Lock& js) { |
| 331 | return js.alloc<MyCallable>(); |
| 332 | } |
| 333 | |
| 334 | bool foo() { |
| 335 | return true; |
| 336 | } |
| 337 | |
| 338 | JSG_RESOURCE_TYPE(MyCallable) { |
| 339 | JSG_CALLABLE(foo); |
| 340 | JSG_METHOD(foo); |
| 341 | } |
| 342 | }; |
| 343 | |
| 344 | Ref<MyCallable> getCallable(jsg::Lock& js) { |
| 345 | return js.alloc<MyCallable>(); |
| 346 | } |
| 347 | |
| 348 | JSG_RESOURCE_TYPE(CallableContext) { |
| 349 | JSG_METHOD(getCallable); |
| 350 | JSG_NESTED_TYPE(MyCallable); |
| 351 | } |
| 352 | }; |
| 353 | JSG_DECLARE_ISOLATE_TYPE(CallableIsolate, CallableContext, CallableContext::MyCallable); |
| 354 | |
| 355 | KJ_TEST("Test JSG_CALLABLE") { |
| 356 | Evaluator<CallableContext, CallableIsolate> e(v8System); |
| 357 | |
| 358 | e.expectEval("let obj = getCallable(); obj.foo();", "boolean", "true"); |
| 359 | e.expectEval("let obj = getCallable(); obj();", "boolean", "true"); |
| 360 | |
| 361 | e.expectEval("let obj = new MyCallable(); obj();", "boolean", "true"); |
| 362 | |
| 363 | // It's weird, but still accepted. |
| 364 | e.expectEval("let obj = getCallable(); new obj();", "boolean", "true"); |
| 365 | } |
| 366 | |
| 367 | // ======================================================================================== |
| 368 | struct InterceptContext: public ContextGlobalObject { |
| 369 | struct ProxyImpl: public jsg::Object { |
| 370 | static jsg::Ref<ProxyImpl> constructor(jsg::Lock& js) { |
| 371 | return js.alloc<ProxyImpl>(); |
| 372 | } |
| 373 | |
| 374 | int getBar() { |
| 375 | return 123; |
| 376 | } |
| 377 | |
| 378 | // JSG_WILDCARD_PROPERTY implementation |
| 379 | kj::Maybe<kj::StringPtr> testGetNamed(jsg::Lock& js, kj::String name) { |
| 380 | if (name == "foo") { |
| 381 | return "bar"_kj; |
| 382 | } else if (name == "abc") { |
| 383 | JSG_FAIL_REQUIRE(TypeError, "boom"); |
| 384 | } |
| 385 | return kj::none; |
| 386 | } |
| 387 | |
| 388 | JSG_RESOURCE_TYPE(ProxyImpl) { |
| 389 | JSG_READONLY_PROTOTYPE_PROPERTY(bar, getBar); |
| 390 | JSG_WILDCARD_PROPERTY(testGetNamed); |
| 391 | } |
| 392 | }; |
| 393 | |
| 394 | JSG_RESOURCE_TYPE(InterceptContext) { |
| 395 | JSG_NESTED_TYPE(ProxyImpl); |
| 396 | } |
| 397 | }; |
| 398 | JSG_DECLARE_ISOLATE_TYPE(InterceptIsolate, InterceptContext, InterceptContext::ProxyImpl); |
| 399 | |
| 400 | KJ_TEST("Named interceptor") { |
| 401 | Evaluator<InterceptContext, InterceptIsolate> e(v8System); |
| 402 | e.expectEval("p = new ProxyImpl; p.bar", "number", "123"); |
| 403 | e.expectEval("p = new ProxyImpl; Reflect.has(p, 'foo')", "boolean", "true"); |
| 404 | e.expectEval("p = new ProxyImpl; p.hasOwnProperty('foo')", "boolean", "false"); |
| 405 | e.expectEval( |
| 406 | "p = new ProxyImpl; Object.getOwnPropertyDescriptor(p, 'foo')", "undefined", "undefined"); |
| 407 | e.expectEval("p = new ProxyImpl; Reflect.has(p, 'bar')", "boolean", "true"); |
| 408 | e.expectEval("p = new ProxyImpl; Reflect.has(p, 'baz')", "boolean", "false"); |
| 409 | e.expectEval("p = new ProxyImpl; p.abc", "throws", "TypeError: boom"); |
| 410 | } |
| 411 | |
| 412 | // ======================================================================================== |
| 413 | struct IsolateUuidContext: public ContextGlobalObject { |
| 414 | JSG_RESOURCE_TYPE(IsolateUuidContext) {} |
| 415 | }; |
| 416 | JSG_DECLARE_ISOLATE_TYPE(IsolateUuidIsolate, IsolateUuidContext); |
| 417 | |
| 418 | KJ_TEST("External memory adjustment") { |
| 419 | IsolateUuidIsolate isolate(v8System, kj::heap<IsolateObserver>()); |
| 420 | isolate.runInLockScope([&](IsolateUuidIsolate::Lock& lock) { |
| 421 | // Creating an inner scope to check the case where the adjustment object does not outlive the isolate |
| 422 | { |
| 423 | // Creating with a specific amount works as expected |
| 424 | auto adjuster = lock.getExternalMemoryAdjustment(100); |
| 425 | KJ_ASSERT(adjuster.getAmount() == 100); |
| 426 | |
| 427 | // Adjusting up works as expected |
| 428 | adjuster.adjust(10); |
| 429 | KJ_ASSERT(adjuster.getAmount() == 110); |
| 430 | |
| 431 | // Adjusting down works as expected |
| 432 | adjuster.adjust(-10); |
| 433 | KJ_ASSERT(adjuster.getAmount() == 100); |
| 434 | |
| 435 | // Setting an explicit value just works |
| 436 | adjuster.set(50); |
| 437 | KJ_ASSERT(adjuster.getAmount() == 50); |
| 438 | |
| 439 | // Decrementing by more than the amount will throw an exception |
| 440 | try { |
| 441 | adjuster.adjust(-200); |
| 442 | } catch (...) { |
| 443 | auto exc = kj::getCaughtExceptionAsKj(); |
| 444 | KJ_ASSERT(exc.getDescription() == |
| 445 | "expected amount >= -static_cast<ssize_t>(this->amount) [-200 >= -50]; Memory usage may not be decreased below zero"); |
| 446 | } |
| 447 | |
| 448 | KJ_ASSERT(adjuster.getAmount() == 50); |
| 449 | |
| 450 | adjuster.set(100); |
| 451 | auto adjuster2 = kj::mv(adjuster); |
| 452 | KJ_ASSERT(adjuster2.getAmount() == 100); |
| 453 | |
| 454 | // Checking that the amount is zero after the adjuster is moved away would be nice to have, |
| 455 | // but we should aim to avoid use-after-move entirely. |
| 456 | // KJ_ASSERT(adjuster.getAmount() == 0); |
| 457 | } |
| 458 | // Note that we are not testing the actual effect on the isolate itself here. |
| 459 | // While we have added a getExternalMemory() API to the isolate via a patch in |
| 460 | // the internal repo, we have not added that patch to workerd so testing the |
| 461 | // specific external memory reported by the isolate is possible but a bit |
| 462 | // more cumbersome here. |
| 463 | }); |
| 464 | } |
| 465 | |
| 466 | KJ_TEST("External memory adjustment - defered") { |
| 467 | kj::Arc<const ExternalMemoryTarget> target; |
| 468 | |
| 469 | // A memory allocation that will outlive the isolate |
| 470 | kj::Array<kj::byte> mem; |
| 471 | |
| 472 | { |
| 473 | IsolateUuidIsolate isolate(v8System, kj::heap<IsolateObserver>()); |
| 474 | |
| 475 | target = isolate.runInLockScope( |
| 476 | [&](IsolateUuidIsolate::Lock& lock) { return lock.getExternalMemoryTarget(); }); |
| 477 | |
| 478 | // Adjustment to memory while not holding lock will be applied later |
| 479 | auto adjuster1 = target->getAdjustment(1000); |
| 480 | KJ_ASSERT(adjuster1.getAmount() == 1000); |
| 481 | KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 1000); |
| 482 | |
| 483 | { |
| 484 | // This adjustment has no effect because the adjuster is destroyed before we take the lock again |
| 485 | auto adjuster2 = target->getAdjustment(1000); |
| 486 | KJ_ASSERT(adjuster2.getAmount() == 1000); |
| 487 | KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 2000); |
| 488 | } |
| 489 | |
| 490 | KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 1000); |
| 491 | KJ_ASSERT(target->isIsolateAliveForTest()); |
| 492 | |
| 493 | isolate.runInLockScope([&](IsolateUuidIsolate::Lock& lock) { |
| 494 | // Once lock is taken, the amount is applied |
| 495 | KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 0); |
| 496 | |
| 497 | // Adjustment made while holding lock applies immediately |
| 498 | adjuster1.adjust(-500); |
| 499 | KJ_ASSERT(adjuster1.getAmount() == 500); |
| 500 | KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 0); |
| 501 | KJ_ASSERT(target->isIsolateAliveForTest()); |
| 502 | }); |
| 503 | |
| 504 | mem = isolate.runInLockScope([&](IsolateUuidIsolate::Lock& lock) { |
| 505 | return kj::heapArray<kj::byte>(100).attach(target->getAdjustment(100)); |
| 506 | }); |
| 507 | } |
| 508 | |
| 509 | KJ_ASSERT(!target->isIsolateAliveForTest()); |
| 510 | |
| 511 | // Delete the long-lived array, which will call the adjustment's destructor, to make sure it's safe. |
| 512 | mem = nullptr; |
| 513 | |
| 514 | // Making an adjustment anyway won't do anything but also won't crash |
| 515 | auto adjuster3 = target->getAdjustment(500); |
| 516 | KJ_ASSERT(target->getPendingMemoryUpdateForTest() == 400); |
| 517 | } |
| 518 | |
| 519 | KJ_TEST("Memory Allocation Error Propagation") { |
| 520 | class MyAllocator final: public v8::ArrayBuffer::Allocator { |
| 521 | public: |
| 522 | void* Allocate(size_t length) override { |
| 523 | return nullptr; |
| 524 | } |
| 525 | void* AllocateUninitialized(size_t length) override { |
| 526 | return nullptr; |
| 527 | } |
| 528 | void Free(void* data, size_t length) override {} |
| 529 | size_t MaxAllocationSize() const override { |
| 530 | return 10; |
| 531 | } |
| 532 | }; |
| 533 | |
| 534 | MyAllocator allocator; |
| 535 | v8::Isolate::CreateParams createParams; |
| 536 | createParams.constraints.ConfigureDefaults(10, 10); |
| 537 | createParams.array_buffer_allocator = &allocator; |
| 538 | IsolateUuidIsolate isolate(v8System, kj::heap<IsolateObserver>(), createParams); |
| 539 | isolate.runInLockScope([&](IsolateUuidIsolate::Lock& lock) { |
| 540 | KJ_EXPECT_THROW_MESSAGE( |
| 541 | "Failed to allocate ArrayBuffer backing store", lock.allocBackingStore(100 * 1024)); |
| 542 | }); |
| 543 | } |
| 544 | |
| 545 | struct MpkContext: public ContextGlobalObject { |
| 546 | JSG_RESOURCE_TYPE(MpkContext) {} |
| 547 | }; |
| 548 | JSG_DECLARE_ISOLATE_TYPE(MpkIsolate, MpkContext); |
| 549 | KJ_TEST("MemoryProtectionKeyScope") { |
| 550 | // In workerd, since V8_ENABLE_SANDBOX is not defined, this test is largely |
| 551 | // a non-op, however, when v8 is built with V8_ENABLE_SANDBOX enabled and |
| 552 | // the isolate has a memory protection key, this test will (eventually) |
| 553 | // verify that the array buffer allocation is writable within the scope. |
| 554 | // Essentially once backing stores are protected, and mpk's are enabled, |
| 555 | // this shouldn't crash. |
| 556 | MpkIsolate isolate(v8System, kj::heap<IsolateObserver>()); |
| 557 | std::shared_ptr<v8::BackingStore> store; |
| 558 | auto mpkScope = isolate.runInLockScope([&](MpkIsolate::Lock& lock) { |
| 559 | store = v8::ArrayBuffer::NewBackingStore(lock.v8Isolate, 10); |
| 560 | return lock.getMemoryProtectionKeyScope(); |
| 561 | }); |
| 562 | bool called = false; |
| 563 | // Now that we have our backing store, try writing to it outside the |
| 564 | // isolate lock, but within the mpk scope. If mpk's are enabled writing |
| 565 | // to the backing store without the mpk scope should segfault, but within |
| 566 | // the mpk scope it should succeed. |
| 567 | kj::ArrayPtr<kj::byte> bytes(static_cast<kj::byte*>(store->Data()), store->ByteLength()); |
| 568 | KJ_EXPECT(mpkScope.runWithKey([&] { |
| 569 | called = true; |
| 570 | bytes.fill(1); |
| 571 | return 1; |
| 572 | }) == 1); |
| 573 | KJ_EXPECT(called); |
| 574 | } |
| 575 | |
| 576 | } // namespace |
| 577 | |
| 578 | } // namespace workerd::jsg::test |