Skip to content

Commit f771c50

Browse files
committed
[JSC] Use FrameTracer in wasm ref_func, table_get, and array_init_elem operations
https://bugs.webkit.org/show_bug.cgi?id=321032 rdar://183455247 Reviewed by Sosuke Suzuki. ref.func, table.get, and array.init_elem can all materialize wrapper functions, which can GC and update ShadowChicken, and therefore need a FrameTracer. Tests: JSTests/wasm/stress/array-init-elem-wrapper-alloc-frame-tracer.js JSTests/wasm/stress/ref-func-wrapper-alloc-frame-tracer.js JSTests/wasm/stress/table-get-wrapper-alloc-frame-tracer.js * JSTests/wasm/stress/array-init-elem-wrapper-alloc-frame-tracer.js: Added. (const.imports.m.f): * JSTests/wasm/stress/ref-func-wrapper-alloc-frame-tracer.js: Added. (let.wat.module.import.string_appeared_here.string_appeared_here.func.import): (func): (const.imports.m.f): * JSTests/wasm/stress/table-get-wrapper-alloc-frame-tracer.js: Added. (funcref.elem): (func): (const.imports.m.f): * Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp: (JSC::IPInt::WASM_IPINT_EXTERN_CPP_DECL): * Source/JavaScriptCore/wasm/WasmOperations.cpp: (JSC::Wasm::JSC_DEFINE_NOEXCEPT_JIT_OPERATION): Canonical link: https://commits.webkit.org/318638@main
1 parent 9ac9df1 commit f771c50

