If I understood it correctly, it was that the compiler suite shipped with newlib, which have or can have printf supporting re-entrant use (several threads can call printf in async manner), or not supporting that case (typically sync usage, blocking across I/O). The re-entrant ones use malloc/free for, iiuc, a per-thread buffer.
In many cases when you have smaller systems, you actually don't want your OS to do dynamic memory allocation, since it opens up for all kinds of bugs related to eg double-free, or use-after-free, or running out of heap, or too defragmented heap.
It's also easier to calculate memory usage (and thus be fairly certain it will be enough) if you allocate as much of the memory as possible as static memory. Hence why some coding standards such as MISRA-C disallows use of dynamic memory allocation.
Exactly what caused the lock-up here isn't delved deeper into, but it might be that there was not enough heap, or not a working malloc-implementation linked into the compiled binary (could be a stub), or could be that a non-re-entrant printf was linked in while they tried printf from several threads.
The Pico SDK has a plugin based stdio, they give you two implementations: UART and USB CDC, but you can add new ones by implementing a few callback functions. The UART plugin provided with the SDK can be disabled and replaced, the default implementation is very simplistic and will block.
The USB CDC version is much faster but may not always be appropriate for your project.
I implemented one that uses a buffer and an interrupt to feed the UART in the background, and then I disabled the plugin the Pico SDK provided. I'm not using an RTOS.
> Printf isn't re-entrant, and they are calling it from multiple threads.
This! Simple schedulers generally only allow system calls (such as printf) from the main thread. If you really want to 'print' from a child thread then send a message to the main thread, asking that it prints the message contents on on behalf of the child thread.
You will need your printf to be written with FreeRTOS in mind: it's often not re-entrant at all, let alone actually blocking on IO instead of busy-waiting.