Skip to content
New issue

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

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

Already on GitHub? # to your account

Fix GH-15834: Segfault with hook "simple get" cache slot and minimal JIT #17909

Open
wants to merge 5 commits into
base: PHP-8.4
Choose a base branch
from

Conversation

nielsdos
Copy link
Member

@nielsdos nielsdos commented Feb 23, 2025

The FETCH_OBJ_R VM handler has an optimization that directly enters into a hook if it is a simpler getter hook. This is not compatible with the minimal JIT because the minimal JIT will try to continue executing the opcodes after the FETCH_OBJ_R.
To solve this, we check whether the opcode is still the expected one after the execution of the VM handler. If it is not, we know that we are going to execute a simple hook. In that case, exit to the VM.

Normally, I wouldn't care too much about this issue, because it is in minimal JIT. However, someone had this cause a crash to them by accident: #17767

@nielsdos nielsdos linked an issue Feb 23, 2025 that may be closed by this pull request
@nielsdos nielsdos marked this pull request as ready for review February 23, 2025 23:02
@nielsdos nielsdos requested a review from dstogov as a code owner February 23, 2025 23:02
@nielsdos nielsdos requested a review from iluuu1994 February 23, 2025 23:03
Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

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

Thank you Niels! It seems I have missed the original bug report. Dmitry should judge whether this is the correct fix.

goto jit_failure;
}

/* If a simple hook is called, exit to the VM. */
Copy link
Member

Choose a reason for hiding this comment

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

Technically this could be omitted for certain property fetches, i.e. the ones that are known not to be polymorphic & don't contain hooks. More concretely, private and final properties.

Comment on lines 2720 to 2738
ir_ref if_hook_enter = ir_IF(jit_CMP_IP(jit, IR_EQ, opline + 1));
ir_IF_FALSE(if_hook_enter);
if (GCC_GLOBAL_REGS) {
ir_TAILCALL(IR_VOID, ir_LOAD_A(jit_IP(jit)));
} else {
ir_RETURN(ir_CONST_I32(1)); /* ZEND_VM_ENTER */
}
ir_IF_TRUE(if_hook_enter);

Copy link
Member

Choose a reason for hiding this comment

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

Checking for a possible hook call after each property read - looks stupid...
Previously we had magic __get and __set, but we didn't optimize their calls through trampolines...
I understand, this is targeted for "-O1", but this also will affect any non-constant property access.
Please check how this affect the resulting code size on typical opcache.jit=1205 and 1254 (on Wordpress and/or Symfony Demo).

Shouldn't you also do the same for FETCH_OBJ_W and maybe others?

Tracing JIT should be also affected by this problem.

Copy link
Member

Choose a reason for hiding this comment

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

Previously we had magic __get and __set, but we didn't optimize their calls through trampolines...

Indeed, but this optimization makes get hooks as fast as getter functions. While the vast majority of public properties will not have hooks, and thus still improve performance, it was still important to us that hooks are not significantly slower than a getter function.

Shouldn't you also do the same for FETCH_OBJ_W and maybe others?

This optimization only exists for property read.

Checking for a possible hook call after each property read - looks stupid...

Maybe we can avoid this by creating a custom VM handler that is only used here, that omits the "simple get" optimization.

Copy link
Member

Choose a reason for hiding this comment

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

Please check how do this work with non-constant property reads in tracing JIT. (I mean $this->{$name} that leads to read property hook).
I guess, tracing JIT also may need to be fixed.

Copy link
Member Author

@nielsdos nielsdos Feb 24, 2025

Choose a reason for hiding this comment

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

I won't have time anymore to check this today. I kinda missed that this codegen path may still be taken a lot of times.
For call VM we can improve the codegen to check the return value of the VM handler directly instead of comparing IP, that should make more compact machine code.
A custom VM specialization may help, the question would be if the trade-off seems reasonable.
I'll think a bit more about possible other solutions.

Copy link
Member Author

@nielsdos nielsdos Feb 27, 2025

Choose a reason for hiding this comment

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

I rebased and I pushed a little optimization.

Unfortunately, I get an assertion failure on Symfony with jit=1205. This already happens without this patch. No influence yet on jit=1254 because I didn't yet change tracing JIT code (and didn't have time to do so yet). To reproduce, just run php bin/console cache:clear then php bin/console cache:warmup and then php -d opcache.jit=1205 public/index.php

For WordPress on jit=1205 I get an increase from 35164971 to 35166107 in JIT memory usage. An increase of 1136 bytes, or 1.000032305x increase. So quite a few checks added, although < 0.01% increase.

…al JIT

The FETCH_OBJ_R VM handler has an optimization that directly enters into
a hook if it is a simpler getter hook. This is not compatible with the
minimal JIT because the minimal JIT will try to continue executing the
opcodes after the FETCH_OBJ_R.
To solve this, we check whether the opcode is still the expected one
after the execution of the VM handler. If it is not, we know that we are
going to execute a simple hook. In that case, exit to the VM.
Comment on lines +2367 to +2371
if (opline->op2_type != IS_CONST
|| Z_TYPE_P(RT_CONSTANT(opline, opline->op2)) != IS_STRING
|| Z_STRVAL_P(RT_CONSTANT(opline, opline->op2))[0] == '\0') {
break;
}
Copy link
Member

Choose a reason for hiding this comment

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

Why did you move this down?
I suppose, you relay on ce selected above in non-optimized path.
This leads to false positive build failures, because of possibly uninitialized ce. This should be fixed.

I'm not completely sure if this code is going to be right for properties overridden with hooked ones in children.
Let we generate the code for parent class context where ce->num_hooked_props is zero. Then we execute it in context of child that overrides the accessed property...@iluuu1994 please check this

Copy link
Member Author

@nielsdos nielsdos Mar 3, 2025

Choose a reason for hiding this comment

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

Why did you move this down? I suppose, you relay on ce selected above in non-optimized path. This leads to false positive build failures, because of possibly uninitialized ce. This should be fixed.

Indees that is the reason for moving down. I can fix build failure.

I'm not completely sure if this code is going to be right for properties overridden with hooked ones in children. Let we generate the code for parent class context where ce->num_hooked_props is zero. Then we execute it in context of child that overrides the accessed property...@iluuu1994 please check this

I only skip the check for final classes without hooks, so this should not be a problem.

Copy link
Member

Choose a reason for hiding this comment

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

I only skip the check for final classes without hooks, so this should not be a problem.

Right. Then this shouldn't be a problem.

@dstogov
Copy link
Member

dstogov commented Mar 3, 2025

did you check, if the same problem affects the tracing JIT?

@nielsdos
Copy link
Member Author

nielsdos commented Mar 3, 2025

did you check, if the same problem affects the tracing JIT?

I had little time this weekend and didn't have time to check this yet. I will try to check this evening as I'm leaving for work soon. I also did not yet have time to investigate the assertion failure I mentioned in #17909 (comment)

@dstogov
Copy link
Member

dstogov commented Mar 3, 2025

I had little time this weekend and didn't have time to check this yet. I will try to check this evening as I'm leaving for work soon. I also did not yet have time to investigate the assertion failure I mentioned in #17909 (comment)

OK. Just let me know when this is ready for the final review. The code looks right and I don't have objections.

@nielsdos
Copy link
Member Author

nielsdos commented Mar 4, 2025

Please check how do this work with non-constant property reads in tracing JIT. (I mean $this->{$name} that leads to read property hook).

The bug does not affect this case. It is because for non-constant names cache_slot is NULL, so ZEND_SET_PROPERTY_HOOK_SIMPLE_GET(cache_slot) in zend_read_property will not do anything.
I confirmed this manually by printing out when the slow path / fast path is taken, and it always takes the slow path.
So this also means that we can skip the opline comparison for non-constant accesses.
I will modify the patch to take this into account.

did you check, if the same problem affects the tracing JIT?

When tracing JIT starts execution, it removes the ZEND_PROPERTY_HOOK_SIMPLE_GET_BIT flag that enables the fast path here:

if (IS_HOOKED_PROPERTY_OFFSET(prop_offset)) {
CACHE_PTR_EX(cache_slot + 1, (void*)((uintptr_t)CACHED_PTR_EX(cache_slot + 1) & ~ZEND_PROPERTY_HOOK_SIMPLE_GET_BIT)); \
}

Because the cache slot is not reset to NULLs, only the flag is cleared, the read_property handler will not set the flag again.

So to me it seems tracing JIT is not affected. I also tested this manually and it always takes the slow path.


I made new memory measurements with the latest patch, and with my IR fix applied so function JIT works on Symfony:

before PR without IS_CONST check PR with IS_CONST check
Symfony (demo) 19,928,312 19,928,456 19,928,312
WordPress (demo install) 32,027,112 32,027,912 32,027,112

So with the new IS_CONST check added, there is no extra overhead for the Symfony demo or WordPress homepage.

Note though that in function JIT it may still generate the check, for example for a constant property name that's either not a string or starts with a '\0' byte.

@nielsdos
Copy link
Member Author

nielsdos commented Mar 5, 2025

@dstogov this is ready for review please

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Segfault with hook "simple get" cache slot and minimal JIT
3 participants