-
Notifications
You must be signed in to change notification settings - Fork 10.8k
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
llvm libc (and musl) are incompatible with llvm memcpy assumptions #73516
Comments
@gchatelet : Do you mind taking a look at this? Thanks! |
Shouldn't this be UB by standards? |
Yes it is, but when the compiler calls a particular memcpy implementation, that implementation could make more guarantees than what the standard prescribes. LLVM assumes this to be the case. However, llvm-libc does not seem to actually provide such guarantees due to the |
Ah, I see. |
@llvm/issue-subscribers-bug Author: Ralf Jung (RalfJung)
The libc llvm seems to declare its `memcpy` with the `restrict` qualifier, matching the signature in the C standard:
llvm-project/libc/src/string/memcpy.cpp Lines 15 to 17 in cb112eb
This means it will be compiled in a way such that when the two pointers are used to access the same memory, and at least one access is a write, there is UB. This is UB both in the C source semantics and in LLVM IR (via However, LLVM itself assumes that the libc Reading the llvm-libc sources, I did not find any guards that would short-circuit the function when musl also uses |
@RalfJung thx for bringing this issue to my attention.
I believe this is stated here for the IR but I haven't seen anything regarding requirements for the libc. Let me run a bunch of tests and get back to you with a proper answer. |
Yes, the semantics of the builtin are documented. But that's guarantees LLVM makes to its users: if you call the builtin that way, all is good. The fact that this becomes a libcall to some libc symbol means that LLVM also makes similar assumptions, where it is then someone else's responsibility to really ensure that memcpy (the library function, not the builtin) behaves that way. For clang there is an old WIP patch to document this at https://reviews.llvm.org/D86993, but I am not aware of anything for LLVM itself. (For Rust, what I am really hoping for here is that we can get a guarantee that if the LLVM IR only calls the builtin in a way that the argument ranges are disjoint, then LLVM will also only ever emit libcalls to memcpy with disjoint arguments. That is, LLVM will never itself do any transformation that would rely on the fact that the memcpy builtin is well-defined for src==dst. That would be great news for Rust since then Rust could avoid making the src==dst assumption about memcpy -- an assumption that is problematic as demonstrated by the |
Cc @nikic who's been involved in a bunch of these discussions. |
Just so we're clear, when Ralf says "incompatible" he means "theoretically incompatible". There is no practical issue here, because this usage of restrict will not lead to an actual miscompile even if both pointers are equal (only if there is non-exact overlap, which is excluded). There is no action that needs to be taken on the part of LLVM libc. If any action is taken, it will be on the side of LLVM. |
If llvm.memcpy does have a looser requirement, isn't it the case that LLVM should emit the check when lowering the code? (Either dynamically or statically if the libc target is known) |
It is theoretical in the sense that I am not aware of a compiler actually compiling memcpy in a way that this would be an issue. But I also have not checked the assembly. Re-reading the definitions of EDIT: it was pointed out to me that the standard also contains this non-normative note:
So probably this is indeed intended to be UB. |
I was reading more of the linked bugs and this GCC comment caught my attention. We don't currently use such strategies and I don't see it happening in the future but there's probably some hypothetical implementation of |
Ah, that's an interesting one. And with |
@RalfJung how do you want to move forward with this issue? It seems to me that this would be an issue for llvm itself. Maybe just documentation improvement? |
Right, I think there's two issues here:
An alternative to (2) would have been to remove |
If you want a copy that's safe even if the ranges overlap then you want
|
|
LLVM requires that memcpy of the same address work, because it uses memcpy for struct copies, e.g. I'm certain we don't want to add a pointer inequality check to the struct copy codegen, nor switch it to call memmove. |
The only alternative is to have a pointer inequality check in
I am aware: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=32667, https://sourceware.org/bugzilla/show_bug.cgi?id=31055 In those discussions, the proposal came up of having a Also, what you are describing is a concern of clang, not LLVM. LLVM is not concerned with C semantics, only with LLVM IR semantics. In Rust we'd ideally like to use LLVM without making this assumption about memcpy. We are taking care to call the LLVM memcpy builtin only with strictly non-overlapping pointers. It would be great to get a guarantee that LLVM will then also never perform any memcpy libcalls where the pointers overlap. In other words, even though the LLVM builtin says that equal pointers are allowed, it would be great to get a guarantee that LLVM transformations will never exploit this and introduce new memcpy with equal pointers. Therefore, all libcalls to memcpy with equal pointers correspond to the frontend generating a builtin memcpy with equal pointers, and if the frontend generates no such call, one can use LLVM with a memcpy implementation that does not allow equal pointers. Basically, frontends should be able to choose whether they require a non-standard memcpy that allows exact overlap or not. clang is obviously free to make its own decisions, but in Rust I'd rather not rely on "yeah it's UB but compilers don't optimize |
Those aren't the only available alternatives. The assembly code generated by today's compilers already works with exact overlap, with restrict, and without an extra branch. I don't know if removing the restrict qualifier affects the performance of the generated code. If it doesn't, simply remove it to make the code semantically correct. But if it does: memcpy is performance-critical enough that it's worth it to find a different solution which preserves the current codegen -- without being theoretically invalid. One option there would be to document an extended guarantee of "restrict" as not being invalid on exact overlap. |
Does that automatically fall out of Rust's model, or do you explicitly take extra care? (That is: could it improve codegen for Rust to stop taking such care, given the guarantee that memcpy does work with equal pointers?) |
I agree with this factual statement. However, which compilers guarantee this to be the case? clang doesn't, as far as I can tell, given the LLVM IR semantics. So in the status quo we now have to audit the generated assembly after each compiler update, or something like that? Certainly not a great solution.
The only way out I can see for
This automatically falls out of Rust's model, specifically the disjointness information in Rust's For the specific situation where this happens in C, |
No, this definitely defeats the intent of
Hmm... it seems that we can say "An implementation may assume reading via expression
Note that the formal defintion (located at, e.g., WG14 N3096 6.7.3.1) uses "expression" that should have a well-defined type, so it shouldn't be hard to specify this. The major problem I see is that just modifying void fun(int * restrict p, int * restrict q)
{
*p = *q;
*q = *p;
} IIUC an implementation can assume that So I guess a possible option would be weakening the preconditions of |
The libc llvm seems to declare its
memcpy
with therestrict
qualifier, matching the signature in the C standard:llvm-project/libc/src/string/memcpy.cpp
Lines 15 to 17 in cb112eb
This means it will be compiled in a way such that when the two pointers are used to access the same memory, and at least one access is a write, there is UB. This is UB both in the C source semantics and in LLVM IR (via
noalias
).However, LLVM itself assumes that the libc
memcpy
is always defined behavior whendst==src
. (This comes up frequently on this issue tracker, e.g. at #60734, #55399. But as far as I know it is not mentioned in the LLVM documentation.)Reading the llvm-libc sources, I did not find any guards that would short-circuit the function when
dst==src
. It seems very much like the usual copy loop will be executed, loading fromsrc
and storing todst
. This is UB due to therestrict
keyword, making LLVM incompatible with its own libc even when said libc is compiled with LLVM -- unless I misunderstood something. (There are so many macros in the libc it's very hard to be sure what the actually compiled code will be.^^)musl also uses
restrict
in itsmemcpy
, so it is likewise incompatible with LLVM.The text was updated successfully, but these errors were encountered: