-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Signed integer overflow causes program to skip the epilogue and fall into another function #48943
Comments
This is indeed UB, and -fsanitize=undefined will tell you about it: You probably want -fwrapv. |
FWIW, I have plans to at least put a ud2/trap at the end of functions that don't otherwise return or at least call (call might be non-returning, so wouldn't want to add an extra instruction if it won't ever be called - but does mean sometimes it could fallthrough to the following function). But yeah, UB is UB, and this is within the realm of failures you can expect from LLVM's optimizations. |
I would consider ud2 a reasonable solution. I understand the compiler can do anything with a UB. By the way, GCC warns this UB by default (without -Wall or -Wextra) as long you don't give it -O0. Anyhow, a ud2 is good enough. |
-mllvm -trap-unreachable will add a trap. Some targets (MachO, Webassembly, PS4) will do this by default. |
Hm, that doesn't pessimize the IR at all: https://godbolt.org/z/q49rq6 |
It will increase code size a bit. If you have stack-protector turned on, |
According to Twitter user @ScottWolchok, this behavior is not conforming to the standard. (§5.10/1): "Two pointers of the same type compare equal if and only if they are both null, both point to the same function, or both represent the same address" However, both f1 and f2 have the same address, yet they are different functions. |
Yes, irrespective of anything else, giving f1 and f2 the same address is a miscompile. I think that's a somewhat different issue than the one originally filed, as that only covers the case where f1 ends up being empty. |
Yes, that particular aspect of this is one of the reasons I was interested in the issue when I came across it due to a debug info problem: https://lists.llvm.org/pipermail/llvm-dev/2020-July/thread.html#143722 Though technically we can fix the conformance issue while still having this kind of UB in some fairly accessible cases - LLVM has an attribute "addrsig" to denote which functions have significant addresses - so, constructors for instance (which can't have their address taken/compared) could still have the "run into another function" failure mode. (& I expect we still have this failure mode in non-empty functions too, FWIW) My hope was to not go as far as trap-on-unreachable, which can be pretty clearly pessimizing (eg: "void f1() { unreachable; } void f2() { if (x) { f1(); } }" will generate code, test the condition, etc, when the whole condition/block should be optimized away instead) to just the narrow case where we could add one extra byte of instructions and stop a function from falling off the end into another function where there's a good chance of that. (exactly what a "good chance" is is debatable - if it's after a call, do we do it because the call might return? what about if the call is never going to return (it might be attributed with noreturn, or it might just not return for this call site/these particular parameter values)) |
As you say, it pessimizes the assembly - how much pessimizing is worth the extra safety is unclear. I think there's some room for a bit more safety & I'm working on that - but my guess is we might still leave some cases where you could fall through. |
Two things: First, when I try this case locally, f1() ends up being a retq instead of Second, IIRC, "trap on unreachable" is an X86 target thing, and not an IR |
Hmm, nope, that was wrong - seems trap-on-unreachable doesn't pessimize the middle end (unreachable blocks can still be removed by SimplifyCFG, etc) - but is only tested in codegen/targets. Perhaps the MachO behavior (trap on unreachable, but don't trap after a noreturn call) might be worth generalizing. I'll see about measuring the impact of that to see what it'd be like to use it by default. (note that the MachO behavior still leaves the possibility of falling off the end if you violate the noreturn contract and return from such a function) |
Oh, thanks for pointing that out - good to know/think about. |
Since Apple uses it for MachO I was thinking "oh, what do they do on their other targets" - and it seems they maybe do enable it for ARM and AArch64 (& it's also enabled for WebAssembly). And maybe even the new old M68K backend... $ grep -ir trap.*unreachable llvm/lib |
Ah, I was mis-remembering. It's a TM flag and we arranged to set it for PS4. |
Pilot error. Sorry for the noise. If I do it right, f1() is empty. |
If I add a puts() to f1(), like so: void f1(void) { then the codegen becomes very sad: 00000000004004d0 <_Z2f1v>: 00000000004004e0 <_Z2f2v>: Note f1() does a push with no pop, then falls into f2(), which means This means UB is a potential safety/security problem, and we really should |
Yeah, falling into another function on UB has been the way this works (except for the targets that opt out with TrapUnreachable) for a while now.
All sorts of UB manifests in lots of security issues, right? Buffer overruns and the like (I guess this is a buffer overrun, of sorts). So I'm not sure that's extra/new motivation. My take on it is with regards to
|
I'm just thinking this particular one is handing someone a gadget. The zero-length function is just a special case of this more general problem. But probably not worth debating the fine points here. |
Two opinions of mine: (1) Adding ud2 is not that expensive. According to Rust: rust-lang/rust#45920 They deliberately add an ud2 for all unreachable places to prevent "falling through" into whatever code next in memory, and found the increase in code size "ranged from 0.0% to 0.1%." (2) Instead of adding ud2, can we emulate an infinite loop as Clang 12 does? From the engineering aspect, I would consider reducing the affected radius of a UB as small as possible, to help debugging. Emulating an infinite loop even though the compiler knows it is a UB might help debugging -- much better than confusing the debugger with an unbalanced stack and a flying %rip register. Again, since nobody answered yet, why this warning is not turned on by default as GCC does? |
Yes, that's roughly the plan I have in mind.
It's not generally that simple - deciding where/how to "recover" from UB would be pretty difficult. The Clang-advised way to deal with this would be to compile with -fsanitize=undefined example.cpp:4:29: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' (not sure if there's a slight bug in the error message there (the "in" being at the end) or just a quirk of godbolt or the like)
Yeah - the ud2's more likely/easier to explain which situations it applies to and which ones it doesn't, etc.
Because Clang doesn't have such a warning - in part because we try pretty hard not to have warnings that are powered by the optimizers (because doing so makes the warnings unreliable - they don't appear in some build modes, or might appear/disappear based on spooky-action-at-a-distance when other code changes and tips optimizations in one way or another). Dynamic tools like the sanitizers provide more reliable & explanatory failures for this sort of situation. |
Note that LLD already pads functions with https://godbolt.org/z/Y46YKfqTc Gold uses NOPs, which I think should be fixed. BFD doesn't pad the functions at all. LLVM also doesn't pad the functions when you remove I'd suggest LLVM at least do something similar to LLD and insist on at least one byte of padding between functions and emitting that as |
Recently we have received a lot of cases where infinite loop/unreachable code is deleted and fallthrough to the next function. Our consensus is the behavior here very strange and surprising, see comment from @AaronBallman #60637 (comment). However this may be legal thanks to UB. There was a proposal to insert a "ud2" here: #48943 (comment). And @dwblaikie mentioned his minds here: #60588 (comment). @shafik mentioned there is a paper intended to address this problem #60622 (comment). |
I'm neutral to optimize such cases if indeed UB (anyway you have |
Thanks @pbo-linaro! The commit introduced loop deletion was: 6c31295, mentioned in #60652 |
@llvm/issue-subscribers-clang-codegen |
Just to reiterate a point raised in some of the linked bugs, one problem is that this can result in the UB function having the same address as another function, so that their function pointers compare equal, which is not allowed by the C/C++ standards. Such a program does not have UB as long as the offending function is never called, so the breakage of this bug isn't limited to UB programs. Code like that might even exist. If you are processing function pointers and you need multiple sentinel values (so that NULL isn't sufficient), you could define a dummy function and use its address as a sentinel. And since the function will never be called, you could put any old garbage code inside it. |
Extended Description
Comment:
Clang 13 simply does not generate any code for f1 after the undefined behavior point. So any call onto f1 will eventually ends up fell into f2.
Although the compiler can do anything with an undefined behavior, including simply crashing, infinite loop, playing some music, or nuke the earth without violating the C++ specification. I still hope this undefined behavior won't be that surprising.
This issue is not observed in C frontend, or Clang 12.
Godbolt link for your convenience: https://godbolt.org/z/r3nWrE
Source code:
Output:
The text was updated successfully, but these errors were encountered: