Skip to content

Commit c54ea99

Browse files
committed
fix(zend_bailout): Fix zend_bailout handling #537
1 parent 38c763f commit c54ea99

File tree

10 files changed

+245
-60
lines changed

10 files changed

+245
-60
lines changed

crates/macros/src/function.rs

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,26 @@ impl<'a> Function<'a> {
207207
retval: &mut ::ext_php_rs::types::Zval,
208208
) {
209209
use ::ext_php_rs::convert::IntoZval;
210+
use ::ext_php_rs::zend::try_catch;
211+
use ::std::panic::AssertUnwindSafe;
212+
213+
// Wrap the handler body with try_catch to ensure Rust destructors
214+
// are called if a bailout occurs (issue #537)
215+
let catch_result = try_catch(AssertUnwindSafe(|| {
216+
#(#arg_declarations)*
217+
let result = {
218+
#result
219+
};
220+
221+
if let Err(e) = result.set_zval(retval, false) {
222+
let e: ::ext_php_rs::exception::PhpException = e.into();
223+
e.throw().expect("Failed to throw PHP exception.");
224+
}
225+
}));
210226

211-
#(#arg_declarations)*
212-
let result = {
213-
#result
214-
};
215-
216-
if let Err(e) = result.set_zval(retval, false) {
217-
let e: ::ext_php_rs::exception::PhpException = e.into();
218-
e.throw().expect("Failed to throw PHP exception.");
227+
// If there was a bailout, re-trigger it after Rust cleanup
228+
if catch_result.is_err() {
229+
unsafe { ::ext_php_rs::zend::bailout(); }
219230
}
220231
}
221232
}
@@ -410,18 +421,31 @@ impl<'a> Function<'a> {
410421
::ext_php_rs::class::ConstructorMeta {
411422
constructor: {
412423
fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> {
413-
#(#arg_declarations)*
414-
let parse = ex.parser()
415-
#(.arg(&mut #required_arg_names))*
416-
.not_required()
417-
#(.arg(&mut #not_required_arg_names))*
418-
#variadic
419-
.parse();
420-
if parse.is_err() {
421-
return ::ext_php_rs::class::ConstructorResult::ArgError;
424+
use ::ext_php_rs::zend::try_catch;
425+
use ::std::panic::AssertUnwindSafe;
426+
427+
// Wrap the constructor body with try_catch to ensure Rust destructors
428+
// are called if a bailout occurs (issue #537)
429+
let catch_result = try_catch(AssertUnwindSafe(|| {
430+
#(#arg_declarations)*
431+
let parse = ex.parser()
432+
#(.arg(&mut #required_arg_names))*
433+
.not_required()
434+
#(.arg(&mut #not_required_arg_names))*
435+
#variadic
436+
.parse();
437+
if parse.is_err() {
438+
return ::ext_php_rs::class::ConstructorResult::ArgError;
439+
}
440+
#(#variadic_bindings)*
441+
#class::#ident(#({#arg_accessors}),*).into()
442+
}));
443+
444+
// If there was a bailout, re-trigger it after Rust cleanup
445+
match catch_result {
446+
Ok(result) => result,
447+
Err(_) => unsafe { ::ext_php_rs::zend::bailout() },
422448
}
423-
#(#variadic_bindings)*
424-
#class::#ident(#({#arg_accessors}),*).into()
425449
}
426450
inner
427451
},

src/builders/class.rs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -202,31 +202,43 @@ impl ClassBuilder {
202202

203203
zend_fastcall! {
204204
extern fn constructor<T: RegisteredClass>(ex: &mut ExecuteData, _: &mut Zval) {
205-
let Some(ConstructorMeta { constructor, .. }) = T::constructor() else {
206-
PhpException::default("You cannot instantiate this class from PHP.".into())
207-
.throw()
208-
.expect("Failed to throw exception when constructing class");
209-
return;
210-
};
211-
212-
let this = match constructor(ex) {
213-
ConstructorResult::Ok(this) => this,
214-
ConstructorResult::Exception(e) => {
215-
e.throw()
205+
use crate::zend::try_catch;
206+
use std::panic::AssertUnwindSafe;
207+
208+
// Wrap the constructor body with try_catch to ensure Rust destructors
209+
// are called if a bailout occurs (issue #537)
210+
let catch_result = try_catch(AssertUnwindSafe(|| {
211+
let Some(ConstructorMeta { constructor, .. }) = T::constructor() else {
212+
PhpException::default("You cannot instantiate this class from PHP.".into())
213+
.throw()
214+
.expect("Failed to throw exception when constructing class");
215+
return;
216+
};
217+
218+
let this = match constructor(ex) {
219+
ConstructorResult::Ok(this) => this,
220+
ConstructorResult::Exception(e) => {
221+
e.throw()
222+
.expect("Failed to throw exception while constructing class");
223+
return;
224+
}
225+
ConstructorResult::ArgError => return,
226+
};
227+
228+
let Some(this_obj) = ex.get_object::<T>() else {
229+
PhpException::default("Failed to retrieve reference to `this` object.".into())
230+
.throw()
216231
.expect("Failed to throw exception while constructing class");
217232
return;
218-
}
219-
ConstructorResult::ArgError => return,
220-
};
221-
222-
let Some(this_obj) = ex.get_object::<T>() else {
223-
PhpException::default("Failed to retrieve reference to `this` object.".into())
224-
.throw()
225-
.expect("Failed to throw exception while constructing class");
226-
return;
227-
};
228-
229-
this_obj.initialize(this);
233+
};
234+
235+
this_obj.initialize(this);
236+
}));
237+
238+
// If there was a bailout, re-trigger it after Rust cleanup
239+
if catch_result.is_err() {
240+
unsafe { crate::zend::bailout(); }
241+
}
230242
}
231243
}
232244

src/embed/mod.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::types::{ZendObject, Zval};
1717
use crate::zend::{ExecutorGlobals, panic_wrapper, try_catch};
1818
use parking_lot::{RwLock, const_rwlock};
1919
use std::ffi::{CString, NulError, c_char, c_void};
20-
use std::panic::{RefUnwindSafe, resume_unwind};
20+
use std::panic::{AssertUnwindSafe, UnwindSafe, resume_unwind};
2121
use std::path::Path;
2222
use std::ptr::null_mut;
2323

@@ -105,7 +105,9 @@ impl Embed {
105105
zend_stream_init_filename(&raw mut file_handle, path.as_ptr());
106106
}
107107

108-
let exec_result = try_catch(|| unsafe { php_execute_script(&raw mut file_handle) });
108+
let exec_result = try_catch(AssertUnwindSafe(|| unsafe {
109+
php_execute_script(&raw mut file_handle)
110+
}));
109111

110112
unsafe { zend_destroy_file_handle(&raw mut file_handle) }
111113

@@ -141,7 +143,7 @@ impl Embed {
141143
/// assert_eq!(foo.unwrap().string().unwrap(), "foo");
142144
/// });
143145
/// ```
144-
pub fn run<R, F: FnMut() -> R + RefUnwindSafe>(func: F) -> R
146+
pub fn run<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> R
145147
where
146148
R: Default,
147149
{
@@ -161,6 +163,9 @@ impl Embed {
161163
)
162164
};
163165

166+
// Prevent the closure from being dropped here since it was consumed in panic_wrapper
167+
std::mem::forget(func);
168+
164169
// This can happen if there is a bailout
165170
if panic.is_null() {
166171
return R::default();
@@ -206,13 +211,13 @@ impl Embed {
206211

207212
let mut result = Zval::new();
208213

209-
let exec_result = try_catch(|| unsafe {
214+
let exec_result = try_catch(AssertUnwindSafe(|| unsafe {
210215
zend_eval_string(
211216
cstr.as_ptr().cast::<c_char>(),
212217
&raw mut result,
213218
c"run".as_ptr().cast(),
214219
)
215-
});
220+
}));
216221

217222
match exec_result {
218223
Err(_) => Err(EmbedError::CatchError),

src/zend/try_catch.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@ use crate::ffi::{
22
ext_php_rs_zend_bailout, ext_php_rs_zend_first_try_catch, ext_php_rs_zend_try_catch,
33
};
44
use std::ffi::c_void;
5-
use std::panic::{RefUnwindSafe, catch_unwind, resume_unwind};
5+
use std::panic::{UnwindSafe, catch_unwind, resume_unwind};
66
use std::ptr::null_mut;
77

88
/// Error returned when a bailout occurs
99
#[derive(Debug)]
1010
pub struct CatchError;
1111

12-
pub(crate) unsafe extern "C" fn panic_wrapper<R, F: FnMut() -> R + RefUnwindSafe>(
12+
pub(crate) unsafe extern "C" fn panic_wrapper<R, F: FnOnce() -> R + UnwindSafe>(
1313
ctx: *const c_void,
1414
) -> *const c_void {
1515
// we try to catch panic here so we correctly shutdown php if it happens
1616
// mandatory when we do assert on test as other test would not run correctly
17-
let panic = catch_unwind(|| unsafe { (*(ctx as *mut F))() });
17+
// SAFETY: We read the closure from the pointer and consume it. This is safe because
18+
// the closure is only called once.
19+
let func = unsafe { std::ptr::read(ctx.cast::<F>()) };
20+
let panic = catch_unwind(func);
1821

1922
Box::into_raw(Box::new(panic)).cast::<c_void>()
2023
}
@@ -33,7 +36,7 @@ pub(crate) unsafe extern "C" fn panic_wrapper<R, F: FnMut() -> R + RefUnwindSafe
3336
/// # Errors
3437
///
3538
/// * [`CatchError`] - A bailout occurred during the execution
36-
pub fn try_catch<R, F: FnMut() -> R + RefUnwindSafe>(func: F) -> Result<R, CatchError> {
39+
pub fn try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
3740
do_try_catch(func, false)
3841
}
3942

@@ -54,11 +57,11 @@ pub fn try_catch<R, F: FnMut() -> R + RefUnwindSafe>(func: F) -> Result<R, Catch
5457
/// # Errors
5558
///
5659
/// * [`CatchError`] - A bailout occurred during the execution
57-
pub fn try_catch_first<R, F: FnMut() -> R + RefUnwindSafe>(func: F) -> Result<R, CatchError> {
60+
pub fn try_catch_first<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
5861
do_try_catch(func, true)
5962
}
6063

61-
fn do_try_catch<R, F: FnMut() -> R + RefUnwindSafe>(func: F, first: bool) -> Result<R, CatchError> {
64+
fn do_try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F, first: bool) -> Result<R, CatchError> {
6265
let mut panic_ptr = null_mut();
6366
let has_bailout = unsafe {
6467
if first {
@@ -76,6 +79,9 @@ fn do_try_catch<R, F: FnMut() -> R + RefUnwindSafe>(func: F, first: bool) -> Res
7679
}
7780
};
7881

82+
// Prevent the closure from being dropped here since it was consumed in panic_wrapper
83+
std::mem::forget(func);
84+
7985
let panic = panic_ptr.cast::<std::thread::Result<R>>();
8086

8187
// can be null if there is a bailout
@@ -190,17 +196,19 @@ mod tests {
190196

191197
#[test]
192198
fn test_memory_leak() {
199+
use std::panic::AssertUnwindSafe;
200+
193201
Embed::run(|| {
194202
let mut ptr = null_mut();
195203

196-
let _ = try_catch(|| {
204+
let _ = try_catch(AssertUnwindSafe(|| {
197205
let mut result = "foo".to_string();
198206
ptr = &raw mut result;
199207

200208
unsafe {
201209
bailout();
202210
}
203-
});
211+
}));
204212

205213
// Check that the string is never released
206214
let result = unsafe { &*ptr as &str };

tests/sapi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ fn test_sapi_multithread() {
166166
Ok(zval) => {
167167
assert!(zval.is_string());
168168
let string = zval.string().unwrap();
169-
let output = string.to_string();
169+
let output = string.clone();
170170
assert_eq!(output, format!("Hello, thread-{i}!"));
171171

172172
results.lock().unwrap().push((i, output));
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
// Control test - verify destructors work without bailout
3+
4+
bailout_test_reset();
5+
6+
// Call function that creates DropTrackers without bailout
7+
bailout_test_without_exit();
8+
9+
// The destructors should have been called
10+
$counter = bailout_test_get_counter();
11+
assert($counter === 2, "Expected 2 destructors to be called, got $counter");
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
// Test that Rust destructors are called when bailout occurs (issue #537)
3+
4+
bailout_test_reset();
5+
6+
// This function creates 3 DropTrackers and then calls a callback.
7+
// The callback triggers exit(), which causes a bailout.
8+
// Thanks to the try_catch wrapper, the Rust destructors should run
9+
// when the function returns, incrementing the counter to 3.
10+
bailout_test_with_callback(function() {
11+
exit(0);
12+
});
13+
14+
// After the function returns, check that all 3 destructors were called
15+
$counter = bailout_test_get_counter();
16+
assert($counter === 3, "Expected 3 destructors to be called after bailout, got $counter");

0 commit comments

Comments
 (0)