Skip to content

Commit e680bfa

Browse files
committed
Stop leaking in new_closure
1 parent 14d61e6 commit e680bfa

File tree

5 files changed

+127
-71
lines changed

5 files changed

+127
-71
lines changed

src/impl_/pymethods.rs

+65-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use crate::internal_tricks::{extract_cstr_or_leak_cstring, NulByteInString};
1+
use crate::exceptions::PyValueError;
22
use crate::{ffi, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, PyTraverseError, Python};
3-
use std::ffi::CStr;
3+
use std::borrow::Cow;
4+
use std::ffi::{CStr, CString};
45
use std::fmt;
6+
use std::mem::ManuallyDrop;
57
use std::os::raw::c_int;
68

79
/// Python 3.8 and up - __ipow__ has modulo argument correctly populated.
@@ -95,6 +97,12 @@ pub struct PyClassAttributeDef {
9597
pub(crate) meth: PyClassAttributeFactory,
9698
}
9799

100+
impl PyClassAttributeDef {
101+
pub(crate) fn attribute_c_string(&self) -> PyResult<Cow<'static, CStr>> {
102+
extract_c_string(self.name, "class attribute name cannot contain nul bytes")
103+
}
104+
}
105+
98106
#[derive(Clone, Debug)]
99107
pub struct PyGetterDef {
100108
pub(crate) name: &'static str,
@@ -161,7 +169,9 @@ impl PyMethodDef {
161169
}
162170

163171
/// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef`
164-
pub(crate) fn as_method_def(&self) -> Result<ffi::PyMethodDef, NulByteInString> {
172+
pub(crate) fn as_method_def(
173+
&self,
174+
) -> PyResult<(ffi::PyMethodDef, ManuallyDrop<PyMethodDefDestructor>)> {
165175
let meth = match self.ml_meth {
166176
PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer {
167177
PyCFunction: meth.0,
@@ -175,12 +185,19 @@ impl PyMethodDef {
175185
},
176186
};
177187

178-
Ok(ffi::PyMethodDef {
179-
ml_name: get_name(self.ml_name)?.as_ptr(),
188+
let name = get_name(self.ml_name)?;
189+
let doc = get_doc(self.ml_doc)?;
190+
let def = ffi::PyMethodDef {
191+
ml_name: name.as_ptr(),
180192
ml_meth: meth,
181193
ml_flags: self.ml_flags,
182-
ml_doc: get_doc(self.ml_doc)?.as_ptr(),
183-
})
194+
ml_doc: doc.as_ptr(),
195+
};
196+
let destructor = ManuallyDrop::new(PyMethodDefDestructor {
197+
name: Some(name),
198+
doc: Some(doc),
199+
});
200+
Ok((def, destructor))
184201
}
185202
}
186203

@@ -214,10 +231,16 @@ impl PyGetterDef {
214231
/// Copy descriptor information to `ffi::PyGetSetDef`
215232
pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) {
216233
if dst.name.is_null() {
217-
dst.name = get_name(self.name).unwrap().as_ptr() as _;
234+
let name = get_name(self.name).unwrap();
235+
dst.name = name.as_ptr() as _;
236+
// FIXME: stop leaking name
237+
std::mem::forget(name);
218238
}
219239
if dst.doc.is_null() {
220-
dst.doc = get_doc(self.doc).unwrap().as_ptr() as _;
240+
let doc = get_doc(self.doc).unwrap();
241+
dst.doc = doc.as_ptr() as _;
242+
// FIXME: stop leaking doc
243+
std::mem::forget(doc);
221244
}
222245
dst.get = Some(self.meth.0);
223246
}
@@ -236,21 +259,27 @@ impl PySetterDef {
236259
/// Copy descriptor information to `ffi::PyGetSetDef`
237260
pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) {
238261
if dst.name.is_null() {
239-
dst.name = get_name(self.name).unwrap().as_ptr() as _;
262+
let name = get_name(self.name).unwrap();
263+
dst.name = name.as_ptr() as _;
264+
// FIXME: stop leaking name
265+
std::mem::forget(name);
240266
}
241267
if dst.doc.is_null() {
242-
dst.doc = get_doc(self.doc).unwrap().as_ptr() as _;
268+
let doc = get_doc(self.doc).unwrap();
269+
dst.doc = doc.as_ptr() as _;
270+
// FIXME: stop leaking doc
271+
std::mem::forget(doc);
243272
}
244273
dst.set = Some(self.meth.0);
245274
}
246275
}
247276

248-
fn get_name(name: &'static str) -> Result<&'static CStr, NulByteInString> {
249-
extract_cstr_or_leak_cstring(name, "Function name cannot contain NUL byte.")
277+
fn get_name(name: &'static str) -> PyResult<Cow<'static, CStr>> {
278+
extract_c_string(name, "Function name cannot contain NUL byte.")
250279
}
251280

252-
fn get_doc(doc: &'static str) -> Result<&'static CStr, NulByteInString> {
253-
extract_cstr_or_leak_cstring(doc, "Document cannot contain NUL byte.")
281+
fn get_doc(doc: &'static str) -> PyResult<Cow<'static, CStr>> {
282+
extract_c_string(doc, "Document cannot contain NUL byte.")
254283
}
255284

256285
/// Unwraps the result of __traverse__ for tp_traverse
@@ -263,6 +292,14 @@ pub fn unwrap_traverse_result(result: Result<(), PyTraverseError>) -> c_int {
263292
}
264293
}
265294

295+
pub(crate) struct PyMethodDefDestructor {
296+
// These members are just to avoid leaking CStrings when possible
297+
#[allow(dead_code)]
298+
name: Option<Cow<'static, CStr>>,
299+
#[allow(dead_code)]
300+
doc: Option<Cow<'static, CStr>>,
301+
}
302+
266303
// The macros need to Ok-wrap the output of user defined functions; i.e. if they're not a result, make them into one.
267304
pub trait OkWrap<T> {
268305
type Error;
@@ -288,3 +325,16 @@ where
288325
self.map(|o| o.into_py(py))
289326
}
290327
}
328+
329+
fn extract_c_string(src: &'static str, err_msg: &'static str) -> PyResult<Cow<'static, CStr>> {
330+
let bytes = src.as_bytes();
331+
if bytes.last() == Some(&0) {
332+
// Last byte is a nul; try to create as a CStr
333+
let c_str = CStr::from_bytes_with_nul(bytes).map_err(|_| PyValueError::new_err(err_msg))?;
334+
Ok(Cow::Borrowed(c_str))
335+
} else {
336+
// Allocate a new CString for this
337+
let c_string = CString::new(bytes).map_err(|_| PyValueError::new_err(err_msg))?;
338+
Ok(Cow::Owned(c_string))
339+
}
340+
}

src/internal_tricks.rs

-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
use crate::ffi::{Py_ssize_t, PY_SSIZE_T_MAX};
2-
use std::ffi::{CStr, CString};
3-
42
pub struct PrivateMarker;
53

64
macro_rules! private_decl {
@@ -32,20 +30,6 @@ macro_rules! pyo3_exception {
3230
};
3331
}
3432

35-
#[derive(Debug)]
36-
pub(crate) struct NulByteInString(pub(crate) &'static str);
37-
38-
pub(crate) fn extract_cstr_or_leak_cstring(
39-
src: &'static str,
40-
err_msg: &'static str,
41-
) -> Result<&'static CStr, NulByteInString> {
42-
CStr::from_bytes_with_nul(src.as_bytes())
43-
.or_else(|_| {
44-
CString::new(src.as_bytes()).map(|c_string| &*Box::leak(c_string.into_boxed_c_str()))
45-
})
46-
.map_err(|_| NulByteInString(err_msg))
47-
}
48-
4933
/// Convert an usize index into a Py_ssize_t index, clamping overflow to
5034
/// PY_SSIZE_T_MAX.
5135
pub(crate) fn get_ssize_index(index: usize) -> Py_ssize_t {

src/pyclass.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ impl PyTypeBuilder {
158158
}
159159
PyMethodDefType::Method(def)
160160
| PyMethodDefType::Class(def)
161-
| PyMethodDefType::Static(def) => self.method_defs.push(def.as_method_def().unwrap()),
161+
| PyMethodDefType::Static(def) => {
162+
let (def, destructor) = def.as_method_def().unwrap();
163+
// FIXME: stop leaking destructor
164+
std::mem::forget(destructor);
165+
self.method_defs.push(def);
166+
}
162167
// These class attributes are added after the type gets created by LazyStaticType
163168
PyMethodDefType::ClassAttribute(_) => {}
164169
}

src/type_object.rs

+4-7
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
//! Python type object information
33
44
use crate::impl_::pyclass::PyClassItemsIter;
5-
use crate::internal_tricks::extract_cstr_or_leak_cstring;
65
use crate::once_cell::GILOnceCell;
76
use crate::pyclass::create_type_object;
87
use crate::pyclass::PyClass;
98
use crate::types::{PyAny, PyType};
109
use crate::{conversion::IntoPyPointer, PyMethodDefType};
1110
use crate::{ffi, AsPyPointer, PyNativeType, PyObject, PyResult, Python};
1211
use parking_lot::{const_mutex, Mutex};
12+
use std::borrow::Cow;
13+
use std::ffi::CStr;
1314
use std::thread::{self, ThreadId};
1415

1516
/// `T: PyLayout<U>` represents that `T` is a concrete representation of `U` in the Python heap.
@@ -180,11 +181,7 @@ impl LazyStaticType {
180181
for class_items in items_iter {
181182
for def in class_items.methods {
182183
if let PyMethodDefType::ClassAttribute(attr) = def {
183-
let key = extract_cstr_or_leak_cstring(
184-
attr.name,
185-
"class attribute name cannot contain nul bytes",
186-
)
187-
.unwrap();
184+
let key = attr.attribute_c_string().unwrap();
188185

189186
match (attr.meth.0)(py) {
190187
Ok(val) => items.push((key, val)),
@@ -221,7 +218,7 @@ impl LazyStaticType {
221218
fn initialize_tp_dict(
222219
py: Python<'_>,
223220
type_object: *mut ffi::PyObject,
224-
items: Vec<(&'static std::ffi::CStr, PyObject)>,
221+
items: Vec<(Cow<'static, CStr>, PyObject)>,
225222
) -> PyResult<()> {
226223
// We hold the GIL: the dictionary update can be considered atomic from
227224
// the POV of other threads.

src/types/function.rs

+52-32
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use crate::derive_utils::PyFunctionArguments;
2-
use crate::exceptions::PyValueError;
32
use crate::impl_::panic::PanicTrap;
3+
use crate::methods::PyMethodDefDestructor;
44
use crate::panic::PanicException;
55
use crate::{
66
ffi,
77
impl_::pymethods::{self, PyMethodDef},
88
types, AsPyPointer,
99
};
1010
use crate::{prelude::*, GILPool};
11+
use std::mem::ManuallyDrop;
1112
use std::os::raw::c_void;
1213

1314
/// Represents a builtin Python function object.
@@ -32,17 +33,25 @@ where
3233
args,
3334
kwargs,
3435
|py, capsule_ptr, args, kwargs| {
35-
let boxed_fn: &F = &*(ffi::PyCapsule_GetPointer(
36+
let boxed_fn: &ClosureDestructor<F> = &*(ffi::PyCapsule_GetPointer(
3637
capsule_ptr,
3738
CLOSURE_CAPSULE_NAME.as_ptr() as *const _,
38-
) as *mut F);
39+
) as *mut ClosureDestructor<F>);
3940
let args = py.from_borrowed_ptr::<types::PyTuple>(args);
4041
let kwargs = py.from_borrowed_ptr_or_opt::<types::PyDict>(kwargs);
41-
crate::callback::convert(py, boxed_fn(args, kwargs))
42+
crate::callback::convert(py, (boxed_fn.closure)(args, kwargs))
4243
},
4344
)
4445
}
4546

47+
struct ClosureDestructor<F> {
48+
closure: F,
49+
def: ffi::PyMethodDef,
50+
// Used to destroy the cstrings in `def`, if necessary.
51+
#[allow(dead_code)]
52+
def_destructor: PyMethodDefDestructor,
53+
}
54+
4655
unsafe extern "C" fn drop_closure<F, R>(capsule_ptr: *mut ffi::PyObject)
4756
where
4857
F: Fn(&types::PyTuple, Option<&types::PyDict>) -> R + Send + 'static,
@@ -51,11 +60,12 @@ where
5160
let trap = PanicTrap::new("uncaught panic during drop_closure");
5261
let pool = GILPool::new();
5362
if let Err(payload) = std::panic::catch_unwind(|| {
54-
let boxed_fn: Box<F> = Box::from_raw(ffi::PyCapsule_GetPointer(
63+
let destructor: Box<ClosureDestructor<F>> = Box::from_raw(ffi::PyCapsule_GetPointer(
5564
capsule_ptr,
5665
CLOSURE_CAPSULE_NAME.as_ptr() as *const _,
57-
) as *mut F);
58-
drop(boxed_fn)
66+
)
67+
as *mut ClosureDestructor<F>);
68+
drop(destructor)
5969
}) {
6070
let py = pool.python();
6171
let err = PanicException::from_panic_payload(payload);
@@ -116,46 +126,44 @@ impl PyCFunction {
116126
py: Python<'a>,
117127
name: Option<&'static str>,
118128
doc: Option<&'static str>,
119-
f: F,
129+
closure: F,
120130
) -> PyResult<&'a PyCFunction>
121131
where
122132
F: Fn(&types::PyTuple, Option<&types::PyDict>) -> R + Send + 'static,
123133
R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>,
124134
{
125-
let function_ptr = Box::into_raw(Box::new(f));
126-
let capsule = unsafe {
135+
let method_def = pymethods::PyMethodDef::cfunction_with_keywords(
136+
name.unwrap_or("pyo3-closure\0"),
137+
pymethods::PyCFunctionWithKeywords(run_closure::<F, R>),
138+
doc.unwrap_or("\0"),
139+
);
140+
let (def, def_destructor) = method_def.as_method_def()?;
141+
let ptr = Box::into_raw(Box::new(ClosureDestructor {
142+
closure,
143+
def,
144+
// Disable the `ManuallyDrop`; we do actually want to drop this later.
145+
def_destructor: ManuallyDrop::into_inner(def_destructor),
146+
}));
147+
148+
let destructor = unsafe {
127149
PyObject::from_owned_ptr_or_err(
128150
py,
129151
ffi::PyCapsule_New(
130-
function_ptr as *mut c_void,
152+
ptr as *mut c_void,
131153
CLOSURE_CAPSULE_NAME.as_ptr() as *const _,
132154
Some(drop_closure::<F, R>),
133155
),
134156
)?
135157
};
136-
let method_def = pymethods::PyMethodDef::cfunction_with_keywords(
137-
name.unwrap_or("pyo3-closure\0"),
138-
pymethods::PyCFunctionWithKeywords(run_closure::<F, R>),
139-
doc.unwrap_or("\0"),
140-
);
141-
Self::internal_new_from_pointers(&method_def, py, capsule.as_ptr(), std::ptr::null_mut())
142-
}
143158

144-
#[doc(hidden)]
145-
fn internal_new_from_pointers<'py>(
146-
method_def: &PyMethodDef,
147-
py: Python<'py>,
148-
mod_ptr: *mut ffi::PyObject,
149-
module_name: *mut ffi::PyObject,
150-
) -> PyResult<&'py Self> {
151-
let def = method_def
152-
.as_method_def()
153-
.map_err(|err| PyValueError::new_err(err.0))?;
154159
unsafe {
155160
py.from_owned_ptr_or_err::<PyCFunction>(ffi::PyCFunction_NewEx(
156-
Box::into_raw(Box::new(def)),
157-
mod_ptr,
158-
module_name,
161+
#[cfg(addr_of)]
162+
core::ptr::addr_of_mut!((*ptr).def),
163+
#[cfg(not(addr_of))]
164+
&mut (*ptr).def,
165+
destructor.as_ptr(),
166+
std::ptr::null_mut(),
159167
))
160168
}
161169
}
@@ -173,7 +181,19 @@ impl PyCFunction {
173181
} else {
174182
(std::ptr::null_mut(), std::ptr::null_mut())
175183
};
176-
Self::internal_new_from_pointers(method_def, py, mod_ptr, module_name)
184+
let (def, destructor) = method_def.as_method_def()?;
185+
186+
// FIXME: stop leaking the def and destructor
187+
let def = Box::into_raw(Box::new(def));
188+
std::mem::forget(destructor);
189+
190+
unsafe {
191+
py.from_owned_ptr_or_err::<PyCFunction>(ffi::PyCFunction_NewEx(
192+
def,
193+
mod_ptr,
194+
module_name,
195+
))
196+
}
177197
}
178198
}
179199

0 commit comments

Comments
 (0)