`--spill-pointers` does not spill a value held on the Wasm value stack across a call
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
Hướng nghiên cứu
Start with SpillPointers.cpp and the --spill-pointers pass logic described in the issue. Run wasm-opt/check.py on wasm-opt/operand.wat using the workflow in .github/workflows/spill-pointers.yml; the reproducer expects spill stores for $operand and $local-operand as well as $control. Done means the checks pass and the end-to-end reproducer no longer frees a live object during collection.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Boehm GC on Emscripten cannot scan Wasm locals or the Wasm value stack. Code
that relies on conservative collection (for example ECL, and Maxima built on
it) links with -sBINARYEN_EXTRA_PASSES=--spill-pointers, so that pointers
live across a call are stored to the shadow stack that the collector scans.
The pass does not spill a value that waits on the value stack as an earlier
operand of a call while a later operand calls out. If that later call
collects, the object can be freed while it is still in use. Running
--flatten immediately before --spill-pointers makes the pass spill it.
We first hit this in Maxima running on ECL compiled to Wasm: with Maxima
built at -Oz, an intermittent Lisp error appeared
while running the STACK-assessment Maxima package. The same workload
built at -O2 did not show it. It turned out the collector had freed a live object
that --spill-pointers had not spilled, and its memory was then reused.
The author used Claude to pin this problem down, and Claude suggested the reproducers
below and pinned the issue down to SpillPointers.cpp as described in this issue.
The author checked the reproducer functions and observed real memory errors both
in his app and with provided reproducers, however this issue lays way outside author's
comfort area when it comes to software development.
Please take this in consideration when reviewing.
Everything below comes from two GitHub Actions runs on commit 6d08b8f:
- C and
wasm-optrun
(this reproducer, 7 jobs) - ECL run
(the companionspill-pointers-eclreproducer, 2 jobs)
Minimal wasm-opt testcase
wasm-opt/operand.wat; $collect stands for any call that can collect:
(func $control ;; $x is read after collect()
(local $x i32)
(local.set $x (call $make))
(drop (call $collect))
(call $use1 (local.get $x)))
(func $operand ;; make()'s result waits on the value stack
(call $use (call $make) (call $collect)))
(func $local-operand ;; local.get $x is evaluated before collect()
(local $x i32)
(local.set $x (call $make))
(call $use (local.get $x) (call $collect)))
wasm-opt operand.wat --spill-pointers -S -o plain.wat
wasm-opt operand.wat --flatten --spill-pointers -S -o flatten.wat
Spill stores (i32.store to the shadow stack) per function:
| Function | --spill-pointers |
--flatten --spill-pointers |
|---|---|---|
$control |
1 | 1 |
$operand |
0 | 1 |
$local-operand |
0 | 1 |
| Binaryen | Job |
|---|---|
version_123-168-g8c82b6884 (emsdk 4.0.10) |
log |
version_133-96-ga631c1445 (emsdk 6.0.13) |
log |
version_133-119-gf1f3a483 (main at f1f3a483, built from source) |
log |
Each log prints both output modules in full. In plain.wat, $operand and
$local-operand are unchanged. $local-operand shows that the value need not
come straight from a call: once $x has been read as an operand it is dead for
liveness purposes, although its value is still in use until $use runs.
End-to-end: C and bdwgc
spill-gap.c evaluates intact(make(id), churn(id), id) through function
pointers, so the calls are not inlined. make() allocates an object with
GC_MALLOC; churn() runs GC_gcollect() while make()'s result waits as
the first operand. Each case runs 2,000 rounds with one full collection each.
- freed: a disappearing link on the object was cleared, so the collector
reclaimed an object that was still live. - spilled: rounds in which the shadow stack inside
main's frame held the
object's address at the collection. - stale: rounds in which a copy of the address lay in dead stack words below
main's frame. The fixture pads and scrubs the stack so that no stale word
keeps the object alive by chance; this was 0 in every case.
freed / spilled, the same for bdwgc 8.2.8 (bundled in the ECL Emscripten fork
at e4bdb2e5) and bdwgc 8.2.12 (4fab5386):
| Optimization | emsdk | No spill pass | --spill-pointers |
--flatten,--spill-pointers |
|---|---|---|---|---|
-O0 |
4.0.10 | 2,000 / 0 | 0 / 2,000 | 0 / 2,000 |
-O0 |
6.0.13 | 2,000 / 0 | 2,000 / 0 | 0 / 2,000 |
-O2 |
both | 2,000 / 0 | 2,000 / 0 | 0 / 2,000 |
-Oz |
both | 2,000 / 0 | 2,000 / 0 | 0 / 2,000 |
At -O2 and -Oz, --spill-pointers changes nothing for this pattern: the
object is freed in every round, as without the pass. At -O0 the outcome
depends on clang: with emsdk 4.0.10 the pass does save the object (it is freed
without the pass and spilled with it), with emsdk 6.0.13 it does not.
| emsdk | bdwgc | Job |
|---|---|---|
4.0.10 (emcc 4.0.10, Binaryen version_123-168, Node 22.16.0) |
8.2.8 | log |
| 4.0.10 | 8.2.12 | log |
6.0.13 (emcc 6.0.13, Binaryen version_133-96, Node 24.19.0) |
8.2.8 | log |
| 6.0.13 | 8.2.12 | log |
Each job uploads report.json (every build and run command, exit code and
output) and O2-spill-names.wat (the -O2 plain-spill module as text).
Further observations from the same jobs:
- With emsdk 6.0.13,
--spill-pointersfails on a module without a name
section:Fatal: getStackSpace: failed to find the stack pointer. The
fixture therefore links with--profiling-funcs, as ECL's own wasm32 link
flags do. With emsdk 4.0.10 the stripped builds give the same results as the
named ones. - The report also counts objects whose contents were overwritten (
lost).
This undercounts frees because a freed block is not always reused in the same
round: 1 of 2,000 at-O2/-Oz, 2,000 of 2,000 at-O0.
The companion ECL reproducer shows the same result through ECL's public API,
with ECL built from source as its INSTALL describes
(ECL run).
Mechanism
SpillPointers.cpp computes liveness over the function's existing locals and
spills the pointer-typed locals live across each call. A call result used
directly as an operand of a later call is not a local, so it is never
considered. A local that has already been read as an operand is no longer live
at the later call. When the pass rewrites a call, it moves that call's own
operands into new temporaries, which are not spilled either. The pass logic is
unchanged between version_123 and main at f1f3a483. --flatten turns
every such value into a local before liveness is computed.
Hardware and system
All jobs ran on GitHub-hosted ubuntu-24.04 runners (image
20261012.334.1). Each job prints its runner in the step "Record the runner
hardware and system". For example, the Binaryen main
job
reports x86-64, 4 CPUs, AMD EPYC 7763, 15 GiB RAM, Ubuntu 24.04.5, Linux
6.17.0-1022-azure. The CPU model differs between jobs: the emsdk 4.0.10 /
bdwgc 8.2.8 job
reports AMD EPYC 9V74. The counts were identical across jobs.
Running it
The workflow .github/workflows/spill-pointers.yml runs everything above. To
run it locally (Linux, Python 3, Git; the SDK supplies clang, Binaryen and
Node), from this directory:
git clone https://github.com/emscripten-core/emsdk.git
emsdk/emsdk install 6.0.13 && emsdk/emsdk activate 6.0.13
git clone https://github.com/ivmai/bdwgc.git && git -C bdwgc checkout v8.2.12
python3 run.py --emsdk "$PWD/emsdk" --gc "$PWD/bdwgc" --output "$PWD/out"
python3 wasm-opt/check.py --wasm-opt emsdk/upstream/bin/wasm-opt --output "$PWD/out-wasm-opt"
run.py fails unless, at every optimization level, the flattened build frees
nothing and spills in every round, and, at -O2 and -Oz, plain spilling
frees in every round with no spill. check.py fails unless the store counts
match the table above.
Limits
- The fixtures force the pattern with indirect calls and a collection in every
round. They show that the pass misses such values, not how often real code
produces them. In real programs a stale stack word often keeps the object
alive, so the failures are intermittent. --flattenraises local counts. On a large module (Maxima compiled by ECL)
we also needed--simplify-locals-notee-nostructure --vacuum --coalesce-locals --reorder-localsafter spilling to stay below V8's limit of
50,000 locals per function. That is not shown by these fixtures.
- Ngôn ngữ chính
- WebAssembly
- Star
- 8.7k
- Fork
- 900
- Merge trung bình
- 2 ngày 5 giờ
- Pull request đã merge (30 ngày)
- 106
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của WebAssembly/binaryen
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 88/100
WebAssembly/binaryen#9135 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 Nửa ngày Mức phù hợp với người mới 76/100
WebAssembly/binaryen#9018 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 76/100
WebAssembly/binaryen#9291 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 62/100
WebAssembly/binaryen#9257 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 45/100
WebAssembly/binaryen#9249 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của WebAssembly/binaryen
Issue tương tự
-
bug ready-for-agent
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
PhyberApex/kuroshiro#1687 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Provider test fails on official OpenAI preset: chat probe sends max_tokens, rejected by newer modelsĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
general_error() logs every 4xx response at ERROR level, including expected 400, 404 and 409 outcomesĐang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
awslabs/visual-asset-management-system#935 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
Maintainer thường phản hồi trong vòng 1 ngày
-
vim.filetype.match() prefixes a URL buffer name with the cwd before passing it to filetype functionsĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
Maintainer thường phản hồi trong vòng 1 ngày