This is an archive of the discontinued LLVM Phabricator instance.

[libc++] Avoid UB in the no-exceptions mode in a few places
ClosedPublic

Authored by ldionne on Feb 5 2019, 9:18 AM.

Details

Summary

A few places in the library seem to behave unexpectedly when the library
is compiled or used with exceptions disabled. For example, not throwing
an exception when a pointer is NULL can lead us to dereference the pointer
later on, which is UB. This patch fixes such occurences.

It's hard to tell whether there are other places where the no-exceptions
mode misbehaves like this, because the replacement for throwing an
exception does not always seem to be abort()ing, but at least this
patch will improve the situation somewhat.

See http://lists.llvm.org/pipermail/libcxx-dev/2019-January/000172.html

Diff Detail

Repository
rL LLVM

Event Timeline

ldionne created this revision.Feb 5 2019, 9:18 AM
ldionne marked 12 inline comments as done.Feb 5 2019, 9:29 AM

Here's the justification for changes in this patch.

libcxx/include/unordered_map
1606 ↗(On Diff #185328)

Justification: If we don't abort(), we dereference __i below, which is a past-the-end iterator.

1616 ↗(On Diff #185328)

Ditto.

libcxx/src/hash.cpp
179 ↗(On Diff #185328)

I guess nothing good happens if we keep going after n has overflown, but TBH I haven't done a deep analysis of this one.

libcxx/src/ios.cpp
271 ↗(On Diff #185328)

Actually, looking at this again, it seems like we probably don't want to abort here, since the stream will report the error with the error flags. So I should not change this one -- @mclow.lists does that seem right to you?

312 ↗(On Diff #185328)

Justification: If we don't abort() when new_callbacks is null, we assign new_callbacks to __fn_ on line 343 and we then access __fn[__event_size] on line 350, which is UB.

317 ↗(On Diff #185328)

Ditto.

324 ↗(On Diff #185328)

Ditto.

331 ↗(On Diff #185328)

Ditto.

350 ↗(On Diff #185328)

UB here if new_callbacks is null and we don't abort().

libcxx/src/locale.cpp
472 ↗(On Diff #185328)

Justification: If we don't abort(), we will access facets_ at an out-of-bounds index id. The check has_facet(id) roughly checks whether id is in bound for facets_.

539 ↗(On Diff #185328)

Justification: If we don't abort(), we create the std::string member name_ inside __imp from the name parameter (which is null). It's UB to construct a std::string from a null char const*.

552 ↗(On Diff #185328)

Ditto.

jfb added a reviewer: jfb.Feb 5 2019, 10:03 AM
dexonsmith added inline comments.Feb 5 2019, 10:03 AM
libcxx/src/ios.cpp
271 ↗(On Diff #185328)

::abort seems more consistent to me. The user has set the exception bit, so I wouldn't expect them to have control flow to handle errors otherwise. Moreover, the direct user could be templated/inlined header code which doesn't know that -fno-exceptions has been passed for this project/TU.

More generally, I think a strict library policy of turning throws into aborts will lead to behaviour that's predictable for users.

I think all of these are no-brainers except the one calling __throw_failure.

libcxx/src/ios.cpp
271 ↗(On Diff #185328)

If we do this, we should be consistent about it.
Do we want to say "Compiling with exceptions disabled makes iostreams act as if state & __exceptions_ is always false?

And then what do we do if the user explicitly sets the __exceptions_ flag? Abort there?

jfb added a comment.Feb 5 2019, 10:29 AM

I'm wondering if, as a follow-up, the XFAIL tests that validate the behavior for exceptions should instead:

  • Not XFAIL
  • Install a SIGABORT handler, and call exit(0) or something
  • Instead of catch, call exit(1) because we expected the abort handler to be called.

That type of testing will find any missing fixes from this patch (and prevent regressions).

ldionne marked an inline comment as done.Feb 5 2019, 10:53 AM
In D57761#1385562, @jfb wrote:

I'm wondering if, as a follow-up, the XFAIL tests that validate the behavior for exceptions should instead:

  • Not XFAIL
  • Install a SIGABORT handler, and call exit(0) or something
  • Instead of catch, call exit(1) because we expected the abort handler to be called.

That type of testing will find any missing fixes from this patch (and prevent regressions).

Yes, I agree we should test the no-exceptions behaviour just like we test the behaviour with exceptions enabled. There's a question of whether this should be part of test/std or test/libcxx, however, since the Standard currently doesn't mention implementations without exceptions (I think).

libcxx/src/ios.cpp
271 ↗(On Diff #185328)

Moreover, the direct user could be templated/inlined header code which doesn't know that -fno-exceptions has been passed for this project/TU.

We don't have a story for mixing code built with -fno-exceptions with code built with -fexceptions. For starters, it's an ODR violation to do that because the functions in the headers will have different definitions. One would technically have to enable per-TU ABI insulation with _LIBCPP_HIDE_FROM_ABI_PER_TU to make this work, but I would _strongly_ recommend against recommending that as a solution.

More generally, I think a strict library policy of turning throws into aborts will lead to behaviour that's predictable for users.

I agree.

Do we want to say "Compiling with exceptions disabled makes iostreams act as if state & __exceptions_ is always false?
And then what do we do if the user explicitly sets the __exceptions_ flag? Abort there?

That's certainly not what I want. I think I'm with Duncan when he says that a simple and strict policy of turning throws into aborts is the right thing to do. Cleverness is the enemy here.

In that case, it seems like I should keep this change.

jfb added a comment.Feb 5 2019, 10:58 AM
In D57761#1385562, @jfb wrote:

I'm wondering if, as a follow-up, the XFAIL tests that validate the behavior for exceptions should instead:

  • Not XFAIL
  • Install a SIGABORT handler, and call exit(0) or something
  • Instead of catch, call exit(1) because we expected the abort handler to be called.

That type of testing will find any missing fixes from this patch (and prevent regressions).

To clarify my logic based on a private chat with @mclow.lists: Louis is changing code that's clearly doing the wrong thing, yet no tests are being changed. Louis' change is correct, therefore the tests were inadequate.

I don't think this patch really needs to fix the tests, but I think the tests need a bit of love for this odd use case.

In D57761#1385646, @jfb wrote:

I don't think this patch really needs to fix the tests, but I think the tests need a bit of love for this odd use case.

Not so odd, really. For example, it's reasonable to test that a user won't get UB if they fetch a key that does not exist in a map and exceptions are disabled. If we don't, we can basically declare our support for -fno-exceptions to be inexistent.

In D57761#1385646, @jfb wrote:

I don't think this patch really needs to fix the tests, but I think the tests need a bit of love for this odd use case.

Not so odd, really. For example, it's reasonable to test that a user won't get UB if they fetch a key that does not exist in a map and exceptions are disabled. If we don't, we can basically declare our support for -fno-exceptions to be inexistent.

(As I said to JF in IRC) It's reasonable to test that in test/libcxx, not lib/std.

dexonsmith added inline comments.Feb 7 2019, 3:41 PM
libcxx/src/ios.cpp
271 ↗(On Diff #185328)

Moreover, the direct user could be templated/inlined header code which doesn't know that -fno-exceptions has been passed for this project/TU.

We don't have a story for mixing code built with -fno-exceptions with code built with -fexceptions. For starters, it's an ODR violation to do that because the functions in the headers will have different definitions. One would technically have to enable per-TU ABI insulation with _LIBCPP_HIDE_FROM_ABI_PER_TU to make this work, but I would _strongly_ recommend against recommending that as a solution.

FWIW, I wasn't necessarily suggesting mixing code between -fexceptions and -fno-exceptions. Consider:

  • A header-only library A has code that uses iostream somehow.
  • A project B builds with -fno-exceptions and uses library A.
  • A project C builds with -fexceptions and uses library A.
  • Project B and project C are completely unrelated, except that they share a dependency.

I don't think library A should need to detect whether exceptions are enabled to do the right thing for both project B and project C. The simple abort-on-throw rule will simplify life for library A.

ldionne updated this revision to Diff 186318.Feb 11 2019, 1:07 PM

Added tests for the abort() behavior and fixed UB I had missed in map::at().

@mclow.lists @jfb I added tests per your request. Please LMK if this is satisfactory.

jfb added a comment.Feb 11 2019, 1:13 PM

The tests are exactly what I had in mind, great! LGTM.

libcxx/test/libcxx/containers/associative/map/at.abort.pass.cpp
30 ↗(On Diff #186318)

*chef's kiss*

mclow.lists accepted this revision.Feb 11 2019, 2:26 PM

This looks fine to me - thanks!

This revision is now accepted and ready to land.Feb 11 2019, 2:26 PM
This revision was automatically updated to reflect the committed changes.
Herald added a project: Restricted Project. · View Herald TranscriptFeb 12 2019, 8:06 AM