Skip to content

CONFIG_SKIP_BOOT_MSG makes newlib stdio races more likely #20067

@mchesser

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions