Skip to content

Commit 5fdd8fa

Browse files
authored
feat(zend): add helper for try catch and bailout in PHP (#275)
* feat(zend): add helper for try catch and bailout in PHP * feat(try): add bindings for bailout * fix(try): add missing feature flag for test * feat(try): add a test that expose memory leak problem * feat(try): make bailout unsafe and explain why * feat(bailout): flag bailout as a panic function * feat(embed): add try catch on script / eval
1 parent dddc07f commit 5fdd8fa

File tree

11 files changed

+259
-37
lines changed

11 files changed

+259
-37
lines changed

allowed_bindings.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,5 +261,6 @@ bind! {
261261
zend_file_handle,
262262
zend_stream_init_filename,
263263
php_execute_script,
264-
zend_register_module_ex
264+
zend_register_module_ex,
265+
_zend_bailout
265266
}

build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ fn main() -> Result<()> {
248248
for path in [
249249
manifest.join("src").join("wrapper.h"),
250250
manifest.join("src").join("wrapper.c"),
251+
manifest.join("src").join("embed").join("embed.h"),
252+
manifest.join("src").join("embed").join("embed.c"),
251253
manifest.join("allowed_bindings.rs"),
252254
manifest.join("windows_build.rs"),
253255
manifest.join("unix_build.rs"),

docsrs_bindings.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,9 @@ pub struct _zend_class_entry__bindgen_ty_4__bindgen_ty_2 {
789789
pub builtin_functions: *const _zend_function_entry,
790790
pub module: *mut _zend_module_entry,
791791
}
792+
extern "C" {
793+
pub fn _zend_bailout(filename: *const ::std::os::raw::c_char, lineno: u32) -> !;
794+
}
792795
extern "C" {
793796
pub static mut zend_interrupt_function:
794797
::std::option::Option<unsafe extern "C" fn(execute_data: *mut zend_execute_data)>;

src/embed/embed.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// We actually use the PHP embed API to run PHP code in test
44
// At some point we might want to use our own SAPI to do that
55
void* ext_php_rs_embed_callback(int argc, char** argv, void* (*callback)(void *), void *ctx) {
6-
void *result;
6+
void *result = NULL;
77

88
PHP_EMBED_START_BLOCK(argc, argv)
99

src/embed/ffi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ extern "C" {
1010
pub fn ext_php_rs_embed_callback(
1111
argc: c_int,
1212
argv: *mut *mut c_char,
13-
func: unsafe extern "C" fn(*const c_void) -> *mut c_void,
13+
func: unsafe extern "C" fn(*const c_void) -> *const c_void,
1414
ctx: *const c_void,
1515
) -> *mut c_void;
1616
}

src/embed/mod.rs

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ use crate::ffi::{
1313
zend_stream_init_filename, ZEND_RESULT_CODE_SUCCESS,
1414
};
1515
use crate::types::{ZendObject, Zval};
16-
use crate::zend::ExecutorGlobals;
16+
use crate::zend::{panic_wrapper, try_catch, ExecutorGlobals};
1717
use parking_lot::{const_rwlock, RwLock};
1818
use std::ffi::{c_char, c_void, CString, NulError};
19-
use std::panic::{catch_unwind, resume_unwind, RefUnwindSafe};
19+
use std::panic::{resume_unwind, RefUnwindSafe};
2020
use std::path::Path;
2121
use std::ptr::null_mut;
2222

@@ -29,6 +29,13 @@ pub enum EmbedError {
2929
ExecuteScriptError,
3030
InvalidEvalString(NulError),
3131
InvalidPath,
32+
CatchError,
33+
}
34+
35+
impl EmbedError {
36+
pub fn is_bailout(&self) -> bool {
37+
matches!(self, EmbedError::CatchError)
38+
}
3239
}
3340

3441
static RUN_FN_LOCK: RwLock<()> = const_rwlock(());
@@ -79,10 +86,12 @@ impl Embed {
7986
zend_stream_init_filename(&mut file_handle, path.as_ptr());
8087
}
8188

82-
if unsafe { php_execute_script(&mut file_handle) } {
83-
Ok(())
84-
} else {
85-
Err(EmbedError::ExecuteScriptError)
89+
let exec_result = try_catch(|| unsafe { php_execute_script(&mut file_handle) });
90+
91+
match exec_result {
92+
Err(_) => Err(EmbedError::CatchError),
93+
Ok(true) => Ok(()),
94+
Ok(false) => Err(EmbedError::ExecuteScriptError),
8695
}
8796
}
8897

@@ -93,6 +102,12 @@ impl Embed {
93102
/// Which means subsequent calls to `Embed::eval` or `Embed::run_script` will be able to access
94103
/// variables defined in previous calls
95104
///
105+
/// # Returns
106+
///
107+
/// * R - The result of the function passed to this method
108+
///
109+
/// R must implement [`Default`] so it can be returned in case of a bailout
110+
///
96111
/// # Example
97112
///
98113
/// ```
@@ -105,41 +120,36 @@ impl Embed {
105120
/// assert_eq!(foo.unwrap().string().unwrap(), "foo");
106121
/// });
107122
/// ```
108-
pub fn run<F: Fn() + RefUnwindSafe>(func: F) {
123+
pub fn run<R, F: FnMut() -> R + RefUnwindSafe>(func: F) -> R
124+
where
125+
R: Default,
126+
{
109127
// @TODO handle php thread safe
110128
//
111129
// This is to prevent multiple threads from running php at the same time
112130
// At some point we should detect if php is compiled with thread safety and avoid doing that in this case
113131
let _guard = RUN_FN_LOCK.write();
114132

115-
unsafe extern "C" fn wrapper<F: Fn() + RefUnwindSafe>(ctx: *const c_void) -> *mut c_void {
116-
// we try to catch panic here so we correctly shutdown php if it happens
117-
// mandatory when we do assert on test as other test would not run correctly
118-
let panic = catch_unwind(|| {
119-
(*(ctx as *const F))();
120-
});
121-
122-
let panic_ptr = Box::into_raw(Box::new(panic));
123-
124-
panic_ptr as *mut c_void
125-
}
126-
127133
let panic = unsafe {
128134
ext_php_rs_embed_callback(
129135
0,
130136
null_mut(),
131-
wrapper::<F>,
137+
panic_wrapper::<R, F>,
132138
&func as *const F as *const c_void,
133139
)
134140
};
135141

142+
// This can happen if there is a bailout
136143
if panic.is_null() {
137-
return;
144+
return R::default();
138145
}
139146

140-
if let Err(err) = unsafe { *Box::from_raw(panic as *mut std::thread::Result<()>) } {
141-
// we resume the panic here so it can be catched correctly by the test framework
142-
resume_unwind(err);
147+
match unsafe { *Box::from_raw(panic as *mut std::thread::Result<R>) } {
148+
Ok(r) => r,
149+
Err(err) => {
150+
// we resume the panic here so it can be catched correctly by the test framework
151+
resume_unwind(err);
152+
}
143153
}
144154
}
145155

@@ -170,21 +180,18 @@ impl Embed {
170180

171181
let mut result = Zval::new();
172182

173-
// this eval is very limited as it only allow simple code, it's the same eval used by php -r
174-
let exec_result = unsafe {
183+
let exec_result = try_catch(|| unsafe {
175184
zend_eval_string(
176185
cstr.as_ptr() as *const c_char,
177186
&mut result,
178187
b"run\0".as_ptr() as *const _,
179188
)
180-
};
181-
182-
let exception = ExecutorGlobals::take_exception();
189+
});
183190

184-
if exec_result != ZEND_RESULT_CODE_SUCCESS {
185-
Err(EmbedError::ExecuteError(exception))
186-
} else {
187-
Ok(result)
191+
match exec_result {
192+
Err(_) => Err(EmbedError::CatchError),
193+
Ok(ZEND_RESULT_CODE_SUCCESS) => Ok(result),
194+
Ok(_) => Err(EmbedError::ExecuteError(ExecutorGlobals::take_exception())),
188195
}
189196
}
190197
}
@@ -244,4 +251,23 @@ mod tests {
244251
panic!("test panic");
245252
});
246253
}
254+
255+
#[test]
256+
fn test_return() {
257+
let foo = Embed::run(|| {
258+
return "foo";
259+
});
260+
261+
assert_eq!(foo, "foo");
262+
}
263+
264+
#[test]
265+
fn test_eval_bailout() {
266+
Embed::run(|| {
267+
let result = Embed::eval("str_repeat('a', 100_000_000_000_000);");
268+
269+
assert!(result.is_err());
270+
assert!(result.unwrap_err().is_bailout());
271+
});
272+
}
247273
}

src/ffi.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ extern "C" {
2626
pub fn ext_php_rs_zend_object_alloc(obj_size: usize, ce: *mut zend_class_entry) -> *mut c_void;
2727
pub fn ext_php_rs_zend_object_release(obj: *mut zend_object);
2828
pub fn ext_php_rs_executor_globals() -> *mut zend_executor_globals;
29+
pub fn ext_php_rs_zend_try_catch(
30+
func: unsafe extern "C" fn(*const c_void) -> *const c_void,
31+
ctx: *const c_void,
32+
result: *mut *mut c_void,
33+
) -> bool;
34+
pub fn ext_php_rs_zend_bailout() -> !;
2935
}
3036

3137
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

src/wrapper.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,17 @@ zend_executor_globals *ext_php_rs_executor_globals() {
3939
return &executor_globals;
4040
#endif
4141
}
42+
43+
bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result) {
44+
zend_try {
45+
*result = callback(ctx);
46+
} zend_catch {
47+
return true;
48+
} zend_end_try();
49+
50+
return false;
51+
}
52+
53+
void ext_php_rs_zend_bailout() {
54+
zend_bailout();
55+
}

src/wrapper.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ void ext_php_rs_set_known_valid_utf8(zend_string *zs);
3030
const char *ext_php_rs_php_build_id();
3131
void *ext_php_rs_zend_object_alloc(size_t obj_size, zend_class_entry *ce);
3232
void ext_php_rs_zend_object_release(zend_object *obj);
33-
zend_executor_globals *ext_php_rs_executor_globals();
33+
zend_executor_globals *ext_php_rs_executor_globals();
34+
bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result);
35+
void ext_php_rs_zend_bailout();

src/zend/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod globals;
99
mod handlers;
1010
mod ini_entry_def;
1111
mod module;
12+
mod try_catch;
1213

1314
use crate::{error::Result, ffi::php_printf};
1415
use std::ffi::CString;
@@ -22,6 +23,9 @@ pub use globals::ExecutorGlobals;
2223
pub use handlers::ZendObjectHandlers;
2324
pub use ini_entry_def::IniEntryDef;
2425
pub use module::ModuleEntry;
26+
#[cfg(feature = "embed")]
27+
pub(crate) use try_catch::panic_wrapper;
28+
pub use try_catch::{bailout, try_catch};
2529

2630
// Used as the format string for `php_printf`.
2731
const FORMAT_STR: &[u8] = b"%s\0";

0 commit comments

Comments
 (0)