Skip to content

Fix uncaught exceptions#5233

Merged
lrineau merged 10 commits intoCGAL:masterfrom
GilesBathgate:fix-uncaught-exceptions
Apr 6, 2021
Merged

Fix uncaught exceptions#5233
lrineau merged 10 commits intoCGAL:masterfrom
GilesBathgate:fix-uncaught-exceptions

Conversation

@GilesBathgate
Copy link
Copy Markdown
Contributor

@GilesBathgate GilesBathgate commented Nov 30, 2020

Summary of Changes

In a previous issue ( #451 ) a CGAL_destructor_assertion macro was added to handle assertions made within destructors. When an exception is thrown from a destructor while the stack is being unwound, the existence of any uncaught exceptions cause the program to terminate immediately, without correct error handling. Sometimes it's safe to throw an exception even while std::uncaught_exception() == true. For example, if stack unwinding causes an object to be destructed, the destructor for that object could run code that throws an exception as long as the exception is caught by some catch block before escaping the destructor. That said coverity static analysis has found several places in the codebase where this is not the case, i.e the destructor calls some code which makes a CGAL_assertion() of some form or another, but doesn't catch the exception.

I propose a new macro:

#define CGAL_destructor_assertion_catch(CODE) try{ CODE } catch(...) { if(std::uncaught_exceptions() <= 0) throw; }

This preserves the assertion exception by rethrowing it if the stack is not being unwound and there are no uncaught exceptions. But in the case where we are recovering from an exception already, only that exception is preserved. A program which demonstrates this behaviour can be seen here: http://cpp.sh/6p7ka

This macro has then been applied to all places identified by static analysis as potentially throwing exceptions from CGAL_assertions. As before ( in #451 ) the CGAL_NOEXCEPT(CGAL_NO_ASSERTIONS_BOOL) macro has to be applied, where needed.

An additional Macro CGAL_destructor_warning was also added, this replicates the behaviour of CGAL_destructor_assertion, except emits a ::CGAL::warning_fail

Release Management

  • Affected package(s): several.
  • Feature/Small Feature (if any): bugfix
  • Link to compiled documentation (obligatory for small feature). I will add documentation if in principle the idea is acceptable.
  • License and copyright ownership: Returned to CGAL authors

@maxGimeno maxGimeno added this to the 5.3-beta milestone Dec 2, 2020
Comment on lines +388 to +402
~HalfedgeDS_list() noexcept {
try {
clear();
} catch (...) {}
}
Copy link
Copy Markdown
Member

@lrineau lrineau Dec 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it possible that a clear() function can trigger? What does it do?!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... Oh... is it about assertions in the clear() functions, that can trigger?

Copy link
Copy Markdown
Contributor Author

@GilesBathgate GilesBathgate Dec 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

precisely, they are all from CGAL_assertion. I didn't think the overhead of this code in release builds was significant, to warrant using CGAL_assertion_code

@lrineau lrineau self-assigned this Dec 14, 2020
@maxGimeno
Copy link
Copy Markdown
Contributor

There are conflicts with master

@lrineau
Copy link
Copy Markdown
Member

lrineau commented Mar 25, 2021

This PR, and particularly 9f9e1e2 is the one responsible for all the compilation errors about boost::any destructor, in the last test results:

/usr/include/boost/any.hpp: In instantiation of 'class boost::any::holder<CGAL::Segment_2<CGAL::Cartesian<double> > >':
/usr/include/boost/any.hpp:253:17:   required from 'ValueType* boost::any_cast(boost::any*) [with ValueType = CGAL::Segment_2<CGAL::Cartesian<double> >]'
/mnt/testsuite/include/CGAL/Object.h:87:43:   required from 'bool CGAL::Object::assign(T&) const [with T = CGAL::Segment_2<CGAL::Cartesian<double> >]'
/mnt/testsuite/include/CGAL/Object.h:157:20:   required from 'bool CGAL::assign(T&, const CGAL::Object&) [with T = CGAL::Segment_2<CGAL::Cartesian<double> >]'
.../test/Triangulation_2/include/CGAL/_test_cls_delaunay_triangulation_2.h:379:24:   required from 'void _test_delaunay_duality(const Del&) [with Del = CGAL::Delaunay_triangulation_2<CGAL::Cartesian<double> >]'
.../test/Triangulation_2/include/CGAL/_test_cls_delaunay_triangulation_2.h:201:26:   required from 'void _test_cls_delaunay_triangulation_2(const Del&) [with Del = CGAL::Delaunay_triangulation_2<CGAL::Cartesian<double> >]'
.../test/Triangulation_2/test_delaunay_triangulation_2.cpp:70:46:   required from here
/usr/include/boost/any.hpp:169:15: error: looser exception specification on overriding virtual function 'virtual boost::any::holder<CGAL::Segment_2<CGAL::Cartesian<double> > >::~holder() noexcept (false)'
  169 |         class holder
      |               ^~~~~~
/usr/include/boost/any.hpp:156:21: note: overridden function is 'virtual boost::any::placeholder::~placeholder() noexcept'
  156 |             virtual ~placeholder()
      |                     ^

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

GilesBathgate commented Mar 25, 2021

This PR, and particularly 9f9e1e2 is the one responsible for all the compilation errors about boost::any destructor

Ah, yes. When the class inherits from another that has a noexcept destructor, it cannot use CGAL_NOEXCEPT(CGAL_NO_ASSERTIONS_BOOL) in fact, the destructor must be noexcept too, which means consequently that the destructor can never throw. Therefore the only sensible pattern that applies is:

~Handle_for() noexcept
    {
      try {
        if (--(ptr_->count) == 0) {
          Allocator_traits::destroy(allocator, ptr_);
          allocator.deallocate( ptr_, 1);
        }
      } catch(...) {}
    }

This means you lose the exception in the destructor entirely, but its better than program termination which is what happens when you throw from a noexcept method.

@GilesBathgate GilesBathgate force-pushed the fix-uncaught-exceptions branch from cdd577d to 1373150 Compare March 25, 2021 09:48
@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@lrineau Fixed as I proposed above.

@maxGimeno
Copy link
Copy Markdown
Contributor

maxGimeno commented Mar 25, 2021

@GilesBathgate Where can we see your fix ? The force push kind of hides the modifications
EDIT: Nevermind, I found it.

@GilesBathgate
Copy link
Copy Markdown
Contributor Author

@GilesBathgate Where can we see your fix ? The force push kind of hides the modifications
EDIT: Nevermind, I found it.

48831ab

@maxGimeno
Copy link
Copy Markdown
Contributor

Thanks !

@maxGimeno
Copy link
Copy Markdown
Contributor

@lrineau lrineau added rm only: ready for master For the release team only: that indicates that a PR is about to be merged in 'master' and removed Ready to be tested Under Testing labels Apr 6, 2021
@lrineau lrineau merged commit 31b817c into CGAL:master Apr 6, 2021
@lrineau lrineau removed the rm only: ready for master For the release team only: that indicates that a PR is about to be merged in 'master' label Apr 7, 2021
}

~Handle_for()
~Handle_for() noexcept
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's useless, it is noexcept by default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably a hang-over from when instead I'd had CGAL_NOEXCEPT(CGAL_NO_ASSERTIONS_BOOL)
I've since found some more uncaught exception issues with the static analysis on the latest runs (where I upgraded from 5.0.3 to master) I will clean this up along with the others and put them in a new PR.

@GilesBathgate GilesBathgate deleted the fix-uncaught-exceptions branch April 19, 2021 14:50
Comment on lines +154 to +159
try{
if (--(ptr_->count) == 0) {
Allocator_traits::destroy(allocator, ptr_);
allocator.deallocate( ptr_, 1);
}
} catch(...) {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not going to fight it, but I want to mention that I am not fond of this new code

  • it is ugly
  • it goes against the default specified by the language

Copy link
Copy Markdown
Contributor Author

@GilesBathgate GilesBathgate Apr 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of the destructors in the CGAL codebase can throw because of a CGAL_assertion, so when it calls destroy/deallocate here the program will terminate. (presumably, that's not desirable, that's what the change intends to prevent)

In most places, this is handled by a CGAL_destructor_assertion_catch which preserves the exception when possible. But here because the noexcept is enforced by a base class we can only swallow the exception.

However you might agree that the correct way to solve all these problems, is never to throw from a destructor in the first place. Then there wouldn't have to be workarounds for the default behaviour specified by the language.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of the destructors in the CGAL codebase can throw because of a CGAL_assertion, so when it calls destroy/deallocate here the program will terminate. (presumably, that's not desirable, that's what the change intends to prevent)

It isn't obvious that it isn't desirable. The standard didn't gratuitously introduce std::terminate, it was better in those circumstances than data corruption or security holes.

In most places, this is handled by a CGAL_destructor_assertion_catch which preserves the exception when possible. But here because the noexcept is enforced by a base class we can only swallow the exception.

However you might agree that the correct way to solve all these problems, is never to throw from a destructor in the first place. Then there wouldn't have to be workarounds for the default behaviour specified by the language.

And assertions are not supposed to trigger, so that's good, no need to introduce workarounds for the case where they trigger 😉
Presumably, we aren't checking preconditions on user input in those destructors. If we assert, it probably means a bug in CGAL, or a side effect of some memory corruption, anyway something bad. We can disable those checks, as your patch does, that makes your checker happy and possibly fails to detect an issue. Or we can have checks that abort if they cannot throw exceptions, which is what the language specifies.

I wonder if separating more clearly debug assertions from checks that can make sense for users would reduce the number of destructors affected.

Some assertions are meant to protect from users who don't know what they are doing, say a user constructing Interval_nt(3,2). CGAL never constructs an interval with a>b, but since CGAL and the user's code call the same constructor, they go through the same assertion, and it looks like the CGAL could throw although it won't. If this happens in a noexcept function (like a destructor), the compiler has to generate instructions on what to do with that non-existing exception, for instance ignore it or abort. The amount of code is essentially the same, there shouldn't be much impact on performance, and since the exception never happens, the behavior is also the same. One code is simpler (no need for try-catch), and on the off chance some memory corruption causes the assertion to trigger, it will safely abort instead of executing random instructions.

Anyway, I said I won't fight...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate your input @mglisse . It's always worthwhile having a discussion even if there are differences in opinion.

Copy link
Copy Markdown
Contributor Author

@GilesBathgate GilesBathgate Apr 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably, we aren't checking preconditions on user input in those destructors.

Not a destructor, but here is a noexcept method with a CGAL_precondition

Handle&
operator=(const Handle& x) noexcept
{
CGAL_precondition( x.PTR != static_cast<Rep*>(0) );
x.PTR->count++;
if ( PTR && (--PTR->count == 0))
delete PTR;
PTR = x.PTR;
return *this;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a destructor, but here is a noexcept method with a CGAL_precondition

Yes, I think I remember adding this noexcept and wondering about the precondition. In the end, I was ok with having the program terminate if a null pointer was found in debug mode. The precondition isn't there in release mode. Not testing in debug mode would be a regression. Making the function conditionally noexcept would have been an option, but it can change the path taken by the code, and it felt more useful to lie a bit and have debug mode test something closer to release mode, than to insist on propagating the error as an exception. Of course different opinions are always possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the function conditionally noexcept

In our usage of the CGAL library (RapCAD and OpenSCAD) we enable CGAL_precondition in release mode, so this would be more suitable for us I think.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you check the prerequisites in inputs before calling the functions? Or is it too expensive compared to do a try-catch?

Copy link
Copy Markdown
Contributor Author

@GilesBathgate GilesBathgate Apr 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you check the prerequisites in inputs before calling the functions?

Not sure I follow what you mean @sloriot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants