Skip to content

CONFIG_SKIP_BOOT_MSG makes newlib stdio races more likely #20067

Description

@mchesser

After spending some time analyzing this issue, I believe that the actual cause is already known -- i.e., #4488 / #8619-comment-569952641 (and is actually documented in the release notes).

Feel free to close as a duplicate, but I thought I would leave the analysis below regarding the impact of CONFIG_SKIP_BOOT_MSG.


Description

The reentrant stdio functions in newlib initialize some shared data (via the function __sinit) the first time an IO function, e.g., puts, is used. Normally, before main, RIOT prints a message using puts (i.e., "This is RIOT" + version), which ends up calling __sinit before user code is executed, avoiding most of the issues with initialization.

However if CONFIG_SKIP_BOOT_MSG is set, the __sinit will not be called until the first print statement. If two threads attempt to print at a roughly similar time, then it is possible1 for the second thread to execute with a partially initialized reent object which causes various crashes depending on how much of the structure has been initialized.

1. Platforms that do not perform any locking as part of _lock_acquire (see: #8619-comment-569952641).

As an example, if we have code configured like this:

USEMODULE += ztimer_usec
USEMODULE += sched_round_robin
CFLAGS += -DCONFIG_SKIP_BOOT_MSG
BOARD=nucleo-f446re

That spawns multiple threads that print -- e.g.:

main.c
#include <stdio.h>
#include <stdint.h>

#include "thread.h"
#include "ztimer.h"
#include "timex.h"

uint32_t WORKER1_SPIN_TIME_US = (10 * US_PER_MS) - 30; // Approximate RR scheduling time - 30 usecs.
volatile uint32_t WORKER1_LOOP_SPIN = 5;               // Loop iterations for sub-microsecond adjustment.

void* worker1(void* arg)
{
    (void)arg;

    ztimer_spin(ZTIMER_USEC, WORKER1_SPIN_TIME_US);
    while (WORKER1_LOOP_SPIN != 0) { WORKER1_LOOP_SPIN -= 1; }

    puts("WORKER 1\n");

    while(1) {};
}

void* worker2(void* arg)
{
    (void)arg;
    puts("WORKER 2\n");

    while(1) {};
}

#define WORKER_STACKSIZE (THREAD_STACKSIZE_TINY+THREAD_EXTRA_STACKSIZE_PRINTF)

int main(void)
{
    static char w1_stack[WORKER_STACKSIZE];
    thread_create(w1_stack, sizeof(w1_stack), 7, THREAD_CREATE_STACKTEST, worker1, NULL, "T1");

    static char w2_stack[WORKER_STACKSIZE];
    thread_create(w2_stack, sizeof(w2_stack), 7, THREAD_CREATE_STACKTEST, worker2, NULL, "T2");

    return 0;
}

Then if worker1 is preempted in the middle of stdio initialization, e.g. at:

worker1 at main.c:27
  _puts_r
    __sinit
      __sfp
        __sfmoreglue
          ...
    <TIMER 1 IRQ>
    <PREEMPTION>

Then worker2 may crash when puts is called:

worker2 at main.c:39
  _puts_r
    __swsetup_r+52

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions