From 08c7db4d85a0cc3cfb7223fe0d1b44f6da853a79 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Oct 2026 11:28:45 -0700 Subject: [PATCH] fix(jsc): initialize VM entry and deferred-scope storage on main --- .github/openclaw/artifacts.py | 2 + .../patches/012-vm-finalization-fixture.patch | 54 +++ .github/openclaw/qualify.sh | 4 + .github/workflows/openclaw-artifacts.yml | 12 + .github/workflows/openclaw-checks.yml | 12 + OPENCLAW.md | 3 + .../API/tests/VMEntryRecordProbeX86_64.S | 116 +++++++ .../API/tests/testVMEntryRecord.cpp | 312 ++++++++++++++++++ .../API/tests/testVMEntryScope.cpp | 239 ++++++++++++++ .../llint/LowLevelInterpreter.asm | 11 + .../llint/LowLevelInterpreter64.asm | 1 + .../runtime/MicrotaskQueueInlines.h | 8 +- Source/JavaScriptCore/runtime/VM.cpp | 8 +- .../JavaScriptCore/runtime/VMEntryScope.cpp | 16 +- Source/JavaScriptCore/runtime/VMEntryScope.h | 31 +- .../runtime/VMEntryScopeInlines.h | 22 +- Source/JavaScriptCore/shell/CMakeLists.txt | 39 +++ 17 files changed, 864 insertions(+), 26 deletions(-) create mode 100644 .github/openclaw/qualification/patches/012-vm-finalization-fixture.patch create mode 100644 Source/JavaScriptCore/API/tests/VMEntryRecordProbeX86_64.S create mode 100644 Source/JavaScriptCore/API/tests/testVMEntryRecord.cpp create mode 100644 Source/JavaScriptCore/API/tests/testVMEntryScope.cpp diff --git a/.github/openclaw/artifacts.py b/.github/openclaw/artifacts.py index 16438c1cb743..04182bf378b4 100644 --- a/.github/openclaw/artifacts.py +++ b/.github/openclaw/artifacts.py @@ -106,6 +106,8 @@ def build(label, destination): run(str(artifact/'bin/testFFI')) run(str(artifact/'bin/testMimallocExit')) run(str(artifact/'bin/testMimallocExitInFlight')) + run(str(artifact/'bin/testVMEntryRecord')) + run(str(artifact/'bin/testVMEntryScope')) shutil.rmtree(artifact) def main(): diff --git a/.github/openclaw/qualification/patches/012-vm-finalization-fixture.patch b/.github/openclaw/qualification/patches/012-vm-finalization-fixture.patch new file mode 100644 index 000000000000..55930ab1d571 --- /dev/null +++ b/.github/openclaw/qualification/patches/012-vm-finalization-fixture.patch @@ -0,0 +1,54 @@ +diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts +index ecf12446020..bed9e7a478d 100644 +--- a/test/js/node/vm/vm.test.ts ++++ b/test/js/node/vm/vm.test.ts +@@ -2895,32 +2895,44 @@ test.concurrent("a FinalizationRegistry cleanup job is dropped when its context + const nextTurn = () => new Promise(resolve => setImmediate(resolve)); + const liveContextCleanedUp = Promise.withResolvers(); + let liveContext; +- let deadContextCleanups = 0; ++ const cleanups = [0, 0, 0, 0]; ++ const dropped = []; + + function setup() { + // A collection sweeps the first 8 cells of a type itself and leaves the rest for later. ~JSGlobalObject + // cancels the job too, so these contexts are past the first 8 and their registries are not. + const swept = Array.from({ length: 8 }, () => vm.createContext({})); +- const contexts = Array.from({ length: 4 }, () => vm.createContext({ onCleanup: () => deadContextCleanups++ })); ++ const contexts = Array.from(cleanups, (_, index) => vm.createContext({ onCleanup: () => cleanups[index]++ })); ++ // Each realm strongly holds its sandbox, so a cleared sandbox witness proves that realm was unmarked. ++ for (const context of contexts) dropped.push(new WeakRef(context)); + liveContext = vm.createContext({ onCleanup: liveContextCleanedUp.resolve }); + contexts.push(liveContext); + for (const context of contexts) vm.runInContext("globalThis.registry = new FinalizationRegistry(onCleanup)", context); + edenGC(); // Old generation now: the next eden collection leaves them marked. + for (const context of contexts) for (let i = 0; i < 5; i++) context.registry.register({ i }, i); ++ contexts.length = 0; // A stale reference to this array must not retain every dropped context. + } + + await nextTurn().then(setup); + await nextTurn(); ++ const cleanupsBeforeCollection = cleanups.slice(); + edenGC(); // The registered objects are dead: every registry posts its cleanup job. +- fullGC(); // All contexts but one are dead, and their registries are destroyed. ++ fullGC(); // Cancel jobs for realms this collection finds dead, before yielding to their cleanup jobs. ++ const collected = dropped.map(context => context.deref() === undefined); + await liveContextCleanedUp.promise; + await nextTurn(); // A job posted after the live context's has run by now too. +- console.log({ deadContextCleanups }); ++ console.log({ ++ cleanupsBeforeCollection, ++ // One collected realm exercises cancellation; requiring all four assumes a particular stack layout. ++ someContextCollected: collected.includes(true), ++ collectedContextsHadNoCleanups: cleanups.every((count, index) => !collected[index] || count === 0), ++ }); + `; + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", fixture], env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ +- stdout: "{\n deadContextCleanups: 0,\n}\n", ++ stdout: ++ "{\n cleanupsBeforeCollection: [ 0, 0, 0, 0 ],\n someContextCollected: true,\n collectedContextsHadNoCleanups: true,\n}\n", + stderr: "", + exitCode: 0, + }); diff --git a/.github/openclaw/qualify.sh b/.github/openclaw/qualify.sh index fc28e225d1cf..46307594e746 100644 --- a/.github/openclaw/qualify.sh +++ b/.github/openclaw/qualify.sh @@ -17,6 +17,9 @@ cd "$QUALIFICATION_DIR/bun" git fetch --depth=1 origin "$BUN_COMMIT" git checkout --detach "$BUN_COMMIT" git apply "$inputs/patches/000-checksums.patch" +# Apply openclaw/bun#151 equally to both frozen-source arms. +git apply "$inputs/patches/012-vm-finalization-fixture.patch" +sha256sum "$inputs/patches/012-vm-finalization-fixture.patch" > "$QUALIFICATION_DIR/vm-finalization-fixture.sha256" bootstrap=$(command -v bun) "$bootstrap" install --frozen-lockfile (cd test && "$bootstrap" install --frozen-lockfile) @@ -41,6 +44,7 @@ for arm in baseline candidate; do export BUN_BUILD_PREFETCH_DIR="$QUALIFICATION_DIR/baseline-prefetch" revision="$BASE" fi + sha256sum test/js/node/vm/vm.test.ts > "$QUALIFICATION_DIR/vm-test-$arm.sha256" export BUN_BUILD_CACHE_DIR="$QUALIFICATION_DIR/cache-$arm" env -u GITHUB_SHA -u BUILDKITE_COMMIT -u GIT_SHA "$bootstrap" run build:release --lto=off --webkit-version="$revision" --buildDir="$QUALIFICATION_DIR/bun/build/qualify-$arm" --timings > "$QUALIFICATION_DIR/build-$arm.log" 2>&1 candidate="$QUALIFICATION_DIR/bun/build/qualify-$arm/bun" diff --git a/.github/workflows/openclaw-artifacts.yml b/.github/workflows/openclaw-artifacts.yml index e0c05f03f62d..4d5ea084851c 100644 --- a/.github/workflows/openclaw-artifacts.yml +++ b/.github/workflows/openclaw-artifacts.yml @@ -121,6 +121,18 @@ jobs: ARTIFACT_DIR: ${{ runner.temp }}/linux-artifact QUALIFICATION_DIR: ${{ runner.temp }}/qualification run: bash .github/openclaw/qualify.sh + - name: Retain qualified Linux Bun executables + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + if: always() + with: + name: linux-bun-executables + path: | + ${{ runner.temp }}/qualification/bun/build/qualify-baseline/bun + ${{ runner.temp }}/qualification/bun/build/qualify-candidate/bun + ${{ runner.temp }}/qualification/*.sha256 + compression-level: 0 + if-no-files-found: error + retention-days: 3 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always() with: diff --git a/.github/workflows/openclaw-checks.yml b/.github/workflows/openclaw-checks.yml index d85c6e30e1c0..8206b2dedc22 100644 --- a/.github/workflows/openclaw-checks.yml +++ b/.github/workflows/openclaw-checks.yml @@ -100,6 +100,18 @@ jobs: ARTIFACT_DIR: ${{ runner.temp }}/linux-artifact QUALIFICATION_DIR: ${{ runner.temp }}/qualification run: bash .github/openclaw/qualify.sh + - name: Retain qualified Linux Bun executables + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + if: always() + with: + name: linux-bun-executables + path: | + ${{ runner.temp }}/qualification/bun/build/qualify-baseline/bun + ${{ runner.temp }}/qualification/bun/build/qualify-candidate/bun + ${{ runner.temp }}/qualification/*.sha256 + compression-level: 0 + if-no-files-found: error + retention-days: 3 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always() with: diff --git a/OPENCLAW.md b/OPENCLAW.md index 8f5f703efa34..2ae32a697d69 100644 --- a/OPENCLAW.md +++ b/OPENCLAW.md @@ -145,6 +145,9 @@ baseline. Engine and Bun headers and libraries must be rebuilt together. ## Unreleased +- Initialize new VM entry scratch buffers and alignment padding, and store deferred microtask entry scopes as two initialized pointers, preventing stale stack values from retaining otherwise unreachable objects; carries #24 onto engine main and adapts the whole-word-state remedy in oven-sh/WebKit#694, thanks @robobun. +- Require the native VM entry storage regressions in the Linux x64 artifact check, qualify dead-realm cleanup with the collected-realm fixture from openclaw/bun#151, and retain both qualified Linux Bun executables and hashes for three days; the fixture adapts oven-sh/bun#44544, thanks @robobun. + - Deliver pending worker heap-limit termination promptly after GC by invalidating optimized code on the mutator, preserving full-GC live accounting and external-buffer exclusions. - Preserve mimalloc pthread TLS-key ownership during shell exit by draining active setters before deletion; cover both late and in-flight allocations with native regressions. Builds on oven-sh/WebKit#698, thanks @dylan-conway. diff --git a/Source/JavaScriptCore/API/tests/VMEntryRecordProbeX86_64.S b/Source/JavaScriptCore/API/tests/VMEntryRecordProbeX86_64.S new file mode 100644 index 000000000000..16e573b23320 --- /dev/null +++ b/Source/JavaScriptCore/API/tests/VMEntryRecordProbeX86_64.S @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2026 Peter Steinberger All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + .text + .p2align 4 + .globl vmEntryRecordProbeInvoke + .type vmEntryRecordProbeInvoke,@function +vmEntryRecordProbeInvoke: + pushq %rbp + movq %rsp, %rbp + pushq %rbx + pushq %r12 + pushq %r13 + pushq %r14 + pushq %r15 + subq $56, %rsp + movq %rdi, %rbx + movq 112(%rbx), %r12 + + /* Six fixed-path stack arguments; the generic path simply ignores them. */ + movq 56(%rbx), %rax + movq %rax, 0(%rsp) + movq 64(%rbx), %rax + movq %rax, 8(%rsp) + movq 72(%rbx), %rax + movq %rax, 16(%rsp) + movq 80(%rbx), %rax + movq %rax, 24(%rsp) + movq 88(%rbx), %rax + movq %rax, 32(%rsp) + movq 96(%rbx), %rax + movq %rax, 40(%rsp) + + /* call pushes PC, and the actual entry's prologue pushes rbp. */ + leaq -16(%rsp), %r10 + movq %r10, 24(%r12) + addq 0(%r12), %r10 + movq 8(%r12), %rcx + movq 16(%r12), %rax +.Lpoison: + movq %rax, (%r10) + addq $8, %r10 + decq %rcx + jnz .Lpoison + + /* No C++ frame, helper call, or compiler-selected spill after poisoning. */ + movq 0(%rbx), %r11 + movq 8(%rbx), %rdi + movq 16(%rbx), %rsi + movq 24(%rbx), %rdx + movq 32(%rbx), %rcx + movq 40(%rbx), %r8 + movq 48(%rbx), %r9 + cmpq $0, 120(%rbx) + je .Lcall + movq 104(%rbx), %rdx +.Lcall: + call *%r11 + addq $56, %rsp + popq %r15 + popq %r14 + popq %r13 + popq %r12 + popq %rbx + popq %rbp + ret + .size vmEntryRecordProbeInvoke, .-vmEntryRecordProbeInvoke + + .p2align 4 + .globl vmEntryRecordProbeCallee + .type vmEntryRecordProbeCallee,@function +vmEntryRecordProbeCallee: + /* First instruction of the callee; rbp is still the real entry frame. */ + movq vmEntryRecordProbeCurrent(%rip), %r11 + movq %rbp, 32(%r11) + movq 0(%r11), %r10 + addq %rbp, %r10 + movq 8(%r11), %rcx + leaq 40(%r11), %rdx +.Lcapture: + movq (%r10), %rax + movq %rax, (%rdx) + addq $8, %r10 + addq $8, %rdx + decq %rcx + jnz .Lcapture + /* Both native and JIT calls arrive with rsp == 8 mod 16 on this ABI. */ + subq $8, %rsp + movq %rbp, %rdi + call vmEntryRecordProbeFinish + addq $8, %rsp + ret + .size vmEntryRecordProbeCallee, .-vmEntryRecordProbeCallee + .section .note.GNU-stack,"",@progbits diff --git a/Source/JavaScriptCore/API/tests/testVMEntryRecord.cpp b/Source/JavaScriptCore/API/tests/testVMEntryRecord.cpp new file mode 100644 index 000000000000..34f0c062814b --- /dev/null +++ b/Source/JavaScriptCore/API/tests/testVMEntryRecord.cpp @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2026 Peter Steinberger All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "VMEntryRecord.h" + +#include "APICast.h" +#include "CallFrame.h" +#include "CodeBlock.h" +#include "EntryFrame.h" +#include "InitializeThreading.h" +#include "JSGlobalObject.h" +#include "JSCInlines.h" +#include "JSLock.h" +#include "JavaScript.h" +#include "LLIntThunks.h" +#include "Options.h" +#include "ProtoCallFrameInlines.h" +#include "VM.h" +#include +#include +#include +#include +#include +#include + +#if !OS(LINUX) || !CPU(X86_64) || ENABLE(C_LOOP) +#error This assembly probe requires Linux x86-64 SysV and the native LLInt. +#endif + +using JSC::CallFrame; +using JSC::CodeBlock; +using JSC::EncodedJSValue; +using JSC::EntryFrame; +using JSC::JSCell; +using JSC::JSLockHolder; +using JSC::JSObject; +using JSC::JSValue; +using JSC::Options; +using JSC::ProtoCallFrame; +using JSC::VM; +using JSC::jsNumber; +using JSC::jsUndefined; +using JSC::vmEntryRecord; +using JSC::vmEntryToJavaScript; +using JSC::vmEntryToJavaScriptWith0Arguments; +using JSC::vmEntryToJavaScriptWith1Arguments; +using JSC::vmEntryToJavaScriptWith2Arguments; +using JSC::vmEntryToJavaScriptWith3Arguments; +using JSC::vmEntryToJavaScriptWith4Arguments; +using JSC::vmEntryToJavaScriptWith5Arguments; +using JSC::vmEntryToJavaScriptWith6Arguments; +using JSC::vmEntryToNative; + +struct Invocation; +struct EntryRecordProbe { + intptr_t tailOffset; + uintptr_t tailWordCount; + uintptr_t poison; + uintptr_t expectedEntryFrame { 0 }; + uintptr_t observedEntryFrame { 0 }; + std::array captured { }; + Invocation* invocation { nullptr }; + EntryFrame* priorEntryFrame { nullptr }; + CallFrame* priorCallFrame { nullptr }; + bool allowNested { false }; + bool headerMatched { false }; + bool locatorPreserved { false }; + bool nestedPassed { false }; + bool outerPreserved { false }; +}; + +struct Invocation { + void* thunk; + void* target; + VM* vm; + CodeBlock* codeBlock; + JSObject* callee; + EncodedJSValue thisValue; + JSCell* context; + std::array arguments; + ProtoCallFrame* proto; + EntryRecordProbe* probe; + uintptr_t generic; +}; + +static_assert(sizeof(uintptr_t) == 8 && sizeof(CPURegister) == 8); +static_assert(!offsetof(EntryRecordProbe, tailOffset)); +static_assert(offsetof(EntryRecordProbe, tailWordCount) == 8); +static_assert(offsetof(EntryRecordProbe, poison) == 16); +static_assert(offsetof(EntryRecordProbe, expectedEntryFrame) == 24); +static_assert(offsetof(EntryRecordProbe, observedEntryFrame) == 32); +static_assert(offsetof(EntryRecordProbe, captured) == 40); +static_assert(!offsetof(Invocation, thunk)); +static_assert(offsetof(Invocation, target) == 8); +static_assert(offsetof(Invocation, vm) == 16); +static_assert(offsetof(Invocation, codeBlock) == 24); +static_assert(offsetof(Invocation, callee) == 32); +static_assert(offsetof(Invocation, thisValue) == 40); +static_assert(offsetof(Invocation, context) == 48); +static_assert(offsetof(Invocation, arguments) == 56); +static_assert(offsetof(Invocation, proto) == 104); +static_assert(offsetof(Invocation, probe) == 112); +static_assert(offsetof(Invocation, generic) == 120); + +extern "C" { +// One serial test thread owns this pointer. No other thread invokes the probe. +EntryRecordProbe* vmEntryRecordProbeCurrent = nullptr; +EncodedJSValue vmEntryRecordProbeInvoke(Invocation*); +void vmEntryRecordProbeCallee(); +EncodedJSValue vmEntryRecordProbeFinish(EntryFrame*); +} + +static bool capturedZero(const EntryRecordProbe& probe) +{ + for (size_t index = 0; index < probe.tailWordCount; ++index) { + if (probe.captured[index]) + return false; + } + return true; +} + +static bool passed(const EntryRecordProbe& probe) +{ + return probe.expectedEntryFrame == probe.observedEntryFrame + && capturedZero(probe) && probe.headerMatched && probe.locatorPreserved + && probe.nestedPassed && probe.outerPreserved; +} + +extern "C" EncodedJSValue vmEntryRecordProbeFinish(EntryFrame* entryFrame) +{ + // The assembly callee already copied the original bytes, before this C++ + // call could introduce a legitimate unwind/callee-save writer. + EntryRecordProbe& probe = *vmEntryRecordProbeCurrent; + Invocation& invocation = *probe.invocation; + auto* record = vmEntryRecord(entryFrame); + probe.headerMatched = invocation.vm->topEntryFrame == entryFrame + && record->m_vm == invocation.vm && record->m_context == invocation.context + && record->prevTopCallFrame() == probe.priorCallFrame + && record->prevTopEntryFrame() == probe.priorEntryFrame; + + auto* tail = reinterpret_cast(reinterpret_cast(entryFrame) + probe.tailOffset); + std::array original { }; + const auto token = [](size_t index) -> uintptr_t { + return 0x5a5a5a5a5a5a0000ULL + index; + }; + for (size_t index = 0; index < probe.tailWordCount; ++index) { + original[index] = tail[index]; + tail[index] = token(index); + } + + // Computing the address of an already-active record must not clear it. + auto* lookedUp = vmEntryRecord(entryFrame); + probe.locatorPreserved = lookedUp == record; + for (size_t index = 0; index < probe.tailWordCount; ++index) + probe.locatorPreserved &= tail[index] == token(index); + + probe.nestedPassed = true; + if (probe.allowNested) { + EntryRecordProbe nested { probe.tailOffset, probe.tailWordCount, probe.poison }; + Invocation nestedInvocation = invocation; + nestedInvocation.thunk = reinterpret_cast(vmEntryToNative); + nestedInvocation.generic = 1; + nestedInvocation.probe = &nested; + nested.invocation = &nestedInvocation; + nested.priorEntryFrame = invocation.vm->topEntryFrame; + nested.priorCallFrame = invocation.vm->topCallFrame; + vmEntryRecordProbeCurrent = &nested; + EncodedJSValue result = vmEntryRecordProbeInvoke(&nestedInvocation); + vmEntryRecordProbeCurrent = &probe; + probe.nestedPassed = passed(nested) && JSValue::decode(result) == jsNumber(42) + && invocation.vm->topEntryFrame == nested.priorEntryFrame + && invocation.vm->topCallFrame == nested.priorCallFrame; + } + + // These non-pointer tokens stand in for an active frame's later contents. + // A nested entry must initialize itself, never clear its predecessor. + probe.outerPreserved = true; + for (size_t index = 0; index < probe.tailWordCount; ++index) { + probe.outerPreserved &= tail[index] == token(index); + tail[index] = original[index]; + } + return JSValue::encode(jsNumber(42)); +} + +static JSValueRef unusedNativeCallee(JSContextRef context, JSObjectRef, JSObjectRef, size_t, const JSValueRef[], JSValueRef*) +{ + return JSValueMakeNumber(context, 42); +} + +static bool ordinaryJavaScriptUnwindSmoke(JSGlobalContextRef context) +{ + // This is semantic coverage only; it does not claim a particular JIT tier. + JSStringRef source = JSStringCreateWithUTF8CString( + "(function outer(input) {" + " const saved = input.value;" + " try { (function inner() { throw input; })(); }" + " catch (error) { return saved + error.increment; }" + "})({value: 40, increment: 2})"); + JSValueRef exception = nullptr; + JSValueRef value = JSEvaluateScript(context, source, nullptr, nullptr, 1, &exception); + JSStringRelease(source); + return !exception && value && JSValueToNumber(context, value, &exception) == 42 && !exception; +} + +int main() +{ + WTF::initializeMainThread(); + JSC::initialize(); + // An assertion-only verification wrapper adds another entry-sized frame. + // The exact-stack probe tests the production entry shape without that wrapper. +#if ASSERT_ENABLED + if (!Options::setOptions("validateVMEntryCalleeSaves=false")) + return 2; +#endif + JSGlobalContextRef context = JSGlobalContextCreate(nullptr); + if (!context) + return 2; + auto* globalObject = toJS(context); + VM& vm = globalObject->vm(); + // Bun's C API leaves locking and atom-table ownership to its embedder. + JSLockHolder lock(vm); + JSObjectRef calleeRef = JSObjectMakeFunctionWithCallback(context, nullptr, unusedNativeCallee); + JSValueProtect(context, calleeRef); + bool allPassed = true; + { + const intptr_t tailOffset = EntryFrame::calleeSaveRegistersBufferOffset(); + const size_t tailBytes = static_cast(-tailOffset); + // End at the entry frame, covering both the save buffer and its trailing + // frame-alignment padding, rather than just sizeof(VMEntryRecord). + // Poisoning stays within the SysV 128-byte red zone until the call + // claims it; the return address and saved rbp consume another 16 bytes. + if (tailOffset >= 0 || tailBytes % sizeof(uintptr_t) || tailBytes > 112) + return 2; + const size_t wordCount = tailBytes / sizeof(uintptr_t); + struct EntryCase { + WTF::ASCIILiteral name; + void* thunk; + unsigned arguments; + bool generic; + }; + const EntryCase cases[] = { + { "fixed0"_s, reinterpret_cast(vmEntryToJavaScriptWith0Arguments), 0, false }, + { "fixed1"_s, reinterpret_cast(vmEntryToJavaScriptWith1Arguments), 1, false }, + { "fixed2"_s, reinterpret_cast(vmEntryToJavaScriptWith2Arguments), 2, false }, + { "fixed3"_s, reinterpret_cast(vmEntryToJavaScriptWith3Arguments), 3, false }, + { "fixed4"_s, reinterpret_cast(vmEntryToJavaScriptWith4Arguments), 4, false }, + { "fixed5"_s, reinterpret_cast(vmEntryToJavaScriptWith5Arguments), 5, false }, + { "fixed6"_s, reinterpret_cast(vmEntryToJavaScriptWith6Arguments), 6, false }, + { "genericJS"_s, reinterpret_cast(vmEntryToJavaScript), 6, true }, + { "genericNative"_s, reinterpret_cast(vmEntryToNative), 6, true }, + }; + for (const auto& entry : cases) { + EntryRecordProbe probe { tailOffset, wordCount, 0xa5a5a5a5a5a5a5a5ULL }; + ProtoCallFrame proto; + Invocation invocation { + entry.thunk, reinterpret_cast(vmEntryRecordProbeCallee), &vm, + nullptr, toJS(calleeRef), JSValue::encode(jsUndefined()), globalObject, + { }, &proto, &probe, entry.generic, + }; + for (size_t index = 0; index < invocation.arguments.size(); ++index) + invocation.arguments[index] = JSValue::encode(jsNumber(static_cast(100 + index))); + proto.init(nullptr, globalObject, invocation.callee, jsUndefined(), invocation.context, + entry.arguments + 1, invocation.arguments.data()); + probe.invocation = &invocation; + probe.priorEntryFrame = vm.topEntryFrame; + probe.priorCallFrame = vm.topCallFrame; + probe.allowNested = true; + vmEntryRecordProbeCurrent = &probe; + EncodedJSValue result = vmEntryRecordProbeInvoke(&invocation); + vmEntryRecordProbeCurrent = nullptr; + bool ok = passed(probe) && JSValue::decode(result) == jsNumber(42) + && vm.topEntryFrame == probe.priorEntryFrame && vm.topCallFrame == probe.priorCallFrame; + SAFE_PRINTF("%s: %s tailWords=%zu zero=%d sameFrame=%d headers=%d locator=%d nested=%d outer=%d\n", + entry.name, ok ? "PASS"_s : "FAIL"_s, wordCount, capturedZero(probe), + probe.expectedEntryFrame == probe.observedEntryFrame, probe.headerMatched, + probe.locatorPreserved, probe.nestedPassed, probe.outerPreserved); + if (!ok) { + for (size_t index = 0; index < wordCount; ++index) + SAFE_PRINTF(" tail[%zu]=0x%016llx\n", index, static_cast(probe.captured[index])); + } + allPassed &= ok; + } + } + bool smokePassed = ordinaryJavaScriptUnwindSmoke(context); + SAFE_PRINTF("ordinaryJavaScriptUnwind: %s\n", smokePassed ? "PASS"_s : "FAIL"_s); + JSValueUnprotect(context, calleeRef); + JSGlobalContextRelease(context); + return allPassed && smokePassed ? 0 : 1; +} diff --git a/Source/JavaScriptCore/API/tests/testVMEntryScope.cpp b/Source/JavaScriptCore/API/tests/testVMEntryScope.cpp new file mode 100644 index 000000000000..5bcab9e0e78c --- /dev/null +++ b/Source/JavaScriptCore/API/tests/testVMEntryScope.cpp @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2026 Peter Steinberger. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// Exercises conservative-stack storage and the real microtask scope lifecycle. +#include "config.h" + +#include "APICast.h" +#include "InitializeThreading.h" +#include "JSGlobalObject.h" +#include "JSMicrotaskDispatcher.h" +#include "JSLock.h" +#include "JavaScript.h" +#include "MicrotaskQueueInlines.h" +#include "VM.h" +#include "VMEntryScopeInlines.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace JSC; + +// The control compiles against the original engine too. It restores the exact +// optional owner used at both checkpoint sites, with the same word assertions. +#if defined(TEST_VM_ENTRY_SCOPE_OPTIONAL_CONTROL) && TEST_VM_ENTRY_SCOPE_OPTIONAL_CONTROL +using ScopeOwner = std::optional; +static constexpr const char* mode = "optional-control"; +static void enter(ScopeOwner& scope, VM& vm, JSGlobalObject* globalObject) { scope.emplace(vm, globalObject); } +static VMEntryScope* activeScope(ScopeOwner& scope) { return scope ? &*scope : nullptr; } +#else +using ScopeOwner = VMEntryScope; +static constexpr const char* mode = "deferred-owner"; +static void enter(ScopeOwner& scope, VM& vm, JSGlobalObject* globalObject) { scope.enter(vm, globalObject); } +static VMEntryScope* activeScope(ScopeOwner& scope) { return scope ? &scope : nullptr; } +static_assert(!std::is_copy_constructible_v && !std::is_copy_assignable_v); +static_assert(!std::is_move_constructible_v && !std::is_move_assignable_v); +#endif + +static unsigned storageChecks; +static unsigned storageFailures; + +static NEVER_INLINE uintptr_t readWord(const void* memory) +{ + std::array bytes; + auto* input = static_cast(memory); + for (size_t index = 0; index < bytes.size(); ++index) + bytes[index] = input[index]; + uintptr_t value; + std::memcpy(&value, bytes.data(), sizeof(value)); + return value; +} + +static NEVER_INLINE void checkStorage(const char* stage, const ScopeOwner& scope, VM* vm, JSGlobalObject* globalObject) +{ + static_assert(sizeof(ScopeOwner) % sizeof(uintptr_t) == 0); + auto* bytes = reinterpret_cast(&scope); + bool clean = true; + for (size_t index = 0; index < sizeof(ScopeOwner) / sizeof(uintptr_t); ++index) { + uintptr_t expected = index == 0 ? reinterpret_cast(vm) + : index == 1 ? reinterpret_cast(globalObject) + : static_cast(!!vm); + clean &= readWord(bytes + index * sizeof(uintptr_t)) == expected; + } + ++storageChecks; + storageFailures += !clean; + std::printf("storage %s %s\n", stage, clean ? "PASS" : "FAIL"); +} + +static bool storageAndLifecycle(VM& vm, JSGlobalObject* globalObject) +{ + alignas(ScopeOwner) std::array storage; + storage.fill(0xa5); + // Match the production declaration's default-initialization, not T() value-initialization. + auto* scope = ::new (static_cast(storage.data())) ScopeOwner; + VMEntryScope* previous = vm.entryScope; + bool valid = !*scope && vm.entryScope == previous; + checkStorage("default", *scope, nullptr, nullptr); + + enter(*scope, vm, nullptr); + valid &= !!*scope && activeScope(*scope)->globalObject() == nullptr; + valid &= &activeScope(*scope)->vm() == &vm; + valid &= vm.entryScope == (previous ? previous : activeScope(*scope)); + checkStorage("entered-null-global", *scope, &vm, nullptr); + activeScope(*scope)->setGlobalObject(globalObject); + checkStorage("global-updated", *scope, &vm, globalObject); + + { + ScopeOwner nested; + VMEntryScope* owner = vm.entryScope; + enter(nested, vm, globalObject); + valid &= vm.entryScope == owner && &activeScope(nested)->vm() == &vm; + nested.reset(); + valid &= !nested && vm.entryScope == owner; + } + scope->reset(); + valid &= !*scope && vm.entryScope == previous; + checkStorage("reset", *scope, nullptr, nullptr); + scope->reset(); + valid &= vm.entryScope == previous; + + enter(*scope, vm, globalObject); + valid &= !!*scope && activeScope(*scope)->globalObject() == globalObject; + checkStorage("reentered", *scope, &vm, globalObject); + scope->reset(); + checkStorage("reset-again", *scope, nullptr, nullptr); + std::destroy_at(scope); + return valid && vm.entryScope == previous; +} + +class ScopeProbeDispatcher final : public MicrotaskDispatcher { + WTF_MAKE_COMPACT_TZONE_ALLOCATED(ScopeProbeDispatcher); +public: + static Ref create(Function&& callback) + { + return adoptRef(*new ScopeProbeDispatcher(WTF::move(callback))); + } + QueuedTask::Result run(QueuedTask&) final + { + m_callback(); + return QueuedTask::Result::Executed; + } + bool isRunnable() const final { return true; } +private: + explicit ScopeProbeDispatcher(Function&& callback) + : MicrotaskDispatcher(Type::None) + , m_callback(WTF::move(callback)) + { } + Function m_callback; +}; + +WTF_MAKE_TZONE_ALLOCATED_IMPL(ScopeProbeDispatcher); + +static void enqueue(VM& vm, JSGlobalObject* globalObject, Function&& callback) +{ + auto* dispatcher = JSMicrotaskDispatcher::create(vm, ScopeProbeDispatcher::create(WTF::move(callback)), globalObject); + vm.defaultMicrotaskQueue().enqueue(QueuedTask { dispatcher }); +} + +static bool checkpointLifecycle(VM& vm, JSGlobalObject* globalObject, bool viaVM) +{ + if (vm.entryScope || !vm.defaultMicrotaskQueue().isEmpty()) + return false; + bool valid = true; + unsigned callbacks = 0; + unsigned switches = 0; + VMEntryScope* owningScope = nullptr; + + // A null-global task resets the inner checkpoint scope; a later task must + // re-enter. The VM-level owner, when present, spans those inner transitions. + enqueue(vm, globalObject, [&] { + ++callbacks; + owningScope = vm.entryScope; + valid &= owningScope && owningScope->globalObject() == globalObject; + VMEntryScope nested(vm, globalObject); + valid &= vm.entryScope == owningScope; + }); + enqueue(vm, nullptr, [&] { + ++callbacks; + valid &= viaVM ? vm.entryScope == owningScope : vm.entryScope == nullptr; + if (viaVM) + valid &= vm.entryScope && vm.entryScope->globalObject() == globalObject; + }); + enqueue(vm, globalObject, [&] { + ++callbacks; + valid &= vm.entryScope && vm.entryScope->globalObject() == globalObject; + if (viaVM) + valid &= vm.entryScope == owningScope; + }); + + if (viaVM) + vm.drainMicrotasks(); + else + vm.defaultMicrotaskQueue().performMicrotaskCheckpoint(vm, [&](JSGlobalObject*, JSGlobalObject*) { ++switches; }); + return valid && callbacks == 3 && (viaVM || switches == 3) + && !vm.entryScope && vm.defaultMicrotaskQueue().isEmpty(); +} + +int main() +{ + WTF::initializeMainThread(); + JSC::initialize(); + JSGlobalContextRef context = JSGlobalContextCreate(nullptr); + if (!context) + return 2; + auto* globalObject = toJS(context); + VM& vm = globalObject->vm(); + bool lifecyclePassed; + bool emptyQueuePassed; + bool directCheckpointPassed; + bool vmCheckpointPassed; + JSLockHolder lock(vm); + { + if (vm.entryScope || !vm.defaultMicrotaskQueue().isEmpty()) + return 2; + lifecyclePassed = storageAndLifecycle(vm, globalObject); + unsigned emptySwitches = 0; + vm.defaultMicrotaskQueue().performMicrotaskCheckpoint(vm, [&](JSGlobalObject*, JSGlobalObject*) { ++emptySwitches; }); + vm.drainMicrotasks(); + emptyQueuePassed = !emptySwitches && !vm.entryScope && vm.defaultMicrotaskQueue().isEmpty(); + directCheckpointPassed = checkpointLifecycle(vm, globalObject, false); + vmCheckpointPassed = checkpointLifecycle(vm, globalObject, true); + } + JSGlobalContextRelease(context); + bool valid = lifecyclePassed && emptyQueuePassed && directCheckpointPassed && vmCheckpointPassed; + std::printf("{\"mode\":\"%s\",\"valid\":%s,\"storageChecks\":%u,\"storageFailures\":%u,\"lifecyclePassed\":%s,\"emptyQueuePassed\":%s,\"directCheckpointPassed\":%s,\"vmCheckpointPassed\":%s}\n", + mode, valid ? "true" : "false", storageChecks, storageFailures, + lifecyclePassed ? "true" : "false", emptyQueuePassed ? "true" : "false", + directCheckpointPassed ? "true" : "false", vmCheckpointPassed ? "true" : "false"); + return valid && !storageFailures ? 0 : 1; +} diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter.asm b/Source/JavaScriptCore/llint/LowLevelInterpreter.asm index ebd6f7827a00..79dff8f25ce3 100644 --- a/Source/JavaScriptCore/llint/LowLevelInterpreter.asm +++ b/Source/JavaScriptCore/llint/LowLevelInterpreter.asm @@ -1083,6 +1083,15 @@ macro vmEntryRecord(entryFramePointer, resultReg) subp entryFramePointer, VMEntryTotalFrameSize, resultReg end +macro initializeVMEntryRecordTail(record, scratch) + # Unwritten callee-save slots and alignment padding are conservative GC roots. + move VMEntryTotalFrameSize - CalleeRegisterSaveSize - VMEntryRecord::calleeSaveRegistersBuffer, scratch +.clear: + subp PtrSize, scratch + storep 0, VMEntryRecord::calleeSaveRegistersBuffer[record, scratch, 1] + btpnz scratch, .clear +end + macro getFrameRegisterSizeForCodeBlock(codeBlock, size) loadi CodeBlock::m_numCalleeLocals[codeBlock], size lshiftp 3, size @@ -1715,10 +1724,12 @@ if ((ARM64E or ARM64) or X86_64) and ADDRESS64 and not C_LOOP functionPrologue() vmEntryRecord(cfr, sp) if ARM64 or ARM64E + initializeVMEntryRecordTail(sp, t8) storepairq a1, a5, VMEntryRecord::m_vm[sp] loadpairq VM::topCallFrame[a1], t8, t9 # topCallFrame and topEntryFrame storepairq t8, t9, VMEntryRecord::m_prevTopCallFrame[sp] else + initializeVMEntryRecordTail(sp, t0) storeq a1, VMEntryRecord::m_vm[sp] storeq a5, VMEntryRecord::m_context[sp] loadq VM::topCallFrame[a1], t0 diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm index 16eb8e13c7d9..db9c67081df6 100644 --- a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm +++ b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm @@ -182,6 +182,7 @@ macro doVMEntry(makeCall) const protoCallFrame = a2 vmEntryRecord(cfr, sp) + initializeVMEntryRecordTail(sp, t4) checkStackPointerAlignment(t4, 0xbad0dc01) diff --git a/Source/JavaScriptCore/runtime/MicrotaskQueueInlines.h b/Source/JavaScriptCore/runtime/MicrotaskQueueInlines.h index 4ca651c9a1ed..8a931cf82886 100644 --- a/Source/JavaScriptCore/runtime/MicrotaskQueueInlines.h +++ b/Source/JavaScriptCore/runtime/MicrotaskQueueInlines.h @@ -137,7 +137,7 @@ inline void MicrotaskQueue::performMicrotaskCheckpoint(VM& vm, NOESCAPE const In return; } - std::optional entryScope; + VMEntryScope entryScope; JSGlobalObject* currentGlobalObject = nullptr; // drain() handles an empty queue too, but it constructs a MicrotaskCallCache first. Check here so that an empty checkpoint skips that. @@ -150,11 +150,11 @@ inline void MicrotaskQueue::performMicrotaskCheckpoint(VM& vm, NOESCAPE const In if (nextGlobalObject) { if (!entryScope) - entryScope.emplace(vm, nextGlobalObject); + entryScope.enter(vm, nextGlobalObject); else - entryScope->setGlobalObject(nextGlobalObject); + entryScope.setGlobalObject(nextGlobalObject); } else - entryScope = std::nullopt; + entryScope.reset(); currentGlobalObject = nextGlobalObject; } diff --git a/Source/JavaScriptCore/runtime/VM.cpp b/Source/JavaScriptCore/runtime/VM.cpp index 7eaf107c16da..e3aa6b17e6de 100644 --- a/Source/JavaScriptCore/runtime/VM.cpp +++ b/Source/JavaScriptCore/runtime/VM.cpp @@ -1723,14 +1723,14 @@ void VM::drainMicrotasks() if (executionForbidden()) [[unlikely]] m_defaultMicrotaskQueue->clear(); else { - std::optional entryScope; + VMEntryScope entryScope; if (!m_defaultMicrotaskQueue->isEmpty()) - entryScope.emplace(*this, nullptr); + entryScope.enter(*this, nullptr); while (true) { m_defaultMicrotaskQueue->performMicrotaskCheckpoint(*this, [&](JSGlobalObject*, JSGlobalObject* nextGlobalObject) { if (entryScope && nextGlobalObject) - entryScope->setGlobalObject(nextGlobalObject); + entryScope.setGlobalObject(nextGlobalObject); }); if (hasPendingTerminationException()) [[unlikely]] return; @@ -1740,7 +1740,7 @@ void VM::drainMicrotasks() if (m_defaultMicrotaskQueue->isEmpty()) break; if (!entryScope) - entryScope.emplace(*this, nullptr); + entryScope.enter(*this, nullptr); } } finalizeSynchronousJSExecution(); diff --git a/Source/JavaScriptCore/runtime/VMEntryScope.cpp b/Source/JavaScriptCore/runtime/VMEntryScope.cpp index f911b37c5c54..8622c3af1068 100644 --- a/Source/JavaScriptCore/runtime/VMEntryScope.cpp +++ b/Source/JavaScriptCore/runtime/VMEntryScope.cpp @@ -38,9 +38,9 @@ namespace JSC { void VMEntryScope::setUpSlow() { - m_vm.entryScope = this; + m_vm->entryScope = this; #if USE(BUN_JSC_ADDITIONS) - m_vm.didEnterFromOutside(); + m_vm->didEnterFromOutside(); #endif auto& thread = Thread::currentSingleton(); @@ -54,18 +54,18 @@ void VMEntryScope::setUpSlow() #endif } - if (m_vm.hasAnyEntryScopeServiceRequest()) [[unlikely]] - m_vm.executeEntryScopeServicesOnEntry(); + if (m_vm->hasAnyEntryScopeServiceRequest()) [[unlikely]] + m_vm->executeEntryScopeServicesOnEntry(); } void VMEntryScope::tearDownSlow() { - ASSERT_WITH_MESSAGE(!m_vm.hasCheckpointOSRSideState(), "Exitting the VM but pending checkpoint side state still available"); + ASSERT_WITH_MESSAGE(!m_vm->hasCheckpointOSRSideState(), "Exitting the VM but pending checkpoint side state still available"); - m_vm.entryScope = nullptr; + m_vm->entryScope = nullptr; - if (m_vm.hasAnyEntryScopeServiceRequest()) [[unlikely]] - m_vm.executeEntryScopeServicesOnExit(); + if (m_vm->hasAnyEntryScopeServiceRequest()) [[unlikely]] + m_vm->executeEntryScopeServicesOnExit(); } } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/VMEntryScope.h b/Source/JavaScriptCore/runtime/VMEntryScope.h index 3c3be09b3c68..049d289be395 100644 --- a/Source/JavaScriptCore/runtime/VMEntryScope.h +++ b/Source/JavaScriptCore/runtime/VMEntryScope.h @@ -26,8 +26,8 @@ #pragma once #include "JSExportMacros.h" -#include -#include +#include +#include namespace JSC { @@ -35,20 +35,39 @@ class JSGlobalObject; class VM; class VMEntryScope { + WTF_MAKE_NONCOPYABLE(VMEntryScope); public: + VMEntryScope() = default; VMEntryScope(VM&, JSGlobalObject*); ~VMEntryScope(); + VMEntryScope(VMEntryScope&&) = delete; + VMEntryScope& operator=(VMEntryScope&&) = delete; - VM& vm() const { return m_vm; } + void enter(VM&, JSGlobalObject*); + void reset(); + explicit operator bool() const { return m_vm; } + + VM& vm() const + { + ASSERT(m_vm); + return *m_vm; + } JSGlobalObject* globalObject() const { return m_globalObject; } - void setGlobalObject(JSGlobalObject* globalObject) { m_globalObject = globalObject; } + void setGlobalObject(JSGlobalObject* globalObject) + { + ASSERT(m_vm); + m_globalObject = globalObject; + } private: JS_EXPORT_PRIVATE void setUpSlow(); JS_EXPORT_PRIVATE void tearDownSlow(); - VM& m_vm; - JSGlobalObject* m_globalObject; + // Pointer-sized state avoids optional's partial-word flag and uninitialized padding on the scanned stack. + VM* m_vm { nullptr }; + JSGlobalObject* m_globalObject { nullptr }; }; +static_assert(sizeof(VMEntryScope) == 2 * sizeof(void*)); + } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/VMEntryScopeInlines.h b/Source/JavaScriptCore/runtime/VMEntryScopeInlines.h index e8570400283f..4ceaf598389b 100644 --- a/Source/JavaScriptCore/runtime/VMEntryScopeInlines.h +++ b/Source/JavaScriptCore/runtime/VMEntryScopeInlines.h @@ -31,9 +31,15 @@ namespace JSC { ALWAYS_INLINE VMEntryScope::VMEntryScope(VM& vm, JSGlobalObject* globalObject) - : m_vm(vm) - , m_globalObject(globalObject) { + enter(vm, globalObject); +} + +ALWAYS_INLINE void VMEntryScope::enter(VM& vm, JSGlobalObject* globalObject) +{ + ASSERT(!m_vm); + m_vm = &vm; + m_globalObject = globalObject; if (!vm.entryScope) setUpSlow(); vm.clearLastException(); @@ -41,9 +47,17 @@ ALWAYS_INLINE VMEntryScope::VMEntryScope(VM& vm, JSGlobalObject* globalObject) ALWAYS_INLINE VMEntryScope::~VMEntryScope() { - if (m_vm.entryScope != this) + reset(); +} + +ALWAYS_INLINE void VMEntryScope::reset() +{ + if (!m_vm) return; - tearDownSlow(); + if (m_vm->entryScope == this) + tearDownSlow(); + m_globalObject = nullptr; + m_vm = nullptr; } } // namespace JSC diff --git a/Source/JavaScriptCore/shell/CMakeLists.txt b/Source/JavaScriptCore/shell/CMakeLists.txt index 0bce9075d8d9..cbe26f7f6bd1 100644 --- a/Source/JavaScriptCore/shell/CMakeLists.txt +++ b/Source/JavaScriptCore/shell/CMakeLists.txt @@ -292,3 +292,42 @@ if (DEVELOPER_MODE) ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ) endif () + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$" AND NOT ENABLE_C_LOOP) + enable_language(ASM) + set(testVMEntryRecord_SOURCES + ../API/tests/VMEntryRecordProbeX86_64.S + ../API/tests/testVMEntryRecord.cpp + ) + set(testVMEntryRecord_DEFINITIONS ${jsc_PRIVATE_DEFINITIONS}) + set(testVMEntryRecord_PRIVATE_INCLUDE_DIRECTORIES ${jsc_PRIVATE_INCLUDE_DIRECTORIES}) + set(testVMEntryRecord_FRAMEWORKS ${jsc_FRAMEWORKS}) + set(testVMEntryRecord_LIBRARIES ${CMAKE_DL_LIBS}) + if (USE_EXTERNAL_MIMALLOC) + list(APPEND testVMEntryRecord_LIBRARIES $) + endif () + WEBKIT_EXECUTABLE_DECLARE(testVMEntryRecord) + WEBKIT_EXECUTABLE(testVMEntryRecord) +endif () + +# Scope retention regression: same source before and after the production fix. +option(TEST_VM_ENTRY_SCOPE_OPTIONAL_CONTROL "Test the historical std::optional owner" OFF) +set(testVMEntryScope_SOURCES ../API/tests/testVMEntryScope.cpp) +set(testVMEntryScope_DEFINITIONS ${jsc_PRIVATE_DEFINITIONS}) +set(testVMEntryScope_PRIVATE_INCLUDE_DIRECTORIES ${jsc_PRIVATE_INCLUDE_DIRECTORIES}) +set(testVMEntryScope_FRAMEWORKS ${jsc_FRAMEWORKS}) +set(testVMEntryScope_LIBRARIES ${CMAKE_DL_LIBS}) +if (WIN32) + list(APPEND testVMEntryScope_LIBRARIES Winmm) +endif () +if (USE_EXTERNAL_MIMALLOC) + list(APPEND testVMEntryScope_LIBRARIES $) +endif () +if (TEST_VM_ENTRY_SCOPE_OPTIONAL_CONTROL) + list(APPEND testVMEntryScope_DEFINITIONS TEST_VM_ENTRY_SCOPE_OPTIONAL_CONTROL=1) +endif () +WEBKIT_EXECUTABLE_DECLARE(testVMEntryScope) +WEBKIT_EXECUTABLE(testVMEntryScope) +if (TARGET testVMEntryRecord) + add_dependencies(jsc testVMEntryRecord testVMEntryScope) +endif ()