-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Incorrect clashing_extern_decl warning in nightly #73735
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
Comments
@jumbatm let me know if you are not able to look into this in near future. |
Thanks for the report. I'll start looking into this tonight. @rustbot claim |
Good pickup on the For the additional pairs you mention, it's not clear to me why they shouldn't warn:
This should be compatible because if the real signature for However, the other way around // On the C side
void test() { /* ... */ } and // In the calling Rust code
extern "C" { fn test() -> usize; } would let you read uninitialised memory, because the Rust code would look for a return value that the C side never set. It's not enough to assume that the first encountered declaration of the function is the correct one and only warn for a specific order of the clashing declaration, because at any point in the code, both versions of the function may be callable, in different modules-- we only know that at least one of the functions is incorrect. That being said, I definitely see how it's not clear in the current diagnostic why this is linted against, though.
I'm not sure about these two, because although this would not be unsound from a memory safety perspective, there's still a change in invariance between the two signatures. If the former is correct, then the latter's signature (and the corresponding invariant that the return value is non-zero) is incorrect and needs to be corrected anyway; if the latter's is correct, then there is an additional invariant that correcting the former adds -- a win-win situation for both cases.
This one's because the size of both return types is the same, right? This relies on the memory layout optimisation of None being 0, which I can't find any mention of being guaranteed (though, to be fair, I'd imagine is a pretty safe bet). It also needs Ah... it seems like what these cases have in common is a differing view on whether clashing_extern_decl should be guarding solely against uninitialised memory reads (which implies to me that this lint would boil down to a simple size + alignment check), or whether the lint should be also guarding against "transmuting" memory reads (which makes these additional cases worth linting against). I'm of the opinion that this lint should also be guarding against transmuting reads, but I'd be keen to hear from any other perspectives. |
Well, we don't know about the actual declaration on the C-side. Even all FFI on the Rust side is compatible, there is no guarantee that it matches the C-side declaration. We are and always will depend on programmer to get that correct. Because I believe there is an intention for this lint to become a hard error in the future, we will have to conservative when generating it, or we will need to different warnings for these two cases.
Just a note, size and alignment check is insufficient for capturing ABI compatibility issues. For example |
One idea, we might define something like struct ExternDeclCompat {
Compat,
Incompat,
Maybe(GuessedCFunctionType) // We unify and infer the actual C type that would make the two declaration compatible
} and generate different warning for the incompatible and maybe compatible case. |
The priority should be soundness here -- both cases need to be detected. Otherwise, we've only fixed a soundness hole half the time, which is to say not at all. mod a {
extern "C" { fn test() -> usize; }
}
mod b {
extern "C" { fn test(); }
}
unsafe {
b:: test();
let s = a::test();
} No matter what order
This is true if we're comparing the C function to the Rust declaration. However, we're comparing the Rust side to the Rust side, without any knowledge of the C side: mod c {
extern "C" { fn foo() -> NonZeroUsize; }
}
mod d {
extern "C" { fn foo() -> usize; }
} In this scenario, the two signatures can't both be correct -- either the real function returns a usize that will never be zero, or it returns a usize which might be zero. Both can't be true at once, so at least one of these declarations must be incorrect.
Sorry, I wasn't clear -- I meant guaranteed as in future-proof (perhaps better wording would have been to ask whether this conversion has been promised to always work in the future). If not, client code shouldn't be relying on this conversion anyway as a future change to enum layout may inadvertently break the code, so we should not encourage this conversion by not linting against it.
Definitely agree with you here. Hence, the lint currently guards more than just uninitialised memory accesses, and guards against "transmuting" cases, as I mentioned above.
Finding a common C type means the Rust compiler would have to understand (and adapt to updates in) C semantics. Generalising it out to the ABI, however (which the compiler needs to understand anyway) -- it's a good idea! It has been suggested before that I write a heuristic that determines compatible signatures. I'm not ruling this out, but I'd like to see the results of a crater-check run before I commit to something like this, because as the lint currently stands, it takes the very maintainable, and very safe bet that a function (whether external or not) can only have a single, correct signature (so inconsistencies in the declared signature on the Rust side are almost certainly a mistake). If it turns out that a large number of projects purposely rely on FFI functions being imported with different signatures to call them in certain ways (as I've already seen for some simd intrinsics), and those imports are largely sound, then this is definitely something I'm happy to implement. |
For the
|
Ah, makes sense. The lint is only triggered if someone inconsistently declares the signature on the Rust side, though -- "fixing" the code causing this lint requires requires either marking the signature as a normal
Thanks heaps for finding this. It's exactly the indication I needed to see (and failed to find). I'll add this exception to
Oh 🤦, you're right. I had originally written |
It seems that there is a case still not being fixed, when mod a {
use std::num::NonZeroUsize;
extern "C" {
fn a() -> NonZeroUsize;
}
}
mod b {
#[repr(transparent)]
struct X(NonZeroUsize);
use std::num::NonZeroUsize;
extern "C" {
fn a() -> X;
}
} |
Thanks, I'll open another issue for this case. |
PR #70946 introduces new clashing_extern_decl lint which causes incorrect warning even when two extern functions are ABI compatible:
Some other example pairs that are ABI compatible that are incorrectly warned
fn test() -> usize
andfn test()
fn test() -> usize
andfn test() -> NonZeroUsize
fn test() -> usize
andfn test() -> Option<NonZeroUsize>
This issue has been assigned to @jumbatm via this comment.
The text was updated successfully, but these errors were encountered: