Skip to content

Abort when throwing std::logic_error#407

Merged
cole-h merged 3 commits intomainfrom
abort-on-logic-error-v2
Mar 31, 2026
Merged

Abort when throwing std::logic_error#407
cole-h merged 3 commits intomainfrom
abort-on-logic-error-v2

Conversation

@cole-h
Copy link
Copy Markdown
Member

@cole-h cole-h commented Mar 30, 2026

There are many places where a thrown std::logic_error is handled like a normal error, making it very difficult to get a good stack trace so we can fix that logic error.

So, we wrap the __cxa_throw function such that we can check if the thrown exception was a std::logic_error, and if so, abort the process so we can figure out exactly where it happened.

Closes #331

Motivation

Context

Summary by CodeRabbit

  • New Features

    • Detects thrown std::logic_error (and derived types) at runtime and triggers immediate termination with configurable behavior.
  • Tests

    • Added death tests to verify process termination for logic_error and several related standard exceptions.
  • Chores

    • Build now includes a new exception-handling subtree and conditionally enables the feature and its tests when supported.

@cole-h cole-h requested a review from edolstra March 30, 2026 17:46
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c93bdb3b-d003-47bd-8485-12a418b9c1f5

📥 Commits

Reviewing files that changed from the base of the PR and between 042f5a5 and f69b305.

📒 Files selected for processing (3)
  • nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc
  • nix-meson-build-support/common/cxa-throw/is-logic-error.hh
  • nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc

📝 Walkthrough

Walkthrough

Adds build-time selectable interception (interpose or linker wrap) of the C++ ABI function __cxa_throw to detect throws of std::logic_error (and derived types) and invoke a configurable handler (defaults to abort); includes build integration and GoogleTest death-tests.

Changes

Cohort / File(s) Summary
Interpose implementation
nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc
New interposed extern "C" void __cxa_throw(...) that classifies tinfo via is_logic_error and calls abort_on_exception on match; otherwise resolves and forwards to the original __cxa_throw via dlsym(RTLD_NEXT, "__cxa_throw").
Linker-wrap implementation
nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc
New __wrap___cxa_throw and declaration of __real___cxa_throw that check is_logic_error and call abort_on_exception when matched, then forward to __real___cxa_throw.
Type classifier & handler
nix-meson-build-support/common/cxa-throw/is-logic-error.hh
New header with static bool is_logic_error(const std::type_info*) (matches std::logic_error, walks single-inheritance ABI info) and static void abort_on_exception(void*, const std::type_info*) that prints info and calls CXA_THROW_ON_LOGIC_ERROR() (defaults to abort()).
cxa-throw Meson probe & libs
nix-meson-build-support/common/cxa-throw/meson.build
Feature detection: test interposition (with dl) or linker wrap (-Wl,--wrap=__cxa_throw); on success build static lib (interpose or wrap), declare cxa_throw_dep, and set have_cxa_throw.
Build subtree inclusion
nix-meson-build-support/common/meson.build
Adds subdir('cxa-throw') so the cxa-throw subtree is processed.
Library integration
src/libutil/meson.build
When have_cxa_throw is true, appends cxa_throw_dep to the library's dependency list.
Test integration & tests
src/libutil-tests/meson.build, src/libutil-tests/cxa-throw.cc
Conditionally include cxa-throw.cc in tests when have_cxa_throw; new GoogleTest death-tests asserting process termination when throwing std::logic_error and several derived std exceptions.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Thrower as Throwing code
participant Interposer as Interposed / Wrapped __cxa_throw
participant Classifier as is_logic_error
participant Handler as CXA_THROW_ON_LOGIC_ERROR()
participant Resolver as dlsym / __real___cxa_throw
participant LibC as original __cxa_throw

Thrower->>Interposer: throw(exc, tinfo, dest)
Interposer->>Classifier: is_logic_error(tinfo)
alt logic_error match
Interposer->>Handler: CXA_THROW_ON_LOGIC_ERROR()
Handler-->>Interposer: (terminates)
else not logic_error
Interposer->>Resolver: resolve original throw
Resolver-->>Interposer: original function ptr
Interposer->>LibC: call original(exc, tinfo, dest)
LibC-->>Interposer: (does not return)
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • grahamc

Poem

🐰 I sniffed the typeinfo with whiskers alert,
If logic_error hopped by, I stamped on my cert.
I whisper to stderr, then stomp and depart,
Else I nudge the old thrower and let it restart.
🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Abort when throwing std::logic_error' clearly and concisely summarizes the main change—adding exception handling to abort on logic errors.
Linked Issues check ✅ Passed The PR implementation fully meets the linked issue #331 objectives by detecting std::logic_error and aborting, while preserving normal behavior for other exceptions.
Out of Scope Changes check ✅ Passed All changes directly support the core objective of detecting and aborting on std::logic_error; no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch abort-on-logic-error-v2

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/libutil-tests/cxa-throw.cc (1)

6-35: Good coverage of std::logic_error family, but missing negative test.

The death tests comprehensively cover std::logic_error and its standard subclasses. However, there's no test verifying that non-logic-error exceptions (e.g., std::runtime_error) are not aborted and can be caught normally. This would validate that the mechanism is selective.

