When it comes to "executing" your program, a runtime (like GHC's runtime) takes your program which is a value and actually performs the requested actions, dealing with mutation, loops, jumps, etc. This is a hack just as much as any other language dealing with variables instead of the stack, heap or registries directly.
In case you're interested, this idea of using some kind of interpreter which loops over your program and performs mutations and side effects is very popular in Scala: libraries like Cats and ZIO do this, allowing you to effectively get Haskell's IO. As far as I know, tail-recursion is not really needed for this, but when implemented on the runtime it allows the end user to write loops using recursion without blowing up the stack.