Skip to content

Commit 6a8a7a9

Browse files
IRusclaude
andauthored
Add missing TCK-8 suspension tests and exnref EXEC-5 coverage (#13)
- withTimeout around deep guest recursion (fib) poisons the store - cancellation while parked in WaitingForFuel, Paused, and InHostImport - cancellation bypasses guest try_table catch_all handlers - try_table catch_all cannot catch host exceptions, mirroring the legacy-EH test for the standardized instruction set Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent fcef4f4 commit 6a8a7a9

2 files changed

Lines changed: 197 additions & 0 deletions

File tree

wasm-core/test/wasm/core/GcEhExecutionTest.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,43 @@ class GcEhExecutionTest {
354354
assertEquals(hostFailure, escaped)
355355
}
356356

357+
@Test
358+
fun tryTableCatchAllCannotCatchHostExceptions(): Unit = runBlocking {
359+
val hostFailure = IllegalStateException("host bug")
360+
val module = validatedModule {
361+
types += FuncType(emptyList(), emptyList())
362+
imports += Import("host", "fail", ImportDesc.Function(0))
363+
functions += Function(
364+
0,
365+
emptyList(),
366+
listOf(
367+
TryTable(
368+
blockType = BlockType.Empty,
369+
catches = listOf(CatchClause.All(depth = 0, withReference = false)),
370+
body = listOf(Call(0)),
371+
),
372+
),
373+
)
374+
exports += Export("run", ExportDesc.Function(1))
375+
}
376+
val instance = Instance(
377+
Store(),
378+
module,
379+
ResolvedImports(
380+
functions = listOf(
381+
HostImport(FuncType(emptyList(), emptyList())) {
382+
throw hostFailure
383+
},
384+
),
385+
),
386+
)
387+
388+
val escaped = assertFailsWith<IllegalStateException> {
389+
instance.invoke("run")
390+
}
391+
assertEquals(hostFailure, escaped)
392+
}
393+
357394
private fun exceptionModule(body: (Int) -> Instr): Module = validatedModule {
358395
types += FuncType(listOf(ValType.I32), emptyList())
359396
types += FuncType(emptyList(), listOf(ValType.I32))

wasm-core/test/wasm/core/SuspensionSemanticsTest.kt

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@ package io.heapy.kwasm
22

33
import io.heapy.kwasm.Instr.Br
44
import io.heapy.kwasm.Instr.Call
5+
import io.heapy.kwasm.Instr.FcIndex
6+
import io.heapy.kwasm.Instr.I32Const
7+
import io.heapy.kwasm.Instr.If
58
import io.heapy.kwasm.Instr.Loop
69
import io.heapy.kwasm.Instr.Nop
10+
import io.heapy.kwasm.Instr.Simple
11+
import io.heapy.kwasm.Instr.TryTable
712
import kotlinx.coroutines.CancellationException
13+
import kotlinx.coroutines.CompletableDeferred
814
import kotlinx.coroutines.CoroutineStart
915
import kotlinx.coroutines.Deferred
16+
import kotlinx.coroutines.TimeoutCancellationException
1017
import kotlinx.coroutines.async
1118
import kotlinx.coroutines.coroutineScope
19+
import kotlinx.coroutines.flow.first
1220
import kotlinx.coroutines.runBlocking
21+
import kotlinx.coroutines.withTimeout
1322
import kotlin.test.Test
1423
import kotlin.test.assertEquals
1524
import kotlin.test.assertFailsWith
@@ -52,6 +61,116 @@ class SuspensionSemanticsTest {
5261
)
5362
}
5463

64+
@Test
65+
fun withTimeoutCancelsDeepRecursionAndPoisonsTheStore(): Unit = runBlocking {
66+
val store = Store()
67+
val instance = Instance(store, fibModule(), ResolvedImports())
68+
69+
assertFailsWith<TimeoutCancellationException> {
70+
withTimeout(50) { instance.invoke("fib", listOf(Value.I32(40))) }
71+
}
72+
assertTrue(store.poisoned, "deep-recursion cancellation must poison the store")
73+
assertEquals(StoreStatus.Poisoned, store.status.value)
74+
assertFailsWith<PoisonedStoreException> {
75+
instance.invoke("fib", listOf(Value.I32(1)))
76+
}
77+
}
78+
79+
@Test
80+
fun cancellationWhileParkedWaitingForFuelPoisonsTheStore(): Unit = runBlocking {
81+
val store = Store(
82+
StoreConfig(
83+
fuelEnabled = true,
84+
initialFuel = 0,
85+
fuelExhaustionPolicy = FuelExhaustionPolicy.Suspend,
86+
),
87+
)
88+
val instance = Instance(store, constantModule(), ResolvedImports())
89+
90+
val invocation = async { instance.invoke("value") }
91+
store.status.first { it == StoreStatus.WaitingForFuel }
92+
invocation.cancel(CancellationException("cancelled while waiting for fuel"))
93+
94+
assertFailsWith<CancellationException> { invocation.await() }
95+
assertTrue(store.poisoned, "fuel-parked cancellation must poison the store")
96+
assertEquals(StoreStatus.Poisoned, store.status.value)
97+
assertFailsWith<PoisonedStoreException> { instance.invoke("value") }
98+
}
99+
100+
@Test
101+
fun cancellationWhileExplicitlyPausedPoisonsTheStore(): Unit = runBlocking {
102+
val store = Store()
103+
val instance = Instance(store, constantModule(), ResolvedImports())
104+
val pause = store.requestPause()
105+
val invocation = async { instance.invoke("value") }
106+
107+
pause.awaitPaused()
108+
assertEquals(StoreStatus.Paused, store.status.value)
109+
invocation.cancel(CancellationException("cancelled while paused"))
110+
111+
assertFailsWith<CancellationException> { invocation.await() }
112+
assertTrue(store.poisoned, "pause-parked cancellation must poison the store")
113+
assertEquals(StoreStatus.Poisoned, store.status.value)
114+
assertFailsWith<PoisonedStoreException> { instance.invoke("value") }
115+
}
116+
117+
@Test
118+
fun cancellationWhileParkedInHostImportPoisonsTheStore(): Unit = runBlocking {
119+
val gate = CompletableDeferred<Int>()
120+
val type = FuncType(emptyList(), listOf(ValType.I32))
121+
val store = Store()
122+
val instance = Instance(
123+
store,
124+
importedFunctionModule(type),
125+
ResolvedImports(
126+
functions = listOf(
127+
HostImport(type) {
128+
listOf(Value.I32(gate.await()))
129+
},
130+
),
131+
),
132+
)
133+
134+
val invocation = async { instance.invoke("host") }
135+
store.status.first { it == StoreStatus.InHostImport }
136+
invocation.cancel(CancellationException("cancelled inside a suspended host import"))
137+
138+
assertFailsWith<CancellationException> { invocation.await() }
139+
assertTrue(
140+
store.poisoned,
141+
"cancellation inside a parked host import must poison the store",
142+
)
143+
assertEquals(StoreStatus.Poisoned, store.status.value)
144+
assertFailsWith<PoisonedStoreException> { instance.invoke("host") }
145+
}
146+
147+
@Test
148+
fun cancellationBypassesGuestCatchAllHandlers(): Unit = runBlocking {
149+
val module = validatedModule {
150+
types += FuncType(emptyList(), emptyList())
151+
functions += Function(
152+
0,
153+
emptyList(),
154+
listOf(
155+
TryTable(
156+
blockType = BlockType.Empty,
157+
catches = listOf(CatchClause.All(depth = 0, withReference = false)),
158+
body = listOf(Loop(BlockType.Empty, listOf(Br(0)))),
159+
),
160+
),
161+
)
162+
exports += Export("spin", ExportDesc.Function(0))
163+
}
164+
val store = Store(StoreConfig(checkpointInterval = 1))
165+
val instance = Instance(store, module, ResolvedImports())
166+
167+
assertFailsWith<TimeoutCancellationException> {
168+
withTimeout(50) { instance.invoke("spin") }
169+
}
170+
assertTrue(store.poisoned, "cancellation must bypass guest catch_all handlers")
171+
assertEquals(StoreStatus.Poisoned, store.status.value)
172+
}
173+
55174
private suspend fun assertCancellationAtNextCheckpoint(
56175
checkpointClass: String,
57176
checkpointInterval: Int,
@@ -98,6 +217,47 @@ class SuspensionSemanticsTest {
98217
assertEquals(StoreStatus.Poisoned, store.status.value)
99218
}
100219

220+
private fun fibModule(): Module = validatedModule {
221+
types += FuncType(listOf(ValType.I32), listOf(ValType.I32))
222+
functions += Function(
223+
typeIndex = 0,
224+
locals = emptyList(),
225+
body = listOf(
226+
FcIndex(0x20, 0),
227+
I32Const(2),
228+
Simple(0x48),
229+
If(
230+
BlockType.Single(ValType.I32),
231+
thenBody = listOf(FcIndex(0x20, 0)),
232+
elseBody = listOf(
233+
FcIndex(0x20, 0),
234+
I32Const(1),
235+
Simple(0x6B),
236+
Call(0),
237+
FcIndex(0x20, 0),
238+
I32Const(2),
239+
Simple(0x6B),
240+
Call(0),
241+
Simple(0x6A),
242+
),
243+
),
244+
),
245+
)
246+
exports += Export("fib", ExportDesc.Function(0))
247+
}
248+
249+
private fun constantModule(): Module = validatedModule {
250+
types += FuncType(emptyList(), listOf(ValType.I32))
251+
functions += Function(0, emptyList(), listOf(I32Const(7)))
252+
exports += Export("value", ExportDesc.Function(0))
253+
}
254+
255+
private fun importedFunctionModule(type: FuncType): Module = validatedModule {
256+
types += type
257+
imports += Import("host", "function", ImportDesc.Function(0))
258+
exports += Export("host", ExportDesc.Function(0))
259+
}
260+
101261
private fun validatedModule(configure: ModuleBuilder.() -> Unit): Module =
102262
ModuleBuilder()
103263
.apply(configure)

0 commit comments

Comments
 (0)