rtt-target icon indicating copy to clipboard operation
rtt-target copied to clipboard

`rtt_init*` macros are unsound

Open japaric opened this issue 4 years ago • 5 comments

rtt_init!

The rtt_init! macro lets you effectively alias a mutable reference to memory. Example below:

#[entry]
fn main() -> ! {
    let c0 = alias();

    // use `c0`
}

#[exception]
fn SysTick() {
    let c0 = alias();
    // use `c0`
}

fn alias() -> UpChannel {
    let cs = rtt_init! {
        up: {
            0: {
                size: 1024
                mode: NoBlockSkip
            }
        }
    };
    cs.up.0
}

Both c0 variables have mutable access to the same chunk of memory and each variable can be accessed from a different priority level (SysTick can preempt main): this alone is a data race and UB in paper (in practice, you may need to call write to make the code explode)

Suggested fix: either make rtt_init! unsafe to call OR make rtt_init! return Some only once. The latter is the owned singleton pattern (described in its zero-sized reference form in this blog post). Basically you want something like this:

fn once() -> Option<Something> {
    static ONCE: AtomicBool = AtomicBool::new(false);
    
    if ONCE
        .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
        .is_ok() {
        // this branch will run at most *once*
        // ..
        Some(something)
    } else {
        None
    }
}

rtt_init_default!

This is unsound for the same reason rtt_init! is unsound: you should not be able to alias Channels in safe code.

Fix: fixing rtt_init! fixes this -- just don't make rtt_init unsafe to call but rtt_init_default safe to call; both should be unsafe OR both should return an Option

rtt_init_print!

This one also lets you run into data races in safe code. See below:

#[entry]
fn main() -> ! {
    init();

    loop {
        rprintln!("Hello, world!");
    }
}

#[exception]
fn SysTick() {
    init();
}

fn init() {
    rtt_init_print!();
}

Here the exception (SysTick) can preempt rprintln! and re-initialize the channel (overwrite static variables) used by rprintln!. The main issue here is the unsafe code in rtt_init! running more than once and/or running in parallel / concurrently to the execution of rprintln!.

Suggested fix: this macro should panic if called more than once OR should be no-op if called more than once

cc @yatekii

japaric avatar May 12 '20 10:05 japaric

Great catch, thanks for reporting :)

Should be easy enough to fix thanks to your suggestions.

Yatekii avatar May 12 '20 11:05 Yatekii

This one is a bit annoying to fix due to the fact we can't rely on the availability of atomics on all platforms. I suppose I could live with atomics by default and a platform feature flag (is it just me or does like every crate have a "cortex-m" flag for instance?) like "no-atomics" that disables atomics and makes everything unsafe instead.

I'd rather not have a requirement for everybody to use "unsafe".

mvirkkunen avatar May 12 '20 12:05 mvirkkunen

Yeah, this won't work on M0 targets indeed. But I think having a sane default is a good trade-off with a flag to disable it :)

Yatekii avatar May 12 '20 14:05 Yatekii

As an update, Rust now has #[cfg(target_has_atomic = 32)] which can be used to omit such code on those platforms.

xobs avatar Jul 13 '22 08:07 xobs

Somewhat tangential, but using the OnceLock pattern would probably also allow panic-rtt-target to work without first needing to call rtt_init_default, which would be convenient.

tgross35 avatar Mar 06 '23 19:03 tgross35