Skip to content

Commit 75a04ea

Browse files
committed
Make exit() unwind properly
exit() is now internally implemented by throwing an exception, performing a normal stack unwind and a clean shutdown. This ensures that no persistent resource leaks occur. The exception is internal, cannot be caught and does not result in the execution of finally blocks. This may be relaxed in the future. Closes GH-5768.
1 parent d005a8e commit 75a04ea

17 files changed

+154
-44
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
exit() and finally (1)
3+
--FILE--
4+
<?php
5+
6+
// TODO: In the future, we should execute the finally block.
7+
8+
try {
9+
exit("Exit\n");
10+
} catch (Throwable $e) {
11+
echo "Not caught\n";
12+
} finally {
13+
echo "Finally\n";
14+
}
15+
echo "Not executed\n";
16+
17+
?>
18+
--EXPECT--
19+
Exit

Zend/tests/exit_finally_2.phpt

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

Zend/tests/exit_finally_3.phpt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
exit() and finally (3)
3+
--FILE--
4+
<?php
5+
6+
// TODO: In the future, we should execute the finally block.
7+
8+
function test() {
9+
try {
10+
exit("Exit\n");
11+
} finally {
12+
return 42;
13+
}
14+
}
15+
var_dump(test());
16+
17+
?>
18+
--EXPECT--
19+
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: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ 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+
4447
ZEND_API void (*zend_throw_exception_hook)(zval *ex);
4548

4649
static zend_object_handlers default_exception_handlers;
@@ -81,6 +84,12 @@ void zend_exception_set_previous(zend_object *exception, zend_object *add_previo
8184
if (exception == add_previous || !add_previous || !exception) {
8285
return;
8386
}
87+
88+
if (zend_is_unwind_exit(add_previous)) {
89+
OBJ_RELEASE(add_previous);
90+
return;
91+
}
92+
8493
ZVAL_OBJ(&pv, add_previous);
8594
if (!instanceof_function(Z_OBJCE(pv), zend_ce_throwable)) {
8695
zend_error_noreturn(E_CORE_ERROR, "Previous exception must implement Throwable");
@@ -803,6 +812,8 @@ void zend_register_default_exception(void) /* {{{ */
803812
INIT_CLASS_ENTRY(ce, "DivisionByZeroError", class_DivisionByZeroError_methods);
804813
zend_ce_division_by_zero_error = zend_register_internal_class_ex(&ce, zend_ce_arithmetic_error);
805814
zend_ce_division_by_zero_error->create_object = zend_default_exception_new;
815+
816+
INIT_CLASS_ENTRY(zend_ce_unwind_exit, "UnwindExit", NULL);
806817
}
807818
/* }}} */
808819

@@ -898,10 +909,11 @@ static void zend_error_va(int type, const char *file, uint32_t lineno, const cha
898909
/* }}} */
899910

900911
/* This function doesn't return if it uses E_ERROR */
901-
ZEND_API ZEND_COLD void zend_exception_error(zend_object *ex, int severity) /* {{{ */
912+
ZEND_API ZEND_COLD int zend_exception_error(zend_object *ex, int severity) /* {{{ */
902913
{
903914
zval exception, rv;
904915
zend_class_entry *ce_exception;
916+
int result = FAILURE;
905917

906918
ZVAL_OBJ(&exception, ex);
907919
ce_exception = ex->ce;
@@ -961,11 +973,15 @@ ZEND_API ZEND_COLD void zend_exception_error(zend_object *ex, int severity) /* {
961973

962974
zend_string_release_ex(str, 0);
963975
zend_string_release_ex(file, 0);
976+
} else if (ce_exception == &zend_ce_unwind_exit) {
977+
/* We successfully unwound, nothing more to do */
978+
result = SUCCESS;
964979
} else {
965980
zend_error(severity, "Uncaught exception '%s'", ZSTR_VAL(ce_exception->name));
966981
}
967982

968983
OBJ_RELEASE(ex);
984+
return result;
969985
}
970986
/* }}} */
971987

@@ -987,3 +1003,16 @@ ZEND_API ZEND_COLD void zend_throw_exception_object(zval *exception) /* {{{ */
9871003
zend_throw_exception_internal(exception);
9881004
}
9891005
/* }}} */
1006+
1007+
ZEND_API ZEND_COLD void zend_throw_unwind_exit()
1008+
{
1009+
ZEND_ASSERT(!EG(exception));
1010+
EG(exception) = zend_objects_new(&zend_ce_unwind_exit);
1011+
EG(opline_before_exception) = EG(current_execute_data)->opline;
1012+
EG(current_execute_data)->opline = EG(exception_op);
1013+
}
1014+
1015+
ZEND_API zend_bool zend_is_unwind_exit(zend_object *ex)
1016+
{
1017+
return ex->ce == &zend_ce_unwind_exit;
1018+
}

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(void);
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: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6893,8 +6893,8 @@ ZEND_VM_COLD_HANDLER(79, ZEND_EXIT, ANY, ANY)
68936893
} while (0);
68946894
FREE_OP1();
68956895
}
6896-
zend_bailout();
6897-
ZEND_VM_NEXT_OPCODE(); /* Never reached */
6896+
zend_throw_unwind_exit();
6897+
HANDLE_EXCEPTION();
68986898
}
68996899

69006900
ZEND_VM_HANDLER(57, ZEND_BEGIN_SILENCE, ANY, ANY)
@@ -7271,7 +7271,7 @@ ZEND_VM_HELPER(zend_dispatch_try_catch_finally_helper, ANY, ANY, uint32_t try_ca
72717271
zend_object *ex = EG(exception);
72727272

72737273
/* Walk try/catch/finally structures upwards, performing the necessary actions */
7274-
while (try_catch_offset != (uint32_t) -1) {
7274+
for (; try_catch_offset != (uint32_t) -1; try_catch_offset--) {
72757275
zend_try_catch_element *try_catch =
72767276
&EX(func)->op_array.try_catch_array[try_catch_offset];
72777277

@@ -7281,6 +7281,11 @@ ZEND_VM_HELPER(zend_dispatch_try_catch_finally_helper, ANY, ANY, uint32_t try_ca
72817281
ZEND_VM_JMP_EX(&EX(func)->op_array.opcodes[try_catch->catch_op], 0);
72827282

72837283
} else if (op_num < try_catch->finally_op) {
7284+
if (ex && zend_is_unwind_exit(ex)) {
7285+
/* Don't execute finally blocks on exit (for now) */
7286+
continue;
7287+
}
7288+
72847289
/* Go to finally block */
72857290
zval *fast_call = EX_VAR(EX(func)->op_array.opcodes[try_catch->finally_end].op1.var);
72867291
cleanup_live_vars(execute_data, op_num, try_catch->finally_op);
@@ -7310,8 +7315,6 @@ ZEND_VM_HELPER(zend_dispatch_try_catch_finally_helper, ANY, ANY, uint32_t try_ca
73107315
ex = Z_OBJ_P(fast_call);
73117316
}
73127317
}
7313-
7314-
try_catch_offset--;
73157318
}
73167319

73177320
/* Uncaught exception */

Zend/zend_vm_execute.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2322,8 +2322,8 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_EXIT_SPEC_HANDLER
23222322
} while (0);
23232323
FREE_OP(opline->op1_type, opline->op1.var);
23242324
}
2325-
zend_bailout();
2326-
ZEND_VM_NEXT_OPCODE(); /* Never reached */
2325+
zend_throw_unwind_exit();
2326+
HANDLE_EXCEPTION();
23272327
}
23282328

23292329
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_BEGIN_SILENCE_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
@@ -2476,9 +2476,10 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_dispatch_try
24762476
{
24772477
/* May be NULL during generator closing (only finally blocks are executed) */
24782478
zend_object *ex = EG(exception);
2479+
zend_bool is_unwind_exit = ex && zend_is_unwind_exit(ex);
24792480

24802481
/* Walk try/catch/finally structures upwards, performing the necessary actions */
2481-
while (try_catch_offset != (uint32_t) -1) {
2482+
for (; try_catch_offset != (uint32_t) -1; try_catch_offset--) {
24822483
zend_try_catch_element *try_catch =
24832484
&EX(func)->op_array.try_catch_array[try_catch_offset];
24842485

@@ -2488,6 +2489,11 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_dispatch_try
24882489
ZEND_VM_JMP_EX(&EX(func)->op_array.opcodes[try_catch->catch_op], 0);
24892490

24902491
} else if (op_num < try_catch->finally_op) {
2492+
if (is_unwind_exit) {
2493+
/* Don't execute finally blocks on exit (for now) */
2494+
continue;
2495+
}
2496+
24912497
/* Go to finally block */
24922498
zval *fast_call = EX_VAR(EX(func)->op_array.opcodes[try_catch->finally_end].op1.var);
24932499
cleanup_live_vars(execute_data, op_num, try_catch->finally_op);
@@ -2517,8 +2523,6 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_dispatch_try
25172523
ex = Z_OBJ_P(fast_call);
25182524
}
25192525
}
2520-
2521-
try_catch_offset--;
25222526
}
25232527

25242528
/* Uncaught exception */

ext/curl/tests/bug68937.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ Bug # #68937 (Segfault in curl_multi_exec)
33
--SKIPIF--
44
<?php
55
include 'skipif.inc';
6-
if (getenv('SKIP_ASAN')) die('skip some curl versions leak on longjmp');
76
?>
87
--FILE--
98
<?php

ext/curl/tests/bug68937_2.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ Bug # #68937 (Segfault in curl_multi_exec)
33
--SKIPIF--
44
<?php
55
include 'skipif.inc';
6-
if (getenv('SKIP_ASAN')) die('skip some curl versions leak on longjmp');
76
?>
87
--FILE--
98
<?php

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

ext/soap/soap.c

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,8 +1382,10 @@ PHP_METHOD(SoapServer, handle)
13821382
xmlFreeDoc(doc_request);
13831383

13841384
if (EG(exception)) {
1385-
php_output_discard();
1386-
_soap_server_exception(service, function, ZEND_THIS);
1385+
if (!zend_is_unwind_exit(EG(exception))) {
1386+
php_output_discard();
1387+
_soap_server_exception(service, function, ZEND_THIS);
1388+
}
13871389
goto fail;
13881390
}
13891391

@@ -1576,15 +1578,17 @@ PHP_METHOD(SoapServer, handle)
15761578
efree(fn_name);
15771579

15781580
if (EG(exception)) {
1579-
php_output_discard();
1580-
_soap_server_exception(service, function, ZEND_THIS);
1581-
if (service->type == SOAP_CLASS) {
1581+
if (!zend_is_unwind_exit(EG(exception))) {
1582+
php_output_discard();
1583+
_soap_server_exception(service, function, ZEND_THIS);
1584+
if (service->type == SOAP_CLASS) {
15821585
#if defined(HAVE_PHP_SESSION) && !defined(COMPILE_DL_SESSION)
1583-
if (soap_obj && service->soap_class.persistence != SOAP_PERSISTENCE_SESSION) {
1586+
if (soap_obj && service->soap_class.persistence != SOAP_PERSISTENCE_SESSION) {
15841587
#else
1585-
if (soap_obj) {
1588+
if (soap_obj) {
15861589
#endif
1587-
zval_ptr_dtor(soap_obj);
1590+
zval_ptr_dtor(soap_obj);
1591+
}
15881592
}
15891593
}
15901594
goto fail;

ext/xsl/tests/bug33853.phpt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ Bug #33853 (php:function call __autoload with lowercase param)
33
--SKIPIF--
44
<?php
55
if (!extension_loaded('xsl')) die('skip xsl not loaded');
6-
if (getenv('SKIP_ASAN')) die('xfail bailing out across foreign C code');
76
?>
87
--FILE--
98
<?php

sapi/phpdbg/phpdbg_prompt.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,11 @@ void phpdbg_execute_ex(zend_execute_data *execute_data) /* {{{ */
17111711
}
17121712
#endif
17131713

1714+
if (exception && zend_is_unwind_exit(exception)) {
1715+
/* Restore bailout based exit. */
1716+
zend_bailout();
1717+
}
1718+
17141719
if (PHPDBG_G(flags) & PHPDBG_PREVENT_INTERACTIVE) {
17151720
phpdbg_print_opline_ex(execute_data, 0);
17161721
goto next;

0 commit comments

Comments
 (0)