Suggested additional test
TEST(CxaThrow, doesNotCatchRuntimeError)
{
    // Verify that non-logic_error exceptions are not intercepted
    bool caught = false;
    try {
        throw std::runtime_error("test");
    } catch (const std::runtime_error &) {
        caught = true;
    }
    ASSERT_TRUE(caught);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libutil-tests/cxa-throw.cc` around lines 6 - 35, Add a negative test
named TEST(CxaThrow, doesNotCatchRuntimeError) that verifies non-logic_error
exceptions are not intercepted: in the test throw std::runtime_error("test")
inside a try/catch, set a local bool (e.g., caught) to true in the catch(const
std::runtime_error&) block, and ASSERT_TRUE(caught) at the end; this complements
the existing TEST(CxaThrow,... ) cases (catchesLogicErrorFromStdlib,
catchesLogicError, catchesOutOfRange, catchesInvalidArgument,
catchesDomainError, catchesLengthError) to ensure runtime_error is handled
normally rather than causing abortion.
nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc (1)

24-32: Consider adding [[noreturn]] attribute.

Same as the interpose version - __wrap___cxa_throw never returns normally.

Proposed fix
-extern "C" void __wrap___cxa_throw(void * exc, std::type_info * tinfo, void (*dest)(void *))
+extern "C" [[noreturn]] void __wrap___cxa_throw(void * exc, std::type_info * tinfo, void (*dest)(void *))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc` around lines 24 -
32, The wrapper function __wrap___cxa_throw never returns, so add the
[[noreturn]] attribute to its declaration/definition (extern "C" [[noreturn]]
void __wrap___cxa_throw(...)) to match the interpose version; keep the existing
logic (is_logic_error check, CXA_THROW_ON_LOGIC_ERROR(), call to
__real___cxa_throw(exc, tinfo, dest) and the __builtin_unreachable()) but mark
the function [[noreturn]] so callers and the compiler know it does not return.
nix-meson-build-support/common/cxa-throw/meson.build (1)

18-24: Hardcoded -ldl in test may cause false negatives on musl/BSD systems.

Line 20 uses args : [ '-ldl' ] unconditionally, but on systems like musl libc or some BSDs, dlsym is in libc and -ldl doesn't exist. This could cause the interpose test to fail compilation even when interposition would work, incorrectly falling through to the wrap approach.

Consider using cxx.find_library('dl', required : false) for the test as well:

Proposed fix
+dl_dep = cxx.find_library('dl', required : false)
+dl_args = dl_dep.found() ? [ '-ldl' ] : []
+
 can_interpose_cxa_throw_result = cxx.run(
   can_interpose_cxa_throw_test_code,
-  args : [ '-ldl' ],
+  args : dl_args,
   include_directories : include_directories('.'),
   name : 'can interpose __cxa_throw (catches libstdc++ throws)',
 )

Or use the dependency directly if cxx.run() supports it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-meson-build-support/common/cxa-throw/meson.build` around lines 18 - 24,
The test for interposing __cxa_throw uses a hardcoded args : [ '-ldl' ] which
can break on musl/BSD where libdl isn't separate; update the cxx.run invocation
that produces can_interpose_cxa_throw_result to use a discovered optional dl
library instead: call cxx.find_library('dl', required : false) (or the project's
equivalent) and pass that library/dependency into cxx.run (or its deps/args)
only when found, so can_interpose_cxa_throw_result.compiled() reflects the true
ability to link dlsym without forcing -ldl; adjust code that sets
can_interpose_cxa_throw to use the new result unchanged.
nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc (1)

25-37: Consider adding [[noreturn]] attribute.

The __cxa_throw function never returns normally (it either aborts or throws). Adding the [[noreturn]] attribute would allow the compiler to better optimize callers and provide better diagnostics.

Proposed fix
-extern "C" void __cxa_throw(void * exc, std::type_info * tinfo, void (*dest)(void *))
+extern "C" [[noreturn]] void __cxa_throw(void * exc, std::type_info * tinfo, void (*dest)(void *))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc` around lines
25 - 37, Add the C++ [[noreturn]] attribute to the interposed function and
matching function-pointer typedef so the compiler knows __cxa_throw never
returns; specifically modify the extern "C" void __cxa_throw(...) definition to
be extern "C" [[noreturn]] void __cxa_throw(void *exc, std::type_info *tinfo,
void (*dest)(void *)) and update the cxa_throw_type alias (the type used for
orig) to a pointer-to-[[noreturn]] function signature so the orig lookup and
subsequent call have the noreturn annotation as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc`:
- Around line 13-23: The wrap implementation’s explicit typeid comparisons must
be replaced to match interpose’s recursive detection: modify the logic in
wrap-cxa-throw.cc that checks exception types (the block doing explicit typeid
comparisons against std::logic_error subclasses) to use the same recursive
traversal as is_logic_error in interpose-cxa-throw.cc — perform a dynamic_cast
to __cxxabiv1::__si_class_type_info, follow __base_type recursively and compare
*tinfo == typeid(std::logic_error) so custom subclasses are detected;
alternatively, if you prefer the explicit-list behavior, update
interpose-cxa-throw.cc to mirror the explicit comparisons instead (choose one
approach and make both files consistent).

---

Nitpick comments:
In `@nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc`:
- Around line 25-37: Add the C++ [[noreturn]] attribute to the interposed
function and matching function-pointer typedef so the compiler knows __cxa_throw
never returns; specifically modify the extern "C" void __cxa_throw(...)
definition to be extern "C" [[noreturn]] void __cxa_throw(void *exc,
std::type_info *tinfo, void (*dest)(void *)) and update the cxa_throw_type alias
(the type used for orig) to a pointer-to-[[noreturn]] function signature so the
orig lookup and subsequent call have the noreturn annotation as well.

In `@nix-meson-build-support/common/cxa-throw/meson.build`:
- Around line 18-24: The test for interposing __cxa_throw uses a hardcoded args
: [ '-ldl' ] which can break on musl/BSD where libdl isn't separate; update the
cxx.run invocation that produces can_interpose_cxa_throw_result to use a
discovered optional dl library instead: call cxx.find_library('dl', required :
false) (or the project's equivalent) and pass that library/dependency into
cxx.run (or its deps/args) only when found, so
can_interpose_cxa_throw_result.compiled() reflects the true ability to link
dlsym without forcing -ldl; adjust code that sets can_interpose_cxa_throw to use
the new result unchanged.

In `@nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc`:
- Around line 24-32: The wrapper function __wrap___cxa_throw never returns, so
add the [[noreturn]] attribute to its declaration/definition (extern "C"
[[noreturn]] void __wrap___cxa_throw(...)) to match the interpose version; keep
the existing logic (is_logic_error check, CXA_THROW_ON_LOGIC_ERROR(), call to
__real___cxa_throw(exc, tinfo, dest) and the __builtin_unreachable()) but mark
the function [[noreturn]] so callers and the compiler know it does not return.

In `@src/libutil-tests/cxa-throw.cc`:
- Around line 6-35: Add a negative test named TEST(CxaThrow,
doesNotCatchRuntimeError) that verifies non-logic_error exceptions are not
intercepted: in the test throw std::runtime_error("test") inside a try/catch,
set a local bool (e.g., caught) to true in the catch(const std::runtime_error&)
block, and ASSERT_TRUE(caught) at the end; this complements the existing
TEST(CxaThrow,... ) cases (catchesLogicErrorFromStdlib, catchesLogicError,
catchesOutOfRange, catchesInvalidArgument, catchesDomainError,
catchesLengthError) to ensure runtime_error is handled normally rather than
causing abortion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15eb542e-b1d2-481d-b597-0dd5a3660fba

📥 Commits

Reviewing files that changed from the base of the PR and between cb9989b and e29e81d.

📒 Files selected for processing (7)
  • nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc
  • nix-meson-build-support/common/cxa-throw/meson.build
  • nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc
  • nix-meson-build-support/common/meson.build
  • src/libutil-tests/cxa-throw.cc
  • src/libutil-tests/meson.build
  • src/libutil/meson.build

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 30, 2026

@github-actions github-actions bot temporarily deployed to pull request March 30, 2026 17:56 Inactive
@cole-h cole-h force-pushed the abort-on-logic-error-v2 branch from e29e81d to debf93d Compare March 30, 2026 18:02
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
nix-meson-build-support/common/cxa-throw/meson.build (1)

54-72: Consider portability of linker flags.

The --wrap and -u linker flags are GNU ld / lld specific. This should be fine since the feature test will naturally fail on systems with incompatible linkers (e.g., macOS ld64), correctly disabling the feature. Just noting for awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-meson-build-support/common/cxa-throw/meson.build` around lines 54 - 72,
The linker flags '-Wl,--wrap=__cxa_throw' and '-Wl,-u,__wrap___cxa_throw' are
GNU/LLD-specific; update the feature gating around
wrap_cxa_throw_args/can_wrap_cxa_throw_result/cxa_throw_dep so we only attempt
the cxx.run test and create the static_library/declare_dependency when the
detected linker/platform supports those flags (e.g., skip on Darwin or check
cc.get_id()/compiler.get_linker_name() or host_machine.system() before using
wrap_cxa_throw_args). Ensure the platform/linker check is performed before
defining wrap_cxa_throw_args or invoking cxx.run so the test won’t run with
incompatible linkers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@nix-meson-build-support/common/cxa-throw/meson.build`:
- Around line 54-72: The linker flags '-Wl,--wrap=__cxa_throw' and
'-Wl,-u,__wrap___cxa_throw' are GNU/LLD-specific; update the feature gating
around wrap_cxa_throw_args/can_wrap_cxa_throw_result/cxa_throw_dep so we only
attempt the cxx.run test and create the static_library/declare_dependency when
the detected linker/platform supports those flags (e.g., skip on Darwin or check
cc.get_id()/compiler.get_linker_name() or host_machine.system() before using
wrap_cxa_throw_args). Ensure the platform/linker check is performed before
defining wrap_cxa_throw_args or invoking cxx.run so the test won’t run with
incompatible linkers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b26fdd5-bd9c-496e-a275-7cf76cafc5f4

📥 Commits

Reviewing files that changed from the base of the PR and between e29e81d and debf93d.

📒 Files selected for processing (8)
  • nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc
  • nix-meson-build-support/common/cxa-throw/is-logic-error.hh
  • nix-meson-build-support/common/cxa-throw/meson.build
  • nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc
  • nix-meson-build-support/common/meson.build
  • src/libutil-tests/cxa-throw.cc
  • src/libutil-tests/meson.build
  • src/libutil/meson.build
✅ Files skipped from review due to trivial changes (2)
  • src/libutil-tests/meson.build
  • src/libutil/meson.build
🚧 Files skipped from review as they are similar to previous changes (1)
  • nix-meson-build-support/common/meson.build

@cole-h cole-h force-pushed the abort-on-logic-error-v2 branch from debf93d to ff6468d Compare March 30, 2026 18:08
This will fail until the next commit.
@cole-h cole-h force-pushed the abort-on-logic-error-v2 branch from ff6468d to 7d4e727 Compare March 30, 2026 18:08
@cole-h
Copy link
Copy Markdown
Member Author

cole-h commented Mar 30, 2026

At the very least, the test does indeed appear to properly abort now (giving us an actual backtrace):

#0  0x00007f39e58e284c in __pthread_kill_implementation ()
   from /nix/store/g8zyryr9cr6540xsyg4avqkwgxpnwj2a-glibc-2.40-66/lib/libc.so.6
#1  0x00007f39e588a406 in raise () from /nix/store/g8zyryr9cr6540xsyg4avqkwgxpnwj2a-glibc-2.40-66/lib/libc.so.6
#2  0x00007f39e587193a in abort () from /nix/store/g8zyryr9cr6540xsyg4avqkwgxpnwj2a-glibc-2.40-66/lib/libc.so.6
#3  0x00007f39e5ecb10f in __cxa_throw ()
   from /nix/store/c0js3shh15shiqkskq93jhxb658ys3bb-determinate-nix-util-3.16.3/lib/libnixutil.so.2.33.3
#4  0x00007f39e5b2e342 in std::__throw_logic_error(char const*) ()
   from /nix/store/dj06r96j515npcqi9d8af1d1c60bx2vn-gcc-14.3.0-lib/lib/libstdc++.so.6
#5  0x000000000042affe in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&) [clone .lto_priv.0] [clone .cold] ()
#6  0x000000000061b124 in CxaThrow_catchesLogicErrorFromStdlib_Test::TestBody() ()
#7  0x00007f39e5d561fb in void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) ()
   from /nix/store/fdi6k88sl1ykmjsngcsanrlzd87a4r91-gtest-1.16.0/lib/libgtest.so.1.16.0
#8  0x00007f39e5d42c96 in testing::Test::Run() ()
   from /nix/store/fdi6k88sl1ykmjsngcsanrlzd87a4r91-gtest-1.16.0/lib/libgtest.so.1.16.0
#9  0x00007f39e5d42e35 in testing::TestInfo::Run() ()
   from /nix/store/fdi6k88sl1ykmjsngcsanrlzd87a4r91-gtest-1.16.0/lib/libgtest.so.1.16.0
#10 0x00007f39e5d4306f in testing::TestSuite::Run() ()
   from /nix/store/fdi6k88sl1ykmjsngcsanrlzd87a4r91-gtest-1.16.0/lib/libgtest.so.1.16.0
#11 0x00007f39e5d4cb27 in testing::internal::UnitTestImpl::RunAllTests() ()
   from /nix/store/fdi6k88sl1ykmjsngcsanrlzd87a4r91-gtest-1.16.0/lib/libgtest.so.1.16.0
#12 0x00007f39e5d4cf27 in testing::UnitTest::Run() ()
   from /nix/store/fdi6k88sl1ykmjsngcsanrlzd87a4r91-gtest-1.16.0/lib/libgtest.so.1.16.0
#13 0x00007f39e5d720c0 in main () from /nix/store/fdi6k88sl1ykmjsngcsanrlzd87a4r91-gtest-1.16.0/lib/libgtest_main.so.1.16.0
#14 0x00007f39e587347e in __libc_start_call_main ()
   from /nix/store/g8zyryr9cr6540xsyg4avqkwgxpnwj2a-glibc-2.40-66/lib/libc.so.6
#15 0x00007f39e5873539 in __libc_start_main_impl ()
   from /nix/store/g8zyryr9cr6540xsyg4avqkwgxpnwj2a-glibc-2.40-66/lib/libc.so.6
#16 0x00000000004b3745 in _start ()

Where it wouldn't have before:

Note: Google Test filter = CxaThrow.catchesLogicErrorFromStdlib
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from CxaThrow
[ RUN      ] CxaThrow.catchesLogicErrorFromStdlib
../cxa-throw.cc:9: Failure
Death test: { std::string s(p); }
    Result: threw an exception.
 Error msg:
[  DEATH   ]
[  DEATH   ] ../cxa-throw.cc:9:: Caught std::exception-derived exception escaping the death test statement. Exception message: basic_string: construction from null is not valid
[  DEATH   ]

[  FAILED  ] CxaThrow.catchesLogicErrorFromStdlib (0 ms)
[----------] 1 test from CxaThrow (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] CxaThrow.catchesLogicErrorFromStdlib

 1 FAILED TEST

There are many places where a thrown `std::logic_error` is handled like
a normal error, making it very difficult to get a good stack trace so
we can fix that logic error.

So, we wrap the __cxa_throw function such that we can check if the thrown
exception was a `std::logic_error`, and if so, abort the process so we
can figure out exactly where it happened.

Co-authored-by: Eelco Dolstra <edolstra@gmail.com>
@cole-h cole-h force-pushed the abort-on-logic-error-v2 branch from 7d4e727 to 4cfdfb2 Compare March 30, 2026 18:09
@github-actions github-actions bot temporarily deployed to pull request March 30, 2026 18:11 Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
nix-meson-build-support/common/cxa-throw/meson.build (1)

12-13: Prefer a defined stdlib throw here unless the null-string backtrace is the point.

std::string(nullptr) is undefined by the C++ API and only throws here because libstdc++ adds a check. If the goal is just to exercise a stdlib std::logic_error path, a defined throw site such as std::string{}.at(1) is much less brittle.

Example change
-    const char * volatile p = nullptr;
-    std::string s(p);
+    std::string s;
+    s.at(1);
-      const char * volatile p = nullptr;
-      std::string s(p);
+      std::string s;
+      s.at(1);

Also applies to: 48-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-meson-build-support/common/cxa-throw/meson.build` around lines 12 - 13,
Replace the undefined call std::string s(p) (where p is declared as const char *
volatile p = nullptr) with a defined standard-library throw site; for example,
remove the null-pointer construction and use a bounds-checked string access like
std::string{}.at(1) to reliably trigger a std::out_of_range (or explicitly throw
a std::logic_error if that exact exception type is required). Apply the same
change at the other occurrence referenced (the code around the symbols at the
second occurrence corresponding to the previous std::string(nullptr) usage on
lines 48-49).
src/libutil-tests/cxa-throw.cc (1)

6-35: Tighten this suite around both the abort and pass-through paths.

These tests only prove that some logic-error paths die. They do not cover the inverse requirement from the PR (std::runtime_error and other non-logic_error throws must still propagate normally), and ASSERT_DEATH(..., "") also accepts deaths that are not specifically abort(). An over-broad matcher or a different fatal path would still go green here.

Example tightening
+#include <csignal>
-    ASSERT_DEATH({ throw std::logic_error("test"); }, "");
+    ASSERT_EXIT({ throw std::logic_error("test"); },
+        ::testing::KilledBySignal(SIGABRT), "");
+TEST(CxaThrow, ignoresRuntimeError)
+{
+    EXPECT_THROW([] { throw std::runtime_error("test"); }(), std::runtime_error);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libutil-tests/cxa-throw.cc` around lines 6 - 35, Replace the broad
ASSERT_DEATH(...) checks in the CxaThrow tests with precise checks that the
process aborts via SIGABRT (use gtest's KilledBySignal(SIGABRT) / a regex that
matches "Aborted") for the logic_error variants (tests named
CxaThrow::catchesLogicError, catchesOutOfRange, catchesInvalidArgument,
catchesDomainError, catchesLengthError and the stdlib string-nullptr case), and
add a new test (e.g., CxaThrow::catchesRuntimeErrorPropagates) that uses
ASSERT_THROW({ throw std::runtime_error("test"); }, std::runtime_error) to
verify std::runtime_error and other non-logic_error exceptions propagate instead
of causing abort; update only the assertions in those tests (no behavior
changes).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nix-meson-build-support/common/cxa-throw/is-logic-error.hh`:
- Around line 12-14: The current is_logic_error function only handles
single-inheritance RTTI (__si_class_type_info) and misses multiple-inheritance
cases; update is_logic_error to also detect __vmi_class_type_info, iterate over
all its base class entries (accessing each base type pointer from the vmi
structure) and recursively call is_logic_error on each base_type, returning true
if any base is a logic_error subclass; keep the existing si branch, add nullptr
checks for the vmi base array/count, and ensure you return false only after all
bases have been checked.

---

Nitpick comments:
In `@nix-meson-build-support/common/cxa-throw/meson.build`:
- Around line 12-13: Replace the undefined call std::string s(p) (where p is
declared as const char * volatile p = nullptr) with a defined standard-library
throw site; for example, remove the null-pointer construction and use a
bounds-checked string access like std::string{}.at(1) to reliably trigger a
std::out_of_range (or explicitly throw a std::logic_error if that exact
exception type is required). Apply the same change at the other occurrence
referenced (the code around the symbols at the second occurrence corresponding
to the previous std::string(nullptr) usage on lines 48-49).

In `@src/libutil-tests/cxa-throw.cc`:
- Around line 6-35: Replace the broad ASSERT_DEATH(...) checks in the CxaThrow
tests with precise checks that the process aborts via SIGABRT (use gtest's
KilledBySignal(SIGABRT) / a regex that matches "Aborted") for the logic_error
variants (tests named CxaThrow::catchesLogicError, catchesOutOfRange,
catchesInvalidArgument, catchesDomainError, catchesLengthError and the stdlib
string-nullptr case), and add a new test (e.g.,
CxaThrow::catchesRuntimeErrorPropagates) that uses ASSERT_THROW({ throw
std::runtime_error("test"); }, std::runtime_error) to verify std::runtime_error
and other non-logic_error exceptions propagate instead of causing abort; update
only the assertions in those tests (no behavior changes).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6142eee-baa0-4937-abdd-0dd079300b61

📥 Commits

Reviewing files that changed from the base of the PR and between debf93d and 4cfdfb2.

📒 Files selected for processing (8)
  • nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc
  • nix-meson-build-support/common/cxa-throw/is-logic-error.hh
  • nix-meson-build-support/common/cxa-throw/meson.build
  • nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc
  • nix-meson-build-support/common/meson.build
  • src/libutil-tests/cxa-throw.cc
  • src/libutil-tests/meson.build
  • src/libutil/meson.build
✅ Files skipped from review due to trivial changes (2)
  • src/libutil-tests/meson.build
  • nix-meson-build-support/common/meson.build
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/libutil/meson.build

Comment on lines +12 to +14
auto * si = dynamic_cast<const __cxxabiv1::__si_class_type_info *>(tinfo);
if (si)
return is_logic_error(si->__base_type);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "is-logic-error.hh" | head -20

Repository: DeterminateSystems/nix-src

Length of output: 130


🏁 Script executed:

cat -n ./nix-meson-build-support/common/cxa-throw/is-logic-error.hh

Repository: DeterminateSystems/nix-src

Length of output: 549


🏁 Script executed:

# Look for context around this function - check if there are tests or usages
rg "is_logic_error" --type-list

Repository: DeterminateSystems/nix-src

Length of output: 459


🏁 Script executed:

# Search for usages of is_logic_error
rg "is_logic_error" -A 3 -B 3

Repository: DeterminateSystems/nix-src

Length of output: 2527


🏁 Script executed:

# Look at the wrap-cxa-throw.cc to understand the context better
cat -n ./nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc

Repository: DeterminateSystems/nix-src

Length of output: 683


🏁 Script executed:

# Search for any other RTTI type handling in the codebase
rg "__si_class_type_info|__vmi_class_type_info" -B 2 -A 2

Repository: DeterminateSystems/nix-src

Length of output: 525


🏁 Script executed:

# Search for cxxabi.h includes to see if there's any documentation
find . -type f -name "*.hh" -o -name "*.h" | xargs grep -l "cxxabi" | head -10

Repository: DeterminateSystems/nix-src

Length of output: 130


🏁 Script executed:

# Check if there are any tests or documentation about exception handling
find . -path "*/test*" -type f | grep -i "cxa\|throw\|logic" | head -20

Repository: DeterminateSystems/nix-src

Length of output: 259


🏁 Script executed:

# Let's search for information about cxxabi RTTI types online
# First, let me check the cxxabi.h header on this system
cat /usr/include/cxxabi.h | grep -A 20 "__si_class_type_info\|__vmi_class_type_info" | head -50

Repository: DeterminateSystems/nix-src

Length of output: 123


🌐 Web query:

Itanium C++ ABI RTTI __si_class_type_info __vmi_class_type_info inheritance

💡 Result:

Under the Itanium C++ ABI, the RTTI object referenced by a polymorphic class’s vtable “typeinfo pointer” (vtable slot -1) is an instance of one of the __class_type_info-family types, chosen based on the class’s inheritance shape. [1]

Which RTTI class is used

  • __cxxabiv1::__class_type_info: used for class types with no base classes (and is also the base of the other class RTTI types). [1]
  • __cxxabiv1::__si_class_type_info (“single inheritance”): used only when the class has exactly one direct base and that base is public, non-virtual, and at offset 0 (i.e., the derived is dynamic iff the base is).
    It adds one field: const __class_type_info* __base_type;. [1]
  • __cxxabiv1::__vmi_class_type_info (“virtual/multiple inheritance”): used for classes whose bases do not meet the __si_class_type_info constraints (i.e., any multiple inheritance, any virtual base, or a non-zero/ non-public single base). [1][2]

How inheritance is encoded in __vmi_class_type_info

__vmi_class_type_info contains: [1]

  • unsigned int __flags; with bits:
    • 0x1 = non-diamond repeated inheritance present
    • 0x2 = diamond-shaped inheritance present
  • unsigned int __base_count; = number of direct proper bases
  • __base_class_type_info __base_info[__base_count]; where each entry is:
    • const __class_type_info* __base_type; (points to the base’s RTTI)
    • long __offset_flags; packing:
      • low 8 bits = flags:
        • 0x1 = base is virtual
        • 0x2 = base is public
        • and __offset_shift = 8
      • high bits (everything above the low byte) = a signed offset:
        • for a non-virtual base: byte offset of that base subobject within the complete object
        • for a virtual base: a (negative) offset into the vtable where the virtual-base offset is found [1]

So, in practice: if you can use __si_class_type_info, you only get the single __base_type. Otherwise you use __vmi_class_type_info and walk __base_info[], interpreting __offset_flags to determine (1) which direct bases exist, and (2) whether each base is public/virtual and where it sits (directly in the object, or via a vtable virtual-base-offset slot). [1]

Sources: Itanium C++ ABI RTTI layout/specification. [1], plus ABI issue discussion clarifying si vs vmi usage. [2]


Handle __vmi_class_type_info too.

This only walks single-inheritance RTTI. A class with multiple bases like struct X : std::logic_error, Tag uses __vmi_class_type_info, so is_logic_error() returns false even though one base is a logic_error subclass, bypassing the abort.

Proposed fix
     auto * si = dynamic_cast<const __cxxabiv1::__si_class_type_info *>(tinfo);
     if (si)
         return is_logic_error(si->__base_type);
+
+    auto * vmi = dynamic_cast<const __cxxabiv1::__vmi_class_type_info *>(tinfo);
+    if (vmi) {
+        for (unsigned i = 0; i < vmi->__base_count; ++i) {
+            if (is_logic_error(vmi->__base_info[i].__base_type))
+                return true;
+        }
+    }
 
     return false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto * si = dynamic_cast<const __cxxabiv1::__si_class_type_info *>(tinfo);
if (si)
return is_logic_error(si->__base_type);
auto * si = dynamic_cast<const __cxxabiv1::__si_class_type_info *>(tinfo);
if (si)
return is_logic_error(si->__base_type);
auto * vmi = dynamic_cast<const __cxxabiv1::__vmi_class_type_info *>(tinfo);
if (vmi) {
for (unsigned i = 0; i < vmi->__base_count; ++i) {
if (is_logic_error(vmi->__base_info[i].__base_type))
return true;
}
}
return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-meson-build-support/common/cxa-throw/is-logic-error.hh` around lines 12 -
14, The current is_logic_error function only handles single-inheritance RTTI
(__si_class_type_info) and misses multiple-inheritance cases; update
is_logic_error to also detect __vmi_class_type_info, iterate over all its base
class entries (accessing each base type pointer from the vmi structure) and
recursively call is_logic_error on each base_type, returning true if any base is
a logic_error subclass; keep the existing si branch, add nullptr checks for the
vmi base array/count, and ensure you return false only after all bases have been
checked.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
nix-meson-build-support/common/cxa-throw/is-logic-error.hh (1)

18-22: ⚠️ Potential issue | 🟠 Major

is_logic_error still misses multiple-inheritance RTTI paths.

Line 18 only walks __si_class_type_info; a std::logic_error-derived type using multiple inheritance can bypass detection. This was already raised earlier and appears unresolved.

Proposed fix
 static bool is_logic_error(const std::type_info * tinfo)
 {
     if (*tinfo == typeid(std::logic_error))
         return true;

     auto * si = dynamic_cast<const __cxxabiv1::__si_class_type_info *>(tinfo);
     if (si)
         return is_logic_error(si->__base_type);
+
+    auto * vmi = dynamic_cast<const __cxxabiv1::__vmi_class_type_info *>(tinfo);
+    if (vmi) {
+        for (unsigned i = 0; i < vmi->__base_count; ++i) {
+            if (is_logic_error(vmi->__base_info[i].__base_type))
+                return true;
+        }
+    }

     return false;
 }
#!/bin/bash
set -euo pipefail
rg -n '__si_class_type_info|__vmi_class_type_info|is_logic_error' nix-meson-build-support/common/cxa-throw/is-logic-error.hh -C3
🧹 Nitpick comments (1)
nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc (1)

11-19: Add a non-logic_error passthrough regression test.

This forwarding path is core to the “don’t abort other exceptions” objective, but current tests only cover death on logic_error variants (src/libutil-tests/cxa-throw.cc, Lines 1-35). Please add one test that throws/catches something like std::runtime_error and verifies no abort.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc` around lines
11 - 19, Add a regression test that verifies non-logic_error exceptions are
forwarded and do not trigger abort: create a test (e.g.,
cxa_throw_passthrough_non_logic_error) that throws a std::runtime_error (or
another non-logic_error type) so the interpose path in is_logic_error(...) is
exercised, ensure the exception is caught by the test (or the child process
exits normally) and assert no abort occurred; this validates the orig
(cxa_throw_type) dlsym(RTLD_NEXT, "__cxa_throw") forwarding path and that
abort_on_exception(exc, tinfo) is not invoked for non-logic_error types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nix-meson-build-support/common/cxa-throw/is-logic-error.hh`:
- Around line 3-11: The header currently relies on transitive includes for
abort() and snprintf() — update the includes to explicitly add <cstdlib> (for
abort) and <cstdio> (for snprintf) so CXA_THROW_ON_LOGIC_ERROR() and any uses of
snprintf() are portable; modify the top-of-file include list where <unistd.h>,
<cxxabi.h>, <stdexcept>, <typeinfo>, and <cstring> are declared to also include
<cstdlib> and <cstdio> so the symbols abort() and snprintf() are provided
explicitly.

In `@nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc`:
- Around line 10-13: The call to abort_on_exception has incorrect arity; update
the call in the cxa-throw wrapper so abort_on_exception is invoked with both
parameters (the exception pointer and its type_info) instead of just tinfo.
Specifically, in the block that checks is_logic_error(tinfo) before delegating
to __real___cxa_throw, change the call to pass exc and tinfo to
abort_on_exception, ensuring the symbol abort_on_exception(void *exc, const
std::type_info *tinfo) is used correctly and __real___cxa_throw(exc, tinfo,
dest) remains unchanged.

---

Nitpick comments:
In `@nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc`:
- Around line 11-19: Add a regression test that verifies non-logic_error
exceptions are forwarded and do not trigger abort: create a test (e.g.,
cxa_throw_passthrough_non_logic_error) that throws a std::runtime_error (or
another non-logic_error type) so the interpose path in is_logic_error(...) is
exercised, ensure the exception is caught by the test (or the child process
exits normally) and assert no abort occurred; this validates the orig
(cxa_throw_type) dlsym(RTLD_NEXT, "__cxa_throw") forwarding path and that
abort_on_exception(exc, tinfo) is not invoked for non-logic_error types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d09b48db-0163-4d5e-a19a-8094ce1a9d17

📥 Commits

Reviewing files that changed from the base of the PR and between 4cfdfb2 and 042f5a5.

📒 Files selected for processing (3)
  • nix-meson-build-support/common/cxa-throw/interpose-cxa-throw.cc
  • nix-meson-build-support/common/cxa-throw/is-logic-error.hh
  • nix-meson-build-support/common/cxa-throw/wrap-cxa-throw.cc

Comment on lines +3 to +11
#include <unistd.h>
#include <cxxabi.h>
#include <stdexcept>
#include <typeinfo>
#include <cstring>

#ifndef CXA_THROW_ON_LOGIC_ERROR
# define CXA_THROW_ON_LOGIC_ERROR() abort()
#endif
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n '^#include|abort\s*\(|snprintf\s*\(' nix-meson-build-support/common/cxa-throw/is-logic-error.hh -C1

Repository: DeterminateSystems/nix-src

Length of output: 336


🏁 Script executed:

cat -n nix-meson-build-support/common/cxa-throw/is-logic-error.hh

Repository: DeterminateSystems/nix-src

Length of output: 1268


Add explicit includes <cstdlib> and <cstdio> for directly used symbols.

Line 10 uses abort() and line 31 uses snprintf(), neither of which are explicitly included. While <unistd.h> may transitively provide these symbols on many POSIX systems, this is not standard and reduces portability. Include <cstdlib> for abort() and <cstdio> for snprintf() explicitly.

Proposed fix
 `#include` <unistd.h>
+#include <cstdlib>
+#include <cstdio>
 `#include` <cxxabi.h>
 `#include` <stdexcept>
 `#include` <typeinfo>
 `#include` <cstring>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#include <unistd.h>
#include <cxxabi.h>
#include <stdexcept>
#include <typeinfo>
#include <cstring>
#ifndef CXA_THROW_ON_LOGIC_ERROR
# define CXA_THROW_ON_LOGIC_ERROR() abort()
#endif
`#include` <unistd.h>
`#include` <cstdlib>
`#include` <cstdio>
`#include` <cxxabi.h>
`#include` <stdexcept>
`#include` <typeinfo>
`#include` <cstring>
`#ifndef` CXA_THROW_ON_LOGIC_ERROR
# define CXA_THROW_ON_LOGIC_ERROR() abort()
`#endif`
🧰 Tools
🪛 Clang (14.0.6)

[error] 3-3: 'unistd.h' file not found

(clang-diagnostic-error)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-meson-build-support/common/cxa-throw/is-logic-error.hh` around lines 3 -
11, The header currently relies on transitive includes for abort() and
snprintf() — update the includes to explicitly add <cstdlib> (for abort) and
<cstdio> (for snprintf) so CXA_THROW_ON_LOGIC_ERROR() and any uses of snprintf()
are portable; modify the top-of-file include list where <unistd.h>, <cxxabi.h>,
<stdexcept>, <typeinfo>, and <cstring> are declared to also include <cstdlib>
and <cstdio> so the symbols abort() and snprintf() are provided explicitly.

@github-actions github-actions bot temporarily deployed to pull request March 31, 2026 14:12 Inactive
Example:

  Aborting on unexpected exception of type 'St16invalid_argument', error: bitset::_M_copy_from_ptr
@cole-h cole-h force-pushed the abort-on-logic-error-v2 branch from 042f5a5 to f69b305 Compare March 31, 2026 14:16
@cole-h cole-h enabled auto-merge March 31, 2026 14:21
@github-actions github-actions bot temporarily deployed to pull request March 31, 2026 14:25 Inactive
@cole-h cole-h added this pull request to the merge queue Mar 31, 2026
Merged via the queue into main with commit 5d6dda9 Mar 31, 2026
28 checks passed
@cole-h cole-h deleted the abort-on-logic-error-v2 branch March 31, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants