binaryen icon indicating copy to clipboard operation
binaryen copied to clipboard

edge case bug with `--asyncify`

Open zamfofex opened this issue 3 years ago • 3 comments

Hello! :wave: I’m using wasm-opt --asyncify on a project of mine to allow the use of coroutines from C.

However, I recently ran into what really seems to me like a bug. I don’t know exactly what causes it, but it seems to happen consistently when compiling with -O0, but not with -O1 or higher.

I was able to reduce the reproduction to just a few dozen lines, but now it seems that if I try to make any further simplifications, the bug doesn’t show up.

Expected output: Empty, as there are supposedly no mistakes in the program.

Actual output: RuntimeError: null function or function signature mismatch

main.c
void *xxx_allocate(unsigned int m)
{
	static unsigned char *memory[0x4000];
	static unsigned int n = 0;
	return memory + (n += m) - m;
}

struct xxx_behavior
{
	void (*behave)(void *data);
	void *data;
};

struct xxx_behavior *xxx_javascript_create_coroutine(void (*start)(struct xxx_behavior *behavior, void *data), void *data);

void xxx_javascript_yield(void *data);
void xxx_yield(void *data) { xxx_javascript_yield(data); }

void xxx_javascript_continue(void *data);
void xxx_continue(void *data) { xxx_javascript_continue(data); }

struct xxx_coroutine
{
	struct xxx_behavior *behavior;
	struct xxx_behavior *compound;
	void (*start)(struct xxx_behavior *behavior, struct xxx_behavior *compound);
};

static void xxx_coroutine_start(struct xxx_behavior *behavior, void *data)
{
	struct xxx_coroutine *coroutine = data;
	(*coroutine->start)(behavior, coroutine->compound);
}

static void xxx_coroutine_behave(void *data)
{
	struct xxx_coroutine *coroutine = data;
	(*coroutine->behavior->behave)(coroutine->behavior->data);
	(*coroutine->compound->behave)(coroutine->compound->data);
}

static void xxx_ignore(void *data) { }

static void xxx_behave(void *data)
{
	void (*behavior)(void *data) = data;
	(*behavior)(0);
}

static void xxx_f(struct xxx_behavior *behavior, struct xxx_behavior *compound)
{
	struct xxx_behavior *other = xxx_allocate(sizeof *other);
	other->behave = &xxx_behave;
	other->data = &xxx_ignore;
	compound->data = other;
	for (;;) (*behavior->behave)(behavior->data);
}

static void xxx_compound_behave(void *data)
{
	struct xxx_behavior *behavior = data;
	(*behavior->behave)(behavior->data);
}

struct xxx_behavior *xxx(void)
{
	struct xxx_coroutine *coroutine = xxx_allocate(sizeof *coroutine);
	coroutine->start = &xxx_f;
	
	coroutine->compound = xxx_allocate(sizeof *coroutine->compound);
	coroutine->compound->behave = &xxx_compound_behave;
	
	struct xxx_behavior *behavior = xxx_javascript_create_coroutine(&xxx_coroutine_start, coroutine);
	coroutine->behavior = behavior;
	
	struct xxx_behavior *result = xxx_allocate(sizeof *result);
	result->behave = &xxx_coroutine_behave;
	result->data = coroutine;
	
	return result;
}
index.html
<!doctype html>
<html lang="en">
<meta charset="utf-8">

<title>wasm-opt bug</title>

<script type="module">

let global = v =>
{
	if (typeof v === "number") return v
	return v.value
}

export let run = async () =>
{
	let memcpy = (a, b, n) =>
	{
		new Uint8Array(memory.buffer, a, n).set(new Uint8Array(memory.buffer, b, n))
		return a
	}
	
	let memmove = (a, b, n) =>
	{
		new Uint8Array(memory.buffer, a, n).set(new Uint8Array(memory.buffer, b, n).slice())
		return a
	}
	
	let memset = (a, b, n) =>
	{
		new Uint8Array(memory.buffer, a, n).set(Array(n).fill(b))
		return a
	}
	
	let memcmp = (a, b, n) =>
	{
		a = new Uint8Array(memory.buffer, a, n)
		b = new Uint8Array(memory.buffer, b, n)
		for (let i = 0 ; i < n ; i++)
			if (a[i] !== b[i]) return a[i] - b[i]
		return 0
	}
	
	let coroutineYield = coroutineBuffer =>
	{
		let struct = getView().getUint32(coroutineBuffer + 16, true)
		
		if (asyncify_get_state() === 2)
			asyncify_stop_rewind()
		else
			asyncify_start_unwind(struct)
	}
	
	let coroutineContinue = coroutineBuffer =>
	{
		let start = getView().getUint32(coroutineBuffer + 0, true)
		let data = getView().getUint32(coroutineBuffer + 12, true)
		let struct = getView().getUint32(coroutineBuffer + 16, true)
		let behavior = getView().getUint32(coroutineBuffer + 20, true)
		let state = getView().getUint32(coroutineBuffer + 24, true)
		
		if (state !== 0)
			asyncify_start_rewind(struct),
			table.get(start)()
		else
			getView().setUint32(coroutineBuffer + 24, 1, true),
			table.get(start)(behavior, data)
		
		if (asyncify_get_state() === 1)
			asyncify_stop_unwind()
	}
	
	let createCoroutine = (start, data) =>
	{
		let buffer = xxx_allocate(8192)
		
		let struct = xxx_allocate(8)
		getView().setUint32(struct + 0, buffer, true)
		getView().setUint32(struct + 4, buffer + 8192, true)
		
		let coroutineBuffer = xxx_allocate(32)
		getView().setUint32(coroutineBuffer + 0, start, true)
		getView().setUint32(coroutineBuffer + 8, buffer, true)
		getView().setUint32(coroutineBuffer + 12, data, true)
		getView().setUint32(coroutineBuffer + 16, struct, true)
		getView().setUint32(coroutineBuffer + 24, 0, true)
		
		let behavior = xxx_allocate(8)
		getView().setUint32(behavior + 0, yieldIndex, true)
		getView().setUint32(behavior + 4, coroutineBuffer, true)
		getView().setUint32(coroutineBuffer + 20, behavior, true)
		
		let coroutine = xxx_allocate(8)
		getView().setUint32(coroutine + 0, continueIndex, true)
		getView().setUint32(coroutine + 4, coroutineBuffer, true)
		getView().setUint32(coroutineBuffer + 28, coroutine, true)
		
		return coroutine
	}
	
	let imports =
	{
		env:
		{
		 	memcpy, memmove, memset, memcmp,
			xxx_javascript_create_coroutine: createCoroutine,
			xxx_javascript_yield: coroutineYield,
			xxx_javascript_continue: coroutineContinue,
		}
	}
	
	let body = await fetch("xxx.wasm")
	let exports
	try { exports = (await WebAssembly.instantiateStreaming(body, imports)).instance.exports }
	catch (e) { exports = (await WebAssembly.instantiate(await body.arrayBuffer(), imports)).instance.exports }
	
	let {memory, __indirect_function_table: table} = exports
	let {xxx_allocate} = exports
	let {xxx_yield, xxx_continue} = exports
	let {asyncify_get_state} = exports
	let {asyncify_start_rewind, asyncify_stop_rewind} = exports
	let {asyncify_start_unwind, asyncify_stop_unwind} = exports
	let {xxx: start} = exports
	
	let getView = () => new DataView(memory.buffer)
	
	let f = table.grow(2)
	table.set(f + 0, xxx_yield)
	table.set(f + 1, xxx_continue)
	let yieldIndex = f + 0
	let continueIndex = f + 1
	
	let behavior = start()
	
	let step = async () =>
	{
		let f = table.get(getView().getUint32(behavior, true))(getView().getUint32(behavior + 4, true))
		setTimeout(step, 500)
	}
	step()
}

run()

</script>

I compile it as follows:

clang --target=wasm32 -ffreestanding -nostdlib -Wl,--no-entry \
        -Wl,--export=xxx_yield \
        -Wl,--export=xxx_continue \
        -Wl,--export=xxx_create_coroutine \
        -Wl,--export=xxx_coroutines \
        -Wl,--export=xxx_allocate \
        -Wl,--export=xxx \
        -Wl,--allow-undefined \
        -Wl,--export-table,--growable-table \
        -o plain-xxx.wasm main.c
wasm-opt --asyncify plain-xxx.wasm -o xxx.wasm

Again, compiling with clang -O1 or higher makes the problem go away.

zamfofex avatar Aug 15 '22 04:08 zamfofex

Compiling with optimizations can use fewer locals, and inlining can reduce the number of call frames, both of which can reduce the size of the Asyncify buffer that is needed. Try increasing it, and also you can build with assertions that should check if that is the issue, --pass-arg=asyncify-asserts

kripken avatar Aug 17 '22 22:08 kripken

I tried to bump the buffer size by 1000 times (from 8192 to 8192000 bytes), and the error persisted.

--pass-arg=asyncify-asserts doesn’t change the error in any significant way (with 8192 and also with 8192000 bytes), but it does change the error (to an unreachable error) if I reduce the size to, say, just 64. So I don’t think the buffer size is the issue in this case.

zamfofex avatar Aug 18 '22 00:08 zamfofex

Hmm, sounds like something else than the buffer size then.

Not sure I have a good idea other than to debug this manually. Unfortunately debugging this kind of thing is a little tricky. But I'd recommend adding some manual debug logging perhaps.

kripken avatar Aug 18 '22 20:08 kripken