Skip to content

Fix GH-20964: fseek() stream seek with PHP_INT_MIN causes undefined behavior - #1

Closed
iliaal wants to merge 10000 commits into
masterfrom
fix/gh-20964-fseek-ubsan
Closed

iliaal wants to merge 10000 commits into
masterfrom
fix/gh-20964-fseek-ubsan

Conversation

@iliaal

@iliaal iliaal commented Mar 13, 2026

Copy link
Copy Markdown
Owner

Summary

php_stream_memory_seek() (and equivalent functions in pdo_sqlite and sqlite3) compute the absolute value of a negative offset using (size_t)(-offset). When offset == ZEND_LONG_MIN, the signed negation overflows -- undefined behavior caught by UBSAN.

The fix swaps the cast order: -(size_t)offset casts to unsigned first (well-defined), then negates (well-defined unsigned arithmetic). Same result for all values, no behavioral change.

Files changed

  • main/streams/memory.c -- php://memory and php://temp stream seek
  • ext/pdo_sqlite/pdo_sqlite.c -- PDO SQLite blob stream seek
  • ext/sqlite3/sqlite3.c -- SQLite3 blob stream seek

Existing PR

PR php#20965 takes a different approach (rejects ZEND_LONG_MIN with a ValueError). This patch is simpler -- it fixes the UB without changing behavior.

Fixes php#20964

Ilija Tovilo and others added 30 commits February 19, 2026 00:57
* PHP-8.5:
  [skip ci] Rename PUSH job to TEST
* PHP-8.5:
  NEWS: fix minor formatting [skip-ci]
git rev-parse fails with the long GH ref, but we need github.ref to check out
branches from PRs. Hence, use github.ref only for PRs, and the ref_name for
everything else.

Closes phpGH-21255
* PHP-8.2:
  Fix nightly ref name
* PHP-8.3:
  Fix nightly ref name
* PHP-8.4:
  Fix nightly ref name
* PHP-8.5:
  Fix nightly ref name
Cache on GHA is immutable. For this reason, hendrikmuhs/ccache-action creates a
new cache entry for each push, each with an appended timestamp, which fills the
cache very quickly. In an attempt to fix this, we've disabled the
append-timestamp option and appended a hash of php_version.h. Hence, we'll only
get a new cache file once this file is touched.

However, since this file rarely ever updates for master, we're relying on an
extremely outdated cache. To fix this, append the current year+week to rebuild
the cache each week instead, as suggested by Tim. Also use major.minor.release
version instead of the has of php_version.h for readability.

Fixes phpGH-14154
Closes phpGH-21258
* PHP-8.2:
  [CI] Fix ineffective ccache
* PHP-8.3:
  [CI] Fix ineffective ccache
* PHP-8.4:
  [CI] Fix ineffective ccache
* PHP-8.5:
  [CI] Fix ineffective ccache
Save errno into a local int before calling close(fd), as close() may
clobber errno on failure. Use int rather than errno_t because errno_t
is defined in C11 Annex K (bounds-checking interfaces) which is
optional and not widely implemented — many platforms (Linux/glibc,
musl, macOS, FreeBSD) do not provide it.

close phpGH-21256
* PHP-8.4:
  ext/pcntl: fix pcntl_setns() error handling.
* PHP-8.5:
  ext/pcntl: fix pcntl_setns() error handling.
…U ID

Add missing PCNTL_CPU_DESTROY(mask) call before RETURN_THROWS() when
the cpu id is out of range, matching the cleanup on other error paths.

close phpGH-21268
* PHP-8.4:
  ext/pcntl: Fix cpuset leak in pcntl_setcpuaffinity on out-of-range CPU ID
* PHP-8.5:
  ext/pcntl: Fix cpuset leak in pcntl_setcpuaffinity on out-of-range CPU ID
By splitting the massive tests into more specific, smaller tests.
- Replace RETVAL_TRUE/FALSE with RETVAL_BOOL
- Replace RETURN_TRUE/FALSE with RETURN_BOOL
…ntl_signal

Move the signal table update after the php_signal4 call, mirroring
what is already done in the SIG_DFL/SIG_IGN (integer) code path.
This prevents a stale entry in the table if sigaction fails.

close phpGH-21270
* PHP-8.4:
  ext/pcntl: Fix signal table updated before php_signal4 succeeds in pcntl_signal
* PHP-8.5:
  ext/pcntl: Fix signal table updated before php_signal4 succeeds in pcntl_signal
When installing executables that were built using libtool, we are
supposed to use

  $ libtool --mode-install <install-command>

rather than the bare <install-command>. This is discussed ever so
briefly in the "Installing executables" section of the GNU libtool
documentation:

  https://www.gnu.org/software/libtool/manual/libtool.html

So far this has not caused a problem with GNU libtool on the platforms
that PHP supports, but there is an alternate libtool implementation
called slibtool that stores wrappers at the locations where PHP is
expecting the true executables to live. As a result, the wrappers (and
not the executables) are installed when slibtool is used to build PHP.

This is fixed by replacing,

  $(INSTALL)

with

  $(LIBTOOL) --mode=install $(INSTALL)

in the install-foo rules for the executables that are built with
libtool.

Closes phpGH-13674
* PHP-8.4:
  sapi/*/Makefile.frag: install programs built with libtool, with libtool
* PHP-8.5:
  sapi/*/Makefile.frag: install programs built with libtool, with libtool
@iliaal
iliaal force-pushed the fix/gh-20964-fseek-ubsan branch from 5da58b7 to 72fddd9 Compare March 13, 2026 21:41
…fined behavior

Negate after casting to unsigned instead of before, avoiding signed
integer overflow when offset is ZEND_LONG_MIN. The same pattern existed
in the pdo_sqlite and sqlite3 stream seek handlers.

Closes phpGH-20964
Closes phpGH-20927
@iliaal
iliaal force-pushed the fix/gh-20964-fseek-ubsan branch from 72fddd9 to da3dad2 Compare March 13, 2026 21:42
@iliaal iliaal closed this Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UBSAN: fseek() on php://memory with PHP_INT_MIN causes undefined behavior