-
Notifications
You must be signed in to change notification settings - Fork 5
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
Concern about soundness of placement of stack memory #6
Comments
Thanks for commenting. I greatly appreciate people taking the time to poke holes
I recall reading a GH (?) discussion about this pattern (manufacturing static
This function is not memory safe in the presence of unwinding. Here's a If you use The other change that may make this function safe is to wrap the
I'm sure this will not be the last time we see this. :-)
All unsafely implemented abstractions have assumptions attached to them, even if My approach to safety / soundness is to spent time finding and documenting these
You are entirely correct. I'm a practical person :-). What I want is the user to
(Sorry, I don't have time look into this one at the moment)
At the moment I'm not overly concerned about people making static references out
|
Indeed, I had forgotten about that as our formal model does not have unwinding. That is easy to fix though (without depending on libstd): struct LoopGuard;
impl Drop for LoopGuard {
fn drop(&mut self) {
loop {}
}
}
pub fn make_static<T: 'static>(x: &T, f: impl FnOnce(&'static T)) {
// Casting away the lifetime is okay because we know there is
// a loop there that makes sure the reference remains valid forever.
// The loop will happen even if we unwind.
let _guard = LoopGuard;
f(unsafe { &*(x as *const T)});
} (I'll come back to the rest of your comment later.) |
Sure. But as much as possible we should try to have one coherent framework that encompasses all of them. This is necessary to maintain basic compositionality -- the alternative is to give up on safety or to have an ecosystem split. Both do not seem like acceptable options to me.
I don't see that. I see nothing about And conversely, I am not aware and I don't think crate authors are aware that safety in To be fair,
And in each case we'll have to find some way to resolve this conflict, or we have to start to build tooling such that cargo can reject compiling programs that link together incompatible libraries. In the other cases that I am aware of (
I can see the need for excluding pointers-to-the-stack. But I am afraid using lifetimes is the wrong tool for this job. This sound more like it would need more refined reference/pointer types that carry something like an "address space" (which is a pattern that also comes up in other contexts).
I don't follow. My point is not that people will get an error about a |
Isn't this basically "relying on destructors running for memory safety" which I Your updated function assumes that the only way anything thread-like can be // instead of std::Mutex because you can construct it in "const context"
use parking_lot::Mutex;
static X: Mutex<i32> = Mutex::new(0);
my_thread::spawn(|| {
let x = X.lock();
let y = Box::new(0);
let z = false;
mk_static(&z, |z: &'static bool| {
// omitted: send `z` to some other thread
// terminates *only* this thread (`exit` syscall instead of `exit_group`)
my_thread::exit();
// `x` is leaked - OK
// `y` is leaked - OK
// `LoopGuard` is leaked - unsound
});
}) This is basically a Alternatively, I can write a I still do not consider your updated function to be sound. If you made it depend More fundamentally, though, to me
There's a human factor.
Perhaps that'd be best but it doesn't exist and I'm targeting today's stable. |
No, this is actually inherently relied on by several libraries to ensure exception safety (e.g.
For this reason, the discussion about setjmp/longjmp also had to take into account that using longjmp to skip over stack frames that have drop glues is in general incorrect. Similar concerns were raised in discussions on the forums. (FWIW, I am a bit surprised that so many people seem to have the incorrect impression that you cannot rely on RAII. Seems like we need a concerted effort to raise awareness that
That abstraction is unsafe and violating guarantees relied upon by a large amount of libraries. And that's not just because of the drop stuff I mentioned before. For example, imagine fn main() {
let m = Mutex::new(0);
rayon::join(
|| my_thread::exit(), // deallocates m because the first closure runs on the main thread
|| m.lock().unwrap(), // *oops* accesses dangling memroy
);
}
I am not aware of any specification for Rust. ;) More seriously though, when you use |
You can only ping rust-lang/teams in repositories of the rust-lang org.. I don't know why this is the case, but this is how it is =/
I've seen users confuse "destructors are not guaranteed to run" (e.g. because you can plug off the computer and prevent them from running, because leaking is safe, etc.) with "you can never rely on destructors running". This was the case, for example, in this issue (see that comment by @nikomatsakis and the following ones), where it wasn't considered bad that some destructors were not running because "we don't guarantee that". While it is true that we don't guarantee that all destructors always run, for each point in a program's execution, we do guarantee that certain destructors have always finished running. Because we do guarantee that, unsafe code can rely on this guarantee, and code that breaks this guarantee is, therefore, not safe (you can only break the guarantee with unsafe code, so that makes any safe abstraction over such unsafe code unsound). Why do people get this idea? I don't know, maybe because "destructors are not guaranteed to run" is repeated more often than what exactly is guaranteed. Remembering this catch phrase is enough to get by, and easier than remembering the rules. Also, many languages don't have destructors, but finalizers, and you can't really rely on any finalizer having run at any point in the execution of a program. So those who extrapole from finalizers to destructors might assume that the same thing applies there. |
I do agree though that I don't know of any place where we positively state when drop is guaranteed to be run, and it seems prudent to do so. I opened rust-lang/nomicon#135 for that. |
Here's another case of what I claim is a safe abstraction that relies on drop glue of a guard to run. |
@japaric I read your post at https://blog.japaric.io/microamp/ with great interest. Most of that embedded stuff is very foreign to me, and I continue to be amazed by your ways to provide safe abstractions in this world. :)
That said, I have a concern about some of the things you describe there. Specifically:
I am not convinced that the latter is true -- that you will never see references to stack variables if you restrict yourself to
T: 'static
.The first, maybe silly example is the following function:
I think this function is safe in the sense that it doesn't break the type system and could conceivably be provided by a library. I even have a formal proof that this is compatible with our formal model of the type system.
But of course if a μAMP program uses this function, it could communicate a stack reference to another core, and suddenly the pointer is using the wrong address space.
Now this may sound somewhat artificial, but that doesn't make it less of a problem, at least in principle. We have two unsafely implemented abstractions (
make_static
and μAMP) which seem fine in isolation, but go bad when used together, and it is not clear "who is right". (Also from a formal perspective, I have no clear idea what is wrong with the function above that would make a correctness proof not go through -- you are using'static
in a way that means much more than just "a lifetime that goes on forever".)I also am not sure if this example is the only problem: with intrusive collections, we can have elements in a collection that live shorter than the collection. This is certainly challenging to get right with concurrency in mind, but I see no fundamental reason why we couldn't have an intrusive concurrent collection where thread A can add elements (that will automatically remove themselves when the stack gets popped) and thread B can access them while they are in there.
So, generally, the rule you are postulating that
T: 'static
cannot refer to any thread's stack, is not something that necessarily has to be true, it would be a new piece in Rust's safety contract and I think it is in conflict with some legitimate uses (intrusive collections in particular, where the entire point is that you can add elements to a collection without showing that the elements outlive the collection).I am curious about your thoughts on this. :)
The text was updated successfully, but these errors were encountered: