Commit e0113cd1d64 for php.net

commit e0113cd1d640144715059189ddae874a829f1669
Author: Calvin Buckley <calvinb@php.net>
Date:   Mon Jun 22 19:07:21 2026 -0300

    gen_stub: Fix handling of escape sequences in generated C strings (#22273)

    When handling sequences like this in a stub:

    ```php
    <?php
    class Whatever {
        public static string $foobar1 = "CCC \n\r\t\v\e\f\\\$\"\101\x41\u{41} CCC";
        public static string $foobar2 = 'CCC \n\r\t\v\e\f\\\$\"\101\x41\u{41} CCC';
    }
    ```

    ...properly generate C headers that properly escape the string.
    Otherwise, the differing escaping rules and differences between PHP's
    single and double quoted strings could lead to mangled headers.

    The output of these strings after the stub has been generated:

    ```
    string(22) "BEGIN

            \$"AAA END"
    string(43) "BEGIN \n\r\t\v\e\f\\\\$\"\101\x41\u{41} END"
    ```

    And the generated arginfo:

    ```c
            zval property_doubleQuoteEscaped_default_value;
            zend_string *property_doubleQuoteEscaped_default_value_str = zend_string_init("BEGIN \n\r\t\v\x1b\f\\$\"AAA END", strlen("BEGIN \n\r\t\v\x1b\f\\$\"AAA END"), 1);
    pp      ZVAL_STR(&property_doubleQuoteEscaped_default_value, property_doubleQuoteEscaped_default_value_str);
            zend_string *property_doubleQuoteEscaped_name = zend_string_init("doubleQuoteEscaped", sizeof("doubleQuoteEscaped") - 1, true);
            zend_declare_typed_property(class_entry, property_doubleQuoteEscaped_name, &property_doubleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
            zend_string_release_ex(property_doubleQuoteEscaped_name, true);

            zval property_singleQuoteEscaped_default_value;
            zend_string *property_singleQuoteEscaped_default_value_str = zend_string_init("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END", strlen("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END"), 1);
            ZVAL_STR(&property_singleQuoteEscaped_default_value, property_singleQuoteEscaped_default_value_str);
            zend_string *property_singleQuoteEscaped_name = zend_string_init("singleQuoteEscaped", sizeof("singleQuoteEscaped") - 1, true);
            zend_declare_typed_property(class_entry, property_singleQuoteEscaped_name, &property_singleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
            zend_string_release_ex(property_singleQuoteEscaped_name, true);
    ```

    Note that the PHP escape sequence "\$" will be handled in a special
    manner to avoid providing it to C, to avoid C compiler warnings.

    Tests are included via the zend_test stub and a phpt file to ensure that
    the escape sequences match.

    Fixes GH-22169.

diff --git a/build/gen_stub.php b/build/gen_stub.php
index 34bbd34fd37..e60e6188520 100755
--- a/build/gen_stub.php
+++ b/build/gen_stub.php
@@ -2344,9 +2344,28 @@ public function getCExpr(): ?string
         // $this->expr has all its PHP constants replaced by C constants
         $prettyPrinter = new Standard;
         $expr = $prettyPrinter->prettyPrintExpr($this->expr);
-        // PHP single-quote to C double-quote string
         if ($this->type->isString()) {
-            $expr = preg_replace("/(^'|'$)/", '"', $expr);
+            // The string in $expr has had the octal, hex, and unicode
+            // backslash sequences already applied for double-quoted strings,
+            // but not the other sequences.
+            //
+            // PHP has single quote strings, C doesn't (they're one char).
+            // Single-quoted strings need handling to replace their escapes
+            // with the double-quoted equivalent; namely single quote escapes.
+            //
+            // Double-quoted strings have similar escape sequences as C does,
+            // so we can pass them through directly. However, C does *not*
+            // support the \$ escape sequence (in ""), so strip that. Variable
+            // interpolation shouldn't be possible in a stub, so we don't need
+            // to worry about mangling such a case.
+            if (preg_match("/(^'|'$)/", $expr)) {
+                $expr = substr($expr, 1, -1); // strip quotes, readd later
+                $expr = str_replace("\\'", "'", $expr);
+                $expr = addcslashes($expr, "\\\"");
+                $expr = "\"$expr\"";
+            } else {
+                $expr = str_replace('\$', "$", $expr);
+            }
         }
         return $expr[0] == '"' ? $expr : preg_replace('(\bnull\b)', 'NULL', str_replace('\\', '', $expr));
     }
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index 489d7d0a260..a4562368735 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -57,6 +57,12 @@ class _ZendTestClass implements _ZendTestInterface {
         public static $_StaticProp;
         public static int $staticIntProp = 123;

+        /* If there's a problem with escapes in quotes in generated headers,
+         * the generated header won't compile. (tests/gh22169.phpt) */
+        public static string $doubleQuoteEscaped = "BEGIN \n\r\t\v\e\f\\\$\"\101\x41\u{41} END";
+        public static string $singleQuoteEscaped = 'BEGIN \n\r\t\v\e\f\\\$\"\101\x41\u{41} END';
+        public static string $escapeInterpolated = "begin \$ \\$ end";
+
         public int $intProp = 123;
         public ?stdClass $classProp = null;
         public stdClass|Iterator|null $classUnionProp = null;
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index 94f75cdb360..bd6548e7bff 100644
Binary files a/ext/zend_test/test_arginfo.h and b/ext/zend_test/test_arginfo.h differ
diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h
index 4a6babbe12b..2561000f4b6 100644
Binary files a/ext/zend_test/test_decl.h and b/ext/zend_test/test_decl.h differ
diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h
index a4c1ae3f2c9..a254a637e07 100644
Binary files a/ext/zend_test/test_legacy_arginfo.h and b/ext/zend_test/test_legacy_arginfo.h differ
diff --git a/ext/zend_test/tests/gh22169.phpt b/ext/zend_test/tests/gh22169.phpt
new file mode 100644
index 00000000000..5ed2ab22fa2
--- /dev/null
+++ b/ext/zend_test/tests/gh22169.phpt
@@ -0,0 +1,22 @@
+--TEST--
+GH-22169: Ensure escaped strings in stubs are valid
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+// Avoid funny control characters in output...
+// "BEGIN "         424547494e20
+// "\n\r\t\v\e\f"   0a0d090b1b0c
+// "\\\$\""         5c2422
+// "\101\x41\u{41}" 414141
+// " END"           20454e44
+var_dump(bin2hex(_ZendTestClass::$doubleQuoteEscaped));
+var_dump(_ZendTestClass::$singleQuoteEscaped);
+var_dump(_ZendTestClass::$escapeInterpolated);
+?>
+--EXPECT--
+string(44) "424547494e200a0d090b1b0c5c242241414120454e44"
+string(43) "BEGIN \n\r\t\v\e\f\\\\$\"\101\x41\u{41} END"
+string(14) "begin $ \$ end"
+