Skip to content

Commit 49e1978

Browse files
authored
[server][dvc] Fix use-after-free in RMD block cache metrics callbacks (linkedin#2742)
Fix JVM SIGSEGV crashes caused by RMD block cache async gauge callbacks dereferencing a freed RocksDB Cache JNI handle after factory shutdown. The callbacks previously captured `rmdCache` directly and could still run on OTel or Tehuti collection threads after native cache memory had already been freed. Store the cache in a volatile field and have callbacks read it into a local variable before the null check and JNI call. Also null the reference in `closeRMDBlockCache()` and invoke it from `RocksDBStorageEngineFactory.close()` before `sharedRMDCache.close()`, so metrics callbacks observe null before native memory is released.
1 parent 9ac8c24 commit 49e1978

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

clients/da-vinci-client/src/main/java/com/linkedin/davinci/stats/RocksDBMemoryStats.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,23 +189,50 @@ public void deregisterPartition(String partitionName) {
189189
}
190190
}
191191

192+
/**
193+
* Volatile reference cleared by {@link #closeRMDBlockCache()} before the native Cache is freed.
194+
* Callbacks read this reference into a local variable — if null, the cache is closed and they
195+
* return 0. The local variable pins the reference on the stack, eliminating any TOCTOU race
196+
* between the null check and the JNI call without requiring locks.
197+
*/
198+
private volatile Cache rmdCache;
199+
192200
public void setRMDBlockCache(Cache rmdCache, long rmdCacheCapacity) {
193201
if (!rmdBlockCacheRegistered.compareAndSet(false, true)) {
194202
LOGGER.warn("setRMDBlockCache called more than once; ignoring duplicate registration");
195203
return;
196204
}
205+
this.rmdCache = rmdCache;
197206
registerAsyncGauge(
198207
RocksDBMemoryOtelMetricEntity.RMD_BLOCK_CACHE_CAPACITY,
199208
TehutiMetricName.RMD_BLOCK_CACHE_CAPACITY,
200209
() -> rmdCacheCapacity);
201210
registerAsyncGauge(
202211
RocksDBMemoryOtelMetricEntity.RMD_BLOCK_CACHE_USAGE,
203212
TehutiMetricName.RMD_BLOCK_CACHE_USAGE,
204-
rmdCache::getUsage);
213+
this::getRMDCacheUsage);
205214
registerAsyncGauge(
206215
RocksDBMemoryOtelMetricEntity.RMD_BLOCK_CACHE_PINNED_USAGE,
207216
TehutiMetricName.RMD_BLOCK_CACHE_PINNED_USAGE,
208-
rmdCache::getPinnedUsage);
217+
this::getRMDCachePinnedUsage);
218+
}
219+
220+
/**
221+
* Must be called before closing the native Cache object to prevent use-after-free.
222+
* Nulls out the volatile reference so in-flight and future callbacks return 0.
223+
*/
224+
public void closeRMDBlockCache() {
225+
this.rmdCache = null;
226+
}
227+
228+
private long getRMDCacheUsage() {
229+
Cache cache = this.rmdCache;
230+
return cache != null ? cache.getUsage() : 0L;
231+
}
232+
233+
private long getRMDCachePinnedUsage() {
234+
Cache cache = this.rmdCache;
235+
return cache != null ? cache.getPinnedUsage() : 0L;
209236
}
210237

211238
/** Registers a joint Tehuti+OTel async gauge metric. */

clients/da-vinci-client/src/main/java/com/linkedin/davinci/store/rocksdb/RocksDBStorageEngineFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@ public synchronized void close() {
303303
storageEngine.close();
304304
});
305305
storageEngineMap.clear();
306+
// Null out the RMD cache reference in metrics BEFORE closing the native object.
307+
// This prevents OTel/Tehuti async gauge callbacks from dereferencing a freed JNI handle.
308+
if (rocksDBMemoryStats != null) {
309+
rocksDBMemoryStats.closeRMDBlockCache();
310+
}
306311
sharedCache.close();
307312
if (sharedRMDCache != null) {
308313
sharedRMDCache.close();

0 commit comments

Comments
 (0)