This is an archive of the discontinued LLVM Phabricator instance.

Respect exitcode sanitizer option in UBSan
AbandonedPublic

Authored by fjricci on Jul 6 2017, 1:54 PM.

Details

Reviewers
vsk
glider
rsmith
Summary

This patch ensures that UBSan returns the exit code specified
by sanitizer_flags if it detects an error during execution.

Event Timeline

fjricci created this revision.Jul 6 2017, 1:54 PM
rsmith edited edge metadata.Jul 7 2017, 1:44 PM

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?

fjricci added inline comments.Jul 7 2017, 5:07 PM
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?

vsk added inline comments.Jul 10 2017, 11:47 AM
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.

fjricci added inline comments.Jul 10 2017, 12:35 PM
lib/ubsan/ubsan_init.cc
44

LSan uses Atext(DoLeakCheck), and DoLeakCheck() calls Die(), so it should be safe.

test/ubsan/TestCases/Float/cast-overflow.cpp
3

The exitcode flag defaults to returning failure on error (lib/sanitizer_common/sanitizer_flags.inc), which I think makes sense.

vsk edited edge metadata.Jul 10 2017, 1:08 PM
vsk added a subscriber: eugenis.

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.

fjricci added inline comments.Jul 10 2017, 1:19 PM
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.

vsk added a comment.Jul 10 2017, 1:38 PM

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.

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?

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.

Just put interceptors in UBSAN_STANDALONE_SOURCES.

vsk added a comment.Jul 11 2017, 4:38 PM

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.

fjricci abandoned this revision.Jul 12 2017, 2:35 PM

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.