Skip to content

[Observer] Provide unused retvals to observers #6422

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 9 commits into from
Closed
28 changes: 21 additions & 7 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -4248,7 +4248,11 @@ ZEND_VM_INLINE_HANDLER(62, ZEND_RETURN, CONST|TMP|VAR|CV, ANY, SPEC(OBSERVER))
if (return_value) {
ZVAL_NULL(return_value);
}
ZEND_OBSERVER_SAVE_OPLINE();
ZEND_OBSERVER_FCALL_END(execute_data, retval_ptr);
Copy link
Member

Choose a reason for hiding this comment

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

Might it make sense to just move this to the very start of the function, rather than duplication it in the branches?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would make sense and would be simpler. But the main issue is that any errors that are raised in the return handler would trigger after the end handler had fired.

For example, the output from observer_retval_05.phpt would change to this:

  <foo>
+  </foo:NULL>

Warning: Undefined variable $i_do_not_exist in %s on line %d
-  </foo:NULL>

For the PHP tracer extension I'm working on, that would mean that we couldn't attach the E_WARNING to the open foo span that raised the error.

} else if (!return_value) {
ZEND_OBSERVER_SAVE_OPLINE();
ZEND_OBSERVER_FCALL_END(execute_data, retval_ptr);
if (OP1_TYPE & (IS_VAR|IS_TMP_VAR)) {
if (Z_REFCOUNTED_P(retval_ptr) && !Z_DELREF_P(retval_ptr)) {
SAVE_OPLINE();
Expand All @@ -4263,6 +4267,8 @@ ZEND_VM_INLINE_HANDLER(62, ZEND_RETURN, CONST|TMP|VAR|CV, ANY, SPEC(OBSERVER))
Z_ADDREF_P(return_value);
}
}
ZEND_OBSERVER_SAVE_OPLINE();
ZEND_OBSERVER_FCALL_END(execute_data, retval_ptr);
} else if (OP1_TYPE == IS_CV) {
do {
if (Z_OPT_REFCOUNTED_P(retval_ptr)) {
Expand All @@ -4274,6 +4280,8 @@ ZEND_VM_INLINE_HANDLER(62, ZEND_RETURN, CONST|TMP|VAR|CV, ANY, SPEC(OBSERVER))
gc_possible_root(ref);
}
ZVAL_NULL(retval_ptr);
ZEND_OBSERVER_SAVE_OPLINE();
ZEND_OBSERVER_FCALL_END(execute_data, return_value);
break;
} else {
Z_ADDREF_P(retval_ptr);
Expand All @@ -4286,6 +4294,8 @@ ZEND_VM_INLINE_HANDLER(62, ZEND_RETURN, CONST|TMP|VAR|CV, ANY, SPEC(OBSERVER))
}
}
ZVAL_COPY_VALUE(return_value, retval_ptr);
ZEND_OBSERVER_SAVE_OPLINE();
ZEND_OBSERVER_FCALL_END(execute_data, retval_ptr);
} while (0);
} else /* if (OP1_TYPE == IS_VAR) */ {
if (UNEXPECTED(Z_ISREF_P(retval_ptr))) {
Expand All @@ -4301,10 +4311,10 @@ ZEND_VM_INLINE_HANDLER(62, ZEND_RETURN, CONST|TMP|VAR|CV, ANY, SPEC(OBSERVER))
} else {
ZVAL_COPY_VALUE(return_value, retval_ptr);
}
ZEND_OBSERVER_SAVE_OPLINE();
ZEND_OBSERVER_FCALL_END(execute_data, retval_ptr);
Copy link
Member

Choose a reason for hiding this comment

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

I think there's still a potential use-after-free here. If retval_ptr is a reference with refcount one, then it will be freed above, but (the new) retval_ptr will still point into it.

Generally I think that you should just do

ZEND_OBSERVER_SAVE_OPLINE();
ZEND_OBSERVER_FCALL_END(execute_data, return_value);

one at the end up the whole if (return_value) branch. There's no need to handle these differently in each case.

}
}
ZEND_OBSERVER_SAVE_OPLINE();
ZEND_OBSERVER_FCALL_END(execute_data, return_value);
ZEND_VM_DISPATCH_TO_HELPER(zend_leave_helper);
}

Expand Down Expand Up @@ -4365,7 +4375,7 @@ ZEND_VM_COLD_CONST_HANDLER(111, ZEND_RETURN_BY_REF, CONST|TMP|VAR|CV, ANY, SRC,
FREE_OP1_VAR_PTR();
} while (0);

ZEND_OBSERVER_FCALL_END(execute_data, EX(return_value));
ZEND_OBSERVER_FCALL_END(execute_data, retval_ptr);
Copy link
Member

Choose a reason for hiding this comment

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

This looks like a use after free for all the !EX(return_value) cases above, that already free the value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very good catch. I didn't realize that freeing OP1 was freeing the retval from the opline. I fixed this in 0fa78a5.

ZEND_VM_DISPATCH_TO_HELPER(zend_leave_helper);
}

Expand Down Expand Up @@ -7709,15 +7719,19 @@ ZEND_VM_HELPER(zend_dispatch_try_catch_finally_helper, ANY, ANY, uint32_t try_ca
}

/* Uncaught exception */
if (zend_observer_fcall_op_array_extension != -1) {
zend_observer_fcall_end(execute_data, EX(return_value));
}
cleanup_live_vars(execute_data, op_num, 0);
if (UNEXPECTED((EX_CALL_INFO() & ZEND_CALL_GENERATOR) != 0)) {
zend_generator *generator = zend_get_running_generator(EXECUTE_DATA_C);
if (zend_observer_fcall_op_array_extension != -1) {
zend_observer_fcall_end(execute_data, &generator->retval);
}
cleanup_live_vars(execute_data, op_num, 0);
zend_generator_close(generator, 1);
ZEND_VM_RETURN();
} else {
if (zend_observer_fcall_op_array_extension != -1) {
zend_observer_fcall_end(execute_data, EX(return_value));
Copy link
Member

Choose a reason for hiding this comment

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

This looks pretty fishy to me, especially in conjunction with the EX(return_value) initialization below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're absolutely right. Retval will always be null with uncaught exceptions. I don't know what I was thinking on this one. 🤦 I reverted this commit in 9332095 and added a proper fix in 95f943a.

}
cleanup_live_vars(execute_data, op_num, 0);
/* We didn't execute RETURN, and have to initialize return_value */
if (EX(return_value)) {
ZVAL_UNDEF(EX(return_value));
Expand Down
Loading