@@ -13,8 +13,9 @@ use crate::{
1313 convert:: { FromZval , FromZvalMut , IntoZval , IntoZvalDyn } ,
1414 error:: { Error , Result } ,
1515 ffi:: {
16- _zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, zend_is_callable,
17- zend_is_identical, zend_is_iterable, zend_resource, zend_value, zval, zval_ptr_dtor,
16+ _zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, ext_php_rs_zend_string_release,
17+ zend_is_callable, zend_is_identical, zend_is_iterable, zend_resource, zend_value, zval,
18+ zval_ptr_dtor,
1819 } ,
1920 flags:: DataType ,
2021 flags:: ZvalTypeFlags ,
@@ -496,6 +497,21 @@ impl Zval {
496497 /// * `val` - The value to set the zval as.
497498 /// * `persistent` - Whether the string should persist between requests.
498499 ///
500+ /// # Persistent Strings
501+ ///
502+ /// When `persistent` is `true`, the string is allocated from PHP's
503+ /// persistent heap (using `malloc`) rather than the request-bound heap.
504+ /// This is typically used for strings that need to survive across multiple
505+ /// PHP requests, such as class names, function names, or module-level data.
506+ ///
507+ /// **Important:** The string will still be freed when the Zval is dropped.
508+ /// The `persistent` flag only affects which memory allocator is used. If
509+ /// you need a string to outlive the Zval, consider using
510+ /// [`std::mem::forget`] on the Zval or storing the string elsewhere.
511+ ///
512+ /// For most use cases (return values, function arguments, temporary
513+ /// storage), you should use `persistent: false`.
514+ ///
499515 /// # Errors
500516 ///
501517 /// Never returns an error.
@@ -507,6 +523,9 @@ impl Zval {
507523
508524 /// Sets the value of the zval as a Zend string.
509525 ///
526+ /// The Zval takes ownership of the string. When the Zval is dropped,
527+ /// the string will be released.
528+ ///
510529 /// # Parameters
511530 ///
512531 /// * `val` - String content.
@@ -527,9 +546,13 @@ impl Zval {
527546 self . value . str_ = ptr;
528547 }
529548
530- /// Sets the value of the zval as a interned string. Returns nothing in a
549+ /// Sets the value of the zval as an interned string. Returns nothing in a
531550 /// result when successful.
532551 ///
552+ /// Interned strings are stored once and are immutable. PHP stores them in
553+ /// an internal hashtable. Unlike regular strings, interned strings are not
554+ /// reference counted and should not be freed by `zval_ptr_dtor`.
555+ ///
533556 /// # Parameters
534557 ///
535558 /// * `val` - The value to set the zval as.
@@ -540,7 +563,10 @@ impl Zval {
540563 /// Never returns an error.
541564 // TODO: Check if we can drop the result here.
542565 pub fn set_interned_string ( & mut self , val : & str , persistent : bool ) -> Result < ( ) > {
543- self . set_zend_string ( ZendStr :: new_interned ( val, persistent) ) ;
566+ // Use InternedStringEx (without RefCounted) because interned strings
567+ // should not have their refcount modified by zval_ptr_dtor.
568+ self . change_type ( ZvalTypeFlags :: InternedStringEx ) ;
569+ self . value . str_ = ZendStr :: new_interned ( val, persistent) . into_raw ( ) ;
544570 Ok ( ( ) )
545571 }
546572
@@ -676,7 +702,21 @@ impl Zval {
676702 fn change_type ( & mut self , ty : ZvalTypeFlags ) {
677703 // SAFETY: we have exclusive mutable access to this zval so can free the
678704 // contents.
679- unsafe { zval_ptr_dtor ( self ) } ;
705+ //
706+ // For strings, we use zend_string_release directly instead of zval_ptr_dtor
707+ // to correctly handle persistent strings. zend_string_release properly checks
708+ // the IS_STR_PERSISTENT flag and uses the correct deallocator (free vs efree).
709+ // This fixes heap corruption issues when dropping Zvals containing persistent
710+ // strings (see issue #424).
711+ if self . is_string ( ) {
712+ unsafe {
713+ if let Some ( str_ptr) = self . value . str_ . as_mut ( ) {
714+ ext_php_rs_zend_string_release ( str_ptr) ;
715+ }
716+ }
717+ } else {
718+ unsafe { zval_ptr_dtor ( self ) } ;
719+ }
680720 self . u1 . type_info = ty. bits ( ) ;
681721 }
682722
0 commit comments