japaric/f3

remove aliased mutable borrows

pftbest opened this issue · 10 comments

I have this crazy idea and maybe you can tell me if I'm mistaken. My point is: why do we need to have mutable reference to volatile register to modify it? As we are talking about hardware registers as the name of the crate implies, the value behind it should only be accessed via volatile operations, so from the end user perspective there is no difference if the value was changed by the hardware or concurrently via another shared reference (for some hardware the value in register can be changed by simply reading the register). This requirement to have mutable reference does not give any guarantees or safety in any way.
So I propose to lift this restriction in volatile register crate and to use only immutable shared references to access the register structs. This will simplify the code considerably and remove this unsafe dance with aliased mutable borrows.

I have this crazy idea

You are not crazy. Zinc did this (R/W registers modifiable via a &- reference).

maybe you can tell me if I'm mistaken

I'm afraid that I can't because I'm not 100% sure myself.

the value was changed by the hardware

Indeed! The hardware can change the value of a register that you are looking at via a &- reference at any time. That's what makes these while !peripheral.status_register.read().some_bit() {} lines so bizarre.


So my problem with making everything modifiable via &- is that things like these become totally safe:

static SERIAL: SerialPort = /* .. */

fn main() -> ! {
    loop {
        // "Blocks"
        SERIAL.write_all(b"The quick brown fox jumps over the lazy dog.");
    }
}

// Interrupt handler that kicks every N micro/milli -seconds
fn _tim7() {
    SERIAL.write_u8(b'X');
}

Even the implementation of SerialPort would contain zero unsafe!

At best this will produce this console output: ThXe XquXicX.... At worst the _tim7 interrupt handler may kick off when the registers are in some "inconsistent state" (probably won't be the case in this simple example but more complicated stuff will probably run into something like that).

And I'd really really like the compiler stopping me from doing this with a compiler error. It should even tell me that I need some synchronization here: "hey, this is not Sync!".

So, yeah. I don't know. I prefer to be conservative here. If I make any change, I'd probably even make the volatile reads unsafe as well because the hardware may change the value of registers at any time. 😄

I'm OK with marking read() and write() as unsafe in volatile register, I am not OK with having this nvic() and nvic_mut() methods that look a bit ugly.

I am not OK with having this nvic() and nvic_mut() methods that look a bit ugly.

What would be the alternative? a static mut NVIC? I don't like static variables because you can't do static mut NVIC: Nvic = unsafe { mem::transmute(0xdeadbeef) };. Instead, you have to declare those addresses in linker scripts. And integration between Cargo and linker scripts is poor.

No I'm not talking about static, I am thinking about single nvic() method that would give &- reference that would allow unsafe writes to its registers.

So my proposal is:

  • Mark read write modify methods in volatile_register as unsafe.
  • Allow write and modify on immutable reference
  • Remove all _mut() functions.

The reason I don't like &mut because it implies some kind of ownership, but we should have none.
For example nothing will prevent me from writing this code:

pub struct NVIC {
  cr1: RW<u32>,
  reserved: u32,
  cr2: RO<u32>,
}

fn main() {
    unsafe {
        let n = nvic_mut();
        n.reserved = 10;
        n.cr2 = RO{register: 0}
    }
}

So my proposal is:

I'm not 100% convinced. It seems to me that this would result in even more unsafe blocks.

The reason I don't like &mut because it implies some kind of ownership

But that's the point! Then you can write:

fn main() -> ! {
    // ...
}

// interrupt handler
fn _tim7() {
    // I solemnly swear that this is the only `&mut -` reference to this peripheral
    // thus I can modify its contents as I see fit without worrying about data races
    let tim7 = unsafe { peripheral::tim7_mut() };

    // ...
}

And it's easy to find where I may be aliasing the register. (grep "unsafe.*_mut(").

Whereas with this:

// interrupts disabled during the execution of this function
fn init() {
    let tim7 = peripheral::tim7();

    // racy? (spoilers: no)
    let arr = unsafe { tim7.arr.read() };

    // ...

    // racy? (spoilers: no)
    let arr = unsafe { tim7.arr.read() };

    // ...
}

fn main() -> ! {
    init();
    enable_interrupts();

    let tim7 = peripheral::tim7();

    loop {
        // racy? (spoilers: yes)
        let arr = unsafe { tim7.arr.read() };
    }
}

fn _tim7() {
    // would have been `tim7_mut()` in the other model
    let tim7 = peripheral::tim7();

    // racy? (spoilers: yes)
    unsafe { tim7.arr.write(number); }
}

NOTE: arr can't be modified by the hardware

Then it's not clear which tim7 may be the source of aliasing.

For example nothing will prevent me from writing this code:

Actually, privacy prevents you from writing that. Except for n.reserved = ... but only if you are in the module where NVIC was defined which it's not usually the case.

// racy? (spoilers: yes)
let arr = unsafe { tim7.arr.read() };

Well, the reads will never be racy because MMIO is not cached. So the only dangerous thing is read-modify-write aliased by another write. (Unless your register changes state on read operation, but in this case the current implementation is also unsafe). So you can grep on arr.write( to find all your race conditions.

It seems to me that this would result in even more unsafe blocks.

Well, maybe having unsafe on read is a bit too much.
Also having unsafe on each write is indeed more unsafe blocks than only one unsafe block on borrow, but I still think that the later case is not right, because you are allowing UB to escape it's unsafe prison.

let tim7 = unsafe {peripheral::tim7_mut()}; // <-- unsafe block is here

// ... 100 lines of code later

tim7.arr.write(number); // <-- Undefined Behaviour is here, 
                        // but this line looks like normal safe code

I was thinking a little about this example

static SERIAL: SerialPort = /* .. */

fn main() -> ! {
    loop {
        // "Blocks"
        SERIAL.write_all(b"The quick brown fox jumps over the lazy dog.");
    }
}

// Interrupt handler that kicks every N micro/milli -seconds
fn _tim7() {
    SERIAL.write_u8(b'X');
}

and I'm not sure if it is even possible to prevent this kind of bugs in compile time. Certainly not on the register level.
At run time we can try to implement some reference counters (like RwLock) for nvic() and nvic_mut() but this would not work either because sometimes it is necessary to concurrently write to the different registers of the same peripheral. Hypothetical example: main thread updates dma.src in a loop while some interrupt updates dma.isr.

I'm not sure if it is even possible to prevent this kind of bugs in compile time

It's certainly possible, at least in theory ... So, first you make SerialPort: !Sync that makes it impossible to put SerialPort in a static variable. Then you create a Mutex<T>: Sync that "locks the processor" by temporarily disabling the interrupts. Then you can write:

// static SERIAL: SerialPort = /* ... */;
//^ error: SerialPort doesn't implement `Sync`

static SERIAL: Mutex<SerialPort> = /* ... */;
// OK Mutex: Sync

fn main() -> ! {
    loop {
        // "Uninterruptible"
        SERIAL.lock().write_all(b"The quick brown fox jumps over the lazy dog.");
    }
}

// Interrupt handler that kicks every N micro/milli -seconds
// BUT this can't interrupt the SERIAL.lock() line in `main`
fn _tim7() {
    SERIAL.lock().write_u8(b'X');
}

With this program you always get a deterministic output:

The quick brown fox jumps over the lazy dog.XThe quick brown fox jumps over the lazy dog.X

Not that this program makes much sense but it gets synchronization right.

Here's an implementation of the Mutex used in the above example. (Here's another example that uses this Mutex)

Certainly not on the register level.

Certainly not but the registers should tell you that there's something wrong going on (through unsafe)

Since we now have much better ways to avoid data races and undefined behavior when accessing registers, this issue is no longer relevant.