Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Nope. I would encourage you to actually read what unsafe does, because nowhere in the Rust docs does it say "scopes bugs to the unsafe block"

See the below code. The unsafe code is doing exactly what it's supposed to. The safe code frees a value while it's being used. This compiles. There's nothing to change here in the unsafe code.

```rust

  use std::slice;
  
  // Unsafe block that creates a slice from raw parts
  fn make_slice(ptr: *const i32, len: usize) -> &'static [i32] {
      unsafe {
          slice::from_raw_parts(ptr, len)
      }
  }
  
  fn main() {
      let vec = vec![1, 2, 3, 4, 5];
      let ptr = vec.as_ptr();
      let len = vec.len();
      
      // Unsafe block creates the slice - this operation itself is fine
      let slice_ref = make_slice(ptr, len);
      
      // Safe code: drop the vec
      drop(vec);
      
      // Safe code: use the slice - CRASH/UB
      println!("{:?}", slice_ref);
  }
```


The unsafe code is not fulfilling the contract of "unsafe", which requires the coder to uphold the memory safety rules as seen from outside. make_slice is putting a very wrong lifetime on the slice it returns.


My whole comment here is that safe code has to care about how it uses data from unsafe code. That's pretty evident from my example and from reading from_raw_parts. How would you fix this by only editing the code inside the unsafe block? Because you said the problem is the lifetime. That's outside of the unsafe block.

> The unsafe code is not fulfilling the contract of "unsafe", which requires the coder to uphold the memory safety rules as seen from outside

It's the exact opposite. From the Rust docs. [1]

> By calling an unsafe function within an unsafe block, we’re saying that we’ve read this function’s documentation and we take responsibility for upholding the function’s contracts

The caller here is what is violating the contract. It's not the unsafe code's job to do that. How could it? It's just making a slice, it doesn't know what from. It can't guarantee a slice it makes is ANY kind of lifetime. If it could, it wouldn't require unsafe.

We don't have to use a static lifetime to have this bug:

  // still broken
  // basically says it has SOME kind of lifetime
  fn make_slice<'a>(ptr: *const i32, len: usize) -> &'a [i32] {
Lifetime annotations are just annotations for the compiler, but they mean very little when using unsafe. I wrote a bad lifetime to illustrate that Rust doesn't care. Normally, the compiler would make sure your lifetimes make sense, but not with unsafe. Because it's the calling code that has to care.

Yes, if you change the function signature and require it to receive the vec so you can see the lifetime, you could technically fix it from this function. That's the only way to get the right lifetime.

  fn make_slice<'a>(ptr: *const i32, len: usize, _lifetime: &'a Vec<i32>) -> &'a [i32] {
So yes, as long as you change your function so much you can basically remove unsafe, you can fix it from the function.

But you can't do this in scenarios where you'd actually want unsafe, like FFI, custom data structures, etc. You don't have the same lifetime information available to you.

[1] https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#call...


> The caller here is what is violating the contract.

"The caller" of the unsafe function is the unsafe block in make_slice. And yes it's violating the contract.

> How would you fix this by only editing the code inside the unsafe block? Because you said the problem is the lifetime. That's outside of the unsafe block.

If it's locked into returning a static lifetime, then the only fix is to return a slice into a static allocation.

Is this the functionality you wanted? Probably not. But an unsafe block having an awkward API isn't an excuse to violate safety. Sometimes the only answer is to say no. Calling from_raw_parts this way is a clear bug.

> How could it? It's just making a slice, it doesn't know what from.

It could know more if you added more to the unsafe block outside the call to from_raw_parts.

> It can't guarantee a slice it makes is ANY kind of lifetime. If it could, it wouldn't require unsafe.

> as long as you change your function so much you can basically remove unsafe, you can fix it from the function.

from_raw_parts exists for situations where the coder can guarantee that the lifetime is valid (no matter what safe code does), but the compiler can't guarantee it (because it's not smart enough).

It's not there for situations where you make the safe code promise to behave.

It does have a lot of uses, in places that do need unsafe. You just happened to make an example that doesn't need it.


