Skip to content

make initialize() transactional (write_global_data is not exception-safe) #81

Description

@jll63

write_global_data() is not exception-safe with respect to policy
initialization, so a throwing initialize() (or re-initialize(), the
documented dynamic-loading flow after a dlopen/dlclose) can leave shared
dispatch state pointing at freed memory.

Mechanism (include/boost/openmethod/initialize.hpp,
write_global_data(), ~line 1542):

  • A local new_dispatch_data vector is built (~1556).
  • Every module's class_info::static_vptr (~1639) and every overrider's
    next pointer (~1484-1509) are patched to point into it.
  • Only after that does detail::initialize_policies<registry>::fn(...)
    run (~1680) — this is where a policy's initialize can throw (e.g.
    fast_perfect_hash's hash-factor search, with throw_error_handler, or a
    plain bad_alloc).
  • The commit (new_dispatch_data.swap(static_::st.dispatch_data)) happens
    last (~1682).

If the policy step throws, new_dispatch_data is destroyed while unwinding,
and every static_vptr/next pointer already patched into it is left
dangling.

Already mitigated: initialize() now resets static_::st.initialized = false at entry (was only set true on success), so require_initialized()
(the runtime_checks guard) correctly reports "not initialized" after a
failed re-initialize instead of a stale true. That closes the
false-positive-success half of the problem.

Still open: without runtime_checks (or even with it, between the
failed re-init and a subsequent successful one), the dangling pointers
themselves are unaddressed — a failed re-initialize clobbers the previous
working dispatch state, not just the new one.

Why it's not a quick fix: the obvious move — run
initialize_policies before the pointer-patching loops — is unsafe as-is:
vptr_vector::initialize reads the just-patched static_vptr values (via
iter->vptr()), so policy init currently depends on the patch having
already happened. A real fix needs the fallible policy work (mainly
fast_perfect_hash's search) to run into local state first, with the shared
mutations (static_vptr/next/slots_strides patches, the dispatch_data
swap, and each policy's committed state) applied only after every fallible
step succeeds — which crosses the current policy interface, where a
policy's initialize writes straight into shared state.

Pre-existing on develop (not introduced by the DLL state-sharing branch),
but the dynamic-loading feature makes re-initialization a documented,
common flow, which is what surfaced it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions