Skip to content

Commit f05b045

Browse files
authored
v.gen.wasm: floor-safe wasm-opt/wasm-validate feature allowlist + memory-contract test (#27526)
1 parent 863a209 commit f05b045

11 files changed

Lines changed: 404 additions & 17 deletions

vlib/v/gen/wasm/features.v

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright (c) 2023 l-m.dev. All rights reserved.
2+
// Use of this source code is governed by an MIT license
3+
// that can be found in the LICENSE file.
4+
module wasm
5+
6+
// WebAssembly feature gating for the native backend.
7+
//
8+
// `wasm-opt` (Binaryen) and `wasm-validate` (WABT) accept *different* flag
9+
// spellings for the same features (e.g. Binaryen `--enable-exception-handling`
10+
// vs WABT `--enable-exceptions`; Binaryen `--enable-nontrapping-float-to-int`
11+
// vs WABT `--disable-saturating-float-to-int`). The logical feature set is
12+
// therefore defined once here and rendered into each tool's own vocabulary.
13+
//
14+
// The default build enables only the Safari-15 "floor" features; everything
15+
// else is opt-in via a `-d <name>` compile define. This keeps a default
16+
// `-os browser`/`-os wasi` build from silently emitting opcodes past the
17+
// baseline (the old `wasm-opt -all` enabled memory64/relaxed-SIMD/exnref/gc).
18+
19+
// wasm_opt_min_version is the Binaryen version the toolchain pins
20+
// (see cmd/tools/install_binaryen.vsh). Used only for a soft warning.
21+
const wasm_opt_min_version = 112
22+
23+
enum WasmFeature {
24+
reference_types
25+
bulk_memory
26+
multivalue
27+
sign_ext
28+
mutable_globals
29+
nontrapping_f2i
30+
simd
31+
gc
32+
exception_handling
33+
tail_call
34+
threads
35+
memory64
36+
relaxed_simd
37+
extended_const
38+
}
39+
40+
// wasm_all_features lists every modelled feature, used to compute the WABT
41+
// validator argument set relative to its defaults.
42+
const wasm_all_features = [WasmFeature.reference_types, .bulk_memory, .multivalue, .sign_ext,
43+
.mutable_globals, .nontrapping_f2i, .simd, .gc, .exception_handling, .tail_call, .threads,
44+
.memory64, .relaxed_simd, .extended_const]
45+
46+
// wasm_floor_features is the always-on Safari-15 baseline. It covers everything
47+
// the emitter produces today: ref.func/ref.null, memory.copy/fill/init,
48+
// multi-return, sign_extendN, the mutable __vsp/__heap_base globals, and
49+
// saturating (nontrapping) float->int casts.
50+
const wasm_floor_features = [WasmFeature.reference_types, .bulk_memory, .multivalue, .sign_ext,
51+
.mutable_globals, .nontrapping_f2i]
52+
53+
// wasm_optin_defines maps a `-d <name>` compile define to the feature it enables.
54+
const wasm_optin_defines = {
55+
'wasm_gc': WasmFeature.gc
56+
'wasm_exceptions': WasmFeature.exception_handling
57+
'wasm_tail_call': WasmFeature.tail_call
58+
'wasm_simd': WasmFeature.simd
59+
'wasm_threads': WasmFeature.threads
60+
'wasm_memory64': WasmFeature.memory64
61+
'wasm_relaxed_simd': WasmFeature.relaxed_simd
62+
'wasm_extended_const': WasmFeature.extended_const
63+
}
64+
65+
struct FeatureFlag {
66+
binaryen string // wasm-opt flag, e.g. '--enable-reference-types'
67+
wabt string // WABT feature token, e.g. 'reference-types'
68+
wabt_default_on bool // whether wasm-validate enables it without any flag
69+
}
70+
71+
fn (f WasmFeature) flag() FeatureFlag {
72+
return match f {
73+
.reference_types {
74+
FeatureFlag{
75+
binaryen: '--enable-reference-types'
76+
wabt: 'reference-types'
77+
wabt_default_on: true
78+
}
79+
}
80+
.bulk_memory {
81+
FeatureFlag{
82+
binaryen: '--enable-bulk-memory'
83+
wabt: 'bulk-memory'
84+
wabt_default_on: true
85+
}
86+
}
87+
.multivalue {
88+
FeatureFlag{
89+
binaryen: '--enable-multivalue'
90+
wabt: 'multi-value'
91+
wabt_default_on: true
92+
}
93+
}
94+
.sign_ext {
95+
FeatureFlag{
96+
binaryen: '--enable-sign-ext'
97+
wabt: 'sign-extension'
98+
wabt_default_on: true
99+
}
100+
}
101+
.mutable_globals {
102+
FeatureFlag{
103+
binaryen: '--enable-mutable-globals'
104+
wabt: 'mutable-globals'
105+
wabt_default_on: true
106+
}
107+
}
108+
.nontrapping_f2i {
109+
FeatureFlag{
110+
binaryen: '--enable-nontrapping-float-to-int'
111+
wabt: 'saturating-float-to-int'
112+
wabt_default_on: true
113+
}
114+
}
115+
.simd {
116+
FeatureFlag{
117+
binaryen: '--enable-simd'
118+
wabt: 'simd'
119+
wabt_default_on: true
120+
}
121+
}
122+
.gc {
123+
FeatureFlag{
124+
binaryen: '--enable-gc'
125+
wabt: 'gc'
126+
wabt_default_on: false
127+
}
128+
}
129+
.exception_handling {
130+
FeatureFlag{
131+
binaryen: '--enable-exception-handling'
132+
wabt: 'exceptions'
133+
wabt_default_on: false
134+
}
135+
}
136+
.tail_call {
137+
FeatureFlag{
138+
binaryen: '--enable-tail-call'
139+
wabt: 'tail-call'
140+
wabt_default_on: false
141+
}
142+
}
143+
.threads {
144+
FeatureFlag{
145+
binaryen: '--enable-threads'
146+
wabt: 'threads'
147+
wabt_default_on: false
148+
}
149+
}
150+
.memory64 {
151+
FeatureFlag{
152+
binaryen: '--enable-memory64'
153+
wabt: 'memory64'
154+
wabt_default_on: false
155+
}
156+
}
157+
.relaxed_simd {
158+
FeatureFlag{
159+
binaryen: '--enable-relaxed-simd'
160+
wabt: 'relaxed-simd'
161+
wabt_default_on: false
162+
}
163+
}
164+
.extended_const {
165+
FeatureFlag{
166+
binaryen: '--enable-extended-const'
167+
wabt: 'extended-const'
168+
wabt_default_on: false
169+
}
170+
}
171+
}
172+
}
173+
174+
// enabled_wasm_features returns the floor set plus any opt-in feature whose
175+
// `-d` define was passed on the command line.
176+
fn (g &Gen) enabled_wasm_features() []WasmFeature {
177+
mut feats := wasm_floor_features.clone()
178+
for define, feat in wasm_optin_defines {
179+
if define in g.pref.compile_defines {
180+
feats << feat
181+
}
182+
}
183+
return apply_feature_implications(feats)
184+
}
185+
186+
// apply_feature_implications expands `feats` with any feature implied by another.
187+
// Relaxed SIMD extends the SIMD/v128 feature, so requesting it must also enable
188+
// SIMD. Otherwise wabt_validate_args() emits `--disable-simd --enable-relaxed-simd`
189+
// and wasm-opt runs from `-mvp` without `--enable-simd`, so a `-d wasm_relaxed_simd`
190+
// build still fails under `-wasm-validate`/`-prod`.
191+
fn apply_feature_implications(feats []WasmFeature) []WasmFeature {
192+
mut res := feats.clone()
193+
if WasmFeature.relaxed_simd in res && WasmFeature.simd !in res {
194+
res << .simd
195+
}
196+
return res
197+
}
198+
199+
// binaryen_feature_flags renders the feature set into `wasm-opt` flags. It
200+
// starts from `-mvp` (all non-MVP features off) and enables only the allowlist,
201+
// so emitting an opcode outside the set makes wasm-opt itself fail rather than
202+
// silently optimise it through.
203+
fn binaryen_feature_flags(feats []WasmFeature) string {
204+
mut flags := ['-mvp']
205+
for feat in feats {
206+
flags << feat.flag().binaryen
207+
}
208+
return flags.join(' ')
209+
}
210+
211+
// wabt_validate_args renders the feature set into `wasm-validate` arguments,
212+
// relative to WABT's defaults: disable any default-on feature not in the set
213+
// (e.g. `--disable-simd` on a floor build), and enable any default-off feature
214+
// that is in the set. This gives the validator teeth - a stray above-floor
215+
// opcode fails validation instead of passing under WABT's permissive defaults.
216+
fn wabt_validate_args(feats []WasmFeature) []string {
217+
mut args := []string{}
218+
for feat in wasm_all_features {
219+
ff := feat.flag()
220+
enabled := feat in feats
221+
if ff.wabt_default_on && !enabled {
222+
args << '--disable-${ff.wabt}'
223+
} else if !ff.wabt_default_on && enabled {
224+
args << '--enable-${ff.wabt}'
225+
}
226+
}
227+
return args
228+
}

vlib/v/gen/wasm/features_test.v

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module wasm
2+
3+
fn test_relaxed_simd_implies_simd() {
4+
// requesting relaxed SIMD on its own must also pull in the base SIMD feature
5+
feats := apply_feature_implications([WasmFeature.relaxed_simd])
6+
assert WasmFeature.simd in feats
7+
assert WasmFeature.relaxed_simd in feats
8+
}
9+
10+
fn test_relaxed_simd_does_not_duplicate_simd() {
11+
// when SIMD is already present, the implication must not add a duplicate
12+
feats := apply_feature_implications([WasmFeature.simd, .relaxed_simd])
13+
assert feats.filter(it == WasmFeature.simd).len == 1
14+
}
15+
16+
fn test_simd_without_relaxed_is_unchanged() {
17+
feats := apply_feature_implications([WasmFeature.simd])
18+
assert WasmFeature.simd in feats
19+
assert WasmFeature.relaxed_simd !in feats
20+
}
21+
22+
fn test_relaxed_simd_renders_simd_in_tool_flags() {
23+
feats := apply_feature_implications([WasmFeature.relaxed_simd])
24+
// wasm-opt must be told to enable SIMD as well as relaxed SIMD
25+
binaryen := binaryen_feature_flags(feats)
26+
assert binaryen.contains('--enable-simd')
27+
assert binaryen.contains('--enable-relaxed-simd')
28+
// wasm-validate must not disable SIMD while enabling relaxed SIMD
29+
wabt := wabt_validate_args(feats)
30+
assert '--disable-simd' !in wabt
31+
assert '--enable-relaxed-simd' in wabt
32+
}

vlib/v/gen/wasm/gen.v

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,33 +2031,32 @@ pub fn gen(files []&ast.File, mut table ast.Table, out_name string, w_pref &pref
20312031

20322032
if out_name != '-' {
20332033
os.write_file_array(out_name, mod) or { panic(err) }
2034+
// The enabled feature set (Safari-15 floor plus any `-d` opt-in) drives
2035+
// both validation and optimisation, so compute it once and share it.
2036+
feats := g.enabled_wasm_features()
20342037
if g.pref.wasm_validate {
2035-
exe := $if windows { 'wasm-validate.exe' } $else { 'wasm-validate' }
2036-
if rt := os.find_abs_path_of_executable(exe) {
2037-
mut p := os.new_process(rt)
2038-
p.set_args([out_name])
2039-
p.set_redirect_stdio()
2040-
p.run()
2041-
err := p.stderr_slurp()
2042-
p.wait()
2043-
if p.code != 0 {
2044-
eprintln(err)
2045-
g.w_error('validation failed, this should not happen. report an issue with the above messages, the webassembly generated, and appropriate code.')
2046-
}
2047-
} else {
2048-
g.w_error('${exe} not found! Try installing WABT (WebAssembly Binary Toolkit). Run `./cmd/tools/install_wabt.vsh`, to download a prebuilt executable for your platform.')
2049-
}
2038+
g.validate_wasm(out_name, feats)
20502039
}
20512040
if g.pref.is_prod {
20522041
exe := $if windows { 'wasm-opt.exe' } $else { 'wasm-opt' }
20532042
if rt := os.find_abs_path_of_executable(exe) {
2054-
// -lmu: low memory unused, very important optimisation
2043+
g.check_wasm_opt_version(rt)
2044+
// -lmu: low memory unused, very important optimisation.
2045+
// Feature flags are an explicit floor-safe allowlist (never `-all`),
2046+
// so wasm-opt cannot silently introduce opcodes past the baseline.
2047+
flags := binaryen_feature_flags(feats)
20552048
res :=
2056-
os.execute('${os.quoted_path(rt)} -all -lmu -c -O4 ${os.quoted_path(out_name)} -o ${os.quoted_path(out_name)}')
2049+
os.execute('${os.quoted_path(rt)} ${flags} -lmu -c -O4 ${os.quoted_path(out_name)} -o ${os.quoted_path(out_name)}')
20572050
if res.exit_code != 0 {
20582051
eprintln(res.output)
20592052
g.w_error('${rt} failed, this should not happen. Report an issue with the above messages, the webassembly generated, and appropriate code.')
20602053
}
2054+
// Re-validate AFTER optimisation: a passing pre-opt validation is
2055+
// not sufficient, wasm-opt must not have introduced any opcode past
2056+
// the enabled feature floor.
2057+
if g.pref.wasm_validate {
2058+
g.validate_wasm(out_name, feats)
2059+
}
20612060
} else {
20622061
g.w_error('${exe} not found! Try installing Binaryen.
20632062
| Run `./cmd/tools/install_binaryen.vsh`, to download a prebuilt executable for your platform.
@@ -2071,3 +2070,41 @@ pub fn gen(files []&ast.File, mut table ast.Table, out_name string, w_pref &pref
20712070
eprintln('stdout output, cannot validate or optimise wasm')
20722071
}
20732072
}
2073+
2074+
// validate_wasm runs `wasm-validate` over the emitted module at `out_name`,
2075+
// constraining it to the enabled feature set so that any opcode past the floor
2076+
// fails validation instead of passing under WABT's permissive defaults.
2077+
fn (mut g Gen) validate_wasm(out_name string, feats []WasmFeature) {
2078+
exe := $if windows { 'wasm-validate.exe' } $else { 'wasm-validate' }
2079+
if rt := os.find_abs_path_of_executable(exe) {
2080+
mut p := os.new_process(rt)
2081+
mut vargs := wabt_validate_args(feats)
2082+
vargs << out_name
2083+
p.set_args(vargs)
2084+
p.set_redirect_stdio()
2085+
p.run()
2086+
err := p.stderr_slurp()
2087+
p.wait()
2088+
if p.code != 0 {
2089+
eprintln(err)
2090+
g.w_error('validation failed, this should not happen. report an issue with the above messages, the webassembly generated, and appropriate code.')
2091+
}
2092+
} else {
2093+
g.w_error('${exe} not found! Try installing WABT (WebAssembly Binary Toolkit). Run `./cmd/tools/install_wabt.vsh`, to download a prebuilt executable for your platform.')
2094+
}
2095+
}
2096+
2097+
// check_wasm_opt_version emits a soft warning if the resolved wasm-opt is older
2098+
// than the version the toolchain pins. It never fails the build (a hard pin with
2099+
// checksums belongs to the build/release pipeline, not the backend).
2100+
fn (mut g Gen) check_wasm_opt_version(exe string) {
2101+
res := os.execute('${os.quoted_path(exe)} --version')
2102+
if res.exit_code != 0 {
2103+
return
2104+
}
2105+
// `wasm-opt --version` prints e.g. "wasm-opt version 108"
2106+
ver := res.output.all_after_last(' ').trim_space().int()
2107+
if ver != 0 && ver < wasm_opt_min_version {
2108+
eprintln('warning: wasm-opt version ${ver} is older than the pinned minimum (${wasm_opt_min_version}); `-prod` output may differ. Run `./cmd/tools/install_binaryen.vsh` to update.')
2109+
}
2110+
}

vlib/v/gen/wasm/mem.v

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
// that can be found in the LICENSE file.
44
module wasm
55

6+
// Memory contract for the native WebAssembly backend
7+
// ---------------------------------------------------
8+
// `-b wasm` selects `-gc none` automatically (every non-C backend defaults to
9+
// no_gc in v.pref.default), and autofree is off by default. Crucially, autofree
10+
// code generation lives only in the C backend (v/gen/c/autofree.v); this backend
11+
// never sees frontend-inserted free/drop calls, so the AST it lowers is "natural".
12+
//
13+
// Today the only memory mechanism is the shadow stack (__vsp, grows down).
14+
// `g.func.drop()` calls in this backend are WebAssembly operand-stack drops, not
15+
// memory frees. There is no malloc/free/GC.
16+
//
17+
// This invariant is load-bearing: a future manual allocator (__v_alloc/__v_free)
18+
// and explicit dispose() will be the sole heap-management entry points, and must
19+
// not collide with frontend-inserted frees. The no-frontend-free property is
20+
// regression-guarded by tests_decompile/no_frontend_free_under_wasm.vv.
621
import wasm
722
import v.ast
823
import v.gen.wasm.serialise

0 commit comments

Comments
 (0)