opaque_predicate_insertion is completely broken on any module that contains at least one single-line (empty-body) function. These are common in compiled C/C++ WASM binaries (stubs, thunks, no-op wrappers). The underlying parser bug also silently corrupts function header lists for all other strategies, potentially modifying the wrong function without raising errors.
Reproduce
Input module (repro.wat):
(module
(type $t0 (func (param i32)))
(type $t1 (func (param i32 i32)))
(type $t2 (func))
(func $stub (type $t0) (param $p0 i32)) ;; single-line, no body
(func $real (type $t1) (param $p0 i32) (param $p1 i32)
block $B0
local.get $p0
local.get $p1
i32.add
drop
end
)
(func $another_stub (type $t2)) ;; single-line, no body
(func $real2 (type $t1) (param $p0 i32) (param $p1 i32)
block $B0
local.get $p0
drop
end
)
(table $T0 1 funcref)
(global $g0 (mut i32) (i32.const 0))
(export "real" (func $real))
(export "real2" (func $real2))
)
wat2wasm repro.wat -o repro.wasm
swamped obfuscate repro.wasm -o out.wasm -s opaque_predicate_insertion --seed 42 --ratio 0.5
(to simplify tests i am using CLI)
out.wast:5:39: error: unexpected token (, expected ).
(func $f0 (type $t0) (param $p0 i32)) (local $wadelocal i32)
^
[wat2wasm failed]
The generated WAT contains two malformed lines:
;; line 5 -- $stub's closing )) followed by dangling (local ...)
(func $f0 (type $t0) (param $p0 i32)) (local $wadelocal i32)
;; line 47 -- same for $another_stub
(func $f2 (type $t2)) (local $wadelocal i32)
On larger modules (e.g. Emscripten-compiled C++ with many stub functions) the error cascades into "undefined variable" errors for tables, globals, and functions that are actually defined in the module, making the output completely unusable.
Root cause
1. Parser doesn't finalize single-line functions (wasmParser/parser.py)
parseWast() tracks open/close parentheses via func_bracketNum. When a (func ...) line is matched, the counter is updated:
func_bracketNum += line.count('(') - line.count(')')
For a single-line function like (func $stub (type $t0) (param $p0 i32)), the brackets balance immediately (3 open, 3 close), so func_bracketNum drops to 0 on the same line. However, the finalization logic (extracting params, result, locals, header, body) only runs inside the if 1 not in whichSection block, which is not entered on the (func ...) line itself (because whichSection[2] = 1).
As a result:
- The function object is created with
header=None, body=None
funcHeaderArea remains True
funcHeaderLine still contains the unfinished function's line
When the next function is matched, its header line is appended to the same funcHeaderLine list. When that next function eventually finalizes, its header contains both its own line and the previous single-line function's line.
2. opaque_predicate_insertion modifies the wrong header line
The strategy adds (local $wadelocal i32) to every function by modifying header[0]:
new_header.append([old_header[0][0].strip() + ' (local $wadelocal i32)\n', ''])
Since header[0] is actually the absorbed single-line function's line (not the current function's), the result is:
(func $stub (type $t0) (param $p0 i32)) (local $wadelocal i32)
The )) closes the (func S-expression, and (local $wadelocal i32) ends up at module scope -- invalid WAT.
opaque_predicate_insertionis completely broken on any module that contains at least one single-line (empty-body) function. These are common in compiled C/C++ WASM binaries (stubs, thunks, no-op wrappers). The underlying parser bug also silently corrupts function header lists for all other strategies, potentially modifying the wrong function without raising errors.Reproduce
Input module (
repro.wat):(to simplify tests i am using CLI)
The generated WAT contains two malformed lines:
On larger modules (e.g. Emscripten-compiled C++ with many stub functions) the error cascades into "undefined variable" errors for tables, globals, and functions that are actually defined in the module, making the output completely unusable.
Root cause
1. Parser doesn't finalize single-line functions (
wasmParser/parser.py)parseWast()tracks open/close parentheses viafunc_bracketNum. When a(func ...)line is matched, the counter is updated:For a single-line function like
(func $stub (type $t0) (param $p0 i32)), the brackets balance immediately (3 open, 3 close), sofunc_bracketNumdrops to0on the same line. However, the finalization logic (extracting params, result, locals, header, body) only runs inside theif 1 not in whichSectionblock, which is not entered on the(func ...)line itself (becausewhichSection[2] = 1).As a result:
header=None,body=NonefuncHeaderArearemainsTruefuncHeaderLinestill contains the unfinished function's lineWhen the next function is matched, its header line is appended to the same
funcHeaderLinelist. When that next function eventually finalizes, itsheadercontains both its own line and the previous single-line function's line.2.
opaque_predicate_insertionmodifies the wrong header lineThe strategy adds
(local $wadelocal i32)to every function by modifyingheader[0]:Since
header[0]is actually the absorbed single-line function's line (not the current function's), the result is:The
))closes the(funcS-expression, and(local $wadelocal i32)ends up at module scope -- invalid WAT.