@@ -11,11 +11,10 @@ use bytes::Buf;
1111use ext_php_rs:: {
1212 alloc:: { efree, estrdup} ,
1313 builders:: SapiBuilder ,
14- embed:: { ext_php_rs_sapi_shutdown , ext_php_rs_sapi_startup , SapiModule } ,
14+ embed:: { SapiModule , ext_php_rs_sapi_shutdown , ext_php_rs_sapi_startup } ,
1515 // exception::register_error_observer,
1616 ffi:: {
17- php_module_startup, php_register_variable, sapi_headers_struct, sapi_send_headers,
18- sapi_shutdown, sapi_startup, ZEND_RESULT_CODE_SUCCESS ,
17+ ZEND_RESULT_CODE_SUCCESS , php_module_shutdown, php_module_startup, php_register_variable, sapi_headers_struct, sapi_send_headers, sapi_shutdown, sapi_startup
1918 } ,
2019 prelude:: * ,
2120 zend:: { SapiGlobals , SapiHeader } ,
@@ -40,16 +39,27 @@ pub(crate) fn fallback_handle() -> &'static tokio::runtime::Handle {
4039 FALLBACK_RUNTIME . handle ( )
4140}
4241
42+ // Global mutex to serialize PHP execution.
43+ // PHP's memory allocator (emalloc/estrdup) is not thread-safe for concurrent access
44+ // even with TSRM per-thread initialization. Multiple spawn_blocking tasks running
45+ // PHP code simultaneously will corrupt the memory allocator state, causing crashes
46+ // like "EXC_BAD_ACCESS in _emalloc/_estrdup".
47+ //
48+ // This mutex must be held for the entire duration of PHP operations in spawn_blocking.
49+ static PHP_EXECUTION_MUTEX : Lazy < std:: sync:: Mutex < ( ) > > = Lazy :: new ( || std:: sync:: Mutex :: new ( ( ) ) ) ;
50+
51+ /// Acquire the PHP execution lock. Returns a guard that must be held for the
52+ /// duration of PHP operations. Only one thread can execute PHP code at a time.
53+ pub ( crate ) fn acquire_php_lock ( ) -> std:: sync:: MutexGuard < ' static , ( ) > {
54+ PHP_EXECUTION_MUTEX
55+ . lock ( )
56+ . expect ( "PHP execution mutex poisoned" )
57+ }
58+
4359// This is a helper to ensure that PHP is initialized and deinitialized at the
4460// appropriate times.
4561#[ derive( Debug ) ]
46- pub ( crate ) struct Sapi {
47- module : RwLock < Box < SapiModule > > ,
48- // Track which thread created this Sapi so we only shutdown from that thread
49- creator_thread : ThreadId ,
50- // Track if shutdown has been called to prevent double-shutdown
51- shutdown_called : Mutex < bool > ,
52- }
62+ pub ( crate ) struct Sapi ( RwLock < Box < SapiModule > > ) ;
5363
5464impl Sapi {
5565 pub fn new ( ) -> Result < Self , EmbedStartError > {
@@ -86,8 +96,6 @@ impl Sapi {
8696 ext_php_rs_sapi_startup ( ) ;
8797 sapi_startup ( boxed. as_mut ( ) ) ;
8898
89- // Call module startup for the main thread that's constructing this Sapi.
90- // Each worker thread also needs its own module startup via module_scope().
9199 if let Some ( startup) = boxed. startup {
92100 startup ( boxed. as_mut ( ) ) ;
93101 }
@@ -109,80 +117,20 @@ impl Sapi {
109117 // }
110118 // });
111119
112- Ok ( Sapi {
113- module : RwLock :: new ( boxed) ,
114- creator_thread : std:: thread:: current ( ) . id ( ) ,
115- shutdown_called : Mutex :: new ( false ) ,
116- } )
117- }
118-
119- /// Creates a new ModuleScope for per-thread module startup.
120- ///
121- /// In ZTS mode, php_module_startup must be called per-thread after the thread's
122- /// local storage has been initialized via ts_resource(0) in ThreadScope::new().
123- ///
124- /// This method provides access to the SAPI module and module entry pointers
125- /// needed by ModuleScope::new().
126- pub fn module_scope ( & self ) -> Result < crate :: scopes:: ModuleScope , EmbedRequestError > {
127- let mut sapi = self
128- . module
129- . write ( )
130- . map_err ( |_| EmbedRequestError :: SapiNotStarted ) ?;
131-
132- unsafe {
133- crate :: scopes:: ModuleScope :: new ( sapi. as_mut ( ) as * mut _ , get_module ( ) )
134- }
135- }
136-
137- pub fn shutdown ( & self ) -> Result < ( ) , EmbedRequestError > {
138- // Only shutdown if we're on the same thread that created this Sapi
139- let current_thread = std:: thread:: current ( ) . id ( ) ;
140- if current_thread != self . creator_thread {
141- return Ok ( ( ) ) ;
142- }
143-
144- // Prevent double-shutdown
145- let mut shutdown_called = self
146- . shutdown_called
147- . lock ( )
148- . map_err ( |_| EmbedRequestError :: SapiNotShutdown ) ?;
149-
150- if * shutdown_called {
151- return Ok ( ( ) ) ;
152- }
153-
154- * shutdown_called = true ;
155-
156- let sapi = & mut self
157- . module
158- . write ( )
159- . map_err ( |_| EmbedRequestError :: SapiNotShutdown ) ?;
160-
161- if let Some ( shutdown) = sapi. shutdown {
162- if unsafe { shutdown ( sapi. as_mut ( ) ) } != ZEND_RESULT_CODE_SUCCESS {
163- return Err ( EmbedRequestError :: SapiNotShutdown ) ;
164- }
165- }
166-
167- Ok ( ( ) )
120+ Ok ( Sapi ( RwLock :: new ( boxed) ) )
168121 }
169122}
170123
171124impl Drop for Sapi {
172125 fn drop ( & mut self ) {
173- // Attempt shutdown, but it will be skipped if we're on the wrong thread
174- let _ = self . shutdown ( ) ;
126+ let sapi = & mut self . 0 . write ( ) . unwrap ( ) ;
127+ if let Some ( shutdown) = sapi. shutdown {
128+ unsafe { shutdown ( sapi. as_mut ( ) ) ; }
129+ }
175130
176- // Only call low-level shutdown functions if on the creator thread and shutdown succeeded
177- if std:: thread:: current ( ) . id ( ) == self . creator_thread {
178- if let Ok ( shutdown_called) = self . shutdown_called . lock ( ) {
179- if * shutdown_called {
180- unsafe {
181- sapi_shutdown ( ) ;
182- ext_php_rs_sapi_shutdown ( ) ;
183- }
184- }
185- }
131+ unsafe {
132+ sapi_shutdown ( ) ;
133+ ext_php_rs_sapi_shutdown ( ) ;
186134 }
187135 }
188136}
@@ -274,6 +222,8 @@ pub extern "C" fn sapi_module_shutdown(
274222) -> ext_php_rs:: ffi:: zend_result {
275223 // CRITICAL: Clear server_context BEFORE php_module_shutdown
276224 // to prevent sapi_flush from accessing freed RequestContext
225+ unsafe { php_module_shutdown ( ) ; }
226+
277227 {
278228 let mut globals = SapiGlobals :: get_mut ( ) ;
279229 globals. server_context = std:: ptr:: null_mut ( ) ;
@@ -541,6 +491,7 @@ where
541491 K : AsRef < str > ,
542492 V : AsRef < str > ,
543493{
494+ eprintln ! ( "DEBUG [env_var] estrdup for value: {}" , value. as_ref( ) ) ;
544495 let c_value = estrdup ( value. as_ref ( ) ) ;
545496 env_var_c ( vars, key, c_value) ?;
546497 maybe_efree ( c_value. cast :: < u8 > ( ) ) ;
@@ -556,6 +507,7 @@ fn env_var_c<K>(
556507where
557508 K : AsRef < str > ,
558509{
510+ eprintln ! ( "DEBUG [env_var] estrdup for key: {}" , key. as_ref( ) ) ;
559511 let c_key = estrdup ( key. as_ref ( ) ) ;
560512 unsafe {
561513 php_register_variable ( c_key, c_value, vars) ;
@@ -635,7 +587,7 @@ pub extern "C" fn sapi_module_register_server_variables(vars: *mut ext_php_rs::t
635587 env_var ( vars, "SERVER_PROTOCOL" , "HTTP/1.1" ) ?;
636588
637589 let sapi = get_sapi ( ) ?;
638- if let Ok ( inner_sapi) = sapi. module . read ( ) {
590+ if let Ok ( inner_sapi) = sapi. 0 . read ( ) {
639591 env_var_c ( vars, "SERVER_SOFTWARE" , inner_sapi. name ) ?;
640592 }
641593
0 commit comments