Here's what I think is happening. mbed error handling string printers look like this:
void mbed_error_puts(const char *str)
{
// blah blah
write(STDERR_FILENO, str, 0);
core_util_critical_section_enter();
#if MBED_CONF_PLATFORM_STDIO_CONVERT_NEWLINES || MBED_CONF_PLATFORM_STDIO_CONVERT_TTY_NEWLINES
char stdio_out_prev = '\0';
for (; *str != '\0'; str++) {
if (*str == '\n' && stdio_out_prev != '\r') {
const char cr = '\r';
write(STDERR_FILENO, &cr, 1);
}
write(STDERR_FILENO, str, 1);
stdio_out_prev = *str;
}
#else
write(STDERR_FILENO, str, strlen(str));
#endif
core_util_critical_section_exit();
}
this means the writes are in a critical section. As I understand it that means IRQs are masked via __disable_irq. When the write happens it ends up in usb/serial code which requires a Semaphore (see AsyncOp::wait as an example). Semaphore asserts when it's constructed in a context with IRQs are masked. That assert then calls up and back through the same mbed_error coding and the cycle repeats, resulting in the device 'locking up'
In Release mode MBED_ASSERTs are compiled out.