From 571cb33dc2db5d5552ce101dc4a9ba2e56020a71 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:03:16 -0700 Subject: [PATCH 01/13] Report instead of terminate on test_records setup failure The global setup in main() runs before Catch2 takes over, so a throw from Layout::create(), RecProcessInit() or the session runner escaped main and aborted the test binary with no diagnostic. Catch the escape, print it, and exit non-zero so the failure is still reportable. Coverity CID 1644283, CID 1528654, CID 1523670. --- src/records/unit_tests/unit_test_main.cc | 40 ++++++++++++++++-------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/records/unit_tests/unit_test_main.cc b/src/records/unit_tests/unit_test_main.cc index e03d219efce..5aba4f0a935 100644 --- a/src/records/unit_tests/unit_test_main.cc +++ b/src/records/unit_tests/unit_test_main.cc @@ -18,6 +18,9 @@ the License. */ +#include +#include +#include #include #include #include "tscore/ink_resolver.h" @@ -34,17 +37,28 @@ extern void ts_session_protocol_well_known_name_indices_init(); int main(int argc, char *argv[]) { - // Set the global diags variable - Layout::create(); // RecProcess will fail if Layout is not created. - DiagsPtr::set(new CatchDiags); - RecProcessInit(); - // Global data initialization needed for the unit tests. - ts_session_protocol_well_known_name_indices_init(); - // Cheat for ts_host_res_global_init as there's no records.config to check for non-default. - host_res_default_preference_order = HOST_RES_DEFAULT_PREFERENCE_ORDER; - int result = Catch::Session().run(argc, argv); - - // global clean-up... - - return result; + // The global initialization below runs outside of any Catch2 assertion, so an exception thrown + // there would otherwise escape main and abort without a diagnostic. + try { + // Set the global diags variable + Layout::create(); // RecProcess will fail if Layout is not created. + DiagsPtr::set(new CatchDiags); + RecProcessInit(); + // Global data initialization needed for the unit tests. + ts_session_protocol_well_known_name_indices_init(); + // Cheat for ts_host_res_global_init as there's no records.config to check for non-default. + host_res_default_preference_order = HOST_RES_DEFAULT_PREFERENCE_ORDER; + + int result = Catch::Session().run(argc, argv); + + // global clean-up... + + return result; + } catch (std::exception const &ex) { + std::fprintf(stderr, "test_records aborted: %s\n", ex.what()); + } catch (...) { + std::fprintf(stderr, "test_records aborted: unknown exception\n"); + } + + return EXIT_FAILURE; } From 40c76318b9928eab245030afac25cef43b59920c Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:03:30 -0700 Subject: [PATCH 02/13] Report exceptions escaping main() in the HPACK test runner Argument parsing, the std::string path setup and the EThread allocation can all throw, and an exception leaving main() unwinds with no diagnostic. Catch it, print it and exit non-zero so the failure is still reportable. Coverity CID 1528601. --- src/proxy/http2/test_HPACK.cc | 51 ++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/proxy/http2/test_HPACK.cc b/src/proxy/http2/test_HPACK.cc index cde31b7c13f..2785b0d1e4b 100644 --- a/src/proxy/http2/test_HPACK.cc +++ b/src/proxy/http2/test_HPACK.cc @@ -27,6 +27,7 @@ #include "iocore/eventsystem/Thread.h" #include #include +#include #include #include #include @@ -393,30 +394,38 @@ REGRESSION_TEST(HPACK_Encoding)(RegressionTest *t, int /* atype ATS_UNUSED */, i int main(int argc, const char **argv) { - auto &version = AppVersionInfo::setup_version("test_HPACK"); - process_args(&version, argument_descriptions, countof(argument_descriptions), argv); + try { + auto &version = AppVersionInfo::setup_version("test_HPACK"); + process_args(&version, argument_descriptions, countof(argument_descriptions), argv); - ink_freelist_init_ops(cmd_disable_freelist, cmd_disable_pfreelist); + ink_freelist_init_ops(cmd_disable_freelist, cmd_disable_pfreelist); - if (*cmd_input_dir) { - input_dir = cmd_input_dir; - if (input_dir.back() != '/') { - input_dir += '/'; + if (*cmd_input_dir) { + input_dir = cmd_input_dir; + if (input_dir.back() != '/') { + input_dir += '/'; + } } - } - if (*cmd_output_dir) { - output_dir = cmd_output_dir; - if (output_dir.back() != '/') { - output_dir += '/'; + if (*cmd_output_dir) { + output_dir = cmd_output_dir; + if (output_dir.back() != '/') { + output_dir += '/'; + } } - } - Thread *main_thread = new EThread; - main_thread->set_specific(); - url_init(); - mime_init(); - http_init(); - prepare(); - int status = RegressionTest::main(argc, argv, REGRESSION_TEST_QUICK); - return status; + Thread *main_thread = new EThread; + main_thread->set_specific(); + url_init(); + mime_init(); + http_init(); + prepare(); + int status = RegressionTest::main(argc, argv, REGRESSION_TEST_QUICK); + return status; + } catch (const std::exception &e) { + cerr << "test_HPACK: unhandled exception: " << e.what() << endl; + return 2; + } catch (...) { + cerr << "test_HPACK: unhandled exception of unknown type" << endl; + return 2; + } } From 262bf9ed3164d87fa1f365c3596a272fa6ef2d7c Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:06:02 -0700 Subject: [PATCH 03/13] Keep client teardown exceptions inside the ParentTest dtor Destructors are implicitly noexcept, so an exception raised while closing or deleting the client transaction terminates traffic_server with no indication of where it came from. Abort with a message instead. Coverity CID 1559190. --- src/api/InkAPITest.cc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/api/InkAPITest.cc b/src/api/InkAPITest.cc index 3ba3d903a96..eab364b3fb6 100644 --- a/src/api/InkAPITest.cc +++ b/src/api/InkAPITest.cc @@ -7271,8 +7271,15 @@ struct ParentTest { ~ParentTest() { - synclient_txn_close(this->browser); - synclient_txn_delete(this->browser); + // A destructor is implicitly noexcept, so anything escaping the client + // teardown would terminate the process without saying where it came from. + try { + synclient_txn_close(this->browser); + synclient_txn_delete(this->browser); + } catch (...) { + ink_abort("ParentTest teardown threw an exception"); + } + synserver_delete(this->os); this->os = nullptr; this->magic = MAGIC_DEAD; From 535037ac624f1955ac2923b6fd518717952849fe Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:06:13 -0700 Subject: [PATCH 04/13] Keep server teardown exceptions inside the ParentTest dtor Deleting the synthetic server is the second path out of this noexcept destructor, and an exception from it terminates traffic_server just as silently, so bring it under the same guard. Coverity CID 1518135. --- src/api/InkAPITest.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/InkAPITest.cc b/src/api/InkAPITest.cc index eab364b3fb6..7900878c543 100644 --- a/src/api/InkAPITest.cc +++ b/src/api/InkAPITest.cc @@ -7271,16 +7271,16 @@ struct ParentTest { ~ParentTest() { - // A destructor is implicitly noexcept, so anything escaping the client - // teardown would terminate the process without saying where it came from. + // A destructor is implicitly noexcept, so anything escaping the teardown + // calls would terminate the process without saying where it came from. try { synclient_txn_close(this->browser); synclient_txn_delete(this->browser); + synserver_delete(this->os); } catch (...) { ink_abort("ParentTest teardown threw an exception"); } - synserver_delete(this->os); this->os = nullptr; this->magic = MAGIC_DEAD; } From 724107ff61bdf16e73ac39ca58bd4c057ae9e46d Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:12:45 -0700 Subject: [PATCH 05/13] Mark the ~CacheTestSM uncaught exception a false positive The only calls the destructor makes are ink_assert() and MIOBuffer teardown, and the _ink_assert() linked into traffic_server aborts. Coverity instead resolves the symbol to the throwing test-only definition in eventsystem/unit_tests/test_MIOBufferWriter.cc, which that file already annotates for the same reason. Coverity CID 1528624. --- src/iocore/cache/CacheTest.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/iocore/cache/CacheTest.cc b/src/iocore/cache/CacheTest.cc index d171ac6b1fc..33cbfc08e69 100644 --- a/src/iocore/cache/CacheTest.cc +++ b/src/iocore/cache/CacheTest.cc @@ -47,6 +47,9 @@ CacheTestSM::CacheTestSM(const CacheTestSM &ao) : RegressionSM(ao) SET_HANDLER(&CacheTestSM::event_handler); } +// Coverity resolves ink_assert() to the throwing _ink_assert() defined by +// eventsystem/unit_tests/test_MIOBufferWriter.cc, not the one linked here. +// coverity[UNCAUGHT_EXCEPT:FALSE] CacheTestSM::~CacheTestSM() { ink_assert(!cache_action); From 25c048122a44fc3d4fc4836d599e47f776ede2e0 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:13:03 -0700 Subject: [PATCH 06/13] Mark the ~CacheWriteTest uncaught exception a false positive The destructor only frees an MIOBuffer and destroys two HTTPInfos, both of which assert. The cache unit tests link the aborting _ink_assert() from tscore, but Coverity resolves the symbol to the throwing test-only definition in eventsystem/unit_tests/test_MIOBufferWriter.cc. Coverity CID 1528646. --- src/iocore/cache/unit_tests/main.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/iocore/cache/unit_tests/main.h b/src/iocore/cache/unit_tests/main.h index c43df36e20b..a736fa6cb8a 100644 --- a/src/iocore/cache/unit_tests/main.h +++ b/src/iocore/cache/unit_tests/main.h @@ -188,6 +188,9 @@ class CacheWriteTest : public CacheTestBase build_hdrs(this->info, url); } + // free_MIOBuffer() and HTTPInfo::destroy() assert; Coverity resolves + // _ink_assert() to the throwing definition in test_MIOBufferWriter.cc. + // coverity[UNCAUGHT_EXCEPT:FALSE] ~CacheWriteTest() override { if (this->_write_buffer) { From 6739b99854058bd85681bff1626429f7ed678936 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:13:20 -0700 Subject: [PATCH 07/13] Mark the ~CacheReadTest uncaught exception a false positive The destructor only frees an MIOBuffer and destroys an HTTPInfo, both of which assert. The cache unit tests link the aborting _ink_assert() from tscore, but Coverity resolves the symbol to the throwing test-only definition in eventsystem/unit_tests/test_MIOBufferWriter.cc. Coverity CID 1528704. --- src/iocore/cache/unit_tests/main.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/iocore/cache/unit_tests/main.h b/src/iocore/cache/unit_tests/main.h index a736fa6cb8a..bdb59ec48f4 100644 --- a/src/iocore/cache/unit_tests/main.h +++ b/src/iocore/cache/unit_tests/main.h @@ -258,6 +258,9 @@ class CacheReadTest : public CacheTestBase build_hdrs(this->info, url); } + // free_MIOBuffer() and HTTPInfo::destroy() assert; Coverity resolves + // _ink_assert() to the throwing definition in test_MIOBufferWriter.cc. + // coverity[UNCAUGHT_EXCEPT:FALSE] ~CacheReadTest() override { if (this->_read_buffer) { From bd73241893728935f5db01e5c06ca18a123161a7 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:13:38 -0700 Subject: [PATCH 08/13] Mark the ~TestContChain uncaught exception a false positive next_test() reaches EThread::schedule(), which asserts. The cache unit tests link the aborting _ink_assert() from tscore, but Coverity resolves the symbol to the throwing test-only definition in eventsystem/unit_tests/test_MIOBufferWriter.cc. Coverity CID 1528771. --- src/iocore/cache/unit_tests/CacheTestHandler.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/iocore/cache/unit_tests/CacheTestHandler.h b/src/iocore/cache/unit_tests/CacheTestHandler.h index d4351a13246..58b08fe95ba 100644 --- a/src/iocore/cache/unit_tests/CacheTestHandler.h +++ b/src/iocore/cache/unit_tests/CacheTestHandler.h @@ -37,6 +37,9 @@ class CacheTestBase; struct TestContChain : public Continuation { TestContChain(); + // EThread::schedule() asserts; Coverity resolves _ink_assert() to the + // throwing definition in eventsystem/unit_tests/test_MIOBufferWriter.cc. + // coverity[UNCAUGHT_EXCEPT:FALSE] virtual ~TestContChain() { this->next_test(); } void From e76e33f40653d5ca3e4075b1a5631da24138a8dc Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:09:43 -0700 Subject: [PATCH 09/13] Report exceptions escaping the AIO benchmark's main() Config parsing, EThread/AIO_Device allocation and the event system startup in main() can all throw, and nothing caught them, so an exception left main() and killed the process with no diagnostic. Coverity CID 1644327. Coverity CID 1528590. Coverity CID 1523686. --- src/iocore/aio/test_AIO.cc | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/iocore/aio/test_AIO.cc b/src/iocore/aio/test_AIO.cc index 3ae808bbd17..fd7240d5916 100644 --- a/src/iocore/aio/test_AIO.cc +++ b/src/iocore/aio/test_AIO.cc @@ -29,6 +29,7 @@ #include "tscore/Layout.h" #include "tscore/TSSystemState.h" #include "tscore/Random.h" +#include #include #include #include @@ -452,9 +453,8 @@ class IOUringLoopTailHandler : public EThread::LoopTailHandler #endif -// coverity[exn_spec_violation] - called functions may throw but this is a test program -int -main(int argc, char *argv[]) +static int +run_test(int argc, char *argv[]) { int i; @@ -552,4 +552,20 @@ main(int argc, char *argv[]) sleep(1); #endif } + + return 0; +} + +int +main(int argc, char *argv[]) +{ + try { + return run_test(argc, argv); + } catch (std::exception const &e) { + fprintf(stderr, "test_AIO aborted: %s\n", e.what()); + } catch (...) { + fprintf(stderr, "test_AIO aborted: unknown exception\n"); + } + + return 1; } From dc1de0471478fe3e563a6d92f333d089828bb455 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:10:25 -0700 Subject: [PATCH 10/13] Report exceptions escaping the RefCountCache test's main() test() allocates caches, strings and set entries, so it can throw, and nothing caught it; the exception left main() and terminated the test with no diagnostic and no exit status distinguishable from a crash. Coverity CID 1686026. Coverity CID 1587267. Coverity CID 1587256. --- src/iocore/hostdb/unit_tests/test_RefCountCache.cc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/iocore/hostdb/unit_tests/test_RefCountCache.cc b/src/iocore/hostdb/unit_tests/test_RefCountCache.cc index 4be77b6c5a5..a28b97a5b8e 100644 --- a/src/iocore/hostdb/unit_tests/test_RefCountCache.cc +++ b/src/iocore/hostdb/unit_tests/test_RefCountCache.cc @@ -25,6 +25,8 @@ #include "iocore/eventsystem/EventSystem.h" #include "tscore/Layout.h" #include "iocore/utils/diags.i" +#include +#include #include // TODO: add tests with expiry_time @@ -260,7 +262,15 @@ test() int main() { - int ret = test(); + int ret = 1; + + try { + ret = test(); + } catch (std::exception const &e) { + fprintf(stderr, "test_RefCountCache aborted: %s\n", e.what()); + } catch (...) { + fprintf(stderr, "test_RefCountCache aborted: unknown exception\n"); + } for (const auto item : ExampleStruct::items_freed) { printf("really freeing: %p\n", item); From 9b58f66e4fa6c1d2b9c92e686cedc0a21a2efaaf Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:14:14 -0700 Subject: [PATCH 11/13] Keep teardown exceptions inside the ~HoldOnEThread dtor Destructors are implicitly noexcept, so an exception raised while cancelling the scheduled callback or waiting for it to finish terminates the test binary with no indication of where it came from. Abort with a message instead. Coverity CID 1686062. --- src/iocore/eventsystem/unit_tests/test_Lock.cc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/iocore/eventsystem/unit_tests/test_Lock.cc b/src/iocore/eventsystem/unit_tests/test_Lock.cc index 2194d059382..b42c80b8d60 100644 --- a/src/iocore/eventsystem/unit_tests/test_Lock.cc +++ b/src/iocore/eventsystem/unit_tests/test_Lock.cc @@ -25,6 +25,7 @@ #include "inkevent_test_fixtures.h" #include +#include #include @@ -48,10 +49,16 @@ class HoldOnEThread : public Continuation // In case of an exception in a thread that would have set release, we set // it here in order to unfreeze any threads that may be waiting on done. + // A destructor is implicitly noexcept, so anything escaping the teardown + // would terminate the test binary without saying where it came from. ~HoldOnEThread() { - this->cancel_callback(); - this->wait_for_callback_finish(); + try { + this->cancel_callback(); + this->wait_for_callback_finish(); + } catch (...) { + ink_abort("HoldOnEThread teardown threw an exception"); + } } bool From d950b5eee4e0261ba38ab874d3af826696b44a44 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:14:38 -0700 Subject: [PATCH 12/13] Keep buffer teardown exceptions inside the ~NetVCTest dtor Destructors are implicitly noexcept, so an exception raised while logging or returning either MIOBuffer terminates traffic_server with no indication of where it came from. Abort with a message instead. Coverity CID 1528569. Coverity CID 1518878. --- src/iocore/net/NetVCTest.cc | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/iocore/net/NetVCTest.cc b/src/iocore/net/NetVCTest.cc index 9c436157e9e..235f75443ea 100644 --- a/src/iocore/net/NetVCTest.cc +++ b/src/iocore/net/NetVCTest.cc @@ -38,6 +38,7 @@ #include "iocore/net/NetVConnection.h" #include "P_NetVCTest.h" #include "tscore/ink_atomic.h" +#include "tscore/ink_error.h" // Each test requires two definition entries. One for the passive // side of the connection and one for the active side @@ -88,18 +89,24 @@ NetVCTest::~NetVCTest() { mutex = nullptr; - if (read_buffer) { - Dbg(dbg_ctl, "Freeing read MIOBuffer with %d blocks on %s", read_buffer->max_block_count(), - (test_cont_type == NET_VC_TEST_ACTIVE) ? "Active" : "Passive"); - free_MIOBuffer(read_buffer); - read_buffer = nullptr; - } + // A destructor is implicitly noexcept, so anything escaping the buffer + // teardown would terminate the process without saying where it came from. + try { + if (read_buffer) { + Dbg(dbg_ctl, "Freeing read MIOBuffer with %d blocks on %s", read_buffer->max_block_count(), + (test_cont_type == NET_VC_TEST_ACTIVE) ? "Active" : "Passive"); + free_MIOBuffer(read_buffer); + read_buffer = nullptr; + } - if (write_buffer) { - Dbg(dbg_ctl, "Freeing write MIOBuffer with %d blocks on %s", write_buffer->max_block_count(), - (test_cont_type == NET_VC_TEST_ACTIVE) ? "Active" : "Passive"); - free_MIOBuffer(write_buffer); - write_buffer = nullptr; + if (write_buffer) { + Dbg(dbg_ctl, "Freeing write MIOBuffer with %d blocks on %s", write_buffer->max_block_count(), + (test_cont_type == NET_VC_TEST_ACTIVE) ? "Active" : "Passive"); + free_MIOBuffer(write_buffer); + write_buffer = nullptr; + } + } catch (...) { + ink_abort("NetVCTest teardown threw an exception"); } } From f0907249d02191c0d48c2eba16a37df47fd56e3e Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Mon, 14 Sep 2026 21:40:07 -0700 Subject: [PATCH 13/13] Mark the ~EasyURL uncaught exception a false positive HdrHeap::destroy() reaches THREAD_FREE(), whose thread_freeup() asserts on its postcondition, and Coverity resolves _ink_assert() to the throwing definition in test_MIOBufferWriter.cc. The exception cannot occur in a binary that links the aborting stub, so suppress rather than handle it. Coverity CID 1591506. --- src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc b/src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc index 513982370c4..0183f06f51a 100644 --- a/src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc +++ b/src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc @@ -145,6 +145,9 @@ struct EasyURL { url.create(heap); url.parse(s); } + // THREAD_FREE() reaches thread_freeup(), which asserts; Coverity resolves + // _ink_assert() to the throwing definition in test_MIOBufferWriter.cc. + // coverity[UNCAUGHT_EXCEPT:FALSE] ~EasyURL() { heap->destroy(); } };