Skip to content
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

Pointer dereference after free with allocator containing shared state #95269

Closed
jhorstmann opened this issue Mar 24, 2022 · 3 comments · Fixed by #95298
Closed

Pointer dereference after free with allocator containing shared state #95269

jhorstmann opened this issue Mar 24, 2022 · 3 comments · Fixed by #95298
Labels
C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-medium Medium priority T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@jhorstmann
Copy link
Contributor

I'm trying out the allocator api with the goal of being able to track memory usage for parts of a program:

#![feature(allocator_api)]
#![feature(core_intrinsics)]

use std::alloc::{AllocError, Allocator, Layout, System};
use std::intrinsics::black_box;
use std::ptr::NonNull;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

#[derive(Default, Clone)]
struct TrackingAllocator {
    memory_usage: Arc<AtomicUsize>,
}

unsafe impl Allocator for TrackingAllocator {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        self.memory_usage
            .fetch_add(layout.size(), Ordering::Relaxed);
        System.allocate(layout)
    }

    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        self.memory_usage
            .fetch_add(layout.size(), Ordering::Relaxed);
        System.allocate_zeroed(layout)
    }

    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
        self.memory_usage
            .fetch_sub(layout.size(), Ordering::Relaxed);
        System.deallocate(ptr, layout)
    }
}

fn bool_to_int(input: Vec<bool, TrackingAllocator>) -> Vec<i32, TrackingAllocator> {
    let allocator = input.allocator().clone();
    let mut output = Vec::with_capacity_in(input.len(), allocator);
    for b in input {
        output.push(b as i32);
    }
    output
}

fn main() {
    let tracking_alloc = TrackingAllocator::default();
    {
        let mut input = Vec::new_in(tracking_alloc.clone());
        input.push(true);
        input.push(false);
        let output = bool_to_int(input);
        black_box(output);
    }
    {
        let mut input = Vec::new_in(tracking_alloc.clone());
        input.push(true);
        input.push(true);
        let output = bool_to_int(input);
        black_box(output);
    }
    eprintln!("{}", tracking_alloc.memory_usage.load(Ordering::Relaxed));
}

I expected this to work (and print 0 at the end), but instead it fails by freeing an invalid pointer:

$ ./target/debug/memory_tracking
free(): invalid pointer
Aborted

Running with miri gives a bit more context:

error: Undefined Behavior: pointer to alloc984 was dereferenced after this allocation got freed
   --> /home/user/.rustup/toolchains/nightly-2022-03-23-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/non_null.rs:328:18
    |
328 |         unsafe { &*self.as_ptr() }
    |                  ^^^^^^^^^^^^^^^ pointer to alloc984 was dereferenced after this allocation got freed
    |
    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
    = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
            
    = note: inside `std::ptr::NonNull::<alloc::sync::ArcInner<std::sync::atomic::AtomicUsize>>::as_ref` at /home/user/.rustup/toolchains/nightly-2022-03-23-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/non_null.rs:328:18
    = note: inside `std::sync::Arc::<std::sync::atomic::AtomicUsize>::inner` at /home/user/.rustup/toolchains/nightly-2022-03-23-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/sync.rs:1084:18
    = note: inside `<std::sync::Arc<std::sync::atomic::AtomicUsize> as std::clone::Clone>::clone` at /home/user/.rustup/toolchains/nightly-2022-03-23-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/sync.rs:1342:24
note: inside `<TrackingAllocator as std::clone::Clone>::clone` at src/main.rs:12:5
   --> src/main.rs:12:5
    |
10  | #[derive(Default, Clone)]
    |                   ----- in this derive macro expansion
