Skip to content

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

Closed
wants to merge 2 commits into from

Conversation

iluuu1994
Copy link
Member

@iluuu1994 iluuu1994 commented Aug 31, 2023

  • Use last label offset, instead of doubling them
  • Avoid spec for opcodes with no CONST (or ANY) operands
  • Reverse loop SPEC items, or fix order
  • Duplicate if (spec & SPEC_RULE_DELAYED) { branch for OP_DATA and convert back to else if
  • Rename ZEND_HANDLE_DELAYED_ERROR to ERRORS
  • Fix JIT

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.
@iluuu1994 iluuu1994 force-pushed the delayed-notice branch 7 times, most recently from 3c0dd4a to 2791514 Compare September 12, 2023 15:12
@dstogov
Copy link
Member

dstogov commented Sep 13, 2023

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?
Otherwise, this doesn't make a lot of sense because the remaining messages may be exploited.
Taking into account warning emitted by callbacks of internal classes, this looks impossible.

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.

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;
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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);
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

Comment on lines +8128 to +8141
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]);
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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))
Copy link
Member

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.

Copy link
Member Author

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.

@iluuu1994
Copy link
Member Author

@dstogov

this is a quite complex solution

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.

Do you like to convert ALL non-fatal errors emitted in VM to be delayed?

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.

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

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.

@dstogov
Copy link
Member

dstogov commented Sep 13, 2023

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.

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.

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.

This kind of solution would fix most related problems.
We may provide an ability to record information about all the warnings.
We may also need an ability to convert warnings to exceptions.

@iluuu1994
Copy link
Member Author

I'm closing this PR, as the approach seems to complex.

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.

3 participants