Skip to content

Enable 64bit integer support on 32bit arch --enable-zend-int64 - #19079

Draft
marc-mabe wants to merge 22 commits into
php:masterfrom
marc-mabe:int64
Draft

Enable 64bit integer support on 32bit arch --enable-zend-int64#19079
marc-mabe wants to merge 22 commits into
php:masterfrom
marc-mabe:int64

Conversation

@marc-mabe

@marc-mabe marc-mabe commented Jul 9, 2025

Copy link
Copy Markdown
Contributor

This PR allows to use 64bit integers on 32bit platforms if compiled with --enable-zend-int64

  • allow to enable 64bit integers on 32bit platforms (--enable-zend-int64)
  • add const for system architecture (currently PHP_SYS_SIZE = [4|8])
  • add guards on casting zend_[u]long to [s]size_t
  • fix all errors in Zend/tests/ and tests/
  • fix all errors in ext/
  • Support 64 bit timestamps
  • Support large files

The main part is that ZEND_ENABLE_ZVAL_LONG64 1 gets defined.

@iluuu1994

iluuu1994 commented Jul 9, 2025

Copy link
Copy Markdown
Member

I think this should go through the RFC process. There have been suggestions to drop 32-bit support, so I'm sure at least some people would object to expanding support further. I could see this as an alternative to dropping 32-bit support iff we also dropped 4-byte zend_long support, as that would align 32 and 64-bit.

Comment thread ext/gmp/gmp.c Outdated
@marc-mabe

marc-mabe commented Jul 9, 2025

Copy link
Copy Markdown
Contributor Author

I have fixed some cases which failed due to SIZEOF_SIZE_T != SIZEOF_ZEND_LONG, but there are probably more.
But there is something off with closure handling and static variables I'm not able to find :(

Currently, the test state is as follows:

$ sapi/cli/php run-tests.php Zend/tests/ tests/
<snip>
Number of tests :  5929              5791
Tests skipped   :   138 (  2.3%) --------
Tests warned    :     0 (  0.0%) (  0.0%)
Tests failed    :    44 (  0.7%) (  0.8%)
Expected fail   :     2 (  0.0%) (  0.0%)
Tests passed    :  5745 ( 96.9%) ( 99.2%)
---------------------------------------------------------------------
Time taken      : 158.811 seconds
=====================================================================

=====================================================================
FAILED TEST SUMMARY
---------------------------------------------------------------------
php://input is empty when enable_post_data_reading=Off [tests/basic/bug67198.phpt]
Static variables in functions [tests/func/002.phpt]
Bug #23384 (use of class constants in statics) [tests/lang/bug23384.phpt]
Static keyword - basic tests [tests/lang/static_basic_001.phpt]
Bug #78502: Incorrect stack size calculation for indirectly recursive function call [Zend/tests/bug78502.phpt]
Bug #54358 (Closure, use and reference) [Zend/tests/closures/bug54358.phpt]
Closure 009: Using static vars inside lambda [Zend/tests/closures/closure_009.phpt]
Closure 047: Use in preg_replace_callback() using variables by reference [Zend/tests/closures/closure_047.phpt]
Closure 048: Use in preg_replace_callback() using variables by reference [Zend/tests/closures/closure_048.phpt]
Closures in const expressions support static variables. [Zend/tests/closures/closure_const_expr/static_variable.phpt]
Closure use list can have trailing commas [Zend/tests/closures/closure_use_trailing_comma.phpt]
Bug #79836 (Segfault in concat_function) [Zend/tests/concat/bug79836.phpt]
new in constant expressions [Zend/tests/constexpr/new.phpt]
Named params in new in const expr [Zend/tests/constexpr/new_named_params.phpt]
GH-13446: Exception handler attempting to free itself [Zend/tests/exceptions/gh13446_2.phpt]
Fiber::getReturn() after bailout [Zend/tests/fibers/get-return-after-bailout.phpt]
GH-15108 003: Segfault with delegated generator in suspended fiber [Zend/tests/fibers/gh15108-003.phpt]
GH-15108 004: Segfault with delegated generator in suspended fiber [Zend/tests/fibers/gh15108-004.phpt]
GH-15108 007: Segfault with delegated generator in suspended fiber [Zend/tests/fibers/gh15108-007.phpt]
Bug GH-9916 001 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-001.phpt]
Bug GH-9916 002 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-002.phpt]
Bug GH-9916 003 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-003.phpt]
Bug GH-9916 004 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-004.phpt]
Bug GH-9916 005 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-005.phpt]
Bug GH-9916 006 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-006.phpt]
Bug GH-9916 007 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-007.phpt]
Bug GH-9916 008 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-008.phpt]
Bug GH-9916 009 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-009.phpt]
Bug GH-9916 010 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-010.phpt]
Bug GH-9916 011 (Entering shutdown sequence with a fiber suspended in a Generator emits an unavoidable fatal error or crashes) [Zend/tests/fibers/gh9916-011.phpt]
GC 049: Objects created during GC do not participate in the same collection [Zend/tests/gc/gc_049.phpt]
GH-15330 003: Do not scan generator frames more than once [Zend/tests/generators/gh15330-003.phpt]
GH-15330 004: Do not scan generator frames more than once [Zend/tests/generators/gh15330-004.phpt]
GH-15330 005: Do not scan generator frames more than once [Zend/tests/generators/gh15330-005.phpt]
GH-15330 006: Do not scan generator frames more than once [Zend/tests/generators/gh15330-006.phpt]
GH-16408: Array to string conversion warning emitted in optimizer [Zend/tests/gh16408.phpt]
Lazy objects: Initializer effects are reverted after exception (nested) [Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_nested.phpt]
Lazy objects: RFC example 011 [Zend/tests/lazy_objects/rfc/rfc_example_011.phpt]
Test nullsafe in binary op [Zend/tests/nullsafe_operator/014.phpt]
Allow fetching properties in static initializer [Zend/tests/prop_const_expr/static_initalizer.phpt]
Hooks containing static variables [Zend/tests/property_hooks/static_variables.phpt]
Test binary operands exposing the same behavior at compile as at run time [Zend/tests/runtime_compile_time_binary_operands.phpt]
Static Variable Expressions [Zend/tests/static_variables/static_variable.phpt]
Zend signed multiply 64-bit, variation 2 [Zend/tests/zend_signed_multiply-64bit-2.phpt]

@marc-mabe

Copy link
Copy Markdown
Contributor Author

I think this should go through the RFC process. There have been suggestions to drop 32-bit support, so I'm sure at least some people would object to expanding support further. I could see this as an alternative to dropping 32-bit support iff we also dropped 4-byte zend_long support, as that would align 32 and 64-bit.

Yes, of course. At the current state I want to get to know the work needed to be done and do some benchmarks.

For dropping 32-bit support entirely it seems to be to early at least for WebAssembly + there might be people hoping for supporting older (in some cases not that old) systems - so I don't thing it would go through currently.

marc-mabe and others added 22 commits September 12, 2026 21:53
The greatest length a string can have, exposed so that userland and the
test suite can reason about the limit without hard coding the header size.

ZSTR_MAX_LEN is SIZE_MAX - ZSTR_MAX_OVERHEAD and so exceeds ZEND_LONG_MAX
wherever size_t is at least as wide as zend_long; registered unclamped it
would land as a negative int. It is capped to ZEND_LONG_MAX instead, which
is also the more accurate value: every userland string length travels
through zend_long -- strlen()'s return value, string offsets and substr()'s
arguments -- so a longer string could not report its own length.

The constant therefore equals PHP_INT_MAX on every platform that ships
today, including 32 bit, and only becomes a distinct value where zend_long
is wider than size_t.
_zend_mm_alloc() verifies that adding the per allocation zend_mm_debug_info
to the requested size does not overflow, but zend_mm_realloc_heap() and
zend_mm_realloc_huge() perform the same addition without that check. In a
debug build a request within a few bytes of SIZE_MAX therefore wraps to a
tiny allocation while the caller believes it received the size it asked for,
and then writes past the end of it.

The only way to reach this from PHP is assigning to a string offset close to
SIZE_MAX, which is possible solely where zend_long is wider than size_t and
is refused before the reallocation by the following commit, so there is no
test that can exercise it.
The allocator works in units of the platform word, not of zend_long.
The two coincide everywhere except with ZEND_INT64 on a 32bit platform,
where the page bitset became 64bit wide and every scan of it did 64bit
arithmetic on 32bit registers.

Type the bitset as size_t, move the two width checks it feeds onto
SIZEOF_SIZE_T, and build its masks from zend_mm_bitset rather than from
Z_UL()/Z_L(). heap->limit is a size_t as well and no longer takes a
detour through zend_long to spell (size_t)-1.

zend_mm_bitset_ntz() replaces the call to zend_ulong_ntz(), which is
part of the zend_bitset family and typed for zend_ulong; a size_t bitset
widened into it on every page scan. It carries the implementation and
zend_mm_bitset_nts() is now ntz() of the complement, so the two cannot
drift apart. The runtime sizeof() test inside the 8 byte branch of the
fallback is gone; the enclosing #if already decides it.
The previous commit commented the whole optimisation out because it did not
work on i386 with a 64-bit zend_long. That switched it off on every platform,
including x86-64 and aarch64 where the sequences were always correct, so
narrow it down to what is actually broken.

The i386 sequences operate on 32 bits: addl/subl on the low half of the value
and jo on a 32-bit overflow flag. With a 64-bit zend_long that is wrong three
ways -- 2147483647 + 1 reports an overflow that did not happen, 4294967295 + 1
drops the carry and yields 0, and PHP_INT_MAX + 1 overflows unnoticed and
yields 9223372032559808512.

Increment and decrement gain a 64-bit sequence that carries across both halves
with adcl/sbbl, where the overflow flag of the second instruction is the one
that matters. Addition and subtraction keep the 32-bit sequence only: holding a
64-bit value needs a second scratch register, and inside execute_ex() %esi and
%edi are pinned as the VM's global register variables, leaving eax/ebx/ecx/edx.
Three "r" operands plus two clobbers cannot be allocated, and gcc loops in
register allocation rather than reporting it. Those two are served by the
__builtin_s{add,sub}ll_overflow() paths that follow.

int_overflow_64bit.phpt covers the increment and decrement boundaries, which
nothing exercised before.
The IR x86 backend emits 64bit integer instructions only when it is built
for X64. On a 32bit target the 8 byte cases of the type dispatch are not
compiled at all, so an 8 byte operand reaches the default label. In a
debug build that is IR_ASSERT(0) and the process aborts; in a release
build IR_ASSERT() expands to nothing and control falls through into the
1 byte case, which emits a byte sized instruction with the immediate
masked to 0xff. The JIT would silently generate wrong code.

Supporting this properly means adding 64bit arithmetic over register
pairs to 21 emission paths in the x86 backend, which belongs upstream in
IR rather than here. Until then refuse to enable the JIT, in the same
place and shape as the existing Apple Silicon ZTS check. The condition
is false wherever zend_long matches the platform word, so every shipping
platform is unaffected.
zend_string_alloc() and zend_string_safe_alloc() add the header and the
terminating NUL to the requested length without an overflow check, so a
length above ZSTR_MAX_LEN wraps to a tiny allocation carrying a huge
ZSTR_LEN. That bound has always been the callers' responsibility, but it
was neither documented nor enforced anywhere.

Assert it in the four allocators that take a length, making the contract
executable in debug builds at no cost to release builds.
strncmp() and strncasecmp() pass $length straight into the size_t parameter
of zend_binary_strncmp()/zend_binary_strncasecmp(). Where zend_long is wider
than size_t the value is narrowed, so a length above SIZE_MAX compares fewer
bytes than was asked for and reports equality for operands that differ
further along: SIZE_MAX + 1 becomes 0 and SIZE_MAX + 3 becomes 2.

Clamp to SIZE_MAX instead, as substr_compare() already does. Both comparators
reduce the length to MIN(length, MIN(len1, len2)) internally, so every value
at or above the operand lengths behaves alike and no new error is needed.
Runs the test suite on a 32bit build whose zend_long is 64bit, the
configuration --enable-zend-int64 makes possible. The job mirrors
LINUX_X32 and adds --enable-zend-int64.

It builds with -mfpmath=sse. On i386 the x87 unit loads a 64bit integer
exactly into an 80bit register, so (double) a * (double) b multiplies an
operand that was never rounded to binary64 and the product differs from
every 64bit platform in the last place. SSE math rounds the operand
first and keeps the results identical. The x87 precision control word
that zend_init_fpu() sets does not help here: it limits the mantissa of
the arithmetic, not the exactness of the integer load.

configure-x32 gained a cflags input rather than growing a second copy of
the action; LINUX_X32 keeps its previous flags through the default.
The synthetic namespace bookkeeping keys a HashTable by the xmlNode it
belongs to and casts the key back to a pointer when unlinking. Both
directions went through a plain cast, which is a narrowing conversion
where zend_long is wider than a pointer.

Use ZEND_PTR_TO_ZEND_ULONG() and ZEND_ULONG_TO_PTR() instead. The low bit
of the key stays available as the marker for the default namespace entry,
as xmlNode is aligned well beyond two bytes.
The signal table keeps handlers in the zend_long payload of a zval while
SIG_DFL, SIG_IGN and SIG_ERR are pointer constants. Converting between
the two with a plain cast narrows where zend_long is wider than a
pointer, and would turn SIG_ERR from -1 into its zero extended image.

Use ZEND_PTR_TO_ZEND_LONG() and ZEND_LONG_TO_PTR() for those conversions
and for the fault address in the siginfo array. LONG_CONST() did the same
job for the constant registration and is replaced by it. php_signal()
takes the SA_SIGINFO form of a handler, so handing it SIG_DFL still needs
a detour over a data pointer to stay clear of -Wcast-function-type, but
that one no longer passes through an integer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants