Skip to content

Zend: handle IS_UNDEF in the generic conversion helpers that still lack it#22824

Closed
brzuchal wants to merge 1 commit into
php:masterfrom
brzuchal:zend-is-undef-conversion-helpers
Closed

Zend: handle IS_UNDEF in the generic conversion helpers that still lack it#22824
brzuchal wants to merge 1 commit into
php:masterfrom
brzuchal:zend-is-undef-conversion-helpers

Conversation

@brzuchal

@brzuchal brzuchal commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Up front: no userland reproducer

This is a consistency fix and I could not construct a PHP-level trigger: every VM handler
normalizes IS_UNDEF to IS_NULL before calling these helpers. I would rather state that than
have it asked. The argument is the asymmetry below plus the fact that this exact bug class has
already been fixed twice upstream, both times by adding the missing case. If the view is that
a reproducer is required, I am happy for this to be closed.

The asymmetry

These are ZEND_API entry points taking an arbitrary zval, and IS_UNDEF is a tag internal
code legitimately holds, most obviously from an object property table, where uninitialized
typed properties are IS_UNDEF. Four helpers account for it, five do not:

handles IS_UNDEF does not
zendi_try_get_long() _zendi_try_convert_scalar_to_number()
zval_get_long_func() zval_get_double_func()
__zval_get_string_func() convert_to_long()
_convert_to_string() convert_to_double()
convert_to_boolean()

zval_get_long(undef) returns 0; zval_get_double(undef) reaches ZEND_UNREACHABLE(), which
is an assertion failure in a debug build and undefined behaviour in a release build. Sibling
APIs, same contract, and the difference does not look designed.

Why it looks like this

Two point fixes, each scoped to the single function that had been reported:

Neither time were the neighbouring helpers revisited. This finishes that sweep.

What it does

Adds case IS_UNDEF: to five helpers, each joining an arm that already exists: IS_UNDEF
behaves as IS_NULL for the cast helpers, and yields a normal TypeError for the operator
helper, matching the bitwise operators, which are already safe through zendi_try_get_long().

What it deliberately does not do

increment_function() and decrement_function() are left alone. IS_UNDEF reaching them is a
genuine invariant violation, not an input to coerce: every VM handler normalizes it before the
call (zend_vm_def.h), and the JIT asserts the precondition outright
(ZEND_ASSERT(Z_TYPE_P(var_ptr) != IS_UNDEF), four sites in zend_jit_helpers.c). Accepting
it there would weaken a check another subsystem relies on.

No catch-all default: is added either. IS_CONSTANT_AST, IS_INDIRECT, IS_PTR,
IS_ALIAS_PTR and _IS_ERROR indicate a caller bug and must keep failing loudly.

Related

Companion fix, same bug class at a call site rather than in the helper:
#22822. Independent, either can merge first.

Behaviour and tests

No behaviour change for any tag that works today. Zend/tests, ext/date/tests,
ext/zlib/tests: 6039 pass, no failures attributable to this change.

No test is added: without a userland path reaching these helpers with IS_UNDEF, the only way
to write one would be a test-only API for fabricating zval tags, which seems a worse trade than
shipping the fix untested.

…ck it

IS_UNDEF is a legitimate runtime tag that internal code can obtain from an
object property table, and the generic conversion helpers in zend_operators.c
are ZEND_API entry points taking an arbitrary zval. Some already account for
that and some do not:

  handled: zendi_try_get_long(), zval_get_long_func(),
           __zval_get_string_func(), _convert_to_string()
  missing: _zendi_try_convert_scalar_to_number(), convert_to_long(),
           convert_to_double(), convert_to_boolean(), zval_get_double_func()

The asymmetry is what is left after two point fixes. 2b38384 taught
zval_try_get_long() about IS_REFERENCE and 9e1b285 (phpGH-22142) taught
zendi_try_get_long() about IS_UNDEF, each by adding the tag to the one
function that had been reported. The siblings were not revisited, so
zval_get_long(undef) returns 0 while zval_get_double(undef) reaches
ZEND_UNREACHABLE(): an assertion failure in a debug build, undefined
behaviour in a release build.

Add the missing cases to arms that already express the intended result, so
IS_UNDEF behaves as IS_NULL does for the cast helpers and produces a normal
TypeError for the operator helper, matching the bitwise operators which are
already safe through zendi_try_get_long().

increment_function() and decrement_function() are deliberately not changed.
IS_UNDEF must not reach them: every VM handler normalizes it to IS_NULL
before the call (zend_vm_def.h), and the JIT asserts the precondition
outright (zend_jit_helpers.c). Accepting it there would weaken a check
another subsystem relies on.

No behaviour changes for any tag that works today, and no catch-all default
is added: IS_CONSTANT_AST, IS_INDIRECT, IS_PTR, IS_ALIAS_PTR and _IS_ERROR
indicate a caller bug and must keep failing loudly.

@Girgias Girgias left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have an IS_UNDEF zval at this point you have massive problems, this is not something that should happen.

@ndossche

Copy link
Copy Markdown
Member

As for your references:
2b383848a73 is a completely different situation
9e1b285f65a might be a wrong fix

@brzuchal brzuchal closed this Jul 20, 2026
@brzuchal

Copy link
Copy Markdown
Contributor Author

Thanks, that distinction makes sense. I'll revisit the affected call sites instead.

@brzuchal
brzuchal deleted the zend-is-undef-conversion-helpers branch July 20, 2026 15:57
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.

Assertion failure at Zend/zend_operators.c zendi_try_get_long(const zval *, _Bool *): Assertion '0' failed.

3 participants