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 functions #51713

Open
newpavlov opened this issue Jun 22, 2018 · 6 comments

Comments

@newpavlov
Copy link
Contributor

commented Jun 22, 2018

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

  • get_unchecked
  • get_unchecked_mut
  • slice_unchecked
  • slice_mut_unchecked
  • unreachable_unchecked
  • etc. (please comment if you know additional examples)

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member

commented Dec 18, 2018

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.

@RalfJung

This comment has been minimized.

Copy link
Member

commented Apr 21, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.