This patch ensures that UBSan returns the exit code specified
by sanitizer_flags if it detects an error during execution.
Details
Diff Detail
- Build Status
Buildable 8023 Build 8023: arc lint + arc unit
Event Timeline
Sure, it makes sense for UBSan to behave like other sanitizers in this regard.
lib/ubsan/ubsan_init.cc | ||
---|---|---|
44 | Is this the right place to do this? If UBSan is hosted inside another sanitizer, does that sanitizer already set up such a callback? |
lib/ubsan/ubsan_init.cc | ||
---|---|---|
44 | That's a good question. I know that all of the tests pass for me on Linux and Darwin (both for UBSan hosted inside other sanitizers and for the other sanitizers themselves), but that's been the extent of my testing. Should that be sufficient, or should I investigate further? |
lib/ubsan/ubsan_init.cc | ||
---|---|---|
44 | Asan implements exitcode support by calling Die after the first error. Tsan has an interceptor for exit, and implements exitcode support in that interceptor. I only took a quick look but I don't think any of the sanitizers call exit() in an atexit() handler, which seems a bit suspicious to me. Couldn't that cause an infinite loop? If calling exit() in an atexit() handler has safe/defined behavior, I'm fine with adding the handler here (though we should only add it if common_flags()->exitcode is set). | |
test/ubsan/TestCases/Float/cast-overflow.cpp | ||
3 | This test doesn't use the exitcode flag, so why does this test need to change? I have the same question about the other test case changes here. |
Could you explain how you intend to make use of these changes? As it stands I'm not convinced this patch is headed in the right direction, but I'd like to give some more constructive feedback.
lib/ubsan/ubsan_init.cc | ||
---|---|---|
44 | I'm still not sure that calling exit in an atexit handler is a good idea. Example: there are two instrumented shared libraries loaded into a program. Both would get atexit handlers. Calling exit() in one handler would prevent the other handlers from being run. This would break -fprofile-instr-generate, and user-provided atexit handlers. CC'ing @eugenis for any comment since he reviewed D12120, which revamped exitcode handling. | |
test/ubsan/TestCases/Float/cast-overflow.cpp | ||
3 | I don't think that enabling a sanitizer should, by default, change the way a program's exit codes work. Users may intend to quit with specific exit codes in scenarios which trigger sanitizer diagnostics. |
It might be possible to change the default exitcode for UBSan, but I still think it should be possible for the user to make UBSan errors to return failure. Two primary reasons:
- Consistency: As far as I'm aware, all of the other sanitizers return a failing exit code when they detect an error in a program, and I'm not sure why UBSan should be different in that respect.
- Utility: A common use case I could imagine would be the case where a user wants to prevent UB regressions in their codebase by running a unit test suite with UBSan enabled. If it's not possible to return a failing exit code (either by default or otherwise), the test suite will continue passing just fine when UB is introduced.
A workaround that can be used currently to force a failing exit code on failure would be to use halt_on_error=1. However, it's not always desirable to limit error output to only one error (for example, brining up a unit test suite in a new codebase, where there may be many UB errors). In addition, ASan currently returns a failing exit code at the end of program execution even when halt_on_error=0 and it doesn't immediately abort.
test/ubsan/TestCases/Float/cast-overflow.cpp | ||
---|---|---|
3 | Failing after observing an error is the default in the other sanitizers already, and I'm not sure it makes sense to special-case UBSan here. |
Makes sense to me.
A workaround that can be used currently to force a failing exit code on failure would be to use halt_on_error=1. However, it's not always desirable to limit error output to only one error (for example, brining up a unit test suite in a new codebase, where there may be many UB errors).
Also fair.
Reading the atexit man page, I'm more convinced that calling exit() in the atexit handler is not OK, since it also breaks dlclose():
"If the provided function is located in a library that has been dynamically loaded (e.g. by dlopen()), it will be called when the library is unloaded (due to a call to dlclose())"
One possible solution is to add an interceptor for exit() to ubsan, just like the one tsan has. But this breaks (a|t)san+ubsan compatibility.
Another possible solution is to make the "HasReportedError" bit shared in sanitizer_common. That way, if ubsan reports an error, _and_ is in use with (a|t)san, the exitcode support would work properly. This is just a partial solution, however, since it doesn't improve the situation for standalone ubsan. In practice people tend to run asan+ubsan together, so it would still be a win.
ASan has a mode where it does not exit after the first error, -fsanitize-recover=address and ASAN_OPTIONS=halt_on_error=0. It appears that the errorcode flag is ignored in that mode.
I think TSan does the right thing here. How about this:
- intercept exit() in asan and ubsan
- put HasReportedError somewhere in sanitizer_common, and use it all sanitizers.
The only issue I have come across is that standalone UBSan currently doesn't link against interception. If we want to intercept _exit, we'll need to add interception as a dependency. Is that something we're okay with?
There's a further complication with intercepting in UBSan - there doesn't seem to be a way to tell whether you're building standalone at compile time (although I could add a cmake define for that), so the UBSan interceptor will conflict with the TSan/ASan interceptor.
Right, there are no interceptors in ubsan. I confused it with LSan, which has its own interceptors that are used in standalone mode only.
I'm not sure if adding interception to UBSan is a good idea. I think the current library is OK to link into shared libraries (i.e. does not have real global state). Or is it not the case any longer? I don't think we use this property anywhere, and clang does not link the ubsan runtime library to shared libraries. So maybe this is fine. Thoughts?
Just put interceptors in UBSAN_STANDALONE_SOURCES.
IMO making ubsan require interceptor support would make using it much less convenient. On macOS, we'd need to change Xcode to get the DYLD_INSERT_LIBRARIES bit right. There are also lots of people who don't use Xcode, and they would need to update their projects as well. I'm in favor of the partial solution where exitcode handling is provided by sanitizer runtimes which already install interceptors (e.g it would work in asan+ubsan mode, or tsan+ubsan mode).
I'll put up the partial solution where we respect exitcode in all sanitizers that already use interceptors when halt_on_error=0. I won't add interception to UBSan. It's unfortunate for me personally, since my codebases only use standalone UBSan, but I can understand why we wouldn't want to add interception to UBSan just for this one feature.
Only adding this for non-standalone builds makes testing hard, since you don't have consistent behavior between standalone and non-standalone ubsan builds. I'm debating leaving UBSan be, and just fixing this for the other sanitizers (ie making sure ASan returns failure when halt_on_error=0).
Hmmm, it actually appears that even TSan doesn't successfully intercept _exit on Linux, only on Darwin (a CHECK(0) statement in OnExit() is never reached on Linux). If we can't successfully intercept _exit, and we can't exit from atexit, I'm not sure that there's a good way to do this.
Looking further into it, _exit family functions don't get called on linux when return from main is used (GIexit in libc calls the exit syscall directly), so interception won't work. TSan and LSan get around this by calling exit() from their atexit() handlers, but we've established that we don't want to do this because of dlclose() issues. I think we're stuck with the current behavior, at least on Linux.
Is this the right place to do this? If UBSan is hosted inside another sanitizer, does that sanitizer already set up such a callback?