Skip to content

Fix panic-safety in AlignedBox::realloc (double-free on panicking element Drop) - #6

Open
tooson9010-spec wants to merge 1 commit into
michaellass:masterfrom
tooson9010-spec:fix/realloc-panic-safety
Open

Fix panic-safety in AlignedBox::realloc (double-free on panicking element Drop)#6
tooson9010-spec wants to merge 1 commit into
michaellass:masterfrom
tooson9010-spec:fix/realloc-panic-safety

Conversation

@tooson9010-spec

Copy link
Copy Markdown

Found while auditing this crate's unsafe teardown paths for panic-safety.

Summary

When shrinking, realloc takes the Box out of self.container and drops the tail
elements. If an element's Drop panics, self.container still holds the old pointer
while ownership has moved out, so AlignedBox's own Drop frees those elements a
second time, a double-free (CWE-415) reachable from safe Rust.

Fix

Drop the tail back to front under a guard. On unwind the guard restores
self.container and self.layout to the still-live prefix [0..valid], so every
element is freed exactly once. On the normal path the guard is disarmed and the
existing realloc flow continues unchanged.

Verification

Added realloc_shrink_panicking_drop_is_sound: a box of elements whose Drop panics
is shrunk, then dropped. Without the fix the second drop double-frees the tail
(glibc "double free detected", SIGABRT); with the fix it unwinds cleanly.
Existing tests pass. Confirmed on 0.3.0.

…ment Drop)

When shrinking, realloc takes the Box out of self.container and drops the tail
elements. Each element's Drop is user-controlled and may panic; if it does,
self.container still holds the old pointer while ownership has moved out, so
AlignedBox's own Drop frees those elements a second time -- a double-free
reachable from safe Rust. Drop the tail back to front under a guard that, on
unwind, restores self.container/self.layout to the still-live prefix, so every
element is freed exactly once. Adds a regression test.
@tooson9010-spec

Copy link
Copy Markdown
Author

Two notes:

  • This only covers the shrink path. The realloc-failure branch re-runs
    initializer on the dropped slots, so a panicking initializer (e.g. a
    panicking T::default) still leaves self.container stale and can double-free.
    Different root cause, so I didn't touch it here; can send a separate PR if
    you want it fixed too.

  • The element being dropped when the panic hits (index valid) is excluded
    from the restored prefix [0..valid], so it's never freed twice. It leaks
    instead, which is fine on an unwinding path.

@michaellass

Copy link
Copy Markdown
Owner

Hi. Thanks for tracking this down and providing a fix! The first CI fail is just a missing cargo fmt. The second one could actually indicate an issue detected by miri:

~/git/crates/aligned_box (git)-[tooson9010-spec-fix/realloc-panic-safety] % cargo miri test
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running unittests src/lib.rs (target/miri/x86_64-unknown-linux-gnu/debug/build/aligned_box/9e2a8e54c3168622/out/aligned_box-9e2a8e54c3168622)

running 16 tests
test tests::aliasing ... ok
test tests::alignment ... ok
test tests::clone ... ok
test tests::clone_rss ... ignored
test tests::copy_sem ... ok
test tests::defaults ... ok
test tests::drop_contained ... ok
test tests::free ... ignored
test tests::min_align ... ignored
test tests::move_sem ... ok
test tests::read_write ... ok
test tests::realloc_shrink_panicking_drop_is_sound ... error: Undefined Behavior: incorrect layout on deallocation: alloc174631 has size 192 and alignment 64, but gave size 168 and alignment 64
  --> src/lib.rs:84:13
   |
84 |             alloc::alloc::dealloc(ptr as *mut u8, self.layout);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
   |
   = 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: this is on thread `tests::realloc_`
   = note: stack backtrace:
           0: <AlignedBox<[tests::realloc_shrink_panicking_drop_is_sound::PanicOnDrop]> as std::ops::Drop>::drop
               at src/lib.rs:84:13: 84:63
           1: std::ptr::drop_glue::<AlignedBox<[tests::realloc_shrink_panicking_drop_is_sound::PanicOnDrop]>> - shim(Some(AlignedBox<[tests::realloc_shrink_panicking_drop_is_sound::PanicOnDrop]>))
               at /scratch/bevan/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:848:1: 850:25
           2: std::mem::drop::<AlignedBox<[tests::realloc_shrink_panicking_drop_is_sound::PanicOnDrop]>>
               at /scratch/bevan/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/mem/mod.rs:1049:1: 1049:2
           3: tests::realloc_shrink_panicking_drop_is_sound
               at src/lib.rs:883:9: 883:16
           4: tests::realloc_shrink_panicking_drop_is_sound::{closure#0}
               at src/lib.rs:846:48: 846:48

I think there is an off-by-one error in the set value for guard.valid. The loop iterates over item indices (0..n-1), so guard.valid has to be set to i+1, right? However, with that modification, the test fails and detects a double-free again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants