Skip to content

Commit 3ab0962

Browse files
Qardclaude
andcommitted
Restore Sapi::startup() with write lock for PHP allocator init
ThreadScope alone (ext_php_rs_sapi_per_thread_init) is not sufficient - it only initializes TSRM thread-local storage but doesn't setup PHP's memory allocator for the thread. The SAPI startup callback (sapi_module_startup -> php_module_startup) must also be called to initialize the allocator. We use a write lock to serialize these calls across threads to prevent concurrent php_module_startup from corrupting global state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent d1428af commit 3ab0962

File tree

2 files changed

+29
-7
lines changed

2 files changed

+29
-7
lines changed

src/embed.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,14 @@ impl Handler for Embed {
341341

342342
// Spawn blocking PHP execution - ALL PHP operations happen here
343343
let blocking_handle = tokio::task::spawn_blocking(move || {
344-
// Keep _sapi alive for the duration of the blocking task
345-
let _ = &_sapi;
346-
347344
// Initialize thread-local storage for this worker thread
348345
let _thread_scope = ThreadScope::new();
346+
// Call SAPI startup on THIS thread to initialize PHP's memory allocator.
347+
// This is required before any estrdup calls can be made.
348+
// The startup call is serialized via write lock to prevent concurrent php_module_startup.
349+
_sapi
350+
.startup()
351+
.map_err(|_| EmbedRequestError::SapiNotStarted)?;
349352

350353
// Setup RequestContext (always streaming from SAPI perspective)
351354
// RequestContext::new() will extract the request body's read stream and add it as RequestStream extension
@@ -360,10 +363,10 @@ impl Handler for Embed {
360363
// All estrdup calls happen here, inside spawn_blocking, after ThreadScope::new()
361364
// has initialized PHP's thread-local storage. These will be freed by efree in
362365
// sapi_module_deactivate during request shutdown.
363-
let request_uri_c = estrdup(request_uri_str.as_str());
364-
let path_translated = estrdup(translated_path_str.as_str());
365-
let request_method = estrdup(method_str.as_str());
366-
let query_string = estrdup(query_str.as_str());
366+
let request_uri_c = estrdup(request_uri_str);
367+
let path_translated = estrdup(translated_path_str.clone());
368+
let request_method = estrdup(method_str);
369+
let query_string = estrdup(query_str);
367370
let content_type = content_type_str
368371
.as_ref()
369372
.map(|s| estrdup(s.as_str()))

src/sapi.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,25 @@ impl Sapi {
114114
})
115115
}
116116

117+
/// Initialize PHP for the current thread and call the SAPI module startup.
118+
/// This must be called on each worker thread before using PHP's memory allocator (estrdup).
119+
/// On ZTS builds, this initializes thread-local storage and module state.
120+
/// Uses a write lock to serialize startup calls across threads.
121+
pub fn startup(&self) -> Result<(), EmbedRequestError> {
122+
let sapi = &mut self
123+
.module
124+
.write()
125+
.map_err(|_| EmbedRequestError::SapiNotStarted)?;
126+
127+
if let Some(startup) = sapi.startup {
128+
if unsafe { startup(sapi.as_mut()) } != ZEND_RESULT_CODE_SUCCESS {
129+
return Err(EmbedRequestError::SapiNotStarted);
130+
}
131+
}
132+
133+
Ok(())
134+
}
135+
117136
pub fn shutdown(&self) -> Result<(), EmbedRequestError> {
118137
// Only shutdown if we're on the same thread that created this Sapi
119138
let current_thread = std::thread::current().id();

0 commit comments

Comments
 (0)