11  | struct TrackingAllocator {
12  |     memory_usage: Arc<AtomicUsize>,
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: inside `main` at src/main.rs:54:37
   --> src/main.rs:54:37
    |
54  |         let mut input = Vec::new_in(tracking_alloc.clone());
    |                                     ^^^^^^^^^^^^^^^^^^^^^^
    = note: this error originates in the derive macro `Clone` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

Meta

$ rustc --version --verbose
rustc 1.61.0-nightly (5f3700105 2022-03-22)
binary: rustc
commit-hash: 5f37001055c29982f4c27ee9edd90449c8e07774
commit-date: 2022-03-22
host: x86_64-unknown-linux-gnu
release: 1.61.0-nightly
LLVM version: 14.0.0
@jhorstmann jhorstmann added the C-bug Category: This is a bug. label Mar 24, 2022
@Urgau
Copy link
Member

Urgau commented Mar 24, 2022

This seems to be a problem with the into_iter() implementation of Vec because if I do:

-    for b in input {
-        output.push(b as i32);
+    for b in &input {
+        output.push(*b as i32);
    }

There is no crash and no Miri error, this to me indicate that there is a problem in the implementation of IntoIter.

Another interesting things is the message I have locally malloc(): unaligned tcache chunk detected, I don't know what it means but it's certainly interesting.

@rustbot label +T-libs +I-unsound

@rustbot rustbot added I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-libs Relevant to the library team, which will review and decide on the PR/issue. I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Mar 24, 2022
@tmiasko
Copy link
Contributor

tmiasko commented Mar 24, 2022

The drop guard used in the implementation of IntoIter makes a raw copy of an allocator when reconstructing RawVec. The allocator is then dropped twice, once by RawVec drop and second time by IntoIter drop.

let alloc = ptr::read(&self.0.alloc);
// RawVec handles deallocation
let _ = RawVec::from_raw_parts_in(self.0.buf.as_ptr(), self.0.cap, alloc);

cc @TimDiekmann #78461.

@orlp
Copy link
Contributor

orlp commented Mar 25, 2022

Minimal reproduction of problem:

#![feature(allocator_api)]

use core::ptr::NonNull;
use core::alloc::{AllocError, Allocator, Layout};

struct PrintOnDrop;
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("dropped")
    }
}

unsafe impl Allocator for PrintOnDrop {
    fn allocate(&self, _layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        Err(AllocError)
    }

    unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) { }
}

fn main() {
    Vec::<u32, _>::new_in(PrintOnDrop { }).into_iter();
}

Prints:

dropped
dropped

Dylan-DPC added a commit to Dylan-DPC/rust that referenced this issue Mar 30, 2022
…ator-in-vec-into-iter, r=oli-obk

Fix double drop of allocator in IntoIter impl of Vec

Fixes rust-lang#95269

The `drop` impl of `IntoIter` reconstructs a `RawVec` from `buf`, `cap` and `alloc`, when that `RawVec` is dropped it also drops the allocator. To avoid dropping the allocator twice we wrap it in `ManuallyDrop` in the `InttoIter` struct.

Note this is my first contribution to the standard library, so I might be missing some details or a better way to solve this.
Dylan-DPC added a commit to Dylan-DPC/rust that referenced this issue Mar 30, 2022
…ator-in-vec-into-iter, r=oli-obk

Fix double drop of allocator in IntoIter impl of Vec

Fixes rust-lang#95269

The `drop` impl of `IntoIter` reconstructs a `RawVec` from `buf`, `cap` and `alloc`, when that `RawVec` is dropped it also drops the allocator. To avoid dropping the allocator twice we wrap it in `ManuallyDrop` in the `InttoIter` struct.

Note this is my first contribution to the standard library, so I might be missing some details or a better way to solve this.
@m-ou-se m-ou-se added P-medium Medium priority and removed I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Mar 30, 2022
Dylan-DPC added a commit to Dylan-DPC/rust that referenced this issue Mar 30, 2022
…ator-in-vec-into-iter, r=oli-obk

Fix double drop of allocator in IntoIter impl of Vec

Fixes rust-lang#95269

The `drop` impl of `IntoIter` reconstructs a `RawVec` from `buf`, `cap` and `alloc`, when that `RawVec` is dropped it also drops the allocator. To avoid dropping the allocator twice we wrap it in `ManuallyDrop` in the `InttoIter` struct.

Note this is my first contribution to the standard library, so I might be missing some details or a better way to solve this.
@bors bors closed this as completed in d6c959c Mar 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-medium Medium priority T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants