@@ -2,14 +2,23 @@ package io.heapy.kwasm
22
33import io.heapy.kwasm.Instr.Br
44import 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
58import io.heapy.kwasm.Instr.Loop
69import io.heapy.kwasm.Instr.Nop
10+ import io.heapy.kwasm.Instr.Simple
11+ import io.heapy.kwasm.Instr.TryTable
712import kotlinx.coroutines.CancellationException
13+ import kotlinx.coroutines.CompletableDeferred
814import kotlinx.coroutines.CoroutineStart
915import kotlinx.coroutines.Deferred
16+ import kotlinx.coroutines.TimeoutCancellationException
1017import kotlinx.coroutines.async
1118import kotlinx.coroutines.coroutineScope
19+ import kotlinx.coroutines.flow.first
1220import kotlinx.coroutines.runBlocking
21+ import kotlinx.coroutines.withTimeout
1322import kotlin.test.Test
1423import kotlin.test.assertEquals
1524import 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