Epic: #929 | Blocked by: #1465 | Phase: 2
Problem
call_intrinsic (mellea/stdlib/components/intrinsic/_util.py) calls
backend.resolve_adapter(intrinsic_name) to lazily register an adapter on
first use. AdapterMixin.resolve_adapter mutates _added_adapters via
add_adapter() without holding LocalHFBackend._generation_lock.
Meanwhile, every verb that reads _added_adapters on the generation path
(load_peft_adapter, called from _IntrinsicPeftBinding.activate()/
.prepare(), and previously from _generate_with_adapter_lock directly)
does so while holding _generation_lock.
Two concurrent first-time calls for the same previously-unregistered
intrinsic_name can therefore race: both may see the name unregistered,
both call add_adapter(), and the outcome depends on unguarded interleaving
in add_adapter()'s own duplicate-registration check.
Impact
Low likelihood in practice (only triggers on truly concurrent first use of a
never-before-resolved adapter function on one backend instance), but a real
gap: the locking discipline that protects activation/generation doesn't
extend to registration.
resolve_adapter's own docstring already flags a related, narrower gap:
warnings.catch_warnings() modifies the process-global filter state and
is not async/thread-safe. Concurrent first-time resolves race on filter
restoration... Phase 2 (see epic #929) adds a lock.
That comment anticipated a lock being added for this method; #1465 added
reentrancy to _generation_lock for the activation/generation path but did
not extend it to resolve_adapter().
Suggested direction
Have resolve_adapter()'s registration path acquire the same
_generation_lock (now reentrant, so this composes safely with the rest of
the locking added in #1465) around the add_adapter() call, or introduce a
dedicated registration lock if serializing against generation is
undesirable.
Context
Found during #1465's code review (routing intrinsic generation through
adapter_scope). Verified this race's shape is unchanged by #1465 — the
same read-under-lock/write-without-lock pattern existed before that PR too
— so it's pre-existing, not a regression.
Epic: #929 | Blocked by: #1465 | Phase: 2
Problem
call_intrinsic(mellea/stdlib/components/intrinsic/_util.py) callsbackend.resolve_adapter(intrinsic_name)to lazily register an adapter onfirst use.
AdapterMixin.resolve_adaptermutates_added_adaptersviaadd_adapter()without holdingLocalHFBackend._generation_lock.Meanwhile, every verb that reads
_added_adapterson the generation path(
load_peft_adapter, called from_IntrinsicPeftBinding.activate()/.prepare(), and previously from_generate_with_adapter_lockdirectly)does so while holding
_generation_lock.Two concurrent first-time calls for the same previously-unregistered
intrinsic_namecan therefore race: both may see the name unregistered,both call
add_adapter(), and the outcome depends on unguarded interleavingin
add_adapter()'s own duplicate-registration check.Impact
Low likelihood in practice (only triggers on truly concurrent first use of a
never-before-resolved adapter function on one backend instance), but a real
gap: the locking discipline that protects activation/generation doesn't
extend to registration.
resolve_adapter's own docstring already flags a related, narrower gap:That comment anticipated a lock being added for this method; #1465 added
reentrancy to
_generation_lockfor the activation/generation path but didnot extend it to
resolve_adapter().Suggested direction
Have
resolve_adapter()'s registration path acquire the same_generation_lock(now reentrant, so this composes safely with the rest ofthe locking added in #1465) around the
add_adapter()call, or introduce adedicated registration lock if serializing against generation is
undesirable.
Context
Found during #1465's code review (routing intrinsic generation through
adapter_scope). Verified this race's shape is unchanged by #1465 — thesame read-under-lock/write-without-lock pattern existed before that PR too
— so it's pre-existing, not a regression.