miri: Miri can't track pointer through atomic bit-flip, which happens as usize
In haphazard, I maintain a concurrent linked list of things that can be “locked” through a bit flip in the next pointer. The relevant code is here:
It basically looks like this:
let head_available = AtomicUsize::new(0);
// ...
let avail = hazptrs.head_available.load(Ordering::Acquire);
if avail == core::ptr::null::<HazPtrRecord>() as usize {
return core::ptr::null_mut();
}
if (avail & LOCK_BIT) == 0 {
let avail: *const HazPtrRecord = avail as _;
// The available list is not currently locked.
// Take the lock by flipping the bit.
if head_available.compare_exchange_weak(
avail as usize,
avail as usize | LOCK_BIT,
Ordering::AcqRel,
Ordering::Relaxed,
).is_ok() {
// ...
I would love to run this code through Miri with -Zmiri-tag-raw-pointers (it runs fine without), but then I (as expected) run into “parent tag <untagged>” due to the int-to-ptr conversion, which matches the README’s note saying " You can recognize false positives by <untagged> occurring in the message – this indicates a pointer that was cast from an integer, so Miri was unable to track this pointer." You can replicate the issue using:
$ env "MIRIFLAGS=-Zmiri-tag-raw-pointers" cargo +nightly miri test --test lib feels_good
Finished test [unoptimized + debuginfo] target(s) in 0.03s
Running tests/lib.rs (/home/jon/.cargo-target/miri/x86_64-unknown-linux-gnu/debug/deps/lib-c166183ccbed9275)
running 1 test
test feels_good ... error: Undefined Behavior: trying to reborrow for SharedReadWrite at alloc65366, but parent tag <untagged> does not have an appropriate item in the borrow stack
--> /home/jon/dev/streams/haphazard/src/domain.rs:310:33
|
310 | let mut next = unsafe { &*tail }.available_next.load(Ordering::Relaxed);
| ^^^^^^ trying to reborrow for SharedReadWrite at alloc65366, but parent tag <untagged> does not have an appropriate item in the borrow stack
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the rules it violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
= note: inside `haphazard::Domain::<haphazard::Global>::try_acquire_available_locked::<1_usize>` at /home/jon/dev/streams/haphazard/src/domain.rs:310:33
= note: inside `haphazard::Domain::<haphazard::Global>::try_acquire_available::<1_usize>` at /home/jon/dev/streams/haphazard/src/domain.rs:284:45
= note: inside `haphazard::Domain::<haphazard::Global>::acquire_many::<1_usize>` at /home/jon/dev/streams/haphazard/src/domain.rs:217:29
= note: inside `haphazard::Domain::<haphazard::Global>::acquire` at /home/jon/dev/streams/haphazard/src/domain.rs:211:9
= note: inside `haphazard::HazardPointer::new_in_domain` at /home/jon/dev/streams/haphazard/src/hazard.rs:54:21
= note: inside `haphazard::HazardPointer::<'static>::new` at /home/jon/dev/streams/haphazard/src/hazard.rs:41:9
note: inside `feels_good` at tests/lib.rs:106:17
--> tests/lib.rs:106:17
|
106 | let mut h = HazardPointer::new();
| ^^^^^^^^^^^^^^^^^^^^
note: inside closure at tests/lib.rs:77:1
--> tests/lib.rs:77:1
|
76 | #[test]
| ------- in this procedural macro expansion
77 | / fn feels_good() {
78 | | let drops_42 = Arc::new(AtomicUsize::new(0));
79 | |
80 | | let x = AtomicPtr::new(Box::into_raw(Box::new((
... |
160 | | assert_eq!(drops_9001.load(Ordering::SeqCst), 1);
161 | | }
| |_^
= note: this error originates in the attribute macro `test` (in Nightly builds, run with -Z macro-backtrace for more info)
Is there any way for me to “help” Miri realize what’s going on here? For reference, the pointer stores happen here:
and here
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 17 (12 by maintainers)
Commits related to this issue
- Use `AtomicPtr` for `head_available` This enables Miri to track pointer provenance even for our raw pointers, which allows us to check with `-Zmiri-tag-raw-pointers`. Yay! See https://github.com/rus... — committed to jonhoo/haphazard by jonhoo 2 years ago
- Auto merge of #2275 - RalfJung:permissive-provenance-for-all, r=RalfJung Enable permissive provenance by default This completes the plan laid out in https://github.com/rust-lang/miri/issues/2133: - ... — committed to rust-lang/miri by bors 2 years ago
FWIW here’s a convenient wrapper around that cast function that has been proposed several times on Zulip:
If you use proper casts to go from a ptr to
usizeand back, this will actually work now in Miri. But it might miss some bugs since the alias tracking has to get somewhat approximate.I hope this will be resolved by https://github.com/rust-lang/miri/issues/2133.
I will never log offSo, at one point I considered trying to mock up what the API would look like for a small family of
AtomicB32/AtomicB64/AtomicB128, where theBrepresentsbytes, since at the time it was being discussed by LLVM. My hope was if it was relatively clean in the end, I could run it by Ralf and mock up a pre-RFC, or something.Unfortunately, largely I came to the conclusion it wasn’t really viable. That it would just be a lot easier to find a way to either allow pointer->integer conversions, to make the atomic types generic, or to just make the existing atomic types behave this way, even if it means that they have provenance now.
That said, the main motivation for this exploration was “how can I CAS a
#[repr(C)] struct { aba_counter: usize, ptr: *mut T }?”, rather than this issue. And in fact, making them generic makes the problem in this issue slightly harder to design the API, since IMO it becomes a bit less reasonable to add bitwise operations, given that pointers don’t support them[^ptrbitop]?I also have some… reasonably strong feelings about turning the current pointer->integer->pointer casts[^pcasts] into UB being a stability break too (and personally I think stability is more important[^important] than allowing aggressive optimizations around pointers, but I know many others disagree with me here, or disagree that it is actually a violation of our stability rules).
[^important]: A lot of my perception here is colored by the fact that I don’t think the optimizations this disallows tend to be that profitable in practice, but… well, perhaps I’m mistaken, and this is super getting off track at this point.
All that said, I suppose it wouldn’t be hard to mock up what adding atomic ops directly to AtomicPtr would look like, as an unstable API. This might require new intrinsics for them, although perhaps the functions are enough, and miri could detect that e.g. the
atomic_orintrinsic is being used on a pointer type. It may require additional changes to the other backends, I don’t know.That said, something here is probably needed to keep miri working when we integrate parking_lot into libstd, or more broadly improve our lock impls. For example, https://github.com/Amanieu/parking_lot/blob/78f09f45d6b8b34ec23afdf7a696cbe54d0a469b/core/src/word_lock.rs [^wordlock] does bitor on an AtomicUsize that holds a pointer.
[^ptrbitop]: Recently, It’s been discussed that perhaps they should. I think this would be… interesting.
[^pcasts]: Hell, I almost feel that way about pointer->integer transmutes, and much of the discussion about transmutes was around “what if we just made them be the same as casts”, which I guess seems like it’s no longer on the table.
[^wordlock]: It’s possible we would just use a system lock instead in the libstd integration, since this lock isn’t exposed. Even so, this kind of MCS-style construction is not uncommon in synchronization primitives.
Yes,
fetch_oris exactly the concern (as @thomcc keeps mentioning whenever I proposeint_to_ptr_with_provenance😉.Basically we’d need fetch_or on AtomicPtr to even make this expressible.
Related issue for the same situation only in LLVM:
Basically we need a way to modify bits of a pointer as long as they only affect things that are not part of the base address. We used to have some of that in miri, but never exposed it to users. In addition to the existing
offsetmethod on pointers we need a way to read/write only the bits that the alignment of the pointee says must be zero if we had a reference instead of a pointer.