Files
Sprimo/skills/unsafe-checker/rules/ffi-07-no-drop-external.md
2026-02-12 22:58:33 +08:00

3.2 KiB

id, original_id, level, impact
id original_id level impact
ffi-07 P.UNS.FFI.07 P HIGH

Do Not Implement Drop for Types Passed to External Code

Summary

If a type will be passed to external code that manages its lifetime, don't implement Drop. Otherwise, both Rust and the external code will try to free it.

Rationale

  • External code (C library) may take ownership of the data
  • If Rust also tries to drop it, you get double-free
  • Need clear ownership boundaries

Bad Example

// DON'T: Drop on type that external code will free
#[repr(C)]
struct EventHandler {
    callback: extern "C" fn(i32),
    user_data: *mut c_void,
}

impl Drop for EventHandler {
    fn drop(&mut self) {
        // BAD: What if the C library already freed user_data?
        unsafe { libc::free(self.user_data); }
    }
}

extern "C" {
    // C takes ownership and frees EventHandler when done
    fn register_handler(h: *mut EventHandler);
}

fn bad_register() {
    let handler = EventHandler { /* ... */ };
    let ptr = Box::into_raw(Box::new(handler));
    unsafe {
        register_handler(ptr);
        // If C code frees this, and Rust's Drop runs too = double-free
    }
}

Good Example

// DO: No Drop for types whose lifetime is managed externally
#[repr(C)]
struct EventHandler {
    callback: extern "C" fn(i32),
    user_data: *mut c_void,
}
// No Drop impl - C library manages lifetime

extern "C" {
    fn register_handler(h: *mut EventHandler);
    fn unregister_handler(h: *mut EventHandler);
}

// DO: Wrap in a Rust type that knows when it's safe to drop
struct RegisteredHandler {
    ptr: *mut EventHandler,
    registered: bool,
}

impl RegisteredHandler {
    fn register(handler: EventHandler) -> Self {
        let ptr = Box::into_raw(Box::new(handler));
        unsafe { register_handler(ptr); }
        Self { ptr, registered: true }
    }

    fn unregister(&mut self) {
        if self.registered {
            unsafe { unregister_handler(self.ptr); }
            self.registered = false;
        }
    }
}

impl Drop for RegisteredHandler {
    fn drop(&mut self) {
        self.unregister();
        // Only free if we still own it
        if !self.registered {
            unsafe { drop(Box::from_raw(self.ptr)); }
        }
    }
}

// DO: Use ManuallyDrop for explicit control
use std::mem::ManuallyDrop;

fn explicit_ownership() {
    let handler = ManuallyDrop::new(EventHandler { /* ... */ });
    let ptr = &*handler as *const EventHandler as *mut EventHandler;
    unsafe {
        register_handler(ptr);
        // C now owns handler, don't drop it in Rust
    }
}

Ownership Patterns

Pattern Who Owns Rust Drop?
Rust creates, Rust frees Rust Yes
Rust creates, C frees C No
C creates, C frees C No (use wrapper)
C creates, Rust frees Rust Yes (in wrapper)

Checklist

  • Who will free this type's memory?
  • If external code frees it, am I avoiding Drop?
  • If ownership is conditional, do I track it?
  • Am I using ManuallyDrop or forget() when transferring ownership?
  • ffi-03: Implement Drop for wrapped C pointers (opposite case)
  • mem-03: Don't let String/Vec drop foreign memory