-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Delay notice emission until end of opcode #12090
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
Conversation
0f57409
to
d2a8ec9
Compare
This is a prototype for fixing a long-standing source of interrupt vulnerabilities: A notice is emitted during execution of an opcode, resulting in an error handling being run. The error handler modifies some data structure the opcode is working on, resulting in UAF or other memory corruption. The idea here is to instead collect notices and only process them after the opcode. This is implemented similarly to exception handling, by switching to a ZEND_HANDLE_DELAYED_ERROR opcode, which will then switch back to the normal opcode stream. Unfortunately, what this prototype implements is not sufficient. Opcodes that acquire direct (INDIRECT) references to zvals require that no interrupts occur between the producing and the consuming opcode. Chains of W/RW opcodes should be executed without interrupt. Currently, the notice is only delayed until after the first opcode, which still results in an illegal interrupt (bug78598.phpt shows a UAF with this change). I'm not sure how to best handle that issue.
3c0dd4a
to
2791514
Compare
2791514
to
0b53735
Compare
The first impression - this is a quite complex solution that is not going to be universal anyway. For example the following code is still not fixed <?php
$a[$y][]='';
set_error_handler(function() {
$GLOBALS['a']='';
});
[$c]="";
$a[$c].=2; Do you like to convert ALL non-fatal errors emitted in VM to be delayed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is improper direction.
It may some sense only if user error handlers are set. Otherwise it significantly complicates the execution flow.
It's not going to resolve all the related problem.
In my opinion, the best way to fix this is limiting the ability of changing sensitive data in user error handlers.
e.g. disabling modification of $GLOBALS
, "locking" sensitive data in zend_error()
before calling the user error handler, etc
zend_hash_next_index_insert_ptr(&EG(delayed_errors), info); | ||
|
||
if (EG(current_execute_data)->opline != EG(delayed_error_op)) { | ||
EG(opline_before_exception) = EG(current_execute_data)->opline; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be dangerous to reuse EG(opline_before_exception)
for delayed errors. I'm not sure how delayed errors and exceptions may interact. What if we had an exception before the delayed error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably don't need to change the EG(opline_before_exception) = EG(current_execute_data)->opline;
if EG(current_execute_data)->opline == EG(exception_op)
, because all pending errors are handled before exceptions. But there might still be other issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here they are not handled but recorded, I can't be sure if some instruction can't throw exception and then emit delayed warning.
At least add ZEND_ASSERT(!EG(exception))
to catch the possible problem.
|
||
zend_error_info *info; | ||
ZEND_HASH_FOREACH_PTR(&ht, info) { | ||
zend_error_zstr_at(info->type, info->filename, info->lineno, info->message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May the first error throw an exception? Then the second. Which exception is going to be caught?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They could be chained, similar to here: https://3v4l.org/um5TH I'm not sure how this currently behaves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm too :)
Probably the exception thrown from the last error handler is going to be caught first.
Or EG(exception)
set by the first handler will prevent the second handler form execution.
EG(delayed_error_op)[0] = *next_op; | ||
EG(delayed_handlers) = true; | ||
ZEND_VM_SET_OPCODE_HANDLER(&EG(delayed_error_op)[0]); | ||
EG(delayed_handlers) = false; | ||
// FIXME: Is this guaranteed to be there? | ||
if (next_op[1].opcode == ZEND_OP_DATA) { | ||
EG(delayed_error_op)[1] = next_op[1]; | ||
} else { | ||
/* Reset to ZEND_HANDLE_DELAYED_ERROR */ | ||
EG(delayed_error_op)[1] = EG(delayed_error_op)[2]; | ||
} | ||
EG(opline_before_exception) = next_op; | ||
|
||
ZEND_VM_SET_NEXT_OPCODE(&EG(delayed_error_op)[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this trick. I can't imagine all the problems in case of executor re-enterancy.
You probably, may not to change EX(opline)
if emit delayed errors from ZEND_FETCH_*
instruction handlers, but then you'll have to check for existence of delayed errors in the following instruction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case of exception during the execution of instruction stored in EG(delayed_error_op)[0]
, EG(opline_before_exception) is going to get wrong value, then opline-op_array->opcodes calculations are going to be wring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm that's true. Exceptions during execution of a delayed opcode would need to check for opline != &EG(delayed_error_op)[0]
. I suppose that's fine for exceptions, as they have some inherent overhead. But yes it complicates the matter further.
@@ -1005,7 +1005,7 @@ ZEND_VM_COLD_HELPER(zend_undefined_function_helper, ANY, ANY) | |||
HANDLE_EXCEPTION(); | |||
} | |||
|
|||
ZEND_VM_HANDLER(28, ZEND_ASSIGN_OBJ_OP, VAR|UNUSED|THIS|CV, CONST|TMPVAR|CV, OP) | |||
ZEND_VM_HANDLER(28, ZEND_ASSIGN_OBJ_OP, VAR|UNUSED|THIS|CV, CONST|TMPVAR|CV, OP, SPEC(DELAYED)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SPEC(DELAYED)
is going to double the number of related handlers. Right?
I see ~40MB zend_execute.o size increase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It shouldn't. SPEC(DELAYED)
should add a single, otherwise unspecialized handler. It's possible that this behavior is still buggy, but that's the idea.
I agree with you that this is quite complex. Unfortunately, I don't have a simpler idea at this time. Collectively, this mechanism might still be simpler than coming up with a separate solution for each delayed warning.
The intention was to change all warnings that are executed in the middle of op handlers or other operations, but I agree that this is both time consuming for us to identify which warnings are affected, and breaking for users.
I'm not sure if this is exclusive to globals. E.g. you can still modify values in static properties, or bind values by-ref to the closure. Another option might be to phase out the problematic errors, and provide some new mechanism for errors. E.g. we may log errors to some file descriptor, or even just add them to a global array. At the end of the request, the user can inspect the logged errors, log them, send them to their logging service, or whatnot. With an additional config to promote these errors to exceptions so that execution halts immediately, we could cover most use cases. |
Right. It was a direction for thoughts. I tried to implement the idea using "locking", but this solution is also not general. At least I didn't found a general solution.
This kind of solution would fix most related problems. |
I'm closing this PR, as the approach seems to complex. |
CONST
(orANY
) operandsSPEC
items, or fix orderif (spec & SPEC_RULE_DELAYED) {
branch forOP_DATA
and convert back toelse if
ZEND_HANDLE_DELAYED_ERROR
toERRORS