Skip to content

implement intptrcast model #224

Closed

Description

@oli-obk

Since right now miri's pointer comparison bails out on &x as *const T < &y as *const T but allows &x as *const T == &y as *const T, I suggest to also allow <. This will not impact ctfe-mode, since ctfe mode disallows any binary ops on pointers into different allocs, even ==.

It's deterministic between multiple miri runs, doesn't leak HashMap ordering and only depends on optimizations and codegen from rustc (but that's already true for ==).

Steps to do:

Activity

  1. added
    C-enhancementCategory: a PR with an enhancement or an issue tracking an accepted enhancement
    on Jun 28, 2017
  2. RalfJung commented on Jun 28, 2017

    @RalfJung
    Member

    My plan was to go even further and essentially implement the intptrcast-model. That would immediately make us support all pointer arithmetic.

    Essentially, I think at some point miri should (in non-CTFE mode!) guarantee that safe Rust code can not error out (if the unsafe libraries it uses don't contain bugs). So hashing raw pointers and printing them and stuff like that all have to work.

  3. oli-obk commented on Jun 29, 2017

    @oli-obk
    ContributorAuthor

    Sounds reasonable to me. Once a pointer has been observed and thus it's integral representation is available, there's no way in safe code to go from that pointer back to the allocation, so we don't even need to support that direction, right? All we need to ensure is hat the integral representation doesn't change when obtaining it multiple times from a real pointer

    We'll also need to generate an integer address when offsetting pointers due to overflow concerns. Overflowing exposes the address essentially.

  4. RalfJung commented on Jun 29, 2017

    @RalfJung
    Member

    We'll also need to generate an integer address when offsetting pointers due to overflow concerns. Overflowing exposes the address essentially.

    That's only true when you do the offsetting after casting to integer though, is it? The offset operations on pointers don't let safe code observe whether an overflow actually happened. Really, we could just generate an integer address immediately on allocation.

    Doing lookup on a Pointer is still more efficient, so that may be a reason to convert back.

  5. oli-obk commented on Jun 30, 2017

    @oli-obk
    ContributorAuthor

    Well... You can do wrapping_offset (safe and stable method), and then do a <= comparison between the resulting pointer and the new one.

    let x = 5;
    for i in 1.. {
        let ptr = &x as *const i32;
        let ptr2 = ptr.wrapping_offset(i);
        if ptr2 <= ptr {
            println!("Address of `x` is {}", std::usize::max_value() - i);
        }
    }

    Really, we could just generate an integer address immediately on allocation.

    We'd run out of integer addresses eventually (and quickly on 32bit emulation) if we don't reuse integer addresses and create a unique integer address for every allocation.

  6. RalfJung commented on Jul 3, 2017

    @RalfJung
    Member

    Heh, nice catch.

    We'd run out of integer addresses eventually (and quickly on 32bit emulation) if we don't reuse integer addresses and create a unique integer address for every allocation.

    "Quickly"? 2^32 is still a large number, I feel.^^
    But anyway, I guess there's no problem with lazy allocation. That also gives us a nice single point of control for rejecting everything that could potentially leak the base address when in CTFE mode -- we just always make the allocation of an integer address fail in that mode.

  7. oli-obk commented on Jul 3, 2017

    @oli-obk
    ContributorAuthor

    The problem is, that once you leak the base address of an array, you also leak the address of the last element's last byte. So allocatingand deallocating 100MB 50 times would fill the address space unless we allow reusing it.

  8. RalfJung commented on Aug 4, 2017

    @RalfJung
    Member

    Actually, given what I wrote in https://internals.rust-lang.org/t/types-as-contracts/5562/81, I think it would make sense for miri to both support pointer comparisons on "abstract" pointers and only provide intptrcast optionally. Without intptrcast, there will always be safe code that miri cannot run (like printing the value of a pointer), but currently there still seems to be value in also having a mode that keeps pointers and integers more separate.

  9. changed the title [-]Allow pointer comparison even between pointers of different allocs[/-] [+]Allow pointer inequality even between pointers of different allocs[/+] on Oct 1, 2018
  10. changed the title [-]Allow pointer inequality even between pointers of different allocs[/-] [+]implement intptrcast model[/+] on Oct 10, 2018
  11. Amanieu commented on Feb 6, 2019

    @Amanieu
    Member

    This code currently fails on miri:

    fn main() {
        let x: Vec<u32> = Vec::new();
        &x == &[];
    }
    error[E0080]: constant evaluation error: attempted to do invalid arithmetic on pointers that would leak base addresses, e.g., comparing pointers into different allocations
        --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/slice/mod.rs:5024:12
         |
    5024 |         if self.as_ptr() == other.as_ptr() {
         |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ attempted to do invalid arithmetic on pointers that would leak base addresses, e.g., comparing pointers into different allocations
         |
         = note: inside call to `<[A] as core::slice::SlicePartialEq<A>><u32>::equal` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/slice/mod.rs:4962:9
         = note: inside call to `core::slice::<impl std::cmp::PartialEq<[B]> for [A]><u32, u32>::eq` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/liballoc/vec.rs:2069:50
         = note: inside call to `<std::vec::Vec<A> as std::cmp::PartialEq<[B; _]>><u32, u32>::eq` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/cmp.rs:1009:48
    note: inside call to `std::cmp::impls::<impl std::cmp::PartialEq<&'b B> for &'a A><std::vec::Vec<u32>, [u32; 0]>::eq` at src/main.rs:3:5
        --> src/main.rs:3:5
         |
    3    |     &x == &[];
         |     ^^^^^^^^^
         = note: inside call to `main` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:64:34
    
  12. RalfJung commented on Mar 8, 2019

    @RalfJung
    Member

    Blocked on #653

  13. oli-obk commented on May 27, 2019

    @oli-obk
    ContributorAuthor

    cc @christianpoveda

    Basic (meaning probably incomplete) implementation instructions:

    1. add a fn int_to_ptr(&self, ptr: Scalar) -> InterpResult<'tcx, Pointer> method to Machine (in rustc). This function should have a default impl that matches on Scalar, returns the Pointer if it's the Ptr variant and returns the ReadBytesAsPointer error variant otherwise
    2. find all current sites where ReadBytesAsPointer is being emitted and call M::int_to_ptr(self, scalar) at said sites instead of emitting the error.
    3. Do the same trick for ReadPointerAsBytes (but name the newly added method ptr_to_int(&self, ptr: Pointer) -> InterpResult<'tcx, Scalar> and fully ignore the argument and just emit the error variant)
    4. Up to now, there should be zero change in behaviour by const eval or miri
    5. go back to miri, all further changes should be doable completely within miri
    6. add a FxHashMap<AllocId, u64> to

      miri/src/lib.rs

      Line 324 in 3c930e4

      pub struct Evaluator<'tcx> {
      which should then be used in the implementation of Machine::ptr_to_int inside miri
    7. add a u64 to Evaluator, which is the current number of bytes used up by the intptr "memory".
    8. whenever there's no entry in the FxHashMap during ptr_to_int, do name_of_the_hash_map.insert(ptr.alloc_id, name_of_the_u64);, compute name_of_the_u64 + ptr.offset in a variable, add memory.get(id).bytes.len() to the u64, and return a new Scalar created from the u64 + offset` variable. This will allow us to go the one way and do it deterministically
    9. for the other direction, you need another field in the Evaluator, this time to go from an integer to an AllocId (so the inverse of the other map). But since we may have added integers to the base address, you can't just compute the inverse map as that would not have entries for every single pointer. Instead we need something like a BTreeMap<u64, AllocId> (which also needs to be filled in in ptr_to_int). Unfortunately BTreeMap doesn't quite support our use case. What we want to find is the entry which our integer points into. So I guess we go with a Vec<(u64, AllocId)> and do a binary search on it, which should be just as fast for finding elements as searching in a BTreeMap.
    10. add an if self.seed.is_none() { return err!(...); } to both of the methods to make sure we only do this behavior in nondeterministic mode.
  14. pvdrz commented on May 27, 2019

    @pvdrz
    Contributor

    Ok I'll start working on this :D

    i'll be working on this branch: https://github.com/christianpoveda/rust/tree/intptrcast-model

  15. 20 remaining items

  16. added a commit that references this issue on Jun 26, 2019
  17. oli-obk commented on Jun 28, 2019

    @oli-obk
    ContributorAuthor

    Some discussion between eddyb and me about miri and threads came to the conclusion that for certain kind of checks it would be helpful to only have the intptrcast part where you can go from ptr to int, but not the other way. I believe that would even cover most real use cases, as I've only seen the other direction in artificial examples so far

  18. RalfJung commented on Jun 28, 2019

    @RalfJung
    Member

    I don't understand what you even mean by that. Could you give some concrete examples of programs we should not support and why?

    Also see this Zulip discussion.

  19. oli-obk commented on Jun 28, 2019

    @oli-obk
    ContributorAuthor

    well, basically I want to forbid (on 64 bit)

    union Foo {
        a: (u32, u32),
        b: usize,
    }
    let (x, y) = Foo { b: &1 as *const i32 as usize }.a;
    let ptr = ((x as usize) << 32) | (y as usize);
    let ptr = *(ptr as *const i32);

    or any other bit modifying operations beyond those below the alignment (so those that will happen on the integer representation instead of on the abstract representation).

    You can do these reading operations, so you can print all bits of a pointer, I just want to forbid int2ptr everywhere.

  20. RalfJung commented on Jun 28, 2019

    @RalfJung
    Member

    I'd rather get rid of all the code that performs arithmetic on the Pointer representation. That code is a mess.

    Why don't you want to allow that code? I do want to forbid this code which is why I added a test for this, but yours seems fine to me.

  21. oli-obk commented on Jun 28, 2019

    @oli-obk
    ContributorAuthor

    Why don't you want to allow that code?

    Essentially to be able to do full thread separation via our symbolic pointers. I want to be able to leverage the knowledge about what is accessible where. If we have integer pointers, that becomes impossible.

  22. RalfJung commented on Jun 28, 2019

    @RalfJung
    Member

    I'm afraid you will have to tell me more, I still don't know what this is all about.^^

    Can you open a new issue for this specific restriction, and describe in more detail the why and how? My plan is to close this one once everything works.

    I should probably put a checklist in the first comment here for what is still on @christianpoveda's TODO list. ;)

  23. pvdrz commented on Jun 28, 2019

    @pvdrz
    Contributor

    Plz do

  24. RalfJung commented on Jun 28, 2019

    @RalfJung
    Member

    Done. :)

  25. added
    A-intptrcastArea: affects int2ptr and ptr2int casts
    C-projectCategory: a larger project is being tracked here, usually with checkmarks for individual steps
    and removed
    C-enhancementCategory: a PR with an enhancement or an issue tracking an accepted enhancement
    on Jun 29, 2019
  26. RalfJung commented on Jul 28, 2019

    @RalfJung
    Member

    Turns out instead of calling force_ptr in Ref, we can have the invariant that places are actually "normalized" to Pointer on Deref, which happens in rust-lang/rust#63075. So that checkbox is also taken care of.

  27. added a commit that references this issue on Jul 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-interpreterArea: affects the core interpreterA-intptrcastArea: affects int2ptr and ptr2int castsC-projectCategory: a larger project is being tracked here, usually with checkmarks for individual steps

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions