Skip to content
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

Add debug asserts to some unsafe operations #51713

Closed
newpavlov opened this issue Jun 22, 2018 · 16 comments
Closed

Add debug asserts to some unsafe operations #51713

newpavlov opened this issue Jun 22, 2018 · 16 comments
Labels
T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@newpavlov
Copy link
Contributor

newpavlov commented Jun 22, 2018

The following functions have restrictions which (ideally) should be respected:

It would be nice to check these restrictions with debug asserts. The main blockers:

  • stdlib is distributed with disabled debug assertions, so either implementation will have to use the same hack as in wrapping checks, or we'll have to distribute two versions of stdlib with enabled and disabled debug assertions, and teach cargo to switch between them depending on a compilation profile.
  • Some code in the stdlib (and probably in some external crates) consciously breaks those restrictions (e.g. src/liballoc/vec.rs). Probably it should be rewritten in a more "correct" fashion.

See internals thread for additional discussion.

@gnzlbg
Copy link
Contributor

gnzlbg commented Jun 22, 2018

or we'll have to distribute two versions of stdlib with enabled and disabled debug assertions

I think we would need at least three stdlib versions for this: debug+debug_assert, release+debug_assert, and release. Depending on how you look at this, we might also need to ship a debug version without debug assertions, so that would mean we need to ship four versions.

This gets out of hand quickly. Given M options with 2 values, we end up with 2^M stdlibs that we have to ship. These are some of the options (from the top of my head) that we might want to ship:

  • opt=debug/release
  • debug-assert=true/false
  • asan=true/false
  • msan=true/false
  • tsan=true/false
  • lsan=true/false

which makes it 2^6 = 64 different stdlibs that we might want to ship... and the list is probably woefully incomplete... (e.g. target-feature=sse2 / sse4.2 / avx2 / ...).

Honestly, we need something better. In my opinion, the options that a user specifies for its crate should apply to the whole dependency graph, including the stdlib which should just be compiled as a normal rust crate. So ideally, instead of shipping any stdlib binaries, we would just ship the stdlib sources. If the std lib takes too long to build for some users, we can just refactor it into smaller crates.

@andrewhickman
Copy link
Contributor

andrewhickman commented Jun 26, 2018

I disagree that code that, for example, calls get_unchecked with an index out of range is necessarily incorrect. Half of the use case for these functions is when the invariant ("this memory is actually valid") is enforced in a different way. This change would only make sense for the other use case (performance). Encouraging people to go through raw pointer methods in the name of safety feels counterproductive, since the method call is far more readable.

This absolutely makes sense for unreachable_unchecked since the invariant can be checked with no 'false positives'

@newpavlov
Copy link
Contributor Author

newpavlov commented Jun 26, 2018

The idea is to explicitly construct slice which will cover valid memory region before indexing it, e.g. Vec code can have an unsafe method get_cap_slice:

// method is unsafe because slice can contain uninitialized data
unsafe fn get_cap_slice(&self) -> &[T] {
    slice::from_raw_parts(self.buf.ptr, self.buf.cap)
}

Yes, this change will require more steps for some use-cases, but arguably it's worth it, as many consider get_unchecked as an escape hatch for additional performance and use it accordingly without any tools at hand to check if used assumptions are indeed correct.

@PaulGrandperrin
Copy link

PaulGrandperrin commented Dec 18, 2018

Hi, I just want to say that I came across the same issue:
PaulGrandperrin/playground@b37152b

and I was thinking, wouldn't it be possible to do something like that for unreachable_unchecked in the std?:

#[macro_export]
macro_rules! unreachable_unchecked {
    () => ({
        #[cfg(debug_assertions)]
        panic!("internal error: entered unreachable code")
        #[cfg(not(debug_assertions))]
        std::hint::unreachable_unchecked()
    });
    ($msg:expr) => ({
        unreachable_unchecked!("{}", $msg)
    });
    ($msg:expr,) => ({
        unreachable_unchecked!($msg)
    });
    ($fmt:expr, $($arg:tt)*) => ({
        #[cfg(debug_assertions)]
        panic!(concat!("internal error: entered unreachable code: ", $fmt), $($arg)*)
        #[cfg(not(debug_assertions))]
        std::hint::unreachable_unchecked()
    });
}

As a macro, it would be expanded at crate compilation time and so would respect the debug_assertion flag.

