mirror of
https://github.com/facebook/rocksdb.git
synced 2026-10-23 08:09:02 +03:00
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/15100 Fix the TSAN race in T284274705 where `CoroutineStatsRequestData::onSet()` can access its borrowed `Env*` after `AsyncCallback::OnComplete()` allows the caller to destroy that environment. When `co_await baton` suspends, the Task's implicit `ViaIfAsync` saves the read's statistics `RequestContext` (`S`) as the context needed by the real RocksDB continuation. The old explicit `executor->add()` also ran the Baton bridge under `S`. Consequently, `baton.post()` resumed the Via trampoline under `S`, and the trampoline's own `executor_->add()` caused EventBase to capture `S` a second time as the queued continuation's outer context. After the read removed its stats scope and called `OnComplete()`, the Via guard restored that outer `S`, triggering a late `onSet()` that dereferenced the potentially destroyed `Env*`. Remove the redundant outer executor hop and call `baton.post()` directly under an explicitly null `RequestContext`. If the read is suspended, `post()` resumes only the Via trampoline; the trampoline still queues the real continuation on the bound read executor. The Via promise retains its saved `S`, so `executeContinuation()` installs `S` while RocksDB executes, but EventBase now captures null as the outer context restored afterward. If filesystem completion happens before `co_await baton`, `post()` only marks the Baton ready and the read continues inline under its existing context. Thread Race: `E` = temporary backup `Env` `S` = statistics `RequestContext` containing `CoroutineStatsRequestData{env_ = E}` `P` = parent context without RocksDB statistics `B` = old explicit Baton bridge `C` = `ViaIfAsync` callback that resumes the real read ``` Caller / stress thread RocksDB read EventBase thread ---------------------- ----------------------------- BackupInfo owns shared_ptr<Env> E Call MultiGetAsync() Wait for OnComplete() Start the RocksDB read coroutine Install S: P -> S S.onSet() Call SubmitReadAsync() No io_uring is available, so: Read() executes synchronously filesystem callback runs inline callback is still under S Old code calls executor->add(B) EventBase records: B.outer_context = S Reach co_await baton B has not executed, so Baton is not ready ViaIfAsync: saves execution_context = S gives trampoline V to Baton Suspend the real read Leave S: S -> P S.onUnset() Later, EventBase runs B Install B.outer_context: P -> S S.onSet() B calls baton.post() -> V resumes inline -> V queues callback C -> EventBase records: C.outer_context = S Later, EventBase runs C Install C.outer_context: P -> S S.onSet() Via executeContinuation(): remembers previous context = S installs saved execution context S resumes the real RocksDB read Read finishes Statistics scope removes S: S -> P S.onUnset() Call OnComplete() Wait returns <--------------- OnComplete signals caller ================ PUBLIC READ IS COMPLETE ================ Destroy restored DB C is still unwinding Release BackupInfo Via guard restores previous context: Last shared_ptr<Env> disappears P -> S Begin Env E destruction S.onSet() -> E->GetThreadID() Env::~Env() <--------------- TSAN race ---------------> S.onSet() ``` The same context was captured in two roles: ``` Via execution_context = S required while the real read executes C.outer_context = S accidentally restored after the read completes ``` The fix removes bridge `B` and posts under null. For an outstanding asynchronous read, the Via trampoline therefore queues `C` with `outer_context = null`, while retaining `execution_context = S`. The real read still executes under `S`, but after `OnComplete()` the Via guard restores null instead of reactivating `S`. Reviewed By: anand1976 Differential Revision: D115967513 fbshipit-source-id: 87d744ecdbad3421c4d2a1a7be6423a3f0373573