Skip to content

Commit

Permalink
Stop leaking in new_closure
Browse files Browse the repository at this point in the history
  • Loading branch information
mejrs committed Oct 16, 2022
1 parent 4a04603 commit 4876f1d
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 50 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ cfg-if = "1.0"
libc = "0.2.62"
parking_lot = ">= 0.11, < 0.13"
memoffset = "0.6.5"
memchr = "1"

# ffi bindings to the python interpreter, split into a separate crate so they can be used independently
pyo3-ffi = { path = "pyo3-ffi", version = "=0.17.2" }
Expand Down
53 changes: 44 additions & 9 deletions src/impl_/pymethods.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::internal_tricks::{extract_cstr_or_leak_cstring, NulByteInString};
use crate::internal_tricks::{extract_cstr_or_leak_cstring, MaybeLeaked, NulByteInString};
use crate::{ffi, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, PyTraverseError, Python};
use std::ffi::CStr;
use std::ffi::CString;
use std::fmt;
use std::mem::ManuallyDrop;
use std::os::raw::c_int;

/// Python 3.8 and up - __ipow__ has modulo argument correctly populated.
Expand Down Expand Up @@ -161,7 +162,9 @@ impl PyMethodDef {
}

/// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef`
pub(crate) fn as_method_def(&self) -> Result<ffi::PyMethodDef, NulByteInString> {
pub(crate) fn as_method_def(
&self,
) -> Result<(ffi::PyMethodDef, ManuallyDrop<PyMethodDefDestructor>), NulByteInString> {
let meth = match self.ml_meth {
PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer {
PyCFunction: meth.0,
Expand All @@ -175,12 +178,28 @@ impl PyMethodDef {
},
};

Ok(ffi::PyMethodDef {
ml_name: get_name(self.ml_name)?.as_ptr(),
let name = get_name(self.ml_name)?;
let doc = match get_doc(self.ml_doc) {
Ok(d) => d,
Err(e) => {
// Free `name` if it got leaked and doc failed.
if let MaybeLeaked::Leaked(ptr) = name {
let _ = unsafe { CString::from_raw(ptr) };
}
return Err(e);
}
};
let destructor = ManuallyDrop::new(PyMethodDefDestructor {
name: Some(name),
doc: Some(doc),
});
let def = ffi::PyMethodDef {
ml_name: name.as_ptr(),
ml_meth: meth,
ml_flags: self.ml_flags,
ml_doc: get_doc(self.ml_doc)?.as_ptr(),
})
ml_doc: doc.as_ptr(),
};
Ok((def, destructor))
}
}

Expand Down Expand Up @@ -245,11 +264,11 @@ impl PySetterDef {
}
}