5 files changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//@ requireOptions("--alwaysUseShadowChicken=true", "--slowPathAllocsBetweenGCs=1", "--forceGCSlowPaths=true")
2+
3+
import * as assert from "../assert.js"
4+
5+
// Companion to ref-func-wrapper-alloc-frame-tracer.js, for array.init_elem.
6+
//
7+
// array.init_elem copies a funcref elem segment through copyElementSegment, which
8+
// materializes each element's JS wrapper. Unlike array.new_elem it does not
9+
// allocate the array itself, so the earlier Wasm GC slow path audit missed it.
10+
//
11+
// Kept as bytes rather than WAT: the Wasm GC text format needs gc/wast.js, whose
12+
// 6.6MB of JS takes ~45s to load under this test's forced-GC options.
13+
//
14+
// (module
15+
// (type $arr (array (mut funcref)))
16+
// (import "m" "f" (func $import))
17+
// (elem $e funcref (ref.func $f0) (ref.func $f1) (ref.func $f2) (ref.func $f3) (ref.func $f4) (ref.func $f5) (ref.func $f6) (ref.func $f7))
18+
// (func $f0)
19+
// (func $f1)
20+
// (func $f2)
21+
// (func $f3)
22+
// (func $f4)
23+
// (func $f5)
24+
// (func $f6)
25+
// (func $f7)
26+
// (func (export "test")
27+
// (local $a (ref null $arr))
28+
// (local.set $a (array.new_default $arr (i32.const 1)))
29+
// (call $import)
30+
// (array.init_elem $arr $e (local.get $a) (i32.const 0) (i32.const 0) (i32.const 1))
31+
// (call $import)
32+
// (array.init_elem $arr $e (local.get $a) (i32.const 0) (i32.const 1) (i32.const 1))
33+
// (call $import)
34+
// (array.init_elem $arr $e (local.get $a) (i32.const 0) (i32.const 2) (i32.const 1))
35+
// (call $import)
36+
// (array.init_elem $arr $e (local.get $a) (i32.const 0) (i32.const 3) (i32.const 1))
37+
// (call $import)
38+
// (array.init_elem $arr $e (local.get $a) (i32.const 0) (i32.const 4) (i32.const 1))
39+
// (call $import)
40+
// (array.init_elem $arr $e (local.get $a) (i32.const 0) (i32.const 5) (i32.const 1))
41+
// (call $import)
42+
// (array.init_elem $arr $e (local.get $a) (i32.const 0) (i32.const 6) (i32.const 1))
43+
// (call $import)
44+
// (array.init_elem $arr $e (local.get $a) (i32.const 0) (i32.const 7) (i32.const 1))
45+
// )
46+
// )
47+
48+
const wasmBytes = new Uint8Array([
49+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x87, 0x80, 0x80, 0x80, 0x00, 0x02, 0x5e,
50+
0x70, 0x01, 0x60, 0x00, 0x00, 0x02, 0x87, 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x6d, 0x01, 0x66,
51+
0x00, 0x01, 0x03, 0x8a, 0x80, 0x80, 0x80, 0x00, 0x09, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
52+
0x01, 0x01, 0x07, 0x88, 0x80, 0x80, 0x80, 0x00, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x09,
53+
0x09, 0x9c, 0x80, 0x80, 0x80, 0x00, 0x01, 0x05, 0x70, 0x08, 0xd2, 0x01, 0x0b, 0xd2, 0x02, 0x0b,
54+
0xd2, 0x03, 0x0b, 0xd2, 0x04, 0x0b, 0xd2, 0x05, 0x0b, 0xd2, 0x06, 0x0b, 0xd2, 0x07, 0x0b, 0xd2,
55+
0x08, 0x0b, 0x0a, 0xba, 0x81, 0x80, 0x80, 0x00, 0x09, 0x82, 0x80, 0x80, 0x80, 0x00, 0x00, 0x0b,
56+
0x82, 0x80, 0x80, 0x80, 0x00, 0x00, 0x0b, 0x82, 0x80, 0x80, 0x80, 0x00, 0x00, 0x0b, 0x82, 0x80,
57+
0x80, 0x80, 0x00, 0x00, 0x0b, 0x82, 0x80, 0x80, 0x80, 0x00, 0x00, 0x0b, 0x82, 0x80, 0x80, 0x80,
58+
0x00, 0x00, 0x0b, 0x82, 0x80, 0x80, 0x80, 0x00, 0x00, 0x0b, 0x82, 0x80, 0x80, 0x80, 0x00, 0x00,
59+
0x0b, 0xfc, 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x63, 0x00, 0x41, 0x01, 0xfb, 0x07, 0x00, 0x21,
60+
0x00, 0x10, 0x00, 0x20, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x01, 0xfb, 0x13, 0x00, 0x00, 0x10,
61+
0x00, 0x20, 0x00, 0x41, 0x00, 0x41, 0x01, 0x41, 0x01, 0xfb, 0x13, 0x00, 0x00, 0x10, 0x00, 0x20,
62+
0x00, 0x41, 0x00, 0x41, 0x02, 0x41, 0x01, 0xfb, 0x13, 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x41,
63+
0x00, 0x41, 0x03, 0x41, 0x01, 0xfb, 0x13, 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x41, 0x00, 0x41,
64+
0x04, 0x41, 0x01, 0xfb, 0x13, 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x41, 0x00, 0x41, 0x05, 0x41,
65+
0x01, 0xfb, 0x13, 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x41, 0x00, 0x41, 0x06, 0x41, 0x01, 0xfb,
66+
0x13, 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x41, 0x00, 0x41, 0x07, 0x41, 0x01, 0xfb, 0x13, 0x00,
67+
0x00, 0x0b,
68+
]);
69+
70+
const numFuncs = 8;
71+
const numInstances = 8;
72+
73+
let calls = 0;
74+
const imports = { m: { f: () => { ++calls; } } };
75+
const module = new WebAssembly.Module(wasmBytes);
76+
77+
for (let i = 0; i < numInstances; ++i)
78+
new WebAssembly.Instance(module, imports).exports.test();
79+
80+
assert.eq(calls, numFuncs * numInstances);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//@ requireOptions("--alwaysUseShadowChicken=true", "--slowPathAllocsBetweenGCs=1", "--forceGCSlowPaths=true")
2+
3+
import { compile } from "../wabt-wrapper.js"
4+
import * as assert from "../assert.js"
5+
6+
// ref.func materializes the funcref's JS wrapper via ensureFunctionWrapper, which
7+
// allocates and so can GC. Wasm tiers update topCallFrame only just-in-time, so
8+
// after a JS import returns it points at dead native state, and a GC there makes
9+
// ShadowChicken read that as a JS CallFrame.
10+
//
11+
// Each ref.func targets a distinct function, preceded by an import call to leave
12+
// topCallFrame stale. Fresh instances have empty wrapper caches, so later
13+
// iterations rerun the path in whichever tier the function reached.
14+
15+
const numFuncs = 8;
16+
const numInstances = 8;
17+
18+
let funcs = "";
19+
let declares = "";
20+
let body = "";
21+
for (let i = 0; i < numFuncs; ++i) {
22+
funcs += ` (func $f${i})\n`;
23+
declares += ` $f${i}`;
24+
body += ` (call $import)\n (drop (ref.func $f${i}))\n`;
25+
}
26+
27+
let wat = `
28+
(module
29+
(import "m" "f" (func $import))
30+
(elem declare func${declares})
31+
${funcs} (func (export "test")
32+
${body} )
33+
)
34+
`;
35+
36+
let calls = 0;
37+
const imports = { m: { f: () => { ++calls; } } };
38+
const module = await compile(wat);
39+
40+
for (let i = 0; i < numInstances; ++i)
41+
new WebAssembly.Instance(module, imports).exports.test();
42+
43+
assert.eq(calls, numFuncs * numInstances);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//@ requireOptions("--alwaysUseShadowChicken=true", "--slowPathAllocsBetweenGCs=1", "--forceGCSlowPaths=true")
2+
3+
import { compile } from "../wabt-wrapper.js"
4+
import * as assert from "../assert.js"
5+
6+
// Companion to ref-func-wrapper-alloc-frame-tracer.js, for table.get.
7+
//
8+
// An active elem segment of non-imported funcrefs installs only Wasm-side metadata
9+
// (FuncRefTable::setLazy); FuncRefTable::get materializes the JS wrapper on first
10+
// read. Each index is read once per instance, so every table.get allocates.
11+
12+
const numFuncs = 8;
13+
const numInstances = 8;
14+
15+
let funcs = "";
16+
let elems = "";
17+
let body = "";
18+
for (let i = 0; i < numFuncs; ++i) {
19+
funcs += ` (func $f${i})\n`;
20+
elems += ` $f${i}`;
21+
body += ` (call $import)\n (drop (table.get $t (i32.const ${i})))\n`;
22+
}
23+
24+
let wat = `
25+
(module
26+
(import "m" "f" (func $import))
27+
(table $t ${numFuncs} funcref)
28+
(elem (i32.const 0)${elems})
29+
${funcs} (func (export "test")
30+
${body} )
31+
)
32+
`;
33+
34+
let calls = 0;
35+
const imports = { m: { f: () => { ++calls; } } };
36+
const module = await compile(wat);
37+
38+
for (let i = 0; i < numInstances; ++i)
39+
new WebAssembly.Instance(module, imports).exports.test();
40+
41+
assert.eq(calls, numFuncs * numInstances);

‎Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,8 @@ static ALWAYS_INLINE std::optional<uint32_t> checkedTableOperand(uint64_t value,
562562

563563
WASM_IPINT_EXTERN_CPP_DECL(table_get, unsigned tableIndex, uint64_t index)
564564
{
565+
// FuncRefTable::get materializes the funcref's JS wrapper on demand.
566+
WasmSlowPathWithoutCallFrameTracer tracer(instance->vm());
565567
const auto& info = instance->module().moduleInformation();
566568
auto checkedIndex = checkedTableOperand(index, info.table(tableIndex).addressType().is64Bit());
567569
if (!checkedIndex)
@@ -1053,6 +1055,8 @@ WASM_IPINT_EXTERN_CPP_DECL(array_init_data, uint32_t dataIndex, IPIntStackEntry*
10531055

10541056
WASM_IPINT_EXTERN_CPP_DECL(array_init_elem, uint32_t elemIndex, IPIntStackEntry* sp)
10551057
{
1058+
WasmSlowPathWithoutCallFrameTracer tracer(instance->vm());
1059+
10561060
// sp[0] = size
10571061
// sp[1] = src_offset
10581062
// sp[2] = dst_offset
@@ -1398,6 +1402,7 @@ WASM_IPINT_EXTERN_CPP_DECL(memory_atomic_notify, IPIntStackEntry* args)
13981402

13991403
WASM_IPINT_EXTERN_CPP_DECL(ref_func, unsigned index)
14001404
{
1405+
WasmSlowPathWithoutCallFrameTracer tracer(instance->vm());
14011406
IPINT_RETURN(Wasm::refFunc(instance, index));
14021407
}
14031408

‎Source/JavaScriptCore/wasm/WasmOperations.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,11 @@ JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationWasmMemoryCopy, UCPUStrictInt32, (JSW
15001500

15011501
JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationGetWasmTableElement, EncodedJSValue, (JSWebAssemblyInstance* instance, unsigned tableIndex, uint64_t index))
15021502
{
1503+
// FuncRefTable::get materializes the funcref's JS wrapper on demand.
1504+
CallFrame* callFrame = DECLARE_WASM_CALL_FRAME(instance);
1505+
assertCalleeIsReferenced(callFrame, instance);
1506+
VM& vm = instance->vm();
1507+
WasmOperationPrologueCallFrameTracer tracer(vm, callFrame, OUR_RETURN_ADDRESS);
15031508
return tableGet(instance, tableIndex, index);
15041509
}
15051510

@@ -1539,6 +1544,10 @@ JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationWasmTableCopy, UCPUStrictInt32, (JSWe
15391544

15401545
JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationWasmRefFunc, EncodedJSValue, (JSWebAssemblyInstance* instance, uint32_t index))
15411546
{
1547+
CallFrame* callFrame = DECLARE_WASM_CALL_FRAME(instance);
1548+
assertCalleeIsReferenced(callFrame, instance);
1549+
VM& vm = instance->vm();
1550+
WasmOperationPrologueCallFrameTracer tracer(vm, callFrame, OUR_RETURN_ADDRESS);
15421551
return refFunc(instance, index);
15431552
}
15441553

@@ -1769,6 +1778,10 @@ JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationWasmArrayCopy, UCPUStrictInt32, (JSWe
17691778

17701779
JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationWasmArrayInitElem, UCPUStrictInt32, (JSWebAssemblyInstance* instance, EncodedJSValue dst, uint32_t dstOffset, uint32_t srcElementIndex, uint32_t srcOffset, uint32_t size))
17711780
{
1781+
CallFrame* callFrame = DECLARE_WASM_CALL_FRAME(instance);
1782+
assertCalleeIsReferenced(callFrame, instance);
1783+
VM& vm = instance->vm();
1784+
WasmOperationPrologueCallFrameTracer tracer(vm, callFrame, OUR_RETURN_ADDRESS);
17721785
return toUCPUStrictInt32(arrayInitElem(instance, dst, dstOffset, srcElementIndex, srcOffset, size));
17731786
}
17741787

0 commit comments

Comments
 (0)