Skip to content

mb_strpos()/mb_strrpos(): the out-of-bounds read fixed in 8.4.27 (GH-23106) is still present in PHP-8.3 and PHP-8.2 #25025

Open

Description

@aygydiewr221

Description

This is not a new bug and not my finding. It is GH-23106, fixed in 8.4.27 /
8.5.12 and credited to Lazizbek Ergashev:

Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in a
truncated UTF-8 sequence). (Lazizbek Ergashev)

Since it is already accepted upstream, there is nothing to re-argue about
whether it is a bug. The only question is the branches: PHP-8.3 and PHP-8.2
did not get the fix, and both are still in security support (8.3 to 2027-12-31,
8.2 to 2026-12-31).

Still present on both branches

The forward branch of the walk can step past end:

	} else {
		const unsigned char *u8_tbl = mbfl_encoding_utf8.mblen_table;
		unsigned char *pos = str;
		while (offset-- > 0) {
			if (pos >= end) {
				return NULL;
			}
			pos += u8_tbl[*pos];     /* the last step can overshoot end */
		}
		return pos;
	}

PHP-8.4 head adds the clamp before the return:

		if (pos > end) {
			pos = end;
		}
		return pos;

PHP-8.3 and PHP-8.2 head do not have it.

This is easy to miss when grepping

The function has a different name and lives in a different file depending on the
branch, which is probably why the backport was not obvious:

Branch Name File Clamp
PHP-8.4 / PHP-8.5 / master offset_to_pointer_utf8 ext/mbstring/mbstring.c:1900 yes
PHP-8.3 offset_to_pointer_utf8 ext/mbstring/mbstring.c:1890 no
PHP-8.2 mbfl_find_offset_utf8 ext/mbstring/libmbfl/mbfl/mbfilter.c:544 no

(Line numbers are branch head as of 2026-10-29.)

So the 8.4 patch does not apply to 8.2 as-is. There the function is still
mbfl_find_offset_utf8() in libmbfl and its pointers are const unsigned char *,
so the clamp has to be rewritten against the older name and location. 8.3 is a
straight apply.

The following code

<?php
// haystack = "a\xF0" (2 chars, ends in a truncated 4-byte UTF-8 lead)
// needle   = "\xE3"  -- does NOT occur in the haystack
// offset   = -1      -- takes the reverse && offset<0 path
$h = "a\xF0";
$n = "\xE3";
echo "haystack=", bin2hex($h), " len=", strlen($h), " mb_strlen=", mb_strlen($h,'UTF-8'), "\n";
echo "needle  =", bin2hex($n), "\n";
$r = mb_strrpos($h, $n, -1, 'UTF-8');
echo "mb_strrpos => ", var_export($r, true), "\n";

Resulted in this output / But I expected this output instead

There is no visible difference, and that is worth saying plainly — the
return value is the same on an affected and on a fixed build. The defect is the
read itself, so it shows up under instrumentation or ASAN rather than in the
output. I am filling this section in honestly rather than inventing a diff.

What I measured, with one probe reporting pos - end on every call:

Build probe hits delta == 0 delta > 0 (out of bounds)
8.3.36 1,773 54 507 (+1: 162, +2: 108, +3: 108, …)
8.5.11 1,773 54 507 (identical)
8.5.12 1,773 561 0

The cases that went out of bounds on 8.3.36 stop exactly at end on 8.5.12,
which is the clamp doing its job. 8.5.11 — the last release before the fix —
measures identical to 8.3.36, which is the cleanest demonstration that 8.3 is in
the pre-fix state.

Trigger conditions: a haystack ending in a truncated multi-byte lead byte
(0xC2–0xF4) and a negative offset. A sweep using only non-negative
offsets produced zero overshoots across 228 calls.

Which versions

7.4 is genuinely not affected: the walk is inlined there and followed by
p += needle_u8_len; if (p > e) goto out;, which neutralises the overshoot. The
probe was confirmed reached (96 and 132 times at the two sites) with zero
overshoots, so that is a measured negative rather than missing coverage. The
defect was introduced by the 8.0 refactor that extracted the walk into
mbfl_find_offset_utf8().
8.1.31 and 8.2.35 both reproduce (8.1.31 measured
returning end + 3).

The ask

Apply the GH-23106 clamp to PHP-8.3, and to PHP-8.2 while that branch is
still in security support. The upstream fix is PR #23107; it applies to 8.3 as
is, and for 8.2 it has to be rewritten for mbfl_find_offset_utf8() in libmbfl,
per the table above.

I am not asking for a CVE or for credit; the credit belongs to Lazizbek
Ergashev. If you judge 8.3 fine as it is, or 8.2 too close to end of life to
warrant a release, that is a reasonable call and I will not argue it.

PHP Version

PHP 8.3.36 (cli) (built from source, gcc 13.3, NTS)

Operating System

Ubuntu 24.04 (WSL2)

Activity

  1. youkidearitai commented on Nov 1, 2026

    @youkidearitai
    Contributor

    8.3 and 8.2 are security fixes only. So probably we can't fixes these bugs

  2. aygydiewr221 commented on Nov 1, 2026

    @aygydiewr221
    Author

    Understood — thanks for looking.

    The only reason I raised it was whether an out-of-bounds read counts as a
    security fix rather than a functional one. If it doesn't, that's a complete
    answer and I am happy to leave both branches as they are.

  3. youkidearitai commented on Nov 1, 2026

    @youkidearitai
    Contributor

    @aygydiewr221 If this issue is security report, Please report via https://github.com/php/php-src/security/advisories/new .

  4. VishnuSrivatsava commented on Nov 1, 2026

    @VishnuSrivatsava

    Understood, thank you for the quick response. Closing both PRs. The fix is already available on master since 8.4.27, so the regression test there covers it. If the security classification for the stable branches ever changes, the backports are easy to revive from the branches on my fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions