Skip to content
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

Catch exceptions without capturing them to variables #5345

Closed
wants to merge 2 commits into from

Conversation

MaxSem
Copy link
Contributor

@MaxSem MaxSem commented Apr 3, 2020

See the RFC currently under discussion: https://wiki.php.net/rfc/non-capturing_catches

@nikic
Copy link
Member

nikic commented Apr 6, 2020

You will also need to adjust the ZEND_CATCH implementation with a RETURN_VALUE_USED check. Right now, you're going to clobber some random piece of stack memory with the exception.

@MaxSem
Copy link
Contributor Author

MaxSem commented Apr 6, 2020

Yeah, I noticed it was leaking, just haven't figured out where it should go yet.

@nikic
Copy link
Member

nikic commented Apr 6, 2020

Adding a check around here should do it:

ex = EX_VAR(opline->result.var);
If RETURN_VALUE_USED do what the code does now, otherwise OBJ_RELEASE(EG(exception)).

@TysonAndre
Copy link
Contributor

TysonAndre commented Apr 11, 2020

👍 for this, I'd rather avoid the unnecessary variable for catch (Exception $unused) or catch (Exception $_)

It'd be useful to add a test of what happens when the destructor gets called, and to fix issues with destructors (keep the test in case this is improperly handled by opcache or the JIT in the future)

I'd imagine the handling of __destruct should be like catch(Exception $e) { unset($e); ... } but be more efficient.

I think it does the right thing right now, because the catch opcode already needs to account for what happens if $e was already an object that could throw in __destruct().

<?php

class ThrowsOnDestruct extends Exception {
    public function __destruct() {
        echo "Throwing\n";
        throw new RuntimeException(__METHOD__);
    }
}
try {
    throw new ThrowsOnDestruct();
} catch (Exception) {
    echo "Unreachable catch\n";
}
echo "Unreachable fallthrough\n";

When I try to run this example (on a branch including this PR and the throws exception PR), I see an unexpected Fatal error: Attempt to destruct pending exception

» php test.php 

Warning: Uncaught ThrowsOnDestruct in /path/to/test.php:9
Stack trace:
#0 {main}
  thrown in /path/to/test.php on line 9

Fatal error: Attempt to destruct pending exception in Unknown on line 0

@Majkl578
Copy link
Contributor

Majkl578 commented Apr 13, 2020

I actually started a patch for this 4 years ago but never forced myself to write an RFC. 😄 So 💯+🥇!

@nikic
Copy link
Member

nikic commented May 11, 2020

Implementation looks fine to me. Don't think any opcache changes needed either, as we bail out on try/catch for all the tricky parts.

@MaxSem
Copy link
Contributor Author

MaxSem commented May 13, 2020

@TysonAndre, yep - committed a test for it, investigating how to fix it.

}
try {
throw new ThrowsOnDestruct();
} catch (Exception $ex) {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't $ex be removed 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.

I must've reinstated it during my experiments. Fixed.

Copy link
Contributor

@TysonAndre TysonAndre left a comment

Choose a reason for hiding this comment

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

A suggested fix for the test exiting without properly reporting the uncaught exception - @nikic might know more about the edge case I'm worried about.

I'm also not sure why the GC_ADDREF leads to a memory leak when the exception has no variable, but taking it out seems to work.

The test for catch_novar_2.phpt seems to be crashing. I think that EG(exception) should be set to null before calling the destructor on the caught exception.

I'm not 100% sure of what to do about public function __destruct() { throw $this; }. The below case is different in that the RuntimeException is the uncaught exception which still has a reference count, not the ThrowsOnDestruct. Maybe the object still exists but __destruct won't be called again?

php > class ThrowsOnDestruct extends Exception { public function __destruct() { echo "throwing self\n"; throw $this; }}
php > $e = new ThrowsOnDestruct();
php > try { throw new RuntimeException(); } catch (Exception $e) {}
throwing self
Warning: Uncaught RuntimeException in php shell code:1
Stack trace:
#0 {main}

Next ThrowsOnDestruct in php shell code:1
Stack trace:
#0 {main}
  thrown in php shell code on line 1
diff --git a/Zend/tests/try/catch_novar_2.phpt b/Zend/tests/try/catch_novar_2.phpt
index 60a7ebb283..846fb86484 100644
--- a/Zend/tests/try/catch_novar_2.phpt
+++ b/Zend/tests/try/catch_novar_2.phpt
@@ -15,5 +15,11 @@ try {
 }
 echo "Unreachable fallthrough\n";
 
---EXPECT--
+--EXPECTF--
 Throwing
+
+Fatal error: Uncaught RuntimeException: ThrowsOnDestruct::__destruct in %scatch_novar_2.php:5
+Stack trace:
+#0 %scatch_novar_2.php(10): ThrowsOnDestruct->__destruct()
+#1 {main}
+  thrown in %scatch_novar_2.php on line 5
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index ab44b9ca7c..20058b5337 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -4493,15 +4493,22 @@ ZEND_VM_HANDLER(107, ZEND_CATCH, CONST, JMP_ADDR, LAST_CATCH|CACHE_SLOT)
 		}
 		zval_ptr_dtor(ex);
 		ZVAL_OBJ(ex, EG(exception));
-	} else {
-		OBJ_RELEASE(EG(exception));
-	}
-	if (UNEXPECTED(EG(exception) != exception)) {
-		GC_ADDREF(EG(exception));
-		HANDLE_EXCEPTION();
+		if (UNEXPECTED(EG(exception) != exception)) {
+			GC_ADDREF(EG(exception));
+			HANDLE_EXCEPTION();
+		} else {
+			EG(exception) = NULL;
+			ZEND_VM_NEXT_OPCODE();
+		}
 	} else {
 		EG(exception) = NULL;
-		ZEND_VM_NEXT_OPCODE();
+		OBJ_RELEASE(exception);
+		// TODO: What about function __destruct() { throw $this; }
+		if (UNEXPECTED(EG(exception) != NULL)) {
+			HANDLE_EXCEPTION();
+		} else {
+			ZEND_VM_NEXT_OPCODE();
+		}
 	}
 }
 
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 7ecc72a280..b8f511075a 100644
--- a/Zend/zend_vm_execute.h
+++ b/Zend/zend_vm_execute.h
@@ -3709,15 +3709,21 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CATCH_SPEC_CONST_HANDLER(ZEND_
 		}
 		zval_ptr_dtor(ex);
 		ZVAL_OBJ(ex, EG(exception));
-	} else {
-		OBJ_RELEASE(EG(exception));
-	}
-	if (UNEXPECTED(EG(exception) != exception)) {
-		GC_ADDREF(EG(exception));
-		HANDLE_EXCEPTION();
+		if (UNEXPECTED(EG(exception) != exception)) {
+			GC_ADDREF(EG(exception));
+			HANDLE_EXCEPTION();
+		} else {
+			EG(exception) = NULL;
+			ZEND_VM_NEXT_OPCODE();
+		}
 	} else {
 		EG(exception) = NULL;
-		ZEND_VM_NEXT_OPCODE();
+		OBJ_RELEASE(exception);
+		if (UNEXPECTED(EG(exception) != NULL)) {
+			HANDLE_EXCEPTION();
+		} else {
+			ZEND_VM_NEXT_OPCODE();
+		}
 	}
 }

echo "Unreachable fallthrough\n";

--EXPECT--
Throwing
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks as if the php process exited - normally, if it rethrew, php's output would include a message about an uncaught exception and a stack trace.

@nikic
Copy link
Member

nikic commented May 26, 2020

I've pushed a fix for the throw in dtor issue, basically following @TysonAndre's suggestion above.

@TysonAndre 81c824f should make it more obvious why the GC_ADDREF doesn't make sense in this case.

I'm not 100% sure of what to do about public function __destruct() { throw $this; }. The below case is different in that the RuntimeException is the uncaught exception which still has a reference count, not the ThrowsOnDestruct. Maybe the object still exists but __destruct won't be called again?

That's correct.

zval *ex = EX_VAR(opline->result.var);
if (UNEXPECTED(Z_ISREF_P(ex))) {
ex = Z_REFVAL_P(ex);
}
Copy link
Member

Choose a reason for hiding this comment

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

Hm... not related to your patch, but I just realized that this can bypass typed references.

Copy link
Member

Choose a reason for hiding this comment

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

This turned into something of a rabbit hole, but is now addressed with 314ab47 and 4a08ca1.

@php-pulls php-pulls closed this in 23ee4d4 May 26, 2020
@MaxSem MaxSem deleted the catch branch May 26, 2020 13:39
@carusogabriel carusogabriel added this to the PHP 8.0 milestone May 29, 2020
jrfnl added a commit to PHPCompatibility/PHPCompatibility that referenced this pull request Jun 29, 2020
…ch` sniff

> Allow catching exceptions without capturing them to variables

Refs:
* https://wiki.php.net/rfc/non-capturing_catches
* php/php-src#5345
* php/php-src@23ee4d4

Includes unit tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants