Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Misbehaving HandleLike implementation can lead to memory safety violation #87

Open
Qwaz opened this issue Aug 31, 2020 · 1 comment
Open

Comments

@Qwaz
Copy link

Qwaz commented Aug 31, 2020

pub trait HandleLike: Debug + Copy + Hash + PartialEq + Eq + Send + Sync {
fn new(index: HandleIndex, version: HandleIndex) -> Self;
fn index(&self) -> HandleIndex;
fn version(&self) -> HandleIndex;
}

/// Returns mutable reference to internal value with name `Handle`.
#[inline]
pub fn get_mut(&mut self, handle: H) -> Option<&mut T> {
if self.handles.contains(handle) {
unsafe { Some(self.entries.get_unchecked_mut(handle.index() as usize)) }
} else {
None
}
}
/// Returns immutable reference to internal value with name `Handle`.
#[inline]
pub fn get(&self, handle: H) -> Option<&T> {
if self.handles.contains(handle) {
unsafe { Some(self.entries.get_unchecked(handle.index() as usize)) }
} else {
None
}
}

impl<H: HandleLike, T: Sized> Drop for ObjectPool<H, T> {
fn drop(&mut self) {
unsafe {
for v in &self.handles {
::std::ptr::drop_in_place(&mut self.entries[v.index() as usize]);
}
self.entries.set_len(0);
}
}
}

Description

Unsafe code in ObjectPool has time-of-check to time-of-use (TOCTOU) bug that can eventually lead to a memory safety violation. ObjectPool and HandlePool implicitly assumes that HandleLike trait methods are pure, i.e., they always return the same value. However, this assumption is unsound since HandleLike is a safe, public trait that allows a custom implementation.

Demonstration

  • Crate: crayon
  • Version: 0.7.1
  • OS: Ubuntu 18.04.5 LTS
  • Rust: rustc 1.46.0 (04488afe3 2020-08-24)
#![forbid(unsafe_code)]

use crayon::utils::handle::{HandleIndex, HandleLike};
use crayon::utils::object_pool::ObjectPool;
use std::sync::atomic::{AtomicBool, Ordering};

#[derive(Debug)]
struct DropDetector(u32);

impl Drop for DropDetector {
    fn drop(&mut self) {
        println!("Dropping {}", self.0);
    }
}

static FLAG: AtomicBool = AtomicBool::new(false);

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
struct MyHandle {
    indices: [HandleIndex; 2],
    version: HandleIndex,
}

impl HandleLike for MyHandle {
    fn new(index: HandleIndex, version: HandleIndex) -> Self {
        MyHandle {
            indices: [index, index],
            version,
        }
    }

    fn index(&self) -> HandleIndex {
        if dbg!(FLAG.fetch_xor(true, Ordering::Relaxed)) {
            self.indices[1]
        } else {
            self.indices[0]
        }
    }

    fn version(&self) -> HandleIndex {
        self.version
    }
}

impl MyHandle {
    fn with_indices(indices: [HandleIndex; 2], version: HandleIndex) -> Self {
        MyHandle { indices, version }
    }
}

fn main() {
    let mut pool = ObjectPool::new();
    let real_handle: MyHandle = pool.create(123);
    let fake_handle =
        MyHandle::with_indices([real_handle.index(), 12345678], real_handle.version());

    // Segfault with OOB, accessing`pool.entries[12345678]` without boundary checking
    dbg!(pool.get(fake_handle));

    // The bug can be similarly triggered in all other methods of `ObjectPool`
    // that call `handle.index()` in an unsafe block.
}

Output:

[src/main.rs:57] FLAG.fetch_xor(true, Ordering::Relaxed) = false
[src/main.rs:57] FLAG.fetch_xor(true, Ordering::Relaxed) = true
[src/main.rs:57] FLAG.fetch_xor(true, Ordering::Relaxed) = false
[src/main.rs:57] FLAG.fetch_xor(true, Ordering::Relaxed) = true
[src/main.rs:82] pool.get(fake_handle) = Some(

Return Code: -11 (SIGSEGV)

@Qwaz
Copy link
Author

Qwaz commented Aug 31, 2020

/// Recycles the value with name `Handle`.
#[inline]
pub fn free(&mut self, handle: H) -> Option<T> {
if self.handles.free(handle) {
unsafe {
let mut v = ::std::mem::uninitialized();
::std::mem::swap(&mut v, &mut self.entries[handle.index() as usize]);
Some(v)
}
} else {
None
}
}

Also, storing uninitialized T to Vec<T> is an undefined behavior even if it is not accessed AFAIK.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant