Repository navigation
fix(storage): avoid use-after-free when callback destroys async writer - #16555
Conversation
There was a problem hiding this comment.
Code Review
This pull request prevents potential use-after-free issues in AsyncWriter and its connection implementations by ensuring member variables and futures are copied or moved before invoking operations that might synchronously destroy the object. It also adds comprehensive unit tests to verify lifetime safety. The review feedback highlights several violations of the repository style guide's type deduction rules, requesting that auto be replaced with explicit types for connection pointers and complex future types to avoid obscuring domain and return types.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #16555 +/- ##
========================================
Coverage 92.38% 92.39%
========================================
Files 2265 2265
Lines 219107 219328 +221
========================================
+ Hits 202416 202641 +225
+ Misses 16691 16687 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
v-pratap
left a comment
There was a problem hiding this comment.
LGTM — the lifetime fixes in AsyncWriter and the buffered/resumed writer connections cleanly prevent member access after synchronous callback destruction, and the new unit tests pass under ASan. (Note: clang-tidy-storage-pr failed after 30m00s in CI and may need a re-run or log check before merging.)
d1ee3c6 to
3d2b102
Compare
In
AsyncWriterConnectionBufferedStateandAsyncWriterConnectionResumedState,Finalize()andClose()read a member variable afterHandleNewData()returns:Problem
HandleNewData()starts it synchronously (HandleNewData() -> StartWriting() -> WriteLoop() -> FlushStep()).FlushStep()callsimpl_->Flush(...)and attaches a.then()continuation that locksWeakFromThis()into a localshared_ptr(self) and callsself->OnFlush(...).impl_->Flush(...)completes inline with a permanent error,OnFlush()callsSetError(), which unlocksmu_and satisfies all pendingWrite()andFlush()promises (flush_handlers_andpending_flush_promises_).promise::set_value()runs attached.then()callbacks synchronously on the calling thread, a user callback on one of those pending futures can destroy the connection (orAsyncWriter) beforeSetError()returns.OnFlush()then returns and theFlushStep()continuation finishes (still insideHandleNewData()) its localselfshared_ptris destroyed. If no other reference to the state remains,*thisis freed beforeHandleNewData()returns, and readingfinalized_future_orclosed_future_is a heap-use-after-free.AsyncWriterhas the same issue one layer up inWrite(),Finalize(),Flush(), andClose():In C++17, the call
impl_->Finalize(...)is sequenced before the lambda capture[impl = impl_]. If a synchronous callback invoked during that call destroys theAsyncWriter, capturingimpl_reads a member of the destroyedAsyncWriter.Fix
This PR updates both layers by moving or copying the required member into a local variable before calling
HandleNewData()orimpl_->..., so no member of*thisis accessed after the call. InAsyncWriter::Close(), movingimpl_before callingimpl->Close(...)also means any callback that re-enters the writer duringClose()observes a closed stream, matching the documented contract that the writer is not usable afterClose().