Skip to content

Commit a5dda26

Browse files
committed
Make exit() unwind properly
1 parent 47cf18b commit a5dda26

15 files changed

+197
-41
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Exception handler should not be invoked for exit()
3+
--FILE--
4+
<?php
5+
6+
set_exception_handler(function($e) {
7+
var_dump($e);
8+
});
9+
10+
exit("Exit\n");
11+
12+
?>
13+
--EXPECT--
14+
Exit

Zend/tests/exit_finally_1.phpt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
exit() and finally (1)
3+
--FILE--
4+
<?php
5+
6+
try {
7+
exit("Exit\n");
8+
} catch (Throwable $e) {
9+
echo "Not caught\n";
10+
} finally {
11+
echo "Finally\n";
12+
}
13+
echo "Not executed\n";
14+
15+
?>
16+
--EXPECT--
17+
Finally
18+
Exit

Zend/tests/exit_finally_2.phpt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
exit() and finally (2)
3+
--FILE--
4+
<?php
5+
6+
try {
7+
try {
8+
exit("Exit\n");
9+
} catch (Throwable $e) {
10+
echo "Not caught\n";
11+
} finally {
12+
throw new Exception("Finally exception");
13+
}
14+
echo "Not executed\n";
15+
} catch (Exception $e) {
16+
echo "Caught {$e->getMessage()}\n";
17+
}
18+
19+
?>
20+
--EXPECT--
21+
Caught Finally exception

Zend/tests/exit_finally_3.phpt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
exit() and finally (3)
3+
--FILE--
4+
<?php
5+
6+
function test() {
7+
try {
8+
exit("Exit\n");
9+
} finally {
10+
return 42;
11+
}
12+
}
13+
var_dump(test());
14+
15+
?>
16+
--EXPECT--
17+
int(42)

Zend/tests/generators/bug75396.phpt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ $gen->send("x");
1919
?>
2020
--EXPECT--
2121
Try
22+
Finally
2223
Exit

Zend/zend.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,6 +1621,11 @@ ZEND_API ZEND_COLD void zend_user_exception_handler(void) /* {{{ */
16211621
zval orig_user_exception_handler;
16221622
zval params[1], retval2;
16231623
zend_object *old_exception;
1624+
1625+
if (zend_is_unwind_exit(EG(exception))) {
1626+
return;
1627+
}
1628+
16241629
old_exception = EG(exception);
16251630
EG(exception) = NULL;
16261631
ZVAL_OBJ(&params[0], old_exception);
@@ -1666,8 +1671,7 @@ ZEND_API int zend_execute_scripts(int type, zval *retval, int file_count, ...) /
16661671
zend_user_exception_handler();
16671672
}
16681673
if (EG(exception)) {
1669-
zend_exception_error(EG(exception), E_ERROR);
1670-
ret = FAILURE;
1674+
ret = zend_exception_error(EG(exception), E_ERROR);
16711675
}
16721676
}
16731677
destroy_op_array(op_array);

Zend/zend_exceptions.c

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ ZEND_API zend_class_entry *zend_ce_value_error;
4141
ZEND_API zend_class_entry *zend_ce_arithmetic_error;
4242
ZEND_API zend_class_entry *zend_ce_division_by_zero_error;
4343

44+
/* Internal pseudo-exception that is not exposed to userland. */
45+
static zend_class_entry zend_ce_unwind_exit;
46+
static zend_object_handlers zend_unwind_exit_handlers;
47+
4448
ZEND_API void (*zend_throw_exception_hook)(zval *ex);
4549

