-
Notifications
You must be signed in to change notification settings - Fork 205
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
[RFC] The Static
wrapper is dead, long live safe &'static mut
references!
#59
Comments
Now I am confused. Didn't you tell me that |
What? No. fn foo() -> &'static mut u32 {
static mut FOO: u32 = 0;
unsafe { &mut FOO }
}
let a: &'static mut u32 = foo();
let b: &'static mut u32 = foo();
// BAD `a` and `b` point to the same thing
assert_ne!(a as *const _ as usize, b as *const _ as usize); But if you have a mechanism to guarantee the uniqueness of the |
I neither see how your suggestion automagically fixes the possibility of mutable aliasing nor how that would break Rusts memory model (since you can only initialise them with known types and |
@therealprof Under this proposal, mutable references are only handed out by RTFM to the init function (uniqueness is enforced by the (I've only skimmed this proposal, but it looks safe - and a safe and simple abstraction over DMA is totally awesome!) |
There's no transmute happening in the example brought up by @japaric and the transmute chapter does not handle aliasing at all as far as I can see, that would be https://doc.rust-lang.org/nomicon/aliasing.html . As soon as you want to use shared memory or hand off the pointer for the use of DMA you're accepting the fact that you (or the hardware) is aliasing memory anyway. And for a lot of applications that is just okay. |
@therealprof You might be right about the safety of aliasing. Certainly, as it stands, Rust defers the UB decision to LLVM, which allows aliasing But giving the user aliasing |
Mr Japaric
Nice writeup…
Best,
Per
|
@jonas-schievink A lot of languages implemented on LLVM do allow aliasing pointers so there shouldn't be any problem there. My point re: aliasing was that aliasing (whether it happens in software an/or hardware is not relevant) is not necessarily a bad thing and sometimes quite expected. Anyhow, I'm very much in favour of this proposal despite the confusion around the badness of aliased mut statics... |
@therealprof Other languages do not set Here is a classical example of UB: https://godbolt.org/g/LrFMjL |
@pftbest Fully understood that you can do really bad stuff with aliased pointers. |
This is just a trivial example, the real code may be much more subtle. And it's not always about what you write, similar code may be generated after some optimization steps. The whole idea of Rust is to make it impossible to trigger UB in a safe code. No matter how bad the code is, if it's in safe rust it should either not compile, or panic at runtime. If you can trigger UB in a safe code that means your unsafe blocks are designed incorrectly. |
@japaric Thanks for tagging me. I've looked over your proposal, and everything looks good to me, with two caveats:
I'm also a bit unclear on how this fits into the larger context. Specifically, how does this proposal relate to rust-embedded/embedded-hal#14? That proposal mentions It seems to me that |
This proposal is for RTFM. I think, though, that Except that I just thought of a way to safely create // this macro could be simplified: for instance `$ident` is kind of useless
macro_rules! alloc {
(static $ident:ident: $ty:ty = $expr:expr) => {
cortex_m::interrupt::free(unsafe {
static mut USED: bool = false;
if USED {
None
} else {
static mut $ident: $ty = $expr;
let e: &'static mut $ty = &mut $ident;
USED = true;
Some(e)
}
})
}
}
let a = alloc!(static BUFFER: [u8; 16] = [0; 16]).unwrap();
let b = alloc!(static BUFFER: [u8; 16] = [0; 16]).unwrap();
// OK `a` and `b` are not aliases -- they are pointing to different `static` variables
assert_ne!(a.as_ptr(), b.as_ptr());
fn alias() -> &'static mut [u8; 16] {
alloc!(static BUFFER: [u8; 16] = [0; 16]).unwrap()
}
let c = alias();
// this will panic! if it didn't it would create an alias to the `BUFFER` variable in `alias`
let d = alias();
I think the only user of |
@pftbest We're fully on the same page here and I'm absolutely not suggesting this to be used in any user facing crate. |
@japaric Thanks for your reply. Interesting proposal. I don't have a firm opinion on the matter right now. Integrating DMA into one of my projects is on my todo list though, so I assume I'll have more to say then. |
Since this has received positive feedback and no objections I'm going to rubber stamp the RFC and land the open PRs. I'll send another PR to cortex-m to discuss a checked version of this that works without RTFM. |
safe `&'static mut` references via init.resources see RFC #59 for details
See rust-embedded/cortex-m#70. Feedback on the macro syntax is welcome! |
safe `&'static mut` references via init.resources see RFC #59 for details
safe `&'static mut` references through a runtime checked macro runtime checked implementation of japaric/cortex-m-rtfm#59 that doesn't depend on RTFM macros TODO - [ ] bikeshed macro syntax
History: The
Static
wrapperIf you have been using RTFM
claim
s you probably have noticed this "pattern":Here you need a double dereference because
claim
returns a&mut Static<T>
, instead of a plainmutable reference (
&mut T
).Static<T>
is a newtype overT
thatDeref
s toT
. You normallywon't notice the
Static
wrapper when using methods because of theDeref
implementation, but thewrapper becomes apparent when you need to assign some new value to a resource.
DMA transfers
So, why is
Static
being used here? The main reason is that I needed some (zero cost) abstractionto make DMA transfers memory safe. You can't build a safe DMA API on top of plain (non-static)
references. See below:
At first glance, this looks like an OK DMA API. While the DMA transfer is writing to the buffer you
can't access the buffer (
on_the_stack
is "frozen" by the outstanding borrow). TheTransfer
valuerepresents the on-going transfer and upon destruction (when
drop
ped) it waits for the transfer tofinish. You can use the
wait
method to make the wait operation more explicit.However, this API is not safe because you can safely
mem::forget
theTransfer
value to gainaccess to the buffer while the transfer is on-going:
This doesn't look too dangerous but it's a violation of Rust aliasing model and will result in UB.
In the last line two mutable references to
on_the_stack
exist: one is being used in the indexingoperation, and the other is owned by the DMA (external hardware).
It gets much worse though because this
mem::forget
hole can be used to corrupt stack memory:Here
foo
starts a DMA transfer that modifies some stack allocation but then immediately returns,releasing the stack allocation. Next
bar
starts while the DMA is still on going; the problem isthat the DMA transfer will write into the stack potentially overwriting
bar
's local variables andcausing undefined behavior.
How does
Static
help?We can prevent the memory corruption by having the API only accept references that point into memory
that will never be de-allocated. And that's what the
Static
wrapper represents:&Static<T>
is areference into a statically allocated (i.e. stored in a
static
variable) value of typeT
. Withthis change the API would look like this:
(Note that this change is not enough to prevent the aliasing problem caused by
mem::forget
.Discussing a solution for that issue is out of scope for this RFC though. The
RefCell
-likeBuffer
abstraction in the blue-pill crate prevents themem::forget
aliasing problem showcasedabove but it still has other issues like
mem::swap
aliasing and that you can e.g. still useSerial
while the transfer is in progress)A value can't be safely wrapped in
Static
but RTFM does that for you in every claim and thatlets you use the memory safer DMA API from above.
Changing
buf
's type to&'static mut
would also have worked but there's no way to safely create&'static mut
references, or rather there wasn't a way before this RFC.Motivation
Being able to safely create
&'static mut
references.Why?
&'static mut
references have interesting properties that I think will enable the creation ofnovel APIs:
&'static mut T
has move semantics. See below:&'static mut T
has'static
lifetime (gasp!) so, unlike its non-static cousin, it can be storedin a
static
variable and thus we can have a resource that protects a&'static mut T
.&'static mut T
is pointer sized. If you need to send (transfer ownership) of a buffer from onetask (execution context) to another then it's cheaper to send
&'static mut [u8; 256]
than to send[u8; 256]
even though they are both semantically a move.So
&'static mut T
is a bit likeBox<T>
: both have move semantics and arepointer sized but the former doesn't need dynamic memory allocation (it's statically allocated!) and
we know that
T
's destructor will never run ('static lifetime).Use case: memory safe DMA transfer
We can revise the DMA API to make it truly memory safe:
Here if you
mem::forget
the transfer then you can't never accessserial
or thebuf
fer again,which may seem overkill but fulfills the memory safety requirement.
Use case: SPSC ring buffer
This use case prompted the original RFC (cf. #47). Basically a ring buffer queue can be split into
one producer end point and one consumer end point. Each end point can locklessly enqueue or dequeue
items into / from the same queue -- even if the end points are in different execution contexts (e.g.
threads or interrupts).
The API for this already exists in the
heapless
crate but theConsumer
andProducer
endpoints have a lifetime parameter that matches the lifetime of the ring buffer queue. To put these
end points in resources the lifetime would have to be
'static
and that requires a&'static mut RingBuffer
, which can't be safely created without this RFC.Detailed design
init.resources
We add a
resources
field toapp.init
. The value of this field is a list of resources, previouslydeclared in
app.resources
, thatinit
will own for the rest of the program. The resources inthis list will appear under the
init::Resources
struct as&'static mut
references. Example:Some constraints apply to
init
owned resources:These resources must have an initial value; i.e. they can't be "late" resources.
Resources assigned to
init
can't appear inidle.resources
or intasks.$T.resources
.Basically, the resources are owned by
init
so they can't be shared with other tasks, or withidle
.These constraints will be enforced by the
app!
macro. An error will be thrown before expansion ifany constraint is violated.
Drop the
Static
wrapperSince this RFC brings proper support for
&'static mut
references to the table I think theStatic
wrapper is no longer useful -- memory safe DMA APIs can be built without it and that was its main
reason for existing.
This will be implementing by changing all
&[mut] Static<T>
to&[mut] T
. This means you will nolonger need to doubly dereference to assign a new value to a resource.
Downsides
This is a breaking change, but we are breaking things due to #50 so it's not much of a problem.
Alternatives
A
pre_init
functionA
pre_init
function with signaturefn() -> T
could be run beforeinit
. The value returned bythis function would be passed as
&'static mut T
toinit
. Unlike the main proposal this valuewould be created at runtime so const eval limitations would not apply; also the value would be
allocated in the stack (in the first frame, which will never be deallocated), not in
.bss
/.data
.With code it would look like this:
I think it may make sense to also support this because it potentially lets you use a different
memory region -- think of the case of microcontrollers with two RAM regions the stack could be on
one region and .bss / .data could be on the other. However, if we get better support for multiple
memory regions in
cortex-m-rt
and support for placing resources in custom linker sections incortex-m-rtfm
then there is no longer a need for this, I think, because then you can place aninit
owned resource in any memory region (in RAM, e.g..bss1
, or in core-coupled RAM,.bss2
).I'm not too concerned about the const eval limitation that affects the main proposal because, in my
limited experience, the
T
in the&'static mut T
references one creates is usually an array ([T; N]
) or a thin abstraction over uninitialized arrays (e.g.heapless::RingBuffer
).Implementation
See #58
cc @pftbest @jonas-schievink @hannobraun
The text was updated successfully, but these errors were encountered: