-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix use-after-free bug on an internal deque node #15
Conversation
Move internal pointers to deque nodes from `ValueEntry` to `EntryInfo`, so that the pointers are no longer copied on updating an entry.
I found the following program can reproduce the panic (and segmentation faults) with v0.10.1. // Cargo.toml
//
// [dependencies]
// mini-moka = "0.10.1"
// #mini-moka = { git = "https://github.com/moka-rs/mini-moka", branch = "fix-panics-in-deque" }
//
// rand = { version = "0.8.5", features = ["small_rng"] }
// rand_distr = "0.4.3"
use std::{io::Write, sync::Arc};
use mini_moka::sync::Cache;
use rand::{rngs::SmallRng, seq::SliceRandom, SeedableRng};
use rand_distr::{Distribution, Normal};
const MAX_CAPACITY: u64 = 100;
const NUM_KEYS: usize = 200;
const NUM_THREADS: usize = 8;
const OPERATIONS_PER_THREAD: usize = 10_000_000;
enum Op {
Get,
Insert,
Invalidate,
}
const OP_CHOICE: &[(Op, usize)] = &[(Op::Get, 4), (Op::Insert, 2), (Op::Invalidate, 1)];
fn main() {
// Create the cache.
let cache = Arc::new(Cache::builder().max_capacity(MAX_CAPACITY).build());
let mut seed_rng = rand::thread_rng();
// Spawn threads that will perform operations on the cache.
let threads = (0..NUM_THREADS)
.map(|id| {
let cache = Arc::clone(&cache);
let mut rng = SmallRng::from_rng(&mut seed_rng).unwrap();
let normal_distr = Normal::new((NUM_KEYS / 2) as f64, 20.0).unwrap();
std::thread::spawn(move || {
for i in 1..=OPERATIONS_PER_THREAD {
let num = normal_distr.sample(&mut rng) as u16;
let key = format!("key-{}", num);
// Generate an operation.
let op = &OP_CHOICE
.choose_weighted(&mut rng, |item| item.1)
.unwrap()
.0;
// Run the operation.
match op {
Op::Get => {
cache.get(&key);
}
Op::Insert => {
cache.insert(key, i);
}
Op::Invalidate => {
cache.invalidate(&key);
}
}
// Print progress if we are the first thread.
if id == 0 {
if i % 10_000 == 0 {
print!(".");
std::io::stdout().flush().unwrap();
}
if i % 1_000_000 == 0 {
println!();
}
}
}
if id == 0 {
println!();
}
})
})
.collect::<Vec<_>>();
// Wait for all threads to finish.
for thread in threads {
thread.join().unwrap();
}
println!("Done.");
}
|
If I run the above program on v0.10.1 with slightly modified:
|
Make `apply_reads` to check if the deque node is still valid before moving it in the deque.
Updated the (entire) descriptions. |
Deques
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merging into v0.10.x branch.
Fixes #11.
This bug caused memory corruption, leading a panic reported by #11 and also segmentation fault.
The root cause
This was a use-after-free bug caused by a dangling
NonNull
pointer.ValueEntry
.ValueEntry
has aNonNull
pointer to a deque node.insert
on an existing entry is called,ValueEntry
is replaced with a new one.NonNull
pointer is copied to the newValueEntry
, so both old and newValueEntry
s have the sameNonNull
pointer.NonNull
pointer is unset in the newValueEntry
.ValueEntry
can be still referenced from a read log, and itsNonNull
pointer becomes dangling.A possible steps to reproduce
This is a timing issue. Here is one of the possible reproducing steps.
insert
for a keyA
.ValueEntry
(1).get
onA
.ValueEntry
(1).A
by callinginsert
.ValueEntry
(2) with copiedNonNull
pointer.A
is evicted from the cache:A
is expired.invalidate
is called onA
.A
is selected to evict.A
is dropped, and then theNonNull
pointer inValueEntry
(2) is set toNone
. (Its type isOption<NonNull<...>>
)NonNull
pointer inValueEntry
(1).NonNull
points to.Fixes
NonNull
pointer is still valid.ValueEntry
has anEntryInfo
struct, and it has a flag to know if the deque node still exists.insert
, do not copyNonNull
pointer.NonNull
pointer fromValueEntry
struct toEntryInfo
struct.EntryInfo
is not copied, but shared between new and oldValueEntry
s by using anArc
pointer.