initialize: move the last throwing statement before the commit - #101
Open
jll63 wants to merge 2 commits into
Open
initialize: move the last throwing statement before the commit#101jll63 wants to merge 2 commits into
jll63 wants to merge 2 commits into
Conversation
boostorg#95 made initialize() transactional, but `transaction.commit()` was called one statement too early: the `++tr << "Installing\n"` that follows it goes through the `output` policy, which is user code and can throw. When it does, the transaction destructor sees a committed transaction and restores nothing, so the policies keep the v-table pointers they read out of `new_dispatch_data` - the local vector unwinding is about to free. The registry still holds the previous dispatch data (the swap never ran), so the next dispatch through the policy state reads freed memory: the exact use-after-free boostorg#95 fixed, reached by a different route. ASan, gcc 13, an `output` policy whose stream throws on "Installing" plus initialize(trace(true)): ERROR: AddressSanitizer: heap-use-after-free READ of size 8 ... in resolve_uni<...> freed by ... write_global_data() Move the trace write above commit(), where a throw rolls the policies back, and extract the writes that follow into `commit_global_data()`, declared `noexcept` so that a throwing statement added there terminates loudly instead of silently reopening this. The test grew a case for that path, and two of its existing assertions were vacuous: the failing initialize saw exactly the input the preceding successful one saw, so `fast_perfect_hash` - which re-seeds a fixed PRNG - recomputed identical factors, and `next<poke_dog>` resolved to the same overrider. Both held whether or not anything was rolled back; reintroducing the dangling-`next` half of boostorg#81 left the suite reporting "No errors detected". Perturb the input between the two calls instead, with two function-local static registrars: an extra class changes the hash factors, the control table and the v-table pointers, and an extra inheritance edge changes what `next<poke_dog>` would be set to. The hash assertion also compares the whole policy state now - factors and control table - rather than `hash_range()` alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS
|
An automated preview of the documentation is available at https://101.openmethod.prtest3.cppalliance.org/libs/openmethod/doc/html/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-09-12 00:17:10 UTC |
The transaction section says the registry is marked as not initialized and that initialize() must be called again, but not what happens if a method is called before it is. Only `runtime_checks` diagnoses that; otherwise the call dispatches through the previous tables, which is harmless for a registry whose classes are all still loaded and a use-after-free for one whose overriders came out of a library that has since been dlclose'd - the case the section is written for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQa4fuiwcfsheZYTCyfPPr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(Written by Claude Code, on behalf of @jll63.)
Follow-up to #95.
transaction.commit()was called one statement too early: the++tr << "Installing\n"that follows it goes through theoutputpolicy, which is user code and can throw. When it does, the transaction destructor sees a committed transaction and restores nothing, so the policies keep the v-table pointers they read out ofnew_dispatch_data— the local vector unwinding is about to free. The dispatch data itself is untouched (the swap never ran), so the next dispatch through the policy state reads freed memory: the use-after-free #95 fixed, reached by a different route.Reproduced with ASan, gcc 13, an
outputpolicy whose stream throws on"Installing", andinitialize(trace(true)):The trace write moves above
commit(), where a throw rolls the policies back, and the writes that follow are extracted intocommit_global_data(), declarednoexceptso a throwing statement added there terminates loudly instead of silently reopening this.Tests
test_initialize_transactiongained a case for the throwing-trace path, and two of its existing assertions turned out to be vacuous: the failinginitializesaw exactly the input the preceding successful one saw, sofast_perfect_hash— which re-seeds a fixed PRNG — recomputed identical factors, andnext<poke_dog>resolved to the same overrider. Both held whether or not anything was rolled back; reintroducing the dangling-nexthalf of #81 left the suite reporting No errors detected.The two calls now see different inputs, via two function-local static registrars: an extra class changes the hash factors, the control table and the v-table pointers, and an extra inheritance edge changes what
next<poke_dog>would be set to. The hash assertion compares the whole policy state — factors and control table — rather thanhash_range()alone.Verified by sabotage: disabling the rollback fails 12 assertions (it failed 7 before, none of them the hash), moving the
nextwrite back before the transaction fails 2, and the new case fails on the original commit ordering.Suites: Debug/gcc 13, 160/160; Debug/g++-16 with reflection and shared libraries, 166/166.
Not addressed here
install_global_tables()still runsprint(report),print_slots()and"Finished\n"after the commit point and beforest.initialized = true. A throw there — the same user-suppliedoutputpolicy, or abad_allocfrom the containersprint_slotsbuilds — leaves the new state fully installed while the registry reads as uninitialized, which contradicts the exception-safety paragraph oninitialize. The remedy is a contract decision (move the reporting before the commit, or narrow the documented guarantee), so it is left for a separate change.🤖 Generated with Claude Code
https://claude.ai/code/session_01RQG6CbE4o2agseE7bDVzHS