Преглед на файлове

Merge pull request #91 from stesie/hack-fatal-error-unwind

Handle PHP Fatal Errors inside JS->PHP callbacks
Patrick Reilly преди 11 години
родител
ревизия
16447f8fce
променени са 5 файла, в които са добавени 142 реда и са изтрити 2 реда
  1. 6 0
      php_v8js_macros.h
  2. 38 0
      tests/fatal_error_recursive.phpt
  3. 26 0
      tests/fatal_error_rethrow.phpt
  4. 11 0
      v8js.cc
  5. 61 2
      v8js_convert.cc

+ 6 - 0
php_v8js_macros.h

@@ -259,6 +259,12 @@ ZEND_BEGIN_MODULE_GLOBALS(v8js)
   bool timer_stop;
 
   std::map<char *, v8::Handle<v8::Object> > modules_loaded;
+
+  // fatal error unwinding
+  bool fatal_error_abort;
+  int error_num;
+  char *error_message;
+  jmp_buf *unwind_env;
 ZEND_END_MODULE_GLOBALS(v8js)
 
 extern zend_v8js_globals v8js_globals;

+ 38 - 0
tests/fatal_error_recursive.phpt

@@ -0,0 +1,38 @@
+--TEST--
+Test V8::executeString() : Fatal Error with recursive executeString calls
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$js = new V8Js();
+
+$js->baz = function() {
+	$bar = null;
+	$bar->bar();
+};
+
+$js->bar = function() {
+	global $js;
+	$js->executeString("PHP.baz();");
+};
+
+$js->foo = function() {
+	global $js;
+	$js->executeString("PHP.bar();");
+};
+
+$js->nofail = function() {
+	echo "foo\n";
+};
+
+$js->executeString("PHP.nofail();");
+$js->executeString("PHP.nofail(); PHP.foo();");
+
+?>
+===EOF===
+--EXPECTF--
+foo
+foo
+
+Fatal error: Call to a member function bar() on a non-object in %s/fatal_error_recursive.php on line 7

+ 26 - 0
tests/fatal_error_rethrow.phpt

@@ -0,0 +1,26 @@
+--TEST--
+Test V8::executeString() : Fatal Error rethrowing
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$js = new V8Js();
+
+$js->foo = function() {
+	$bar = null;
+	$bar->bar();
+};
+
+$script = <<<END
+
+PHP.foo();
+
+END;
+
+$js->executeString($script);
+
+?>
+===EOF===
+--EXPECTF--
+Fatal error: Call to a member function bar() on a non-object in %s/fatal_error_rethrow.php on line 7

+ 11 - 0
v8js.cc

@@ -1134,6 +1134,12 @@ static PHP_METHOD(V8Js, executeString)
 		php_v8js_timer_pop(TSRMLS_C);
 	}
 
+	/* Check for fatal error marker possibly set by php_v8js_error_handler; just
+	 * rethrow the error since we're now out of V8. */
+	if(V8JSG(fatal_error_abort)) {
+		zend_error(V8JSG(error_num), "%s", V8JSG(error_message));
+	}
+
 	char exception_string[64];
 
 	if (c->time_limit_hit) {
@@ -1868,6 +1874,11 @@ static PHP_GINIT_FUNCTION(v8js)
 	new(&v8js_globals->timer_mutex) std::mutex;
 	new(&v8js_globals->timer_stack) std::stack<php_v8js_timer_ctx *>;
 	new(&v8js_globals->modules_loaded) std::map<char *, v8::Handle<v8::Object>>;
+
+	v8js_globals->fatal_error_abort = 0;
+	v8js_globals->error_num = 0;
+	v8js_globals->error_message = 0;
+	v8js_globals->unwind_env = NULL;
 #endif
 }
 /* }}} */

+ 61 - 2
v8js_convert.cc

@@ -30,6 +30,29 @@ extern "C" {
 #include <stdexcept>
 #include <limits>
 
+
+/* Callback for PHP's zend_error_cb; catching any fatal PHP error.
+ * The callback is installed in the lowest (stack wise) php_v8js_call_php_func
+ * frame.  Just store the error message and jump right back there and fall
+ * back into V8 context. */
+static void php_v8js_error_handler(int error_num, const char *error_filename,
+								   const uint error_lineno, const char *format,
+								   va_list args) /* {{{ */
+{
+	char *buffer;
+	int buffer_len;
+
+	buffer_len = vspprintf(&buffer, PG(log_errors_max_len), format, args);
+
+	V8JSG(fatal_error_abort) = true;
+	V8JSG(error_num) = error_num;
+	V8JSG(error_message) = buffer;
+
+	longjmp(*V8JSG(unwind_env), 1);
+}
+/* }}} */
+
+
 static void php_v8js_weak_object_callback(const v8::WeakCallbackData<v8::Object, zval> &data);
 
 /* Callback for PHP methods and functions */
@@ -43,6 +66,13 @@ static void php_v8js_call_php_func(zval *value, zend_class_entry *ce, zend_funct
 	char *error;
 	int error_len, i, flags = V8JS_FLAG_NONE;
 
+#if PHP_V8_API_VERSION <= 3023008
+	/* Until V8 3.23.8 Isolate could only take one external pointer. */
+	php_v8js_ctx *ctx = (php_v8js_ctx *) isolate->GetData();
+#else
+	php_v8js_ctx *ctx = (php_v8js_ctx *) isolate->GetData(0);
+#endif
+
 	/* Set parameter limits */
 	min_num_args = method_ptr->common.required_num_args;
 	max_num_args = method_ptr->common.num_args;
@@ -125,12 +155,41 @@ static void php_v8js_call_php_func(zval *value, zend_class_entry *ce, zend_funct
 		fcc.called_scope = ce;
 		fcc.object_ptr = value;
 
-		/* Call the method */
-		zend_call_function(&fci, &fcc TSRMLS_CC);
+		jmp_buf env;
+		int val = 0;
+
+		void (*old_error_handler)(int, const char *, const uint, const char*, va_list);
+
+		/* If this is the first level call from V8 back to PHP, install a
+		 * handler for fatal errors; we must fall back through V8 to keep
+		 * it from crashing. */
+		if (V8JSG(unwind_env) == NULL) {
+			old_error_handler = zend_error_cb;
+			zend_error_cb = php_v8js_error_handler;
+
+			val = setjmp (env);
+			V8JSG(unwind_env) = &env;
+		}
+
+		if (!val) {
+			/* Call the method */
+			zend_call_function(&fci, &fcc TSRMLS_CC);
+		}
+
+		if (old_error_handler != NULL) {
+			zend_error_cb = old_error_handler;
+			V8JSG(unwind_env) = NULL;
+		}
 	}
 
 	isolate->Enter();
 
+	if (V8JSG(fatal_error_abort)) {
+		v8::V8::TerminateExecution(isolate);
+		info.GetReturnValue().Set(V8JS_NULL);
+		return;
+	}
+
 failure:
 	/* Cleanup */
 	if (argc) {