Skip to content

Make exit() unwind properly #5243

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 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Zend/tests/exit_exception_handler.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
Exception handler should not be invoked for exit()
--FILE--
<?php

set_exception_handler(function($e) {
var_dump($e);
});

exit("Exit\n");

?>
--EXPECT--
Exit
18 changes: 18 additions & 0 deletions Zend/tests/exit_finally_1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
exit() and finally (1)
--FILE--
<?php

try {
exit("Exit\n");
} catch (Throwable $e) {
echo "Not caught\n";
} finally {
echo "Finally\n";
}
echo "Not executed\n";

?>
--EXPECT--
Finally
Exit
21 changes: 21 additions & 0 deletions Zend/tests/exit_finally_2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
exit() and finally (2)
--FILE--
<?php

try {
try {
exit("Exit\n");
} catch (Throwable $e) {
echo "Not caught\n";
} finally {
throw new Exception("Finally exception");
}
echo "Not executed\n";
} catch (Exception $e) {
echo "Caught {$e->getMessage()}\n";
}

?>
--EXPECT--
Caught Finally exception
17 changes: 17 additions & 0 deletions Zend/tests/exit_finally_3.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
exit() and finally (3)
--FILE--
<?php

function test() {
try {
exit("Exit\n");
} finally {
return 42;
}
}
var_dump(test());

?>
--EXPECT--
int(42)
1 change: 1 addition & 0 deletions Zend/tests/generators/bug75396.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ $gen->send("x");
?>
--EXPECT--
Try
Finally
Exit
8 changes: 6 additions & 2 deletions Zend/zend.c
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,11 @@ ZEND_API ZEND_COLD void zend_user_exception_handler(void) /* {{{ */
zval orig_user_exception_handler;
zval params[1], retval2;
zend_object *old_exception;

if (zend_is_unwind_exit(EG(exception))) {
return;
}

old_exception = EG(exception);
EG(exception) = NULL;
ZVAL_OBJ(&params[0], old_exception);
Expand Down Expand Up @@ -1666,8 +1671,7 @@ ZEND_API int zend_execute_scripts(int type, zval *retval, int file_count, ...) /
zend_user_exception_handler();
}
if (EG(exception)) {
zend_exception_error(EG(exception), E_ERROR);
ret = FAILURE;
ret = zend_exception_error(EG(exception), E_ERROR);
}
}
destroy_op_array(op_array);
Expand Down
77 changes: 76 additions & 1 deletion Zend/zend_exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ ZEND_API zend_class_entry *zend_ce_value_error;
ZEND_API zend_class_entry *zend_ce_arithmetic_error;
ZEND_API zend_class_entry *zend_ce_division_by_zero_error;

/* Internal pseudo-exception that is not exposed to userland. */
static zend_class_entry zend_ce_unwind_exit;
static zend_object_handlers zend_unwind_exit_handlers;

ZEND_API void (*zend_throw_exception_hook)(zval *ex);

static zend_object_handlers default_exception_handlers;
Expand Down Expand Up @@ -81,6 +85,12 @@ void zend_exception_set_previous(zend_object *exception, zend_object *add_previo
if (exception == add_previous || !add_previous || !exception) {
return;
}

if (zend_is_unwind_exit(add_previous)) {
OBJ_RELEASE(add_previous);
return;
}

ZVAL_OBJ(&pv, add_previous);
if (!instanceof_function(Z_OBJCE(pv), zend_ce_throwable)) {
zend_error_noreturn(E_CORE_ERROR, "Previous exception must implement Throwable");
Expand Down Expand Up @@ -728,6 +738,42 @@ ZEND_METHOD(Exception, __toString)
}
/* }}} */

typedef struct {
zend_object std;
zend_string *message;
int exit_status;
} zend_unwind_exit_object;

static zend_unwind_exit_object *zend_create_unwind_exit(zend_string *message, int exit_status) {
zend_unwind_exit_object *intern = emalloc(sizeof(zend_unwind_exit_object));
zend_object_std_init(&intern->std, &zend_ce_unwind_exit);
intern->std.handlers = &zend_unwind_exit_handlers;
intern->message = message;
intern->exit_status = exit_status;
return intern;
}

static void zend_destroy_unwind_exit(zend_object *obj) {
zend_unwind_exit_object *intern = (zend_unwind_exit_object *) obj;
if (intern->message) {
zend_string_release(intern->message);
}
zend_object_std_dtor(&intern->std);
}

/** {{{ Throwable method definition */
static const zend_function_entry zend_funcs_throwable[] = {
ZEND_ABSTRACT_ME(throwable, getMessage, arginfo_class_Throwable_getMessage)
ZEND_ABSTRACT_ME(throwable, getCode, arginfo_class_Throwable_getCode)
ZEND_ABSTRACT_ME(throwable, getFile, arginfo_class_Throwable_getFile)
ZEND_ABSTRACT_ME(throwable, getLine, arginfo_class_Throwable_getLine)
ZEND_ABSTRACT_ME(throwable, getTrace, arginfo_class_Throwable_getTrace)
ZEND_ABSTRACT_ME(throwable, getPrevious, arginfo_class_Throwable_getPrevious)
ZEND_ABSTRACT_ME(throwable, getTraceAsString, arginfo_class_Throwable_getTraceAsString)
ZEND_FE_END
};
/* }}} */

static void declare_exception_properties(zend_class_entry *ce)
{
zval val;
Expand Down Expand Up @@ -803,6 +849,10 @@ void zend_register_default_exception(void) /* {{{ */
INIT_CLASS_ENTRY(ce, "DivisionByZeroError", class_DivisionByZeroError_methods);
zend_ce_division_by_zero_error = zend_register_internal_class_ex(&ce, zend_ce_arithmetic_error);
zend_ce_division_by_zero_error->create_object = zend_default_exception_new;

INIT_CLASS_ENTRY(zend_ce_unwind_exit, "UnwindExit", NULL);
memcpy(&zend_unwind_exit_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
zend_unwind_exit_handlers.free_obj = zend_destroy_unwind_exit;
}
/* }}} */

Expand Down Expand Up @@ -898,10 +948,11 @@ static void zend_error_va(int type, const char *file, uint32_t lineno, const cha
/* }}} */

/* This function doesn't return if it uses E_ERROR */
ZEND_API ZEND_COLD void zend_exception_error(zend_object *ex, int severity) /* {{{ */
ZEND_API ZEND_COLD int zend_exception_error(zend_object *ex, int severity) /* {{{ */
{
zval exception, rv;
zend_class_entry *ce_exception;
int result = FAILURE;

ZVAL_OBJ(&exception, ex);
ce_exception = ex->ce;
Expand Down Expand Up @@ -961,11 +1012,20 @@ ZEND_API ZEND_COLD void zend_exception_error(zend_object *ex, int severity) /* {

zend_string_release_ex(str, 0);
zend_string_release_ex(file, 0);
} else if (ce_exception == &zend_ce_unwind_exit) {
zend_unwind_exit_object *intern = (zend_unwind_exit_object *) ex;
if (intern->message) {
zend_write(ZSTR_VAL(intern->message), ZSTR_LEN(intern->message));
} else {
EG(exit_status) = intern->exit_status;
}
result = SUCCESS;
} else {
zend_error(severity, "Uncaught exception '%s'", ZSTR_VAL(ce_exception->name));
}

OBJ_RELEASE(ex);
return result;
}
/* }}} */

Expand All @@ -987,3 +1047,18 @@ ZEND_API ZEND_COLD void zend_throw_exception_object(zval *exception) /* {{{ */
zend_throw_exception_internal(exception);
}
/* }}} */

ZEND_API ZEND_COLD void zend_throw_unwind_exit(zend_string *message, int exit_status)
{
zend_unwind_exit_object *intern = zend_create_unwind_exit(message, exit_status);

ZEND_ASSERT(!EG(exception));
EG(exception) = &intern->std;
EG(opline_before_exception) = EG(current_execute_data)->opline;
EG(current_execute_data)->opline = EG(exception_op);
}

ZEND_API zend_bool zend_is_unwind_exit(zend_object *ex)
{
return ex->ce == &zend_ce_unwind_exit;
}
5 changes: 4 additions & 1 deletion Zend/zend_exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ ZEND_API zend_object *zend_throw_error_exception(zend_class_entry *exception_ce,
extern ZEND_API void (*zend_throw_exception_hook)(zval *ex);

/* show an exception using zend_error(severity,...), severity should be E_ERROR */
ZEND_API ZEND_COLD void zend_exception_error(zend_object *exception, int severity);
ZEND_API ZEND_COLD int zend_exception_error(zend_object *exception, int severity);

ZEND_API ZEND_COLD void zend_throw_unwind_exit(zend_string *message, int exit_status);
ZEND_API zend_bool zend_is_unwind_exit(zend_object *ex);

#include "zend_globals.h"

Expand Down
3 changes: 1 addition & 2 deletions Zend/zend_execute_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1141,8 +1141,7 @@ ZEND_API int zend_eval_stringl_ex(const char *str, size_t str_len, zval *retval_

result = zend_eval_stringl(str, str_len, retval_ptr, string_name);
if (handle_exceptions && EG(exception)) {
zend_exception_error(EG(exception), E_ERROR);
result = FAILURE;
result = zend_exception_error(EG(exception), E_ERROR);
}
return result;
}
Expand Down
14 changes: 9 additions & 5 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -6874,27 +6874,31 @@ ZEND_VM_COLD_HANDLER(79, ZEND_EXIT, ANY, ANY)
USE_OPLINE

SAVE_OPLINE();

zend_string *message = NULL;
int exit_status = 0;
if (OP1_TYPE != IS_UNUSED) {
zval *ptr = GET_OP1_ZVAL_PTR(BP_VAR_R);

do {
if (Z_TYPE_P(ptr) == IS_LONG) {
EG(exit_status) = Z_LVAL_P(ptr);
exit_status = Z_LVAL_P(ptr);
} else {
if ((OP1_TYPE & (IS_VAR|IS_CV)) && Z_ISREF_P(ptr)) {
ptr = Z_REFVAL_P(ptr);
if (Z_TYPE_P(ptr) == IS_LONG) {
EG(exit_status) = Z_LVAL_P(ptr);
exit_status = Z_LVAL_P(ptr);
break;
}
}
zend_print_zval(ptr, 0);
message = zval_get_string(ptr);
}
} while (0);
FREE_OP1();
}
zend_bailout();
Copy link
Contributor

Choose a reason for hiding this comment

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

\0/ Burn it with 🔥! 😄

ZEND_VM_NEXT_OPCODE(); /* Never reached */

zend_throw_unwind_exit(message, exit_status);
HANDLE_EXCEPTION();
}

ZEND_VM_HANDLER(57, ZEND_BEGIN_SILENCE, ANY, ANY)
Expand Down
14 changes: 9 additions & 5 deletions Zend/zend_vm_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -2303,27 +2303,31 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_EXIT_SPEC_HANDLER
USE_OPLINE

SAVE_OPLINE();

zend_string *message = NULL;
int exit_status = 0;
if (opline->op1_type != IS_UNUSED) {
zval *ptr = get_zval_ptr(opline->op1_type, opline->op1, BP_VAR_R);

do {
if (Z_TYPE_P(ptr) == IS_LONG) {
EG(exit_status) = Z_LVAL_P(ptr);
exit_status = Z_LVAL_P(ptr);
} else {
if ((opline->op1_type & (IS_VAR|IS_CV)) && Z_ISREF_P(ptr)) {
ptr = Z_REFVAL_P(ptr);
if (Z_TYPE_P(ptr) == IS_LONG) {
EG(exit_status) = Z_LVAL_P(ptr);
exit_status = Z_LVAL_P(ptr);
break;
}
}
zend_print_zval(ptr, 0);
message = zval_get_string(ptr);
}
} while (0);
FREE_OP(opline->op1_type, opline->op1.var);
}
zend_bailout();
ZEND_VM_NEXT_OPCODE(); /* Never reached */

zend_throw_unwind_exit(message, exit_status);
HANDLE_EXCEPTION();
}

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_BEGIN_SILENCE_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
Expand Down
7 changes: 4 additions & 3 deletions ext/opcache/ZendAccelerator.c
Original file line number Diff line number Diff line change
Expand Up @@ -4484,9 +4484,10 @@ static int accel_preload(const char *config)
zend_user_exception_handler();
}
if (EG(exception)) {
zend_exception_error(EG(exception), E_ERROR);
CG(unclean_shutdown) = 1;
ret = FAILURE;
ret = zend_exception_error(EG(exception), E_ERROR);
if (ret == FAILURE) {
CG(unclean_shutdown) = 1;
}
}
}
destroy_op_array(op_array);
Expand Down
14 changes: 0 additions & 14 deletions ext/session/tests/bug60634.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,6 @@ session_start();
session_write_close();
echo "um, hi\n";

/*
* write() calls die(). This results in calling session_flush() which calls
* write() in request shutdown. The code is inside save handler still and
* calls save close handler.
*
* Because session_write_close() fails by die(), write() is called twice.
* close() is still called at request shutdown since session is active.
*/

?>
--EXPECT--
write: goodbye cruel world

Warning: Unknown: Cannot call session save handler in a recursive manner in Unknown on line 0

Warning: Unknown: Failed to write session data using user defined save handler. (session.save_path: ) in Unknown on line 0
close: goodbye cruel world
Loading