(this code is inspired by the unreachable! macro source code in the std: https://doc.rust-lang.org/src/core/macros.rs.html#485-498)

@sfackler
Copy link
Member

As a macro, it would be expanded at crate compilation time and so would respect the debug_assertion flag.

"crate compilation time" is the time that you're compiling std, not the downstream crate.

@jonas-schievink jonas-schievink added the T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. label Mar 23, 2019
@RalfJung
Copy link
Member

Cc #53871 #54915

@RalfJung RalfJung changed the title Add debug asserts to some unsafe functions Add debug asserts to some unsafe operations Aug 11, 2020
@RalfJung
Copy link
Member

It seems like the checks we already have already cause a very notable slowdown when building rustc with debug assertions enabled. We mitigated this a bit by making some checks abort instead of panic on failure (avoiding unwind paths), but this still still something to be aware of.

It might be a good idea to have a separate cfg flag for checks that would run a lot, like the ones we already have in copy/copy_nonoverlapping/write (and potentially more) or the ones that this issue proposes.

@newpavlov
Copy link
Contributor Author

@RalfJung
Considering that debug asserts are usually enabled only for debug builds with low optimization level, shouldn't this slowdown be acceptable?

@RalfJung
Copy link
Member

It is quite common for compiler contributors to enable debug assertions in the compiler (this helps catch many bugs). This also enables debug assertions in libstd which makes the generated compiler a lot slower.

With #72146 we now have the option of only enabling debug assertions in rustc and not libstd, but contributors still have to know to do that. And the more people do this, the less coverage we have for these debug assertions...

@saethlin
Copy link
Member

saethlin commented Jan 8, 2022

I think we now have answers to the two main blockers. Just commenting here because I haven't seen these answers mentioned.

stdlib is distributed with disabled debug assertions...

These assertions would still be useful for developers, because Cargo has -Zbuild-std. You have to know to turn this on which is less than awesome, but IMO that's not a huge leap because developers of unsafe code should be using other opt-in tools such as ASan, Miri, and fuzzers.

Perhaps if there is any concern left here, it is that just adding -Zbuild-std has a surprising effect, and users may keep asking for the checks to be defaulted?

Some code in the stdlib (and probably in some external crates) consciously breaks those restrictions (e.g. src/liballoc/vec.rs). Probably it should be rewritten in a more "correct" fashion.

core and alloc are unlikely to trip any suggested assertion on their own, I'm pretty sure their test suites now pass Miri's Stacked Borrows checker. std is perhaps a different matter, but I didn't see any suggestions for assertions in that crate. I have absolutely encountered third-party crates that expect to be able to index past the end of a slice with get_unchecked, and that has been clearly documented to be UB for 3 years now. Btw, it's unclear to me if the examples I've seen are people consciously breaking the rules.

@saethlin
Copy link
Member

saethlin commented Jan 8, 2022

Maybe I deserve this for commenting so quickly after learning about this issue, but it seems that while std/alloc do not rely on indexing out of bounds, the compiler does.

bors added a commit to rust-lang-ci/rust that referenced this issue Apr 3, 2022
Add debug assertions to some unsafe functions

As suggested by rust-lang#51713

~~Some similar code calls `abort()` instead of `panic!()` but aborting doesn't work in a `const fn`, and the intrinsic for doing dispatch based on whether execution is in a const is unstable.~~

This picked up some invalid uses of `get_unchecked` in the compiler, and fixes them.

I can confirm that they do in fact pick up invalid uses of `get_unchecked` in the wild, though the user experience is less-than-awesome:
```
     Running unittests (target/x86_64-unknown-linux-gnu/debug/deps/rle_decode_fast-04b7918da2001b50)

running 6 tests
error: test failed, to rerun pass '--lib'

Caused by:
  process didn't exit successfully: `/home/ben/rle-decode-helper/target/x86_64-unknown-linux-gnu/debug/deps/rle_decode_fast-04b7918da2001b50` (signal: 4, SIGILL: illegal instruction)
```

~~As best I can tell these changes produce a 6% regression in the runtime of `./x.py test` when `[rust] debug = true` is set.~~
Latest commit (rust-lang@6894d55) brings the additional overhead from this PR down to 0.5%, while also adding a few more assertions. I think this actually covers all the places in `core` that it is reasonable to check for safety requirements at runtime.

Thoughts?
@RalfJung
Copy link
Member

FWIW, with https://github.com/RalfJung/cargo-careful it becomes fairly easy for people to run their code with a stdlib that has debug assertions, so these assertions should be much easier to apply to real-world code now. :)

@pitaj
Copy link
Contributor

pitaj commented Dec 1, 2022

So it looks like a lot of debug assertions were added in #92686. Looking through various cases, it appears that the following from the list above are covered by some form of debug assertion:

  • slice::get_unchecked(_mut)
  • unreachable_unchecked
  • unwrap_unchecked
  • new_unchecked

Some notable missing cases I found:

  • str::get_unchecked(_mut)
  • Arc/Rc::get_mut_unchecked
  • from_utf8_unchecked
  • slice::split_at_unchecked (but split_at_mut_unchecked has a debug assertion)

I've opened #105117 to address these.

Also there are a lot of number-related unchecked operations that's aren't covered:

  • unchecked math operations
  • to_int_unchecked
  • forward/backward_unchecked
  • unchecked SIMD-related operations

But I'm not sure if adding debug assertions for those would be welcomed.

@saethlin
Copy link
Member

But I'm not sure if adding debug assertions for those would be welcomed.

My not-libs opinion is that we should have some way to make all of these checked. Debug assertions might not be the best switch for these, but keeping them all behind assert_unsafe_precondition! makes it possible to swap out for some custom --cfg or something else.

And just in case this is what you're worried about: I do not think runtime overhead is a good argument against adding more of these. We started with things like copy_nonoverlapping and slice::get_unchecked where the overhead in runtime or compile time is highest.

bors added a commit to rust-lang-ci/rust that referenced this issue Mar 5, 2023
Add more debug assertions to unsafe functions

related to rust-lang#51713
@saethlin saethlin added T-libs Relevant to the library team, which will review and decide on the PR/issue. and removed T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Feb 9, 2024
@newpavlov
Copy link
Contributor Author

I think this issue can be closed in favor of #120848?

@RalfJung
Copy link
Member

RalfJung commented May 3, 2024

Yes, that list is much more up-to-date than the one here.

@RalfJung RalfJung closed this as completed May 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

9 participants