Skip to content

#255: allocate the literals the way the platform frees them - #258

Merged
EdmondDantes merged 1 commit into
mainfrom
255-literals-block-32bit
Aug 20, 2026
Merged

#255: allocate the literals the way the platform frees them#258
EdmondDantes merged 1 commit into
mainfrom
255-literals-block-32bit

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Closes #255.

What was wrong

op_array_to_emalloc allocated opcodes and literals as one block and pointed op_array->literals at the tail of it. That is the layout pass_two() leaves on 64-bit, where destroy_op_array frees only op_array->opcodes.

Under ZEND_USE_ABS_CONST_ADDRSIZEOF_SIZE_T == 4, i.e. 32-bit — the compiler keeps the two in separate allocations, and destroy_op_array frees both:

if (ZEND_USE_ABS_CONST_ADDR || !(op_array->fn_flags & ZEND_ACC_DONE_PASS_TWO)) {
    efree(op_array->literals);
}
efree(op_array->opcodes);

So on 32-bit every materialized closure with at least one literal handed the allocator a pointer into the middle of a block it never issued, and then freed the block a second time. The opline fix-up already branched on the macro; the allocation did not.

#251 made op_array_to_emalloc unconditional, which turned this from some closures into every closure that reaches a worker.

What changed

The copy follows the same rule as the compiler on both platforms: one block under !ZEND_USE_ABS_CONST_ADDR, where RT_CONSTANT addresses the literal by an int32_t offset from the opline and separate allocations would overflow it, two blocks otherwise.

Verification

No 32-bit runner exists here, so the configuration was reproduced on x86-64 by forcing ZEND_USE_ABS_JMP_ADDR and ZEND_USE_ABS_CONST_ADDR to 1 in Zend/zend_compile.h and rebuilding the engine. In that build:

  • before the change: 35 of 102 thread_pool tests fail;
  • after: 0.

In the ordinary 64-bit build the layout is unchanged: tests 1191 passed / 0 failed, fuzzy-tests/_generated 724 / 0. The one warning is the stale --XFAIL-- on 030, removed by #256.

Build: ZTS DEBUG, TrueAsync ABI v0.24.0.

op_array_to_emalloc laid the literals inside the opcode block. That is what
pass_two() leaves on 64-bit, where destroy_op_array frees only the opcode
block. Under ZEND_USE_ABS_CONST_ADDR the compiler keeps the two apart and
destroy_op_array frees op_array->literals on its own, so the allocator got
an interior pointer and the block was freed twice.

Reproduced on x86-64 by forcing ZEND_USE_ABS_JMP_ADDR and
ZEND_USE_ABS_CONST_ADDR to 1: 35 of 102 thread_pool tests failed before the
change, none after. #251 made the copy unconditional, which is why the
count is that high.
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes
EdmondDantes merged commit e6eea90 into main Aug 20, 2026
9 checks passed
@EdmondDantes
EdmondDantes deleted the 255-literals-block-32bit branch August 20, 2026 11:58
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.

op_array_to_emalloc puts literals inside the opcode block, which destroy_op_array frees separately on 32-bit

1 participant