Allow propagating exceptions from server methods

sdbusplus calls the D-Bus method implementation functions in such a way
that the callstack contains sd_bus C functions from libsystemd, and
sdbusplus only catches the specific exceptions that match the errors
defined for the D-Bus method before returning execution to the C
functions. If any D-Bus method implementation propagates any other
exception than the errors the method declares, the C functions in the
callstack may leak resources.

There isn't any documentation or other statement that would prohibit
exceptions from being thrown / propagated from method implementations.
This seems to be a known issue though [1]:

    By throwing an exception, you're not making an error return to the
    calling client, but instead blowing through all of the `sd_bus` C
    code with your C++ exception and putting your application into an
    invalid state. At a minimum you are leaking memory.

Thus, catch any exceptions propagated from method implementations and
re-throw them from sdbusplus::bus::bus::process*().

Catching and re-throwing avoids resource leaks from sd_bus C functions.
And as a consequence it would allow propagating exceptions in the normal
way until the caller that is prepared to handle them -- ultimately even
up to main(). Propagating exceptions to `main()` allows for terminating
the process in a controlled fashion in case unexpected failures, which,
in turn, allows for a chance to recover from potential failures modes
that persist  until application restart (e.g. due to some invalid state
stored in RAM).

Also, terminating the application, in which case the D-Bus daemon
returns the standard error org.freedesktop.DBus.Error.NoReply to the
client, avoids the need to declare an error for internal failures in the
D-Bus API. Internal errors in API add little value over
org.freedesktop.DBus.Error.NoReply and bloat the API as clients many
times can't handle them in any better way than
org.freedesktop.DBus.Error.NoReply.

Only std::exceptions are catched (i.e. not `catch (...)`) and propagated
to keep the implementation simple. The assumption is that only
exceptions (i.e.  classes derived from std::exception) are used for
reporting errors.

Tested: With the calculator example changed to throw an unexpected exception:

    From 914e045d1f92a1730d32f84ac7a74cd867c76760 Mon Sep 17 00:00:00 2001
    From: Hannu Lounento <hannu.lounento@vaisala.com>
    Date: Mon, 13 Mar 2023 15:53:17 +0200
    Subject: [PATCH] [dbg] Add a propagating exception to the calculator example

    Not to be merged to sdbusplus master but only for RFC / demonstration
    purposes.

    Can be triggered by commands like

        busctl --user call net.poettering.Calculator /net/poettering/calculator net.poettering.Calculator Clear
        busctl --user get-property net.poettering.Calculator /net/poettering/calculator net.poettering.Calculator LastResult
        busctl --user set-property net.poettering.Calculator /net/poettering/calculator net.poettering.Calculator LastResult x 1

    Change-Id: Ie9f47227afe7e0fa9765c602bec987aea71b9b86
    Signed-off-by: Hannu Lounento <hannu.lounento@vaisala.com>
    ---
    example/calculator-server.cpp | 31 ++++++++++++++++++++++++++-----
    1 file changed, 26 insertions(+), 5 deletions(-)

    diff --git a/example/calculator-server.cpp b/example/calculator-server.cpp
    index aa60662..eba9cc0 100644
    --- a/example/calculator-server.cpp
    +++ b/example/calculator-server.cpp
    @@ -3,6 +3,7 @@
    #include <net/poettering/Calculator/server.hpp>
    #include <sdbusplus/server.hpp>

    +#include <cstdlib>
    #include <iostream>
    #include <string_view>

    @@ -42,10 +43,22 @@ struct Calculator : Calculator_inherit
        /** Clear lastResult, broadcast 'Cleared' signal */
        void clear() override
        {
    -        auto v = lastResult();
    -        lastResult(0);
    -        cleared(v);
    -        return;
    +        throw std::runtime_error{"A propagating exception"};
    +    }
    +
    +    int64_t lastResult() const override
    +    {
    +        throw std::runtime_error{"A propagating exception"};
    +    }
    +
    +    int64_t lastResult(int64_t /*value*/, bool /*skipSignal*/) override
    +    {
    +        throw std::runtime_error{"A propagating exception"};
    +    }
    +
    +    int64_t lastResult(int64_t /*value*/) override
    +    {
    +        throw std::runtime_error{"A propagating exception"};
        }
    };

    @@ -71,5 +84,13 @@ int main()
        Calculator c1{b, path};

        // Handle dbus processing forever.
    -    b.process_loop();
    +    try
    +    {
    +        b.process_loop();
    +    }
    +    catch (const std::exception& e)
    +    {
    +        std::cerr << "Terminating due to a fatal condition: " << e.what() << std::endl;
    +        return EXIT_FAILURE;
    +    }
    }
    --
    2.40.0

1) Built and executed unit tests:

    sdbusplus$ rm -rf build/ && CXXFLAGS=-Wno-maybe-uninitialized meson setup build && cd build && ninja && ninja test
    The Meson build system
    Version: 0.62.2
    Source dir: /path/to/sdbusplus
    Build dir: /path/to/sdbusplus/build
    Build type: native build
    Project name: sdbusplus
    Project version: 1.0.0
    C compiler for the host machine: ccache cc (gcc 12.2.1 "cc (GCC) 12.2.1 20221121 (Red Hat 12.2.1-4)")
    C linker for the host machine: cc ld.bfd 2.37-37
    C++ compiler for the host machine: ccache c++ (gcc 12.2.1 "c++ (GCC) 12.2.1 20221121 (Red Hat 12.2.1-4)")
    C++ linker for the host machine: c++ ld.bfd 2.37-37
    Host machine cpu family: x86_64
    Host machine cpu: x86_64
    Found pkg-config: /usr/bin/pkg-config (1.8.0)
    Run-time dependency libsystemd found: YES 250
    Program python3 (inflection, yaml, mako) found: YES (/usr/bin/python3) modules: inflection, yaml, mako
    Run-time dependency Boost found: YES 1.76.0 (/usr)
    Program sdbus++ found: YES (/path/to/sdbusplus/tools/sdbus++)
    Program sdbus++ found: YES (overridden)
    Program sdbus++-gen-meson found: YES (/path/to/sdbusplus/tools/sdbus++-gen-meson)
    Program sdbus++-gen-meson found: YES (overridden)
    Header <boost/asio.hpp> has symbol "boost::asio::io_context" : YES
    Run-time dependency Boost (found: context, coroutine) found: YES 1.76.0 (/usr)
    Run-time dependency GTest found: YES 1.11.0
    Run-time dependency GMock found: YES 1.11.0
    Build targets in project: 34

    Found ninja-1.10.2 at /usr/bin/ninja
    [77/77] Linking target example/calculator-client
    [0/1] Running all tests.
    1/21 test_async_task                     OK              0.03s
    2/21 test_bus_list_names                 OK              0.02s
    3/21 test_bus_match                      OK              0.02s
    4/21 test_bus_exception                  OK              0.02s
    5/21 test_exception_sdbus_error          OK              0.02s
    6/21 test_message_append                 OK              0.01s
    7/21 test_message_native_types           OK              0.01s
    8/21 test_message_read                   OK              0.03s
    9/21 test_message_types                  OK              0.02s
    10/21 test_unpack_properties              OK              0.02s
    11/21 test_utility_tuple_to_array         OK              0.02s
    12/21 test_utility_type_traits            OK              0.01s
    13/21 test-bus_aio                        OK              0.01s
    14/21 test-vtable                         OK              0.01s
    15/21 test-server                         OK              0.01s
    16/21 test-server-message-variant         OK              0.01s
    17/21 test_message_call                   OK              0.07s
    18/21 test_event_event                    OK              0.31s
    19/21 test_async_timer                    OK              0.51s
    20/21 test_async_context                  OK              0.52s
    21/21 test_timer                          OK             18.02s

    Ok:                 21
    Expected Fail:      0
    Fail:               0
    Unexpected Pass:    0
    Skipped:            0
    Timeout:            0

    [...]

-Wno-maybe-uninitialized was used because the current master
384943be7e81b08ed102abfaa3f2be2ad915e800 ("sdbus++: async: client: fix
client_t usage") failed to build with

    In file included from /usr/include/gtest/internal/gtest-death-test-internal.h:39,
                    from /usr/include/gtest/gtest-death-test.h:41,
                    from /usr/include/gtest/gtest.h:64,
                    from /usr/include/gmock/internal/gmock-internal-utils.h:47,
                    from /usr/include/gmock/gmock-actions.h:145,
                    from /usr/include/gmock/gmock.h:59,
                    from ../include/sdbusplus/test/sdbus_mock.hpp:6,
                    from ../test/exception/sdbus_error.cpp:4:
    In copy constructor ‘testing::internal::MatcherBase<T>::MatcherBase(const testing::internal::MatcherBase<T>&) [with T = sd_bus_error*]’,
        inlined from ‘testing::Matcher<sd_bus_error*>::Matcher(const testing::Matcher<sd_bus_error*>&)’ at /usr/include/gtest/gtest-matchers.h:479:7,
        inlined from ‘testing::internal::MockSpec<int(sd_bus_error*, int)> sdbusplus::SdBusMock::gmock_sd_bus_error_set_errno(const testing::Matcher<sd_bus_error*>&, const testing::Matcher<int>&)’ at ../include/sdbusplus/test/sdbus_mock.hpp:51:5,
        inlined from ‘virtual void {anonymous}::SdBusError_NotSetErrno_Test::TestBody()’ at ../test/exception/sdbus_error.cpp:59:5:
    /usr/include/gtest/gtest-matchers.h:302:33: error: ‘<unnamed>.testing::Matcher<sd_bus_error*>::<unnamed>.testing::internal::MatcherBase<sd_bus_error*>::buffer_’ may be used uninitialized [-Werror=maybe-uninitialized]
    302 |       : vtable_(other.vtable_), buffer_(other.buffer_) {
        |                                 ^~~~~~~~~~~~~~~~~~~~~~

on Fedora 36 with

    $ gcc --version
    gcc (GCC) 12.2.1 20221121 (Red Hat 12.2.1-4)

and

    $ dnf info gtest-devel
    [...]
    Name         : gtest-devel
    Version      : 1.11.0
    Release      : 2.fc36
    Architecture : x86_64

2) Verified no memory leaks were reported by Valgrind anymore:

    sdbusplus/build$ valgrind --leak-check=full --show-leak-kinds=all ./example/calculator-server
    ==61886== Memcheck, a memory error detector
    ==61886== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
    ==61886== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
    ==61886== Command: ./example/calculator-server
    ==61886==

and called the throwing method:

    build$ busctl --user call net.poettering.Calculator /net/poettering/calculator net.poettering.Calculator Clear
    Call failed: Remote peer disconnected

and verified the process terminates but produces no memory leaks:

    Terminating due to a fatal condition: A propagating exception
    ==61886==
    ==61886== HEAP SUMMARY:
    ==61886==     in use at exit: 0 bytes in 0 blocks
    ==61886==   total heap usage: 143 allocs, 143 frees, 96,732 bytes allocated
    ==61886==
    ==61886== All heap blocks were freed -- no leaks are possible
    ==61886==
    ==61886== For lists of detected and suppressed errors, rerun with: -s
    ==61886== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Similarly reading the `LastResult` property:

    build$ valgrind --leak-check=full --show-leak-kinds=all ./example/calculator-server
    ==36140== Memcheck, a memory error detector
    ==36140== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
    ==36140== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
    ==36140== Command: ./example/calculator-server
    ==36140==

    build$ busctl --user get-property net.poettering.Calculator /net/poettering/calculator net.poettering.Calculator LastResult
    Failed to get property LastResult on interface net.poettering.Calculator: Remote peer disconnected

    Terminating due to a fatal condition: A propagating exception
    ==36140==
    ==36140== HEAP SUMMARY:
    ==36140==     in use at exit: 0 bytes in 0 blocks
    ==36140==   total heap usage: 148 allocs, 148 frees, 96,992 bytes allocated
    ==36140==
    ==36140== All heap blocks were freed -- no leaks are possible
    ==36140==
    ==36140== For lists of detected and suppressed errors, rerun with: -s
    ==36140== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

and writing the property:

    ==36383== Memcheck, a memory error detector
    ==36383== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
    ==36383== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
    ==36383== Command: ./example/calculator-server
    ==36383==

    build$ busctl --user set-property net.poettering.Calculator /net/poettering/calculator net.poettering.Calculator LastResult x 1

    Terminating due to a fatal condition: A propagating exception
    ==36383==
    ==36383== HEAP SUMMARY:
    ==36383==     in use at exit: 0 bytes in 0 blocks
    ==36383==   total heap usage: 146 allocs, 146 frees, 97,010 bytes allocated
    ==36383==
    ==36383== All heap blocks were freed -- no leaks are possible
    ==36383==
    ==36383== For lists of detected and suppressed errors, rerun with: -s
    ==36383== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

[1] https://lore.kernel.org/openbmc/YTDvfIn4Z05mGdCx@heinlein/

Change-Id: I014099a91f526a92c0f6646e75b4029513ad19a8
Signed-off-by: Hannu Lounento <hannu.lounento@vaisala.com>
diff --git a/include/sdbusplus/bus.hpp b/include/sdbusplus/bus.hpp
index 21dfaf9..697804d 100644
--- a/include/sdbusplus/bus.hpp
+++ b/include/sdbusplus/bus.hpp
@@ -9,6 +9,7 @@
 
 #include <algorithm>
 #include <climits>
+#include <exception>
 #include <memory>
 #include <optional>
 #include <string>
@@ -194,6 +195,11 @@
     {
         sd_bus_message* m = nullptr;
         int r = _intf->sd_bus_process(_bus.get(), &m);
+        if (current_exception)
+        {
+            auto ex = std::exchange(current_exception, nullptr);
+            std::rethrow_exception(ex);
+        }
         if (r < 0)
         {
             throw exception::SdBusError(-r, "sd_bus_process");
@@ -207,6 +213,11 @@
     auto process_discard()
     {
         int r = _intf->sd_bus_process(_bus.get(), nullptr);
+        if (current_exception)
+        {
+            auto ex = std::exchange(current_exception, nullptr);
+            std::rethrow_exception(ex);
+        }
         if (r < 0)
         {
             throw exception::SdBusError(-r, "sd_bus_process discard");
@@ -477,6 +488,11 @@
         return _intf;
     }
 
+    void set_current_exception(std::exception_ptr exception)
+    {
+        current_exception = exception;
+    }
+
     friend struct details::bus_friend;
 
   protected:
@@ -486,6 +502,9 @@
     }
     sdbusplus::SdBusInterface* _intf;
     details::bus _bus;
+
+  private:
+    std::exception_ptr current_exception;
 };
 
 inline bus::bus(busp_t b, sdbusplus::SdBusInterface* intf) :
diff --git a/test/bus/exception.cpp b/test/bus/exception.cpp
new file mode 100644
index 0000000..b34398d
--- /dev/null
+++ b/test/bus/exception.cpp
@@ -0,0 +1,30 @@
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/test/sdbus_mock.hpp>
+
+#include <exception>
+
+#include <gtest/gtest.h>
+
+class Exception : public ::testing::Test
+{
+  protected:
+    sdbusplus::SdBusMock sdbusMock;
+    sdbusplus::bus_t bus = sdbusplus::get_mocked_new(&sdbusMock);
+    std::exception_ptr e =
+        std::make_exception_ptr(std::runtime_error{"current exception"});
+
+    void SetUp() override
+    {
+        bus.set_current_exception(e);
+    }
+};
+
+TEST_F(Exception, BusProcessRethrowsTheCurrentException)
+{
+    EXPECT_THROW(bus.process(), std::runtime_error);
+}
+
+TEST_F(Exception, BusProcessDiscardRethrowsTheCurrentException)
+{
+    EXPECT_THROW(bus.process_discard(), std::runtime_error);
+}
diff --git a/test/meson.build b/test/meson.build
index 0181766..2efdfeb 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -23,6 +23,7 @@
     'async/context',
     'async/task',
     'async/timer',
+    'bus/exception',
     'bus/list_names',
     'bus/match',
     'event/event',
diff --git a/tools/sdbusplus/templates/interface.server.cpp.mako b/tools/sdbusplus/templates/interface.server.cpp.mako
index 2f9af97..2d7d32d 100644
--- a/tools/sdbusplus/templates/interface.server.cpp.mako
+++ b/tools/sdbusplus/templates/interface.server.cpp.mako
@@ -1,3 +1,4 @@
+#include <exception>
 #include <map>
 #include <sdbusplus/sdbus.hpp>
 #include <sdbusplus/sdbuspp_support/server.hpp>
diff --git a/tools/sdbusplus/templates/method.prototype.hpp.mako b/tools/sdbusplus/templates/method.prototype.hpp.mako
index d39491a..9618abd 100644
--- a/tools/sdbusplus/templates/method.prototype.hpp.mako
+++ b/tools/sdbusplus/templates/method.prototype.hpp.mako
@@ -68,9 +68,7 @@
 {
     auto o = static_cast<${interface.classname}*>(context);
 
-    % if method.errors:
     try
-    % endif
     {
         return sdbusplus::sdbuspp::method_callback\
     % if len(method.returns) > 1:
@@ -92,6 +90,11 @@
         return o->get_bus().getInterface()->sd_bus_error_set(error, e.name(), e.description());
     }
     % endfor
+    catch (const std::exception&)
+    {
+        o->get_bus().set_current_exception(std::current_exception());
+        return 1;
+    }
 }
 
 namespace details
diff --git a/tools/sdbusplus/templates/property.prototype.hpp.mako b/tools/sdbusplus/templates/property.prototype.hpp.mako
index aa35671..bc7c323 100644
--- a/tools/sdbusplus/templates/property.prototype.hpp.mako
+++ b/tools/sdbusplus/templates/property.prototype.hpp.mako
@@ -28,9 +28,7 @@
 {
     auto o = static_cast<${interface.classname}*>(context);
 
-    % if property.errors:
     try
-    % endif
     {
         return sdbusplus::sdbuspp::property_callback(
                 reply, o->get_bus().getInterface(), error,
@@ -47,6 +45,11 @@
         return o->get_bus().getInterface()->sd_bus_error_set(error, e.name(), e.description());
     }
     % endfor
+    catch (const std::exception&)
+    {
+        o->get_bus().set_current_exception(std::current_exception());
+        return 1;
+    }
 }
 
 auto ${interface.classname}::${property.camelCase}(${property.cppTypeParam(interface.name)} value,
@@ -79,9 +82,7 @@
 {
     auto o = static_cast<${interface.classname}*>(context);
 
-    % if property.errors:
     try
-    % endif
     {
         return sdbusplus::sdbuspp::property_callback(
                 value, o->get_bus().getInterface(), error,
@@ -98,6 +99,11 @@
         return o->get_bus().getInterface()->sd_bus_error_set(error, e.name(), e.description());
     }
     % endfor
+    catch (const std::exception&)
+    {
+        o->get_bus().set_current_exception(std::current_exception());
+        return 1;
+    }
 }
 % endif