It's not a bug until main uses it incorrectly.

The lifetime is outside of the unsafe block. I can change it from static and we still have the same bug.


> It's not a bug until main uses it incorrectly.

It's always a bug. The lifetime is wrong.

The mere ability to "use it incorrectly" and break memory safety is evidence of a bug. Safe rust in a program without unsafe can't use anything incorrectly in that way. Safe rust in a program with appropriately careful unsafe can't use anything incorrectly in that way.

> The lifetime is outside of the unsafe block.

The unsafe block uses the types from the surrounding function, so you can't change those types without double checking the unsafe block is still valid.

> I can change it from static and we still have the same bug.

Change it to something correct and have the same bug?

Because your continued examples look like no. If you make the lifetime arbitrary then it's still messed up, and if you get the lifetime off the vec then the bug is fixed.


Forget lifetimes, do this. It's a bug. It's in the safe code. You fix it in the safe code, because it misused unsafe code.

  let vec: Vec<i32> = vec![1, 2, 3, 4, 5];
  let ptr = vec.as_ptr();
  let len = vec.len();
  
  let slice_ref = unsafe {slice::from_raw_parts(ptr, len)};
  
  drop(vec);
  
  println!("{:?}", slice_ref);


Okay I ended up replying to this example over here: https://news.ycombinator.com/item?id=46051496


> How would you fix this by only editing the code inside the unsafe block?

In this case, the solution is to mark make_slice as unsafe. This should make sense if you think about it for a second, because it's a thin wrapper around an unsafe function. If it were possible to make a function which did what std::slice::from_raw_parts does safe, it wouldn't be marked as unsafe.

> It's the exact opposite. From the Rust docs.

The docs you quote say the exact opposite of what you claim. Where do you do call the unsafe function in an unsafe block? In make_slice, not main.

> So yes, as long as you change your function so much you can basically remove unsafe, you can fix it from the function.

The actual use case for std::slice::from_raw_parts is for when you control the pointer and length and thus can guarantee that they're valid (specifically, by encapsulating them and making sure no safe functions can get them into an invalid state). For example, std::Vec::as_slice uses it [1] to get a slice from the pointer to it's managed buffer and it's stored length.

> FFI

There are two possible ways FFI could cause memory safety violations despite being called from safe code:

- The safe wrapper makes incorrect assumptions about the contract of the functions it's calling (e.g. maybe it assumes that a c function returns a pointer which it's the caller's responsibility to free, but is actually freed by the c code). In this case, the bug is in the unsafe rust. - The foreign code violates it's contract (e.g. same as above, but the documentation of the c code claims otherwise). In that case, the bug is in the foreign code.

Either way, the memory safety bug is not in safe rust.

[1] https://github.com/rust-lang/rust/blob/7934bbdf84a6b9a30297c...


> The actual use case for std::slice::from_raw_parts is for when you control the pointer and length and thus can guarantee that they're valid (specifically, by encapsulating them and making sure no safe functions can get them into an invalid state)

Right...so you fix it from the safe code, because the safe code is in charge of using from_raw_parts correctly.

> Either way, the memory safety bug is not in safe rust

This might just be a difference of semantics between us, but I really don't see how you can arrive at that conclusion. Just remove the function entirely and let's use from_raw_parts directly. The bug is literally not in unsafe, it's when we drop the vec after making a slice from it. To fix this, we would not change anything in unsafe.

  let vec: Vec<i32> = vec![1, 2, 3, 4, 5];
  let ptr = vec.as_ptr();
  let len = vec.len();
  
  let slice_ref = unsafe {slice::from_raw_parts(ptr, len)};
  
  drop(vec);
  
  println!("{:?}", slice_ref);


> Right...so you fix it from the safe code, because the safe code is in charge of using from_raw_parts correctly.

No, you fix it from the unsafe code (just not your unsafe code, since the API you're trying to expose is not possible to make safe). You encapsulate the arguments to from_raw_parts, provide a safe interface, and only manipulate the encapsulated data inside other unsafe blocks.

> This might just be a difference of semantics between us, but I really don't see how you can arrive at that conclusion. Just remove the function entirely and let's use from_raw_parts directly. The bug is literally not in unsafe, it's when we drop the vec after making a slice from it.

I think you're correct that semantics are at the core of our disagreement. Specifically, there are three different possible ways to answer the question of where a bug is:

1. Where the bug manifests a problem (produces an incorrect value, segfaults, etc).

2. Where the bug is directly caused.

3. Where the incorrect code is.

In the code you provided, 1 happens at the println, 2 happens at the drop, but 3 happens in the unsafe block. All your safe code is correct according to rust's memory safety rules rules (which is why it would pass the borrow checker), the problem is that by using an unsafe block you promised to uphold those rules yourself, and have failed to do so.


> Right...so you fix it from the safe code, because the safe code is in charge of using from_raw_parts correctly.

Safe code cannot corrupt the Vec variables to make Vec do something incorrect.

> To fix this, we would not change anything in unsafe.

You're supposed to.

Some coders might implicitly count the rest of the function as trusted code that's part of enforcing the invariants of the unsafe code. So there's an implied "unsafe" block that fills up the rest of the function, as far as responsibility goes.

I won't argue one way or the other on that philosophy, but if anything outside the function with the unsafe block is part of enforcing invariants, then that is clearly wrong.

If you subscribe to that philosophy, then you could say the drop is the problem in this single-function case. But in the original the bug is still inside make_slice, because make_slice is unleashing a fragile and uncontrolled slice into the safe world of main.

(There might be some more subtleties when it comes to class API boundaries but I don't want to dig through that right now, it's not relevant to the important part of this conversation.)


>> To fix this, we would not change anything in unsafe.

> You're supposed to.

To me, it just sounds like we're disagreeing on definitions. You can view it as unsafe corrupting the vec, I don't disagree with that, but it's the way the safe code coordinated the unsafe code that caused that problem.

I'm happy to be shown a different way. Would you kindly show me how you would fix this in the unsafe block only?

> Some coders might implicitly count the rest of the function as trusted code that's part of enforcing the invariants of the unsafe code. So there's an implied "unsafe" that fills up the rest of the function, as far as responsibility goes

That's essentially what I'm arguing.

> but if anything outside the function with the unsafe block is part of enforcing invariants, then that is clearly wrong

What is clearly wrong? I'm not saying none of it is wrong, I'm just saying unsafe allows you to make bugs outside of it. And yes, it can happen at arbitrary depth, not just in the function containing unsafe code.


> Would you kindly show me how you would fix this in the unsafe block only?

I'd probably hold a reference to the vector or something.

> That's essentially what I'm arguing.

The thing is it's only the safe code in the same function as the unsafe block that you could call responsible in any reasonable sense.

In the original version the main() code cannot be blamed for dropping a vector 'early', because it didn't invoke this scheme that requires the vector to be kept alive, make_slice did. It was make_slice's responsibility to guarantee the underlying allocation stays alive, and it didn't do that.

> What is clearly wrong? I'm not saying none of it is wrong, I'm just saying unsafe allows you to make bugs outside of it. And yes, it can happen at arbitrary depth, not just in the function containing unsafe code.

If you're putting responsibility on code in the same function, you're not really treating it as safe code. There's an unsafe{} around the entire function body that you should have written if you were going for maximum clarity.

If the depth goes any higher, you screwed up. It should be impossible for safe code outside the function to use your code and cause a memory error. If it's not impossible then that's a bug in the unsafe blocks (or the function with the unsafe blocks).


> I'd probably hold a reference to the vector or something

In a way that only changes the unsafe block?

> make_slice's responsibility to guarantee the underlying allocation stays alive, and it didn't do that

We çan talk about the simpler code example. How can my unsafe block make sure an allocation stays alive past the unsafe block itself?

> If you're putting responsibility on code in the same function, you're not really treating it as safe code. There's an unsafe{} around the entire function body that you should have written if you were going for maximum clarity

I'm not talking about what should've been done, I'm saying this is something to be aware of on projects with multiple people who may not catch something like this. People think bugs cannot happen in safe code. I just showed it happening. The bug was not in unsafe, or in the drop, it was in println.


> How can my unsafe block make sure an allocation stays alive past the unsafe block itself?

Put it in another data structure or something?

Listen, if you can't make the invariant work, then you need to change the function. An unfixable unsafe is not an excuse to allow errors

> I'm saying this is something to be aware of on projects with multiple people who may not catch something like this.

It's good to be aware but put the blame in the right place. It's the unsafe code that's actually at fault. If you are seeing corruption, look at the unsafe code first with an adversarial mindset.

> The bug was not in unsafe, or in the drop, it was in println.

N. O.

Any unsafe code that outsources invariant enforcement that affects memory safety to safe code has bugs. There's wiggle room on "outsources" but that's the only wiggle room.


> It's good to be aware but put the blame in the right place. It's the unsafe code that's actually at fault. If you are seeing corruption, look at the unsafe code first with an adversarial mindset.

Yeah, that's why my first comment in this thread was "Just having unsafe in your codebase means changing code outside the unsafe block could cause UB". You can caveat that with "that's bad code", but that's the reality.

> An unfixable unsafe is not an excuse to allow errors

Nobody is saying that, you're just moving goalposts now. Nobody is saying you should allow errors, I'm telling you that everyone, including you, will inevitably have to change the code around the unsafe block, because that's what has to enforce memory at the end of the day.

You said you don't need to look at the safe code, I'm asking how would you fix the unsafe, and you haven't. That's fine, and I'm not faulting the language for it.

>> The bug was not in unsafe, or in the drop, it was in println.

> N. O.

Of course it is. That line becomes instructions that access memory freed by the previous line, drop(vec). That's called a dangling pointer. You remove println and the slice is now dropped immediately. You remove drop and the println will work. The vector is not "corrupted" by unsafe. That's not how computers work. We just lose guarantees from Rust when using unsafe, including in safe code. Doesn't mean there's a bug. That's the whole point of unsafe, is to be trusted by the compiler.

> Any unsafe code that outsources invariant enforcement that affects memory safety to safe code has bugs.

All useful unsafe code outsources invariants to safe code. If we could verify the integrity of the memory, we wouldn't be using unsafe.

Now you have been interchangeably using unsafe to mean the literal blocks and the surrounding code. But here is my point: If you are saying that "unsafe code" means "just the unsafe blocks," then yes, unsafe fundamentally relies on safe code to do the right thing.

But if "unsafe code" means "everything that must uphold the invariant," then unsafe can span your entire codebase. Which is true. Just the presence of unsafe in your code base means you're looking at UB anywhere in the call stack if people don't pay attention. And that's been my whole point of this thread. The presence of unsafe means everyone now has to pay attention not just to the safe block, but all safe code interacting with that data, especially in multi-threaded scenarios.


As I said here[0], although I can't speak for what @Dylan16807 intends, invariants required by unsafe code are required exactly to the extent that some code can alter the invariants (the module boundary). In this sense, Rust's unsafe is just a particular example of encapsulation, where all notions of invariants in programming have the same essence.

[0] https://news.ycombinator.com/item?id=46030407


> Yeah, that's why my first comment in this thread was "Just having unsafe in your codebase means changing code outside the unsafe block could cause UB". You can caveat that with "that's bad code", but that's the reality.

I agreed with you that changing safe code could trigger the bug, but the safe code is not where the bug is.

> Nobody is saying that, you're just moving goalposts now. Nobody is saying you should allow errors, I'm telling you that everyone, including you, will inevitably have to change the code around the unsafe block, because that's what has to enforce memory at the end of the day.

When fixing a vulnerable unsafe block, you might have to redesign the unsafe API, and that might require changing some safe code.

Once you decide on an API, you will not have to change safe code. The unsafe code handles all of the enforcement. If safe code is enforcing anything, you broke the rules of unsafe.

> You said you don't need to look at the safe code, I'm asking how would you fix the unsafe, and you haven't. That's fine, and I'm not faulting the language for it.

I'm not an expert at rust. I gave a couple prose suggestions but they require redesigning the way the safe and unsafe code talk to each other. Because your original design is inherently flawed. The unsafe code cannot protect itself, so it must not be used this way. You're saying we should make the safe code protect the unsafe code, and that is not right. Unsafe code needs to protect itself.

> Of course it is. That line becomes instructions that access memory freed by the previous line, drop(vec). That's called a dangling pointer. You remove println and the slice is now dropped immediately. You remove drop and the println will work. The vector is not "corrupted" by unsafe. That's not how computers work. We just lose guarantees from Rust when using unsafe, including in safe code. Doesn't mean there's a bug. That's the whole point of unsafe, is to be trusted by the compiler.

"unsafe" means "trust me compiler, I verified this myself"

Losing the guarantee is a bug. You told the compiler it didn't need to prevent a dangling pointer via the unsafe block, that you would prevent a dangling pointer via the unsafe block, and then you didn't prevent it.

If you didn't tell the compiler to trust you, the part that wouldn't have compiled is the unsafe block. You tricked it into compiling that block, so that block is where the bug is.

> All useful unsafe code outsources invariants to safe code.

That's extremely untrue. Lots of data structures protect all their invariants in their unsafe code.

> If we could verify the integrity of the memory, we wouldn't be using unsafe.

"we" are smarter than the compiler. Unsafe is for things "we" can verify but the compiler cannot. You're not supposed to use it for unverified stuff.

> Now you have been interchangeably using unsafe to mean the literal blocks and the surrounding code. But here is my point: If you are saying that "unsafe code" means "just the unsafe blocks," then yes, unsafe fundamentally relies on safe code to do the right thing.

If you're doing things 100% properly, you will expand your unsafe blocks to include everything that verifies and upholds invariants. But even after that expansion, it's still going to be a tiny fraction of your codebase.

> But if "unsafe code" means "everything that must uphold the invariant," then unsafe can span your entire codebase.

Not if your design is competent.

> Just the presence of unsafe in your code base means you're looking at UB anywhere in the call stack if people don't pay attention. And that's been my whole point of this thread.

"if people don't pay attention" is a huge factor here. If your unsafe code is wrong then that makes it hard to write safe code. But if you go fix the unsafe code then you stop needing to worry about safe code triggering a memory error.

> The presence of unsafe means everyone now has to pay attention not just to the safe block, but all safe code interacting with that data, especially in multi-threaded scenarios.

If you did things correctly, any safe function can be ignored for memory safety. Unsafe blocks are supposed to assume that the safe code calling them is actively malicious, and make themselves impossible to misuse.


See my comment to @Capricorn2481 here[0]. You seem to be saying that unsafe code can be made correct so as to ensure soundness. This can be done by verifying invariants in unsafe code, but it is generally discouraged, as it tempts large unsafe blocks that could be partially safe code. Both you and @Capricorn2481 don't seem to make the distinction between "arbitrary safe code" and "my safe code within the module with unsafe", which is a crucial part of the idea of writing encapsulated unsafe. Technically, I believe unsafe code can still be made sound in the presence of arbitrary safe code, but it would have to tiptoe around violating memory safety, lowering its utility and performance substantially, if not completely.

[0] https://news.ycombinator.com/item?id=46057382


I accept that criticism. It's just that it makes things more complicated and harder to argue about (and I couldn't remember the exact boundaries). I was making the distinction earlier but dropped it for the sake of explaining the point of safe/unsafe.

So the categories of code: the unsafe block, the friend code that shares responsibility for invariants, the outside world code

Capricorn2481's mistake is putting the entire program in the second category, when only code in the same module is supposed to be there.

If a memory violation happens, you know you have a bug in a module containing unsafe blocks, which helps a lot. Those modules should be relatively rare and as small as possible.


I think one can argue make_slice is in fact at fault here since it's unsound (i.e., it doesn't enforce the requirements of from_raw_parts() and so allows safe code to invoke UB) and therefore either should be marked unsafe or should actually enforce the requirements of from_raw_parts().

That being said, I think in this context that's more of a nitpick since you're right in stating that bugs that result in UB need not be scoped strictly to unsafe blocks.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: