Kotlin coroutines are actually a hybrid between stackful and stackless. For every function invocation there is a regular Java stack frame. When you invoke a suspend fun from a suspend fun, the JVM stack grows the usual way, and if the call returns without any suspension happening, the stack unwinds also in the common JVM way.
Things become different when a function suspends. At that point, the Java methods return and the JVM stack unwinds. However, while the call chain was being built up, another, on-heap structure was being formed: a linked list of Continuation objects. Every suspend fun invocation creates another such object, which you can think of as a stack frame (it contains the values of all the local variables), but implemented at the bytecode level, as a regular Java object.
This Continuation chain is the "stackless" aspect of Kotlin coroutines. When you resume a continuation, you'll enter the innermost function call (containing the location where the function suspended). When that function wants to return, it won't return the normal way, instead it will resume its caller's continuation. This will repeat in the caller, and so as you travel up the suspend fun call stack, you'll be also traveling down the JVM call stack: the JVM stack grows as the suspendable functions return.