Skip to content

Commit f0cfdf4

Browse files
authored
fix(universaldb): avoid rocksdb boxing iterator UB on empty values # (#5319)
[SLOP(claude-opus-4-8-high)] fix(universaldb): avoid rocksdb boxing iterator UB on empty values
1 parent 4be475c commit f0cfdf4

2 files changed

Lines changed: 264 additions & 93 deletions

File tree

engine/packages/universaldb/src/driver/rocksdb/transaction_task.rs

Lines changed: 119 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,24 @@ use crate::{
1717
versionstamp::{generate_versionstamp, substitute_raw_versionstamp},
1818
};
1919

20+
/// Copy bytes borrowed from a rocksdb iterator into an owned `Vec`.
21+
///
22+
/// RocksDB hands back a null data pointer for zero-length keys and values.
23+
/// Copying from a null pointer (which `<[u8]>::to_vec` does internally via
24+
/// `ptr::copy_nonoverlapping`) violates its non-null precondition and aborts the
25+
/// process on builds compiled with UB checks (debug assertions) enabled, even
26+
/// though the copy length is zero. This is also why the boxing `DBIterator`
27+
/// adapter must be avoided here: its `Iterator::next` unconditionally boxes the
28+
/// value via `Box::<[u8]>::from(&[])`, hitting the same null-pointer copy. Guard
29+
/// the empty case so we never copy from the null pointer.
30+
fn iter_bytes_to_vec(bytes: &[u8]) -> Vec<u8> {
31+
if bytes.is_empty() {
32+
Vec::new()
33+
} else {
34+
bytes.to_vec()
35+
}
36+
}
37+
2038
pub enum TransactionCommand {
2139
Get {
2240
key: Vec<u8>,
@@ -174,67 +192,62 @@ impl TransactionTask {
174192
match (or_equal, offset) {
175193
(false, 1) => {
176194
// first_greater_or_equal: find first key >= search_key
177-
let iter = txn.iterator_opt(
178-
rocksdb::IteratorMode::From(key, rocksdb::Direction::Forward),
179-
read_opts,
180-
);
181-
for item in iter {
182-
let (k, _v) =
183-
item.context("failed to iterate rocksdb for first_greater_or_equal")?;
184-
return Ok(Some(k.to_vec().into()));
185-
}
186-
Ok(None)
195+
let mut iter = txn.raw_iterator_opt(read_opts);
196+
iter.seek(key);
197+
let result = iter.key().map(iter_bytes_to_vec);
198+
iter.status()
199+
.context("failed to iterate rocksdb for first_greater_or_equal")?;
200+
Ok(result.map(Into::into))
187201
}
188202
(true, 1) => {
189203
// first_greater_than: find first key > search_key
190-
let iter = txn.iterator_opt(
191-
rocksdb::IteratorMode::From(key, rocksdb::Direction::Forward),
192-
read_opts,
193-
);
194-
for item in iter {
195-
let (k, _v) =
196-
item.context("failed to iterate rocksdb for first_greater_than")?;
204+
let mut iter = txn.raw_iterator_opt(read_opts);
205+
iter.seek(key);
206+
while iter.valid() {
207+
let k = iter.key().expect("iterator should be valid");
197208
// Skip if it's the exact key
198-
if k.as_ref() == key {
209+
if k == key {
210+
iter.next();
199211
continue;
200212
}
201-
return Ok(Some(k.to_vec().into()));
213+
return Ok(Some(iter_bytes_to_vec(k).into()));
202214
}
215+
iter.status()
216+
.context("failed to iterate rocksdb for first_greater_than")?;
203217
Ok(None)
204218
}
205219
(false, 0) => {
206220
// last_less_than: find last key < search_key
207221
// Use reverse iterator starting just before the key
208-
let iter = txn.iterator_opt(
209-
rocksdb::IteratorMode::From(key, rocksdb::Direction::Reverse),
210-
read_opts,
211-
);
212-
213-
for item in iter {
214-
let (k, _v) = item.context("failed to iterate rocksdb for last_less_than")?;
222+
let mut iter = txn.raw_iterator_opt(read_opts);
223+
iter.seek_for_prev(key);
224+
while iter.valid() {
225+
let k = iter.key().expect("iterator should be valid");
215226
// We want strictly less than
216-
if k.as_ref() < key {
217-
return Ok(Some(k.to_vec().into()));
227+
if k < key {
228+
return Ok(Some(iter_bytes_to_vec(k).into()));
218229
}
230+
iter.prev();
219231
}
232+
iter.status()
233+
.context("failed to iterate rocksdb for last_less_than")?;
220234
Ok(None)
221235
}
222236
(true, 0) => {
223237
// last_less_or_equal: find last key <= search_key
224238
// Use reverse iterator starting from the key
225-
let iter = txn.iterator_opt(
226-
rocksdb::IteratorMode::From(key, rocksdb::Direction::Reverse),
227-
read_opts,
228-
);
229-
230-
for item in iter {
231-
let (k, _v) =
232-
item.context("failed to iterate rocksdb for last_less_or_equal")?;
239+
let mut iter = txn.raw_iterator_opt(read_opts);
240+
iter.seek_for_prev(key);
241+
while iter.valid() {
242+
let k = iter.key().expect("iterator should be valid");
233243
// We want less than or equal
234-
if k.as_ref() <= key {
235-
return Ok(Some(k.to_vec().into()));
244+
if k <= key {
245+
return Ok(Some(iter_bytes_to_vec(k).into()));
236246
}
247+
iter.prev();
237248
}
249+
iter.status()
250+
.context("failed to iterate rocksdb for last_less_or_equal")?;
238251
Ok(None)
239252
}
240253
_ => {
@@ -261,20 +274,21 @@ impl TransactionTask {
261274
}
262275

263276
// Create an iterator to find the key
264-
let iter = txn.iterator_opt(
265-
rocksdb::IteratorMode::From(key, rocksdb::Direction::Forward),
266-
ReadOptions::default(),
267-
);
277+
let mut iter = txn.raw_iterator_opt(ReadOptions::default());
278+
iter.seek(key);
268279

269280
let mut keys: Vec<Vec<u8>> = Vec::new();
270281

271-
for item in iter {
272-
let (k, _v) = item.context("failed to iterate rocksdb for key selector")?;
273-
keys.push(k.to_vec());
282+
while iter.valid() {
283+
let k = iter.key().expect("iterator should be valid");
284+
keys.push(iter_bytes_to_vec(k));
274285
if keys.len() > (offset.abs() + 1) as usize {
275286
break;
276287
}
288+
iter.next();
277289
}
290+
iter.status()
291+
.context("failed to iterate rocksdb for key selector")?;
278292

279293
// Apply the selector logic
280294
let idx = if or_equal {
@@ -328,19 +342,28 @@ impl TransactionTask {
328342
.context("failed to delete key from rocksdb")?;
329343
}
330344
Operation::ClearRange { begin, end } => {
331-
// RocksDB doesn't have a native clear_range, so we need to iterate and delete
345+
// RocksDB doesn't have a native clear_range, so we need to iterate and delete.
346+
// Collect the in-range keys first, then delete after dropping the iterator, so
347+
// we never mutate the transaction's write batch while iterating it.
332348
let read_opts = ReadOptions::default();
333-
let iter = txn.iterator_opt(
334-
rocksdb::IteratorMode::From(&begin, rocksdb::Direction::Forward),
335-
read_opts,
336-
);
337-
338-
for item in iter {
339-
let (k, _v) = item.context("failed to iterate rocksdb for clear range")?;
340-
if k.as_ref() >= end.as_slice() {
349+
let mut iter = txn.raw_iterator_opt(read_opts);
350+
iter.seek(&begin);
351+
352+
let mut keys_to_delete: Vec<Vec<u8>> = Vec::new();
353+
while iter.valid() {
354+
let k = iter.key().expect("iterator should be valid");
355+
if k >= end.as_slice() {
341356
break;
342357
}
343-
txn.delete(&k)
358+
keys_to_delete.push(iter_bytes_to_vec(k));
359+
iter.next();
360+
}
361+
iter.status()
362+
.context("failed to iterate rocksdb for clear range")?;
363+
drop(iter);
364+
365+
for key in keys_to_delete {
366+
txn.delete(&key)
344367
.context("failed to delete key in range from rocksdb")?;
345368
}
346369
}
@@ -447,47 +470,54 @@ impl TransactionTask {
447470
// during a forward scan and reversing afterward would instead return the
448471
// lowest keys, which is wrong for reverse range reads.
449472
if reverse {
450-
let iter = txn.iterator_opt(
451-
rocksdb::IteratorMode::From(&resolved_end, rocksdb::Direction::Reverse),
452-
read_opts,
453-
);
473+
let mut iter = txn.raw_iterator_opt(read_opts);
474+
iter.seek_for_prev(&resolved_end);
454475

455-
for item in iter {
456-
let (k, v) = item.context("failed to iterate rocksdb for get range")?;
476+
while iter.valid() {
477+
let k = iter.key().expect("iterator should be valid");
457478
// The end key is exclusive, so skip anything at or above it.
458-
if k.as_ref() >= resolved_end.as_slice() {
479+
if k >= resolved_end.as_slice() {
480+
iter.prev();
459481
continue;
460482
}
461483
// The begin key is inclusive; once we drop below it we are done.
462-
if k.as_ref() < resolved_begin.as_slice() {
484+
if k < resolved_begin.as_slice() {
463485
break;
464486
}
465487

466-
results.push(KeyValue::new(k.to_vec(), v.to_vec()));
488+
let key = iter_bytes_to_vec(k);
489+
let value = iter.value().map(iter_bytes_to_vec).unwrap_or_default();
490+
results.push(KeyValue::new(key, value));
467491

468492
if results.len() >= limit {
469493
break;
470494
}
495+
iter.prev();
471496
}
497+
iter.status()
498+
.context("failed to iterate rocksdb for get range")?;
472499
} else {
473-
let iter = txn.iterator_opt(
474-
rocksdb::IteratorMode::From(&resolved_begin, rocksdb::Direction::Forward),
475-
read_opts,
476-
);
500+
let mut iter = txn.raw_iterator_opt(read_opts);
501+
iter.seek(&resolved_begin);
477502

478-
for item in iter {
479-
let (k, v) = item.context("failed to iterate rocksdb for get range")?;
503+
while iter.valid() {
504+
let k = iter.key().expect("iterator should be valid");
480505
// Check if we've reached the end key
481-
if k.as_ref() >= resolved_end.as_slice() {
506+
if k >= resolved_end.as_slice() {
482507
break;
483508
}
484509

485-
results.push(KeyValue::new(k.to_vec(), v.to_vec()));
510+
let key = iter_bytes_to_vec(k);
511+
let value = iter.value().map(iter_bytes_to_vec).unwrap_or_default();
512+
results.push(KeyValue::new(key, value));
486513

487514
if results.len() >= limit {
488515
break;
489516
}
517+
iter.next();
490518
}
519+
iter.status()
520+
.context("failed to iterate rocksdb for get range")?;
491521
}
492522

493523
Ok(Values::new(results))
@@ -511,35 +541,31 @@ impl TransactionTask {
511541
match (or_equal, offset) {
512542
(false, 1) => {
513543
// first_greater_or_equal: find first key >= search_key
514-
let iter = txn.iterator_opt(
515-
rocksdb::IteratorMode::From(key, rocksdb::Direction::Forward),
516-
read_opts,
517-
);
518-
for item in iter {
519-
let (k, _v) = item.context(
520-
"failed to iterate rocksdb for range selector first_greater_or_equal",
521-
)?;
522-
return Ok(k.to_vec());
523-
}
544+
let mut iter = txn.raw_iterator_opt(read_opts);
545+
iter.seek(key);
546+
let result = iter.key().map(iter_bytes_to_vec);
547+
iter.status().context(
548+
"failed to iterate rocksdb for range selector first_greater_or_equal",
549+
)?;
524550
// If no key found, return a key that will make the range empty
525-
Ok(vec![0xff; 255])
551+
Ok(result.unwrap_or_else(|| vec![0xff; 255]))
526552
}
527553
(true, 1) => {
528554
// first_greater_than: find first key > search_key
529-
let iter = txn.iterator_opt(
530-
rocksdb::IteratorMode::From(key, rocksdb::Direction::Forward),
531-
read_opts,
532-
);
533-
for item in iter {
534-
let (k, _v) = item.context(
535-
"failed to iterate rocksdb for range selector first_greater_than",
536-
)?;
555+
let mut iter = txn.raw_iterator_opt(read_opts);
556+
iter.seek(key);
557+
while iter.valid() {
558+
let k = iter.key().expect("iterator should be valid");
537559
// Skip if it's the exact key
538-
if k.as_ref() == key {
560+
if k == key {
561+
iter.next();
539562
continue;
540563
}
541-
return Ok(k.to_vec());
564+
return Ok(iter_bytes_to_vec(k));
542565
}
566+
iter.status().context(
567+
"failed to iterate rocksdb for range selector first_greater_than",
568+
)?;
543569
// If no key found, return a key that will make the range empty
544570
Ok(vec![0xff; 255])
545571
}

0 commit comments

Comments
 (0)