Skip to content

Catchable "Call to a member function bar() on a non-object" #647

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

Conversation

thekid
Copy link
Contributor

@thekid thekid commented Apr 19, 2014

This pull request turns fatal errors generated from calls to methods on a non-object into E_RECOVERABLE_ERRORs.

Example

set_error_handler(function($code, $message) {
  var_dump($code, $message);
});

$x= null;
var_dump($x->method());
echo "Alive\n";

The above produces the following output:

int(4096)
string(50) "Call to a member function method() on a non-object"
NULL
Alive

Reasoning

If you want to run PHP as a webserver itself, fatal errors are problematic. For a long story on why you would want to do that in the first place, see http://marcjschmidt.de/blog/2014/02/08/php-high-performance.html

Consistency

This behavior is consistent with how type hints work. Framework authors can turn this into exceptions if they wish.

Inner workings

The essence of this PR is a patch to the Zend Engine, which raises an E_RECOVERABLE_ERROR and then skips over all opcodes until it finds the one ending the current call, and sets return value to NULL.

See this gist for the patch to Zend/zend_vm_def.h only, the full diff view contains tests as well as generated code.

Further reading

@smalyshev
Copy link
Contributor

This is a very nicely made pull request. However, I would say this would require an RFC or at least discussion on internals list to see if we have support for it.

@thekid
Copy link
Contributor Author

thekid commented Apr 22, 2014

@smalyshev - thanks for the feedback! I will come up with an RFC ASAP.

@thekid
Copy link
Contributor Author

thekid commented Apr 26, 2014

@thekid
Copy link
Contributor Author

thekid commented Apr 27, 2014

Set the RFC to 1️⃣.0️⃣ and started discussion on internals mailing list.

@thekid
Copy link
Contributor Author

thekid commented Jun 29, 2014

EX_T(opline->result.var).var.fcall_returned_reference = 0;
EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr;
ZEND_VM_NEXT_OPCODE();
return;
Copy link
Member

Choose a reason for hiding this comment

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

This return; will break the goto and switch VMs. You can just drop it, as ZEND_VM_NEXT_OPCODE() will already transfer control from here.

@nikic
Copy link
Member

nikic commented Jul 4, 2014

Another thing to consider is how this interacts with EXT_FCALL_BEGIN/END. As currently implemented, I think this will result in EXT_FCALL_BEGIN to be skipped, but EXT_FCALL_END being run, which doesn't sounds right. You can test this using sapi/cli/php -E, if I remember correctly.

From the tests side, It would be nice to test calls with non-cv operands, e.g. $arr[0]->{$arr[1]}(), as well as nested calls.

@thekid
Copy link
Contributor Author

thekid commented Jul 4, 2014

@nikic - thanks for the detailled feedback! I'll look into this ASAP and give you feedback one by one.

…thekid/php-src into catchable-fatals/methods-on-non-objects
* nesting level. Return NULL (except when return value unused) */
do {
ZEND_VM_INC_OPCODE();
opline++;
Copy link
Member

Choose a reason for hiding this comment

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

This won't work correctly with switch/goto VMs, because both lines will increment the opline variable. I would suggest to instead only increment opline here (and also on line 9347) and replace the CHECK_EXCEPTION() below with SAVE_OPLINE(). (Actually check_exception and save_opline are the same, but save_opline is probably more clear about what you want here)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using the following didn't work for me:

diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 42d3f60..628db06 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -2477,7 +2477,6 @@ ZEND_VM_HANDLER(112, ZEND_INIT_METHOD_CALL, TMP|VAR|UNUSED|CV, CONST|TMP
                /* No exception raised: Skip over arguments until fcall opcode with correct
                 * nesting level. Return NULL (except when return value unused) */
                do {
-                       ZEND_VM_INC_OPCODE();
                        opline++;
                } while (opline->opcode == ZEND_DO_FCALL_BY_NAME ? opline->op2.num > nesting :

@@ -2488,10 +2487,10 @@ ZEND_VM_HANDLER(112, ZEND_INIT_METHOD_CALL, TMP|VAR|UNUSED|CV, CONST|T
                }

                if ((opline + 1)->opcode == ZEND_EXT_FCALL_END) {
-                       ZEND_VM_INC_OPCODE();
+                       opline++;
                }

-               CHECK_EXCEPTION();
+               SAVE_OPLINE();
                ZEND_VM_NEXT_OPCODE();
        }

The SAVE_OPLINE() macro only sets EX(opline) whereas ZEND_VM_INC_OPCODE(); affects OPLINE which is authorative for the executor AFAIS.

Seems ZEND_VM_SET_OPCODE() might be what I'm looking for.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, sorry, I forgot that SAVE_OPLINE only sets EX(opline) for GOTO/SWITCH, but is a noop for CALL. ZEND_VM_SET_OPCODE sounds more like it.

@nikic
Copy link
Member

nikic commented Jul 9, 2014

@thekid Thanks for updating everything so quickly! I've added two last notes, though one is purely cosmetic, so feel free to ignore it ;)

@thekid
Copy link
Contributor Author

thekid commented Jul 31, 2014

This RFC has now been accepted! I've updated the RFC to reflect this.

Last step: Merge it. @nikic - can you do this? I don't have karma for Zend/...

thekid added 2 commits August 16, 2014 17:38
  with `zend_do_convert_call_user_func()` in Zend/zend_compile.c
@thekid
Copy link
Contributor Author

thekid commented Aug 16, 2014

I've brought this pull request up-to-date so it's once again mergeable with master. I had to resort back to counting ZEND_INIT_*CALL* and ZEND_DO_FCALL opcodes for handling nesting correctly again since the nesting level no longer is in op2.num. The code now uses the same list of opcodes as in zend_do_convert_call_user_func() in Zend/zend_compile.c...

$ make test TESTS=Zend/tests/*methods-*phpt
# [...]
=====================================================================
Number of tests :   16                16
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    0 (  0.0%) (  0.0%)
Expected fail   :    0 (  0.0%) (  0.0%)
Tests passed    :   16 (100.0%) (100.0%)
---------------------------------------------------------------------
Time taken      :    1 seconds
=====================================================================

@thekid
Copy link
Contributor Author

thekid commented Aug 29, 2014

From https://www.mail-archive.com/[email protected]/msg68933.html


On Aug 16, 2014, at 9:09, Timm Friebe [email protected] wrote:
two weeks ago, the RFC Catchable "Call to a member function bar() on a non-object" was accepted by a vote. I don't have commit access to
php-src/Zend,

If it's not sorted by someone else, I land this when I get back home later
today.


@sgolemon any news?

@datibbaw
Copy link
Contributor

Could you squash the branch into a single commit and edit the NEWS and UPGRADING files?

@thekid
Copy link
Contributor Author

thekid commented Sep 16, 2014

I'm off to a conference for the next couple of days and will look into getting this done ASAP after I get back; the earliest next Sunday

@thekid
Copy link
Contributor Author

thekid commented Sep 27, 2014

Could you squash the branch into a single commit

No matter how hard I try git rebase always fails somewhere in the middle. I'll create a new PR with a single commit.

thekid added a commit to thekid/php-src that referenced this pull request Sep 27, 2014
Changed ZEND_INIT_METHOD_CALL to raise an E_RECOVERABLE_ERROR and handle
error handler outcome, returning NULL if error handler doesn't terminate
the script.

Adjusted outcome in MySQL / PDO_MySQL and ext/date
Added various examples and tests
Added tests for indirect calls - see http://news.php.net/php.internals/73823
Added tests verifying method calls on non-objects work inside eval()
Originally developed inside php#647
thekid added a commit to thekid/php-src that referenced this pull request Sep 27, 2014
Changed ZEND_INIT_METHOD_CALL to raise an E_RECOVERABLE_ERROR and handle
error handler outcome, returning NULL if error handler doesn't terminate
the script.

Adjusted outcome in MySQL / PDO_MySQL and ext/date
Added various examples and tests
Added tests for indirect calls - see http://news.php.net/php.internals/73823
Added tests verifying method calls on non-objects work inside eval()
Originally developed inside php#647
thekid added a commit to thekid/php-src that referenced this pull request Sep 27, 2014
Changed ZEND_INIT_METHOD_CALL to raise an E_RECOVERABLE_ERROR and handle
error handler outcome, returning NULL if error handler doesn't terminate
the script.

Adjusted outcome in MySQL / PDO_MySQL and ext/date
Added various examples and tests
Added tests for indirect calls - see http://news.php.net/php.internals/73823
Added tests verifying method calls on non-objects work inside eval()
Originally developed inside php#647
@hikari-no-yume
Copy link
Contributor

Is this going to be merged?

@thekid
Copy link
Contributor Author

thekid commented Oct 5, 2014

@TazeTSchnitzel I' m trying, but everytime it is a mergeable state there's noone around to do so. I myself don't have commit access to the upstream project...

@thekid
Copy link
Contributor Author

thekid commented Oct 5, 2014

Closing this, #847 is the new place

@thekid thekid closed this Oct 5, 2014
php-pulls pushed a commit that referenced this pull request Oct 5, 2014
* pr/647: (33 commits)
  zend_uint -> uint32_t
  Fix nesting for *non*-compile-time-resolveable functions See thekid@a1a4ba9#commitcomment-7414223
  Add tests for calls to nested, *non*-compile-time-resolveable functions See thekid@a1a4ba9#commitcomment-7414362
  Make list of opcodes used for nesting calculation consistent   with `zend_do_convert_call_user_func()` in Zend/zend_compile.c
  Rewrite code to use ZEND_VM_JMP() instead of repeated ZEND_VM_INC_OPCODE() calls
  QA: Simplify code to find matching ZEND_DO_FCALL_BY_NAME CG(context).nested_calls is stored inside the initializer's result.num and inside the finalizer's op2.num, by comparing these we don't need to count manually, and are thus safer from future expansion with specialized opcodes e.g.
  Fix expected fatal error, now is catchable fatal
  Adjust expected fatal error message Now also includes "on [TYPE]" after merge from master
  Check for memory leaks when not using return value
  Adjust expected fatal error message Now also includes "on [TYPE]" after merge from master
  Add tests with arrays as parameters
  Handle ZEND_NEW nesting
  Also verify nesting with dynamically called static methods
  Handle ZEND_INIT_NS_FCALL_BY_NAME nesting
  QA: Refactor: Split tests a bit to make them more comprehendable
  Support nested static calls
  Handle ZEND_EXT_FCALL_END, skipping if necessary Verified with running tests with new "-e" run-tests arg: $ make test TESTS=Zend/tests/*-on-non-objects-*phpt TEST_PHP_ARGS=-e # Tests passed    :   11 (100.0%)
  Add support for PHP's 'extended information for debugger/profiler' mode
  Verify non-CV-operands also work See discussion #647 (comment)
  Only allocate NULL return value if it's actually used
  ...

Conflicts:
	ext/date/tests/bug67118.phpt
@datibbaw
Copy link
Contributor

datibbaw commented Oct 5, 2014

Oh shit, the numbers looked so alike that I had merged in this PR instead of the newer one.

They're the same changes minus the added test cases, so instead of reverting what I have already done I shall just add the additional test cases.

My bad =(

@thekid
Copy link
Contributor Author

thekid commented Oct 5, 2014

I [...] merged in this PR

\o/

@thekid thekid deleted the catchable-fatals/methods-on-non-objects branch October 5, 2014 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants