Originally reported by @yueshangzuo
Summary
DnsCacheManagerImpl is a server-wide singleton that retains a non-owning stats-scope reference from its first caller. If that caller's listener filter chain is destroyed, a later cache miss dereferences the stale scope, causing a use-after-free that can crash Envoy.
Details
The manager's copied context preserves the caller's bare Stats::Scope&. Cache hits return directly, but a miss calls DnsCacheImpl::createDnsCacheImpl(); its constructor then calls:
context.scope().createScope("dns_cache.<name>.")
If the first caller's filter chain has already been destroyed, this dereferences the freed scope.
Tests using IsolatedStoreImpl can mask the bug because it retains strong references to child scopes. Production ThreadLocalStoreImpl tracks them weakly, so destroying the filter chain releases the scope.
Reproduction
A focused test using ThreadLocalStoreImpl:
- Create filter-chain scope A and instantiate the manager from its factory context.
- Create cache A.
- Destroy context A and scope A.
- Request cache B from a new caller, forcing a cache miss.
Before the fix, ASAN reports:
ERROR: AddressSanitizer: heap-use-after-free
#0 Envoy::Stats::Scope::createScope(...)
#1 DnsCacheImpl::DnsCacheImpl(...)
#2 DnsCacheImpl::createDnsCacheImpl(...)
#3 DnsCacheManagerImpl::getCache(...)
freed by thread T0 here:
std::__shared_ptr_emplace<Stats::ThreadLocalStoreImpl::ScopeImpl>::__on_zero_shared_weak()
Impact
The issue can crash Envoy during dynamic listener updates; no request-only trigger has been identified.
Proposed fix
Bind DnsCacheManagerImpl and its DNS cache stats to server-lifetime state, eliminating references to caller-owned scopes. Metric names remain unchanged; the stats are now governed by the global stats matcher.
Originally reported by @yueshangzuo
Summary
DnsCacheManagerImplis a server-wide singleton that retains a non-owning stats-scope reference from its first caller. If that caller's listener filter chain is destroyed, a later cache miss dereferences the stale scope, causing a use-after-free that can crash Envoy.Details
The manager's copied context preserves the caller's bare
Stats::Scope&. Cache hits return directly, but a miss callsDnsCacheImpl::createDnsCacheImpl(); its constructor then calls:context.scope().createScope("dns_cache.<name>.")If the first caller's filter chain has already been destroyed, this dereferences the freed scope.
Tests using
IsolatedStoreImplcan mask the bug because it retains strong references to child scopes. ProductionThreadLocalStoreImpltracks them weakly, so destroying the filter chain releases the scope.Reproduction
A focused test using
ThreadLocalStoreImpl:Before the fix, ASAN reports:
Impact
The issue can crash Envoy during dynamic listener updates; no request-only trigger has been identified.
Proposed fix
Bind
DnsCacheManagerImpland its DNS cache stats to server-lifetime state, eliminating references to caller-owned scopes. Metric names remain unchanged; the stats are now governed by the global stats matcher.