4650
static zend_object_handlers default_exception_handlers;
@@ -81,6 +85,12 @@ void zend_exception_set_previous(zend_object *exception, zend_object *add_previo
8185
if (exception == add_previous || !add_previous || !exception) {
8286
return;
8387
}
88+
89+
if (zend_is_unwind_exit(add_previous)) {
90+
OBJ_RELEASE(add_previous);
91+
return;
92+
}
93+
8494
ZVAL_OBJ(&pv, add_previous);
8595
if (!instanceof_function(Z_OBJCE(pv), zend_ce_throwable)) {
8696
zend_error_noreturn(E_CORE_ERROR, "Previous exception must implement Throwable");
@@ -728,6 +738,42 @@ ZEND_METHOD(Exception, __toString)
728738
}
729739
/* }}} */
730740

741+
typedef struct {
742+
zend_object std;
743+
zend_string *message;
744+
int exit_status;
745+
} zend_unwind_exit_object;
746+
747+
static zend_unwind_exit_object *zend_create_unwind_exit(zend_string *message, int exit_status) {
748+
zend_unwind_exit_object *intern = emalloc(sizeof(zend_unwind_exit_object));
749+
zend_object_std_init(&intern->std, &zend_ce_unwind_exit);
750+
intern->std.handlers = &zend_unwind_exit_handlers;
751+
intern->message = message;
752+
intern->exit_status = exit_status;
753+
return intern;
754+
}
755+
756+
static void zend_destroy_unwind_exit(zend_object *obj) {
757+
zend_unwind_exit_object *intern = (zend_unwind_exit_object *) obj;
758+
if (intern->message) {
759+
zend_string_release(intern->message);
760+
}
761+
zend_object_std_dtor(&intern->std);
762+
}
763+
764+
/** {{{ Throwable method definition */
765+
static const zend_function_entry zend_funcs_throwable[] = {
766+
ZEND_ABSTRACT_ME(throwable, getMessage, arginfo_class_Throwable_getMessage)
767+
ZEND_ABSTRACT_ME(throwable, getCode, arginfo_class_Throwable_getCode)
768+
ZEND_ABSTRACT_ME(throwable, getFile, arginfo_class_Throwable_getFile)
769+
ZEND_ABSTRACT_ME(throwable, getLine, arginfo_class_Throwable_getLine)
770+
ZEND_ABSTRACT_ME(throwable, getTrace, arginfo_class_Throwable_getTrace)
771+
ZEND_ABSTRACT_ME(throwable, getPrevious, arginfo_class_Throwable_getPrevious)
772+
ZEND_ABSTRACT_ME(throwable, getTraceAsString, arginfo_class_Throwable_getTraceAsString)
773+
ZEND_FE_END
774+
};
775+
/* }}} */
776+
731777
static void declare_exception_properties(zend_class_entry *ce)
732778
{
733779
zval val;
@@ -803,6 +849,10 @@ void zend_register_default_exception(void) /* {{{ */
803849
INIT_CLASS_ENTRY(ce, "DivisionByZeroError", class_DivisionByZeroError_methods);
804850
zend_ce_division_by_zero_error = zend_register_internal_class_ex(&ce, zend_ce_arithmetic_error);
805851
zend_ce_division_by_zero_error->create_object = zend_default_exception_new;
852+
853+
INIT_CLASS_ENTRY(zend_ce_unwind_exit, "UnwindExit", NULL);
854+
memcpy(&zend_unwind_exit_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
855+
zend_unwind_exit_handlers.free_obj = zend_destroy_unwind_exit;
806856
}
807857
/* }}} */
808858

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

900950
/* This function doesn't return if it uses E_ERROR */
901-
ZEND_API ZEND_COLD void zend_exception_error(zend_object *ex, int severity) /* {{{ */
951+
ZEND_API ZEND_COLD int zend_exception_error(zend_object *ex, int severity) /* {{{ */
902952
{
903953
zval exception, rv;
904954
zend_class_entry *ce_exception;
955+
int result = FAILURE;
905956

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

9621013
zend_string_release_ex(str, 0);
9631014
zend_string_release_ex(file, 0);
1015+
} else if (ce_exception == &zend_ce_unwind_exit) {
1016+
zend_unwind_exit_object *intern = (zend_unwind_exit_object *) ex;
1017+
if (intern->message) {
1018+
zend_write(ZSTR_VAL(intern->message), ZSTR_LEN(intern->message));
1019+
} else {
1020+
EG(exit_status) = intern->exit_status;
1021+
}
1022+
result = SUCCESS;
9641023
} else {
9651024
zend_error(severity, "Uncaught exception '%s'", ZSTR_VAL(ce_exception->name));
9661025
}
9671026

9681027
OBJ_RELEASE(ex);
1028+
return result;
9691029
}
9701030
/* }}} */
9711031

@@ -987,3 +1047,18 @@ ZEND_API ZEND_COLD void zend_throw_exception_object(zval *exception) /* {{{ */
9871047
zend_throw_exception_internal(exception);
9881048
}
9891049
/* }}} */
1050+
1051+
ZEND_API ZEND_COLD void zend_throw_unwind_exit(zend_string *message, int exit_status)
1052+
{
1053+
zend_unwind_exit_object *intern = zend_create_unwind_exit(message, exit_status);
1054+
1055+
ZEND_ASSERT(!EG(exception));
1056+
EG(exception) = &intern->std;
1057+
EG(opline_before_exception) = EG(current_execute_data)->opline;
1058+
EG(current_execute_data)->opline = EG(exception_op);
1059+
}
1060+
1061+
ZEND_API zend_bool zend_is_unwind_exit(zend_object *ex)
1062+
{
1063+
return ex->ce == &zend_ce_unwind_exit;
1064+
}

Zend/zend_exceptions.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ ZEND_API zend_object *zend_throw_error_exception(zend_class_entry *exception_ce,
6666
extern ZEND_API void (*zend_throw_exception_hook)(zval *ex);
6767

6868
/* show an exception using zend_error(severity,...), severity should be E_ERROR */
69-
ZEND_API ZEND_COLD void zend_exception_error(zend_object *exception, int severity);
69+
ZEND_API ZEND_COLD int zend_exception_error(zend_object *exception, int severity);
70+
71+
ZEND_API ZEND_COLD void zend_throw_unwind_exit(zend_string *message, int exit_status);
72+
ZEND_API zend_bool zend_is_unwind_exit(zend_object *ex);
7073

7174
#include "zend_globals.h"
7275

Zend/zend_execute_API.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,8 +1141,7 @@ ZEND_API int zend_eval_stringl_ex(const char *str, size_t str_len, zval *retval_
11411141

11421142
result = zend_eval_stringl(str, str_len, retval_ptr, string_name);
11431143
if (handle_exceptions && EG(exception)) {
1144-
zend_exception_error(EG(exception), E_ERROR);
1145-
result = FAILURE;
1144+
result = zend_exception_error(EG(exception), E_ERROR);
11461145
}
11471146
return result;
11481147
}

Zend/zend_vm_def.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6874,27 +6874,31 @@ ZEND_VM_COLD_HANDLER(79, ZEND_EXIT, ANY, ANY)
68746874
USE_OPLINE
68756875

68766876
SAVE_OPLINE();
6877+
6878+
zend_string *message = NULL;
6879+
int exit_status = 0;
68776880
if (OP1_TYPE != IS_UNUSED) {
68786881
zval *ptr = GET_OP1_ZVAL_PTR(BP_VAR_R);
68796882

68806883
do {
68816884
if (Z_TYPE_P(ptr) == IS_LONG) {
6882-
EG(exit_status) = Z_LVAL_P(ptr);
6885+
exit_status = Z_LVAL_P(ptr);
68836886
} else {
68846887
if ((OP1_TYPE & (IS_VAR|IS_CV)) && Z_ISREF_P(ptr)) {
68856888
ptr = Z_REFVAL_P(ptr);
68866889
if (Z_TYPE_P(ptr) == IS_LONG) {
6887-
EG(exit_status) = Z_LVAL_P(ptr);
6890+
exit_status = Z_LVAL_P(ptr);
68886891
break;
68896892
}
68906893
}
6891-
zend_print_zval(ptr, 0);
6894+
message = zval_get_string(ptr);
68926895
}
68936896
} while (0);
68946897
FREE_OP1();
68956898
}
6896-
zend_bailout();
6897-
ZEND_VM_NEXT_OPCODE(); /* Never reached */
6899+
6900+
zend_throw_unwind_exit(message, exit_status);
6901+
HANDLE_EXCEPTION();
68986902
}
68996903

69006904
ZEND_VM_HANDLER(57, ZEND_BEGIN_SILENCE, ANY, ANY)

Zend/zend_vm_execute.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,27 +2303,31 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_EXIT_SPEC_HANDLER
23032303
USE_OPLINE
23042304

23052305
SAVE_OPLINE();
2306+
2307+
zend_string *message = NULL;
2308+
int exit_status = 0;
23062309
if (opline->op1_type != IS_UNUSED) {
23072310
zval *ptr = get_zval_ptr(opline->op1_type, opline->op1, BP_VAR_R);
23082311

23092312
do {
23102313
if (Z_TYPE_P(ptr) == IS_LONG) {
2311-
EG(exit_status) = Z_LVAL_P(ptr);
2314+
exit_status = Z_LVAL_P(ptr);
23122315
} else {
23132316
if ((opline->op1_type & (IS_VAR|IS_CV)) && Z_ISREF_P(ptr)) {
23142317
ptr = Z_REFVAL_P(ptr);
23152318
if (Z_TYPE_P(ptr) == IS_LONG) {
2316-
EG(exit_status) = Z_LVAL_P(ptr);
2319+
exit_status = Z_LVAL_P(ptr);
23172320
break;
23182321
}
23192322
}
2320-
zend_print_zval(ptr, 0);
2323+
message = zval_get_string(ptr);
23212324
}
23222325
} while (0);
23232326
FREE_OP(opline->op1_type, opline->op1.var);
23242327
}
2325-
zend_bailout();
2326-
ZEND_VM_NEXT_OPCODE(); /* Never reached */
2328+
2329+
zend_throw_unwind_exit(message, exit_status);
2330+
HANDLE_EXCEPTION();
23272331
}
23282332

23292333
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_BEGIN_SILENCE_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

ext/opcache/ZendAccelerator.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4484,9 +4484,10 @@ static int accel_preload(const char *config)
44844484
zend_user_exception_handler();
44854485
}
44864486
if (EG(exception)) {
4487-
zend_exception_error(EG(exception), E_ERROR);
4488-
CG(unclean_shutdown) = 1;
4489-
ret = FAILURE;
4487+
ret = zend_exception_error(EG(exception), E_ERROR);
4488+
if (ret == FAILURE) {
4489+
CG(unclean_shutdown) = 1;
4490+
}
44904491
}
44914492
}
44924493
destroy_op_array(op_array);

ext/session/tests/bug60634.phpt

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,6 @@ session_start();
4040
session_write_close();
4141
echo "um, hi\n";
4242

43-
/*
44-
* write() calls die(). This results in calling session_flush() which calls
45-
* write() in request shutdown. The code is inside save handler still and
46-
* calls save close handler.
47-
*
48-
* Because session_write_close() fails by die(), write() is called twice.
49-
* close() is still called at request shutdown since session is active.
50-
*/
51-
5243
?>
5344
--EXPECT--
5445
write: goodbye cruel world
55-
56-
Warning: Unknown: Cannot call session save handler in a recursive manner in Unknown on line 0
57-
58-
Warning: Unknown: Failed to write session data using user defined save handler. (session.save_path: ) in Unknown on line 0
59-
close: goodbye cruel world

0 commit comments

Comments
 (0)