fn get_name(name: &'static str) -> Result<&'static CStr, NulByteInString> {
fn get_name(name: &'static str) -> Result<MaybeLeaked, NulByteInString> {
extract_cstr_or_leak_cstring(name, "Function name cannot contain NUL byte.")
}

fn get_doc(doc: &'static str) -> Result<&'static CStr, NulByteInString> {
fn get_doc(doc: &'static str) -> Result<MaybeLeaked, NulByteInString> {
extract_cstr_or_leak_cstring(doc, "Document cannot contain NUL byte.")
}

Expand All @@ -263,6 +282,22 @@ pub fn unwrap_traverse_result(result: Result<(), PyTraverseError>) -> c_int {
}
}

pub(crate) struct PyMethodDefDestructor {
name: Option<MaybeLeaked>,
doc: Option<MaybeLeaked>,
}

impl Drop for PyMethodDefDestructor {
fn drop(&mut self) {
if let Some(MaybeLeaked::Leaked(ptr)) = self.name.take() {
let _ = unsafe { CString::from_raw(ptr) };
}
if let Some(MaybeLeaked::Leaked(ptr)) = self.doc.take() {
let _ = unsafe { CString::from_raw(ptr) };
}
}
}

// The macros need to Ok-wrap the output of user defined functions; i.e. if they're not a result, make them into one.
pub trait OkWrap<T> {
type Error;
Expand Down
44 changes: 37 additions & 7 deletions src/internal_tricks.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::ffi::{Py_ssize_t, PY_SSIZE_T_MAX};
use std::ffi::{CStr, CString};

use std::os::raw::c_char;
pub struct PrivateMarker;

macro_rules! private_decl {
Expand Down Expand Up @@ -35,15 +35,45 @@ macro_rules! pyo3_exception {
#[derive(Debug)]
pub(crate) struct NulByteInString(pub(crate) &'static str);

#[derive(Copy, Clone)]
pub(crate) enum MaybeLeaked {
Static(*const c_char),
Leaked(*mut c_char),
}

impl MaybeLeaked {
pub(crate) fn as_ptr(&self) -> *const c_char {
match *self {
Self::Static(ptr) => ptr,
Self::Leaked(ptr) => ptr as *const c_char,
}
}
}

pub(crate) fn extract_cstr_or_leak_cstring(
src: &'static str,
err_msg: &'static str,
) -> Result<&'static CStr, NulByteInString> {
CStr::from_bytes_with_nul(src.as_bytes())
.or_else(|_| {
CString::new(src.as_bytes()).map(|c_string| &*Box::leak(c_string.into_boxed_c_str()))
})
.map_err(|_| NulByteInString(err_msg))
) -> Result<MaybeLeaked, NulByteInString> {
let bytes = src.as_bytes();
let nul_pos = memchr::memchr(0, bytes);
match nul_pos {
Some(nul_pos) if nul_pos + 1 == bytes.len() => {
// SAFETY: We know there is only one nul byte, at the end
// of the byte slice.
let ptr = unsafe { CStr::from_bytes_with_nul_unchecked(bytes).as_ptr() };
Ok(MaybeLeaked::Static(ptr))
}
Some(_) => Err(NulByteInString(err_msg)),
None => {
let mut v = Vec::with_capacity(bytes.len() + 1);
v.extend_from_slice(bytes);

// SAFETY: There is no null byte, and `from_vec_unchecked` will append one.
let cstring = unsafe { CString::from_vec_unchecked(v) };
let ptr = cstring.into_raw();
Ok(MaybeLeaked::Leaked(ptr))
}
}
}

/// Convert an usize index into a Py_ssize_t index, clamping overflow to
Expand Down
5 changes: 4 additions & 1 deletion src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ impl PyTypeBuilder {
}
PyMethodDefType::Method(def)
| PyMethodDefType::Class(def)
| PyMethodDefType::Static(def) => self.method_defs.push(def.as_method_def().unwrap()),
| PyMethodDefType::Static(def) => {
let (def, _destructor) = def.as_method_def().unwrap();
self.method_defs.push(def);
}
// These class attributes are added after the type gets created by LazyStaticType
PyMethodDefType::ClassAttribute(_) => {}
}
Expand Down
4 changes: 2 additions & 2 deletions src/type_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! Python type object information
use crate::impl_::pyclass::PyClassItemsIter;
use crate::internal_tricks::extract_cstr_or_leak_cstring;
use crate::internal_tricks::{extract_cstr_or_leak_cstring, MaybeLeaked};
use crate::once_cell::GILOnceCell;
use crate::pyclass::create_type_object;
use crate::pyclass::PyClass;
Expand Down Expand Up @@ -221,7 +221,7 @@ impl LazyStaticType {
fn initialize_tp_dict(
py: Python<'_>,
type_object: *mut ffi::PyObject,
items: Vec<(&'static std::ffi::CStr, PyObject)>,
items: Vec<(MaybeLeaked, PyObject)>,
) -> PyResult<()> {
// We hold the GIL: the dictionary update can be considered atomic from
// the POV of other threads.
Expand Down
85 changes: 54 additions & 31 deletions src/types/function.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use crate::derive_utils::PyFunctionArguments;
use crate::exceptions::PyValueError;
use crate::impl_::panic::PanicTrap;
use crate::methods::PyMethodDefDestructor;
use crate::panic::PanicException;
use crate::{
ffi,
impl_::pymethods::{self, PyMethodDef},
types, AsPyPointer,
};
use crate::{prelude::*, GILPool};
use std::mem::ManuallyDrop;
use std::os::raw::c_void;

/// Represents a builtin Python function object.
Expand All @@ -28,15 +30,23 @@ where
R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>,
{
crate::callback_body!(py, {
let boxed_fn: &F =
let boxed_fn: &ClosureDestructor<F> =
&*(ffi::PyCapsule_GetPointer(capsule_ptr, CLOSURE_CAPSULE_NAME.as_ptr() as *const _)
as *mut F);
as *mut ClosureDestructor<F>);
let args = py.from_borrowed_ptr::<types::PyTuple>(args);
let kwargs = py.from_borrowed_ptr_or_opt::<types::PyDict>(kwargs);
boxed_fn(args, kwargs)
(boxed_fn.closure)(args, kwargs)
})
}

struct ClosureDestructor<F> {
closure: F,
def: ffi::PyMethodDef,
// Used to destroy the cstrings in `def`, if necessary.
#[allow(dead_code)]
def_destructor: PyMethodDefDestructor,
}

unsafe extern "C" fn drop_closure<F, R>(capsule_ptr: *mut ffi::PyObject)
where
F: Fn(&types::PyTuple, Option<&types::PyDict>) -> R + Send + 'static,
Expand All @@ -45,11 +55,12 @@ where
let trap = PanicTrap::new("uncaught panic during drop_closure");
let pool = GILPool::new();
if let Err(payload) = std::panic::catch_unwind(|| {
let boxed_fn: Box<F> = Box::from_raw(ffi::PyCapsule_GetPointer(
let destructor: Box<ClosureDestructor<F>> = Box::from_raw(ffi::PyCapsule_GetPointer(
capsule_ptr,
CLOSURE_CAPSULE_NAME.as_ptr() as *const _,
) as *mut F);
drop(boxed_fn)
)
as *mut ClosureDestructor<F>);
drop(destructor)
}) {
let py = pool.python();
let err = PanicException::from_panic_payload(payload);
Expand Down Expand Up @@ -106,45 +117,45 @@ impl PyCFunction {
/// py_run!(py, add_one, "assert add_one(42) == 43");
/// });
/// ```
pub fn new_closure<F, R>(f: F, py: Python<'_>) -> PyResult<&PyCFunction>
pub fn new_closure<F, R>(closure: F, py: Python<'_>) -> PyResult<&PyCFunction>
where
F: Fn(&types::PyTuple, Option<&types::PyDict>) -> R + Send + 'static,
R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>,
{
let function_ptr = Box::into_raw(Box::new(f));
let capsule = unsafe {
let method_def = pymethods::PyMethodDef::cfunction_with_keywords(
"pyo3-closure\0",
pymethods::PyCFunctionWithKeywords(run_closure::<F, R>),
"\0",
);
let (def, def_destructor) = method_def
.as_method_def()
.map_err(|err| PyValueError::new_err(err.0))?;
let ptr = Box::into_raw(Box::new(ClosureDestructor {
closure,
def,
// Disable the `ManuallyDrop`; we do actually want to drop this later.
def_destructor: ManuallyDrop::into_inner(def_destructor),
}));

let destructor = unsafe {
PyObject::from_owned_ptr_or_err(
py,
ffi::PyCapsule_New(
function_ptr as *mut c_void,
ptr as *mut c_void,
CLOSURE_CAPSULE_NAME.as_ptr() as *const _,
Some(drop_closure::<F, R>),
),
)?
};
let method_def = pymethods::PyMethodDef::cfunction_with_keywords(
"pyo3-closure",
pymethods::PyCFunctionWithKeywords(run_closure::<F, R>),
"",
);
Self::internal_new_from_pointers(&method_def, py, capsule.as_ptr(), std::ptr::null_mut())
}

#[doc(hidden)]
fn internal_new_from_pointers<'py>(
method_def: &PyMethodDef,
py: Python<'py>,
mod_ptr: *mut ffi::PyObject,
module_name: *mut ffi::PyObject,
) -> PyResult<&'py Self> {
let def = method_def
.as_method_def()
.map_err(|err| PyValueError::new_err(err.0))?;
unsafe {
py.from_owned_ptr_or_err::<PyCFunction>(ffi::PyCFunction_NewEx(
Box::into_raw(Box::new(def)),
mod_ptr,
module_name,
#[cfg(addr_of)]
core::ptr::addr_of_mut!((*ptr).def),
#[cfg(not(addr_of))]
&mut (*ptr).def,
destructor.as_ptr(),
std::ptr::null_mut(),
))
}
}
Expand All @@ -162,7 +173,19 @@ impl PyCFunction {
} else {
(std::ptr::null_mut(), std::ptr::null_mut())
};
Self::internal_new_from_pointers(method_def, py, mod_ptr, module_name)
let (def, _destructor) = method_def
.as_method_def()
.map_err(|err| PyValueError::new_err(err.0))?;

let def = Box::into_raw(Box::new(def));

unsafe {
py.from_owned_ptr_or_err::<PyCFunction>(ffi::PyCFunction_NewEx(
def,
mod_ptr,
module_name,
))
}
}
}

Expand Down

0 comments on commit 4876f1d

Please sign in to comment.