-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DMA soundness issue #137
Comments
One way I've seen this being fixed is by taking ownership, like in rp2040-hal. Here, the |
Thanks for bringing this up. I made this decision; I'm hoping to learn more about your concerns so we can adapt this API. But first, here's my thinking. The
By calling In isolation, creating the Notice how the
In the end, we ensure that someone, somewhere, has called an Could you help me understand why the drop guarantee isn't sufficient? I'd love more information on why the rp2040-hal took a different approach. I'm eager to make the change in this HAL if I'm misunderstanding the drop guarantee. |
Let me think about this! This is a lot of information, and there is a good chance that I'm wrong and learn something in the process. |
Based on the discussions I've seen around DMA and embedded Rust, I think my position is unconventional. So there's a greater chance that I need to change my ways. Thanks for considering all this! |
Some generic thoughts about a potential rework:
|
@mciantyre The following code compiles and is undefined behavior: let data: [u32; 5] = [1, 2, 3, 4, 5];
{
let dma_transfer = spi.dma_write(&mut spi_dma, &data).unwrap();
cassette::pin_mut!(dma_transfer);
let mut cm = cassette::Cassette::new(dma_transfer);
cm.poll_on();
core::mem::forget(cm);
}
drop(data); Which means that either in
|
let dma_transfer = spi.dma_write(&mut spi_dma, &data).unwrap(); // 1
cassette::pin_mut!(dma_transfer); // 2 At 1, we construct the There's nuance in By shadowing the let mut cm = cassette::Cassette::new(dma_transfer); // 3
cm.poll_on(); // 4
core::mem::forget(cm); // 5 The call at 3 requires an unpin-able future. We have that in the form of It seems the concern is that 4 starts the DMA transfer, and then 5 prevents To show that ARM assembly demonstrating call to drop for the Write objectf2: f000 f830 bl 0x156 <cassette::Cassette<T>::new::h522c056dbaa80667> @ imm = #96
f6: 9808 ldr r0, [sp, #32]
f8: f000 f856 bl 0x1a8 <cassette::Cassette<T>::poll_on::h59f73d76efb2134b> @ imm = #172
fc: f857 0c3c ldr r0, [r7, #-60]
100: f857 1c38 ldr r1, [r7, #-56]
104: f857 2c34 ldr r2, [r7, #-52]
108: f857 3c30 ldr r3, [r7, #-48]
10c: f847 3c20 str r3, [r7, #-32]
110: f847 2c24 str r2, [r7, #-36]
114: f847 1c28 str r1, [r7, #-40]
118: f847 0c2c str r0, [r7, #-44]
11c: f1a7 002c sub.w r0, r7, #44
120: f000 fa68 bl 0x5f4 <core::mem::forget::h033deddf4eb85593> @ imm = #1232
124: 9809 ldr r0, [sp, #36]
126: f000 fd0e bl 0xb46 <core::ptr::drop_in_place<imxrt_dma::peripheral::Write<imxrt_hal::common::lpspi::Lpspi<imxrt_hal::common::lpspi::Pins<imxrt_iomuxc::Pad<1075806532_u32,1075807028_u32>,imxrt_iomuxc::Pad<10
75806528_u32,1075807024_u32>,imxrt_iomuxc::Pad<1075806536_u32,1075807032_u32>,imxrt_iomuxc::Pad<1075806524_u32,1075807020_u32>>,4_u8>,u32>>::h5a594ae43a19c9e8> @ imm = #2588
12a: 9a0a ldr r2, [sp, #40]
12c: f1a7 001c sub.w r0, r7, #28
130: 4601 mov r1, r0
132: e892 5038 ldm.w r2, {r3, r4, r5, r12, lr}
136: e881 5038 stm.w r1, {r3, r4, r5, r12, lr}
13a: f000 fa5a bl 0x5f2 <core::mem::drop::h9b09f1976801029b> @ imm = #1204
13e: f248 403c movw r0, #33852
142: f2c2 0000 movt r0, #8192
146: f248 4264 movw r2, #33892
14a: f2c2 0200 movt r2, #8192
14e: 2128 movs r1, #40
150: f007 f831 bl 0x71b6 <core::panicking::panic::h7d36b6ac83a18dce> @ imm = #28770
154: defe trap The program shows a This example meets the drop guarantee as of the I don't see undefined behavior. Is there another opportunity for undefined behavior that I'm missing? Footnotes
|
The more I think about it, the more I feel like you might have developed a novel way of implementing DMA drivers on embedded. I'd love an opinion from the author of https://docs.rust-embedded.org/embedonomicon/dma.html. |
My only novel contribution is
We can thank the designers of async Rust for the drop guarantee. Remember that my position is unconventional. I'd like to understand what's changing your thinking, and I'm still seeking answers to my specific questions. Sorry, but I don't know why the drop guarantee is not covered in the Embedonomicon's DMA chapter. From my position, the drop guarantee, and intentional design towards the drop guarantee, can solve the soundness concerns reiterated throughout the chapter. |
Im still not entirely sure that that's actually what the drop guarantee is taking about, but so far I have yet to find an example that states otherwise. |
I'm afraid though that
Note that in order to regain control of a I'm not sure what exactly prevents this from happening in all the examples I've looked at so far; it feels like there's always something enforcing a drop when dealing with futures. I still don't know where this comes from, though. |
Sorry, but I'm missing the connection between the quote from the drop guarantee docs and this observation:
Could you elaborate?
Could you share the examples you're studying? I looking for prompts so that I can dispute my own position. Even if the example looks good, there might be small tweaks that we can come up with to make the example troublesome. |
@mciantyre This is what I'm talking about: use std::{marker::PhantomPinned, pin::Pin};
struct AutoClear<'a> {
val: &'a mut [u8],
_pin: PhantomPinned,
}
impl<'a> AutoClear<'a> {
pub fn new(val: &'a mut [u8]) -> Pin<Box<Self>> {
Box::pin(Self {
val,
_pin: PhantomPinned,
})
}
}
impl Drop for AutoClear<'_> {
fn drop(&mut self) {
println!("Drop!");
self.val.fill(0);
}
}
fn main() {
{
{
let mut data = [1, 2, 3];
{
let _autoclear = AutoClear::new(&mut data);
// Not usable, as expected. The following is a compile time error:
// data[0] = 42;
}
// Prints [0, 0, 0], as expected
println!("{:?}", data);
}
{
let mut data = [1, 2, 3];
{
let autoclear = AutoClear::new(&mut data);
std::mem::forget(autoclear);
// Drop never got called, and yet the lifetime of `&mut data` is free again now.
// It is still bound somewhere, but the fact that `autoclear` is not reachable
// any more is enough for the borrow checker to release `data` again.
// Note that we didn't use `unsafe`, and that autoclear is `!Unpin` and was pinned.
data[0] = 42;
}
// Prints [42, 2, 3], `AutoClear::drop()` never got called. Yet `data` is reachable and modifiable again.
println!("{:?}", data);
}
}
}
|
There aren't any specific ones; I was just trying to recreate the effect shown in the previous message with a I think that So the most obvious way to transfer my code to a |
I see the issue. I agree that a forgotten, pinned, heap-allocated DMA transfer object satisfies the drop guarantee while also performing I/O into likely-invalid stack memory / mutating a You've revealed my implicit assumption: I only considered stack-allocated DMA transfer objects. When we forget an object on the stack, its memory should be considered invalid as late as the end of scope. I figured that's why Although I haven't tried, I think I can produce the troublesome example we're looking for (without an executor). I'll give it a shot this weekend. If you want to keep going with your example, here is a way to create a simple |
@mciantyre Fyi, |
I don't think a lack of shadowing in Here's a contrived example of stack pinning with Does Here's a small example showing the heap-allocated DMA future unsoundness. I only built it successfully; didn't run it. There's no executor. It represents a soundness issue for a user who's polling futures, including an executor. Discussions here and in the example focus on corruption of the stack-allocated buffer. Just noting that the DMA channel object can also be invalidated once the heap-allocated DMA object is forgotten. Any solution that doubles-down on the drop guarantee will also need to consider the channel lifetime. |
No, I think you are right :) |
Let's keep it as-is. I really like the |
Lpspi::dma_write
takes a&mut Channel
and returns aWrite
object whose lifetime is attached to&mut Channel
and the&[u32]
input buffer. This causes the channel to be blocked until the write is finished or cancelled, and the source buffers valid. In theory.There is one detail, however:
core::mem::forget
is notunsafe
because Rust’s safety guarantees do not include a guarantee that destructors will always run. That means, by usingforget
to bypass thedrop()
function ofWrite
, we can drop the input buffer while the DMA is still reading. Thus, through purely safe means we can produce a situation where the DMA reads from uninitialized memory. In the case ofdma_read
(same problem), it is even possible to write into uninitialized memory.This behavior is called unsound.
Is this a deliberate decision of the
hal
maintainers or is this an oversight?The text was updated successfully, but these errors were encountered: