Description
Edited: to account for an initial misunderstanding about how dyn Trait
wide pointers are compared
TL;DR
The implementation of Arc::ptr_eq
is implemented based on comparing pointers to the inner value (not something that the documentation suggests/requires), which may involve comparing wide pointer meta data that is not relevant to it's stated function - and could lead to false negatives
How Arc::ptr_eq
is documented, and current expectations:
For reference here this is the (I think concise) documentation for Arc::ptr_eq
:
Returns true if the two Arcs point to the same allocation (in a vein similar to ptr::eq).
My expectation from this is rather simple: the implementation will get the pointer for the Box
/"allocation" that is allocated as the backing storage for two arcs (holding the reference count and inner value) and it will compare those pointers with ptr::eq
.
Notably: the wrapped type should have no bearing on the implementation / semantics here, since it's the pointer of the "allocation" that is supposed (documented) to be compared.
This way you can create any number of references for a single Arc
that wraps some inner value and cheaply determine whether two Arc
s point back to the same state allocation.
I've been relying on those semantics for Arc::ptr_eq
across a number of projects for exactly that purpose.
For a few projects I should also note that I have generalized how I wrap state in an Arc
as an aid for also exposing that state across FFI boundaries. I note this just to highlight the separation of concerns that I have relied on - namely that I assume I can use Arc::ptr_eq
to check whether two Arcs point to the same state without having to worry about what kind of type is wrapped by the Arc.
Actual implementation of Arc::ptr_eq
:
Instead of comparing the pointers of the backing "allocation", the current implementation of Arc::ptr_eq
instead figures out the offset pointer to the inner value and does a comparison of those value pointers with semantics that could vary depending on what is wrapped by the Arc.
I.e. it does Arc::as_ptr(&a) == Arc::as_ptr(&b)
edited, to address an initial misunderstanding:
In particular if an Arc contains a dyn Trait
then the pointers would be wide/fat pointers and the comparison will compare the vtable pointer for those trait objects which could be equal even though the two Arc
s are otherwise unrelated, not referencing the same "allocation". (I.e. it could return true
for two Arcs that don't point to the same allocation)
In particular if an Arc contains a dyn Trait
then the pointers would be wide/fat pointers and the comparison will
check that target_a == target_b && vtable_a == vtable_b
which makes it possible for the comparison to return false
for two Arc
s that have the same allocations but differing vtables.
Example
Based on #80505 (comment)
use std::{sync::Arc, mem};
struct A(u8);
#[repr(transparent)]
struct B(A);
trait T {
fn f(&self);
}
impl T for A {
fn f(&self) { println!("A") }
}
impl T for B {
fn f(&self) { println!("B") }
}
fn main() {
let b = Arc::new(B(A(92)));
let a = unsafe {
mem::transmute::<Arc<B>, Arc<A>>(b.clone())
};
let a: Arc<dyn T> = a;
let b: Arc<dyn T> = b;
println!("{}", Arc::ptr_eq(&a, &b));
}
Discussion
I'm not sure what safety rules have been established for transmuting Arc
s in such a way as to hit this but the more general consideration here is that Rust supports the notion of wide pointers with meta data whereby that meta data may affect pointer comparisons in various ways depending on the types involved.
The Arc::ptr_eq
API is specifically documented to compare whether the "allocation" of two Arcs are the same and so I don't believe that the semantics for comparing any form of wide pointer to the inner value should be relevant here.
Additionally comparing meta data associated with the inner value poses a rather open-ended risk that this function may deviate from its documented function - something that might change further if new types of wide pointers + meta data are added to Rust in the future.
Especially in the context of state that's being shared over unsafe FFI boundaries I think the potential for deviation from the documentation could be rather dangerous.
Even unrelated to FFI this breaks the basic notion that you can rely on this API compare whether two Arcs reference the same state which may easily break logical invariants that code relies on, for example to implement the Eq
trait of a higher level type.
As it happens I discovered this in a project for Json Web Tokens after I introduced Clippy to the project's CI. There was some validation state that was wrapped in an Arc
(closures that the application could add for validating a token) and discovering that the logic for comparing this validation state is broken by this was particularly concerning for me.
Duplicate issue
This issue has been discussed before as #80505 but I think it was, perhaps, a mistake that the discussion focused on the question of consistency between ptr::eq
and Arc::ptr_eq
in terms of how wide pointers should be compared.
The documentation for Arc::ptr_eq
gives no suggestion that it is comparing pointers to the wrapped type and so (imho) there is no cause for concern regarding the consistent handling of wide pointer comparisons.
Even considering the mention of ptr::eq
in the documentation where it says:
(in a vein similar to ptr::eq)
there is no implication that the inner type of the Arc
affects what pointers are being compared.
ptr::eq
should conceptually be used to compare the pointers to the Arc
s inner allocation, which should just be the raw (non-wide) pointer for the Box
that encompasses the Arc
s reference count. In this way the comparison is done "in a vein similar to ptr::eq
" (it may literally use ptr::eq
internally).
Previous agreement that this is a bug
The discussion for #80505 seemed to go around in circles but it seems important to highlight that the libraries team did discuss the issue and actually agreed that it was a bug #80505 (comment)
With that agreement it's hard to understand why #80505 was closed. (It looks like it was essentially closed since it was a pull request not an issue, and there had been some questions raised about the patch that maybe weren't addressed.)
Meta
rustc --version --verbose
:
rustc 1.64.0 (a55dd71d5 2022-09-19)
binary: rustc
commit-hash: a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52
commit-date: 2022-09-19
host: x86_64-pc-windows-msvc
release: 1.64.0
LLVM version: 14.0.6