Skip to content

Replace ZEND_ASSUME() by ZEND_ASSERT() in zend_hash_*_ptr setters #14466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

TimWolla
Copy link
Member

@TimWolla TimWolla commented Jun 4, 2024

I had a case where I was intentionally storing a NULL pointer within a HashTable to mark an entry as “blocked”, without paying for the overhead of checking the entry type when reading the pointer, resulting in semantics that are similar to using isset() instead of array_key_exists() in userland.

This worked fine in unoptimized test builds, but due to the ZEND_ASSUME() in the zend_hash_find_ptr functions, the optimized release builds turned the logic of:

my_pointer = zend_hash_find_ptr(ht, key);
if (my_pointer == NULL) {
    return;
}
*my_pointer;

into

zv = zend_hash_find(ht, key);
if (zv) {
    *Z_PTR_P(zv);
} else {
    return;
}

thus introducing a hard-to-debug and compiler-dependent crash when the entry exists, but the stored pointer is NULL.

Change the ZEND_ASSUME() in the setters to ZEND_ASSERT(). This would have made my mistake immediately obvious in debug builds when storing the pointer. The getters still use ZEND_ASSUME() under the assumption that they are called much more often, reducing the impact on debug builds: Assuming the developer uses the _ptr variants for both reading and writing the entries, the mistake will be reliably caught during writing, making the assert during reading unnecessary.

For release builds the ZEND_ASSERT() will be equivalent to ZEND_ASSUME(), avoiding any performance impact for those.

I had a case where I was intentionally storing a `NULL` pointer within a
HashTable to mark an entry as “blocked”, without paying for the overhead of
checking the entry type when reading the pointer, resulting in semantics that
are similar to using `isset()` instead of `array_key_exists()` in userland.

This worked fine in unoptimized test builds, but due to the `ZEND_ASSUME()` in
the `zend_hash_find_ptr` functions, the optimized release builds turned the
logic of:

    my_pointer = zend_hash_find_ptr(ht, key);
    if (my_pointer == NULL) {
        return;
    }
    *my_pointer;

into

    zv = zend_hash_find(ht, key);
    if (zv) {
        *Z_PTR_P(zv);
    } else {
        return;
    }

thus introducing a hard-to-debug and compiler-dependent crash when the entry
exists, but the stored pointer is `NULL`.

Change the `ZEND_ASSUME()` in the setters to `ZEND_ASSERT()`. This would have
made my mistake immediately obvious in debug builds when storing the pointer.
The getters still use `ZEND_ASSUME()` under the assumption that they are called
much more often, reducing the impact on debug builds: Assuming the developer
uses the `_ptr` variants for both reading and writing the entries, the mistake
will be reliably caught during writing, making the assert during reading
unnecessary.

For release builds the `ZEND_ASSERT()` will be equivalent to `ZEND_ASSUME()`,
avoiding any performance impact for those.
Copy link
Member

@dstogov dstogov left a comment

Choose a reason for hiding this comment

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

This shouldn't make any problems.

@TimWolla TimWolla merged commit 7c2a4db into php:master Jun 5, 2024
11 checks passed
@TimWolla TimWolla deleted the zend-hash-pointer-write-assert-not-assume branch June 5, 2024 09:08
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.

2 participants