Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/openclaw/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
4 changes: 4 additions & 0 deletions .github/openclaw/qualify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/openclaw-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/openclaw-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions OPENCLAW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
116 changes: 116 additions & 0 deletions Source/JavaScriptCore/API/tests/VMEntryRecordProbeX86_64.S
Original file line number Diff line number Diff line change
@@ -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
Loading