-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
TypeId: use a (v0) mangled type to remain sound in the face of hash collisions. #95845
Conversation
Some changes occured to rustc_codegen_gcc cc @antoyo Some changes occured to the CTFE / Miri engine cc @rust-lang/miri |
r? @lcnr (rust-highfive has picked a reviewer for you, use r? to override) |
That sounds awesome! Will this fix #10389? |
@@ -39,6 +39,87 @@ pub(crate) fn const_caller_location( | |||
ConstValue::Scalar(Scalar::from_pointer(loc_place.ptr.into_pointer_or_addr().unwrap(), &tcx)) | |||
} | |||
|
|||
pub(crate) fn const_type_id<'tcx>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'd prefer this helper to be in the interpret
module, not const_eval
-- it is needed for all interpreter instances after all, const_eval
is meant for specifically the CTFE instance of the general interpreter infrastruture.
EDIT: It is needed also for regular codegen, but I think my point remains -- const_eval
should be for things that are specific for CTFE. interpret
should not usually import anything from const_eval
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're also creating your own ecx
here, and returning this as a ConstValue
-- why all that?
I guess an alternative might be to treat this like vtable_allocation
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type_name::alloc_type_name
also seems similar in spirit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooh I see, I was modelling it after const_caller_location
but that's only here because it's a query instead of an intrinsic. Should it have its own module under interpret::intrinsics
like caller_location
does?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, note in particular that the caller_location
implementation in interpret
does not call the const_eval
code. It is not a nullary intrinsic so it takes slightly different paths. Though probably the const_caller_location
query should be made more like the vtable query, which is not inside const_eval
either.
Should it have its own module under interpret::intrinsics like caller_location does?
Yeah I guess that would make sense.
It would almost make sense to move these queries for intrinsics that are implemented via interpreter stuff but also used for codegen in a separate module rustc_const_eval::intrinsics
, i.e., outside interpret
. @oli-obk any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at it, caller_location
actually has a impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M>
block so having that inside interpret
makes sense. type_name
, OTOH, doesn't really do anything with the interpreter, it is very much like the vtable
code (but the two are in totally different locations, which we should probably fix). "give me the vtable for type T" is not technically an intrinsic but not very different, either, is it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think cleaning this up is orthogonal.
Except for mplace_field
we don't even need an InterpCx
and can just create and intern the Allocation
s directly. If we added more convenience operations for writing data of a specific struct to an Allocation
we can probably stop using InterpCx
for caller_location
, too.
Imo we should just merge this PR and then work on cleaning these up now that we have multiple intrinsics to clean up in a similar way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree cleaning up the existing stuff is a separate task.
But we shouldn't have anything load-bearing inside the const_eval
module, so I do think that this code should be moved somewhere else before landing. I don't care strongly which of the existing precedent it follows, but none of it has load-bearing code in const_eval, so let's not add yet another different style of implementing such an intrinsic. :)
(Even caller_location
, which was mentioned as a template, has its load-bearing code in interpret::intrinsics::caller_location
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is still relevant, should move this function.
Some backstory on how this took so long: #77125 (comment) is from August last year, and in September I already had half of this implemented - but around late October I ended up with a dead NVMe in the desktop where the branch was on. Only a few days ago I accidentally found a copy of it on the build server I typically use ( |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
Good point, yeah, I believe it would (cc @workingjubilee) - I'll go add it to the description. |
Heh, code in the wild seems to be relying on hasher behavior - at least I've been meaning to revert that to what it was before (for performance reasons) so we don't need to break anyone who does that (as suspicious as it is). |
impl fmt::Debug for TypeId { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
// FIXME(eddyb) perhaps attempt demangling `mangling`. | ||
f.debug_struct("TypeId").finish_non_exhaustive() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a debug format regression. Previously it was possible to tell different TypeId
apart.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, weird, I don't remember doing this but I guess I noticed TypeId
was infoleaking and reacted by doing this? Ideally we would print the mangling but I'm not sure we should commit to that without a more involved proposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think showing TypeId { <mangling> }
is what we want here.
I don't feel like this will add forward compat issues we should care about and being able to differentiate TypeId
s is quite important. There won't be too many people relying on the concrete Debug
string returned and if they do, I really don't mind breaking their code ^^
This comment was marked as resolved.
This comment was marked as resolved.
#[inline] | ||
fn eq(&self, other: &Self) -> bool { | ||
// FIXME(eddyb) should `likely` be used around the `ptr::eq`? | ||
if ptr::eq(self, other) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: this could be CTFE-friendly if it used guaranteed_eq
instead.
@bors try @rust-timer queue |
Awaiting bors try build completion. @rustbot label: +S-waiting-on-perf |
Changing from |
It would be an interesting project to try to structure |
Putting all mangled type names in a specific section and then using |
I don't think it makes much sense to repurpose this PR for implementing the cryptographic hash variant, and I'd rather leave the comment history here be about the mangled type variant. Also it isn't a strong priority for me wrt limited time (as per #95845 (comment) this was stalled for over half a year in the first place), but if anyone wants to repurpose the few relevant bits, feel free to get them from this PR. Acouple smaller things could also be done (not sure if I'll be able to help with either):
In the end, I'm sorry for wasting people's time with an approach that I misjudged as having stronger support (from both the formal side and the precedent of C++ RTTI) than it actually did. |
The compiler's internal implementations of these can be fixed without having to consult users, however. The results of the TypeId hashing will be public-facing. Many Rust PRs have been historically stalled and pushed back on for the concern of potentially breaking users, even in the absence of evidence of any users who will actually be broken, even against the weight of correctness. Even, again, for situations where there was no guarantee in the public interface, merely "someone may have come to rely on it". So I do not understand why this is so cheerfully a part of discussion. And it feels like being able to write something in Coq that ignores that the TypeId hash may collide does not automatically make our code correct in the face of actual implementations. After all, people are talking about the implications of the contents of the binary, now, and worrying what that will imply and what people will come to depend on in those. By then, the Rust language has long since left the picture and can provide very little control in actuality because the artifact has been fashioned. And Rust is included in dynamic libraries that cannot simply compile all this away. If we're concerned about people looking at TypeId symbols, why not about looking at our TypeId hashes, also? Meanwhile, the concern of large symbols is primarily evinced by recursive symbols, which are subject to compression techniques. I am absolutely certain that in 2040, assuming the Rust project still stands by then, that if the hash we use is broken, that someone is going to open a PR for upgrading the hash and everyone is going to hem and haw about these things then. It feels doubtful to me that, in the absence of well-defined ways to upgrade |
@workingjubilee I think there's a huge difference between "hey the transmute stopped working" and "the value in use is different now". I can't imagine there being any concern about changing the values generated here -- they can already change today when the rustc details change how the current unsound hash is generated. |
I'm not sure I understand the worry behind a hash algorithm being broken in the future. When a hash is broken, it doesn't suddenly start producing accidental collisions for small strings like type names, it just means that it's now computationally feasible to find a collision, if you try really hard. Would that even matter for this use case? If it was possible to calculate a 'malicious' typename (probably of several kilo- or megabytes) that would collide with another, would that change anything? It seems to me that if an attacker can insert a malicious type name into the code, they can do much more damage by inserting much simpler things into the code. |
It is an attack vector (defense in depth, etc) when using However, any case where this would be an actual attack vector does include trusting some property about the malicious actor, either because they're a transitive native dependency, or they're a runtime plugin and you're trusting them to give you a unique (Part of the reason this is the case is that it's not sufficient to get a So yes, I believe it is completely reasonable to restrict this to a safe/unsafe protection boundary against (Perhaps a collision attack would make it easier to get a malicious crate through review... but that requires not just a hash collision, but a meaningful hash collision, which is much more difficult to find even with a broken hash.) Taking that as an axiom, a reasonably-sized hash is certainly sufficient. (But that also said, I think it is also reasonable to choose a hash size designed to be resilient against a hash collision in the entire ecosystem rather than just within a compilation. This prevents our malicious actor from just finding a preexisting hash collision and adding a dependency on it to create UB.) |
This feels dangerously close to "if we can't totally fix it there's no point in making things any better" which really sucks. If we can easily improve the state of things and leave the door open for future improvements then we 100% should |
I would really hate for Besides, those who are going to transmute illegally will likely do it again, since what they are really trying to accomplish is compile-time comparison of |
A small TypeId is simply unsound, as we do have real-world examples of collisions. So that is not an option. |
Also the issue with rogue crates illegally transmuting |
You can argue that the first isn't necessary, but the second is very desirable. If you want to argue against |
Note that the fast accept also doesn't require indirection! With the right linker integration, we should be able to almost always hit one of these two:
Oh and when being passed around, such two fields will e.g. go into two registers (as they ake So for the most part, you would have double the size and double the comparisons - that shouldn't cause as big of a deal as "O(1) operation is now O(N) on average". |
To be frank, I think TypeId being 2-word large is still fine. But looking at the discussion, I worry the same arguments might be used to enlarge it yet again to 256 bits in the future, which certainly will be too large to put inline. Anyhow, I don't have a better idea to argue against this effectively. Maybe pointer-tagging some bits from the hash could help in some way to keep the struct small but the number of bits available to be used makes it seem negligible. |
Nobody is arguing to make |
…apkin Use 128 bits for TypeId hash Preliminary/Draft impl of rust-lang/compiler-team#608 Prior art (probably incomplete list) - rust-lang#75923 - rust-lang#95845
Use 128 bits for TypeId hash Preliminary/Draft impl of rust-lang/compiler-team#608 Prior art (probably incomplete list) - rust-lang/rust#75923 - rust-lang/rust#95845
This implements the strategy described in #77125 (comment), resulting in:
TypeId
s become as sound as v0 symbol nameson some platforms we may be able to instruct the linker to error based on e.g. different data in a symbol with a name only depends on crate identity, but in which we place the SVH (which depends on the crate's whole source/AST/HIR)
however, improving symbol name soundness is not in the scope of this PR, only
TypeId
parity with the best tool we have for serializing Rust identities in the face of separate compilation (i.e. v0 symbol names)transmute
-ingTypeId
s tou64
(u64, ptr)
layout was used in this PR (instead of a single pointer with theu64
hash stored behind it) - it's strictly larger irrespective of targetTypeId::of
at compile-time (still unstable) then eithertransmute
-ing it to an integer, or using it in a pattern (turns out we had a test for the latter)const fn
type_id
#77125TypeId
+rustc_demangle
(whichstd
already depends on) could offer access to the type name at runtime, which has been asked for before:type_name_of_id
to power a betterimpl Debug for TypeId
? #61533type_name()
method on Any. #68379type_info::name
from C++ RTTI (wheretype_info
is obtained with thetypeid(T)
operator, which acts like ourTypeId::of::<T>()
)rustc_demangle
intocore
would be harder to do (and justify)This implementation adds a new field to
TypeId
:The
&TypeManglingStr
type is like&str
but "thin", with theusize
length being stored before thestr
data (an optimization to avoid makingTypeId
more than double in size).To avoid comparing the string contents of the
mangling
field every time,a == b
has:a.hash != b.hash
a
andb
are definitely different if theirhash
es differptr::eq(a.mangling, b.mangling)
a
andb
are definitely the same if theirmangling
s are the same in memory&TypeManglingStr
type, which its length being stored behind the pointermangling
string contentsWhile 1. and 2. could come in either order (as
hash
andmangling
equalities are intrinsically linked), for simplicity (and being able to rely on#[derive]
a bit), the order used above is the order they are checked in, in this PR currently.Ideally, the fallback path won't be hit outside of a hash collision, and this should be the case today if the same crate (or at least CGU) has caused the codegen of both
TypeId::of
which produced the twoTypeId
s in the first place (which will cause theirmangling
fields to point at the same place in read-only static memory).However, to get global deduplication (across all crates/CGUs), we'd have to start using certain linker features (discussed in e.g. #75923), which I've left out of this PR.
As it stands, this implementation is comparable to the
typeinfo
implementationlibcxx
callsNonUnique
, which seems to be used at least on Windows (and its existence suggest we may never be able to guarantee deduplication, only do it on a best-effort basis).Fixes #10389
cc @oli-obk @RalfJung @KodrAus @mystor