Aangbaeck opened a new pull request, #22717:
URL: https://github.com/apache/kafka/pull/22717

   ## What
   
   `RocksDBMetricsRecorder` reads native RocksDB value providers (`RocksDB` and 
`Statistics`)
   in three places:
   
   - `record()` — `statistics.getAndResetTickerCount(...)` / 
`getHistogramData(...)`
   - the property gauges (`gaugeToComputeSumOfProperties`) — 
`db.getAggregatedLongProperty(...)`
   - the block-cache gauges (`gaugeToComputeBlockCacheMetrics`) — 
`db.getLongProperty(...)`
   
   These run with no mutual exclusion against `removeValueProviders(...)`. 
`RocksDBStore.close()`
   calls `removeValueProviders(name)` and then **closes (frees) the native 
RocksDB and
   Statistics**. Because the reads and the removal are not mutually exclusive, 
a metrics read
   that is in flight when a store is closed (during a rebalance / task 
migration) can
   dereference a native handle that `close()` is concurrently freeing — a 
**native
   use-after-free / SIGSEGV**.
   
   Two observed crash frames, same root cause:
   - `record()` path → `Statistics::getAndResetTickerCount` — this is 
**KAFKA-10025** (open since 2020).
   - gauge path → `rocksdb::DBImpl::GetAggregatedIntProperty` — observed in 
production under a
     from-zero state rebuild, where warmup/probing rebalances close stores 
continuously while a
     metrics reporter / JMX scrape evaluates the (INFO-level) RocksDB property 
gauges.
   
   Note the gauge metrics are registered at `RecordingLevel.INFO`, so they are 
active and
   scraped even when `metrics.recording.level=INFO`; only the `record()` 
(statistics) path is
   gated to DEBUG. So the crash is reachable at INFO.
   
   ## Why the current code is unsafe
   
   `storeToValueProviders` is a `ConcurrentHashMap`, which makes the map 
operations
   thread-safe, but does **not** prevent a reader that has already obtained a
   `DbAndCacheAndStatistics` from calling a native method on its 
`db`/`statistics` after (or
   while) `RocksDBStore.close()` frees them. There is no happens-before between 
"recorder reads
   the provider" and "store closes the provider".
   
   ## Fix
   
   Introduce a single lock (`valueProvidersLock`) in `RocksDBMetricsRecorder` 
and hold it
   around every read of the value providers (`record()`, both gauge lambdas) 
and around the
   map mutations (`addValueProviders`, `removeValueProviders`). Since 
`RocksDBStore.close()`
   already calls `removeValueProviders(...)` **before** it frees the native 
db/statistics,
   `removeValueProviders(...)` acquiring the lock guarantees:
   
   - any in-flight read completes before the segment is removed and the native 
handles freed, and
   - any read that starts after removal no longer sees the segment,
   
   so no read can ever dereference a freed handle.
   
   No lock-ordering risk: `RocksDBMetricsRecordingTrigger` holds no lock while 
calling
   `record()`, and the guarded reads never call back into `RocksDBStore`. 
`RocksDBStore.close()`
   takes the store monitor then this lock; opens take the same order — 
consistent, no cycle.
   
   ## Testing
   
   - New test 
`RocksDBMetricsRecorderGaugesTest#shouldNotRemoveValueProvidersWhileGaugeIsReadingThem`:
     blocks a gauge evaluation inside `getAggregatedLongProperty` (holding the 
lock) and asserts
     `removeValueProviders(...)` cannot return until the read completes — i.e. 
the use-after-free
     window is closed. **Verified it fails without the fix** (`AssertionError: 
…the use-after-free
     window is open`) and passes with it.
   - Existing `RocksDBMetricsRecorderTest` / 
`RocksDBMetricsRecorderGaugesTest`, checkstyle and
     spotbugs all pass.
   
   End-to-end confirmation (outside this PR, in a standalone Docker harness): a 
real Kafka
   Streams app on the released `kafka-streams 8.2.1-ce`, at 
`metrics.recording.level=INFO`, with
   a JMX-style metrics scrape (reading the INFO-level RocksDB property gauges) 
plus forced
   rebalances, **SIGSEGVs in `rocksdb::DBImpl::GetAggregatedIntProperty+0x83`** 
on a scrape thread
   after ~27M gauge reads. Running the *same* binary with only 
`RocksDBMetricsRecorder` replaced
   by the patched class (classpath shadow, load-verified) survived 600s / ~336M 
gauge reads / 25
   rebalances with zero crashes. The exact native crash was also reproduced in 
a pure `rocksdbjni`
   harness by racing `getAggregatedLongProperty` against a concurrent DB 
close+reopen.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to