Commit 8376904aeb6 for php.net

commit 8376904aeb636bc2a44dad4b712f1458ad0a65e1
Author: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date:   Fri Apr 18 00:34:46 2025 +0200

    Implement GH-17321: Add setAuthorizer to Pdo\Sqlite (#17905)

diff --git a/NEWS b/NEWS
index 38af4b7681d..80bda686a1b 100644
--- a/NEWS
+++ b/NEWS
@@ -114,6 +114,7 @@ PHP                                                                        NEWS

 - PDO_SQLITE:
   . throw on null bytes / resolve GH-13952 (divinity76).
+  . Implement GH-17321: Add setAuthorizer to Pdo\Sqlite. (nielsdos)

 - PGSQL:
   . Added pg_close_stmt to close a prepared statement while allowing
diff --git a/UPGRADING b/UPGRADING
index 2b7abad173c..2ce2ba3c120 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -309,6 +309,11 @@ PHP 8.5 UPGRADE NOTES
   . Added enchant_dict_remove() to put a word on the exclusion list and
     remove it from the session dictionary.

+- Pdo\Sqlite:
+  . Added support for Pdo\Sqlite::setAuthorizer(), which is the equivalent of
+    SQLite3::setAuthorizer(). The only interface difference is that the
+    pdo version returns void.
+
 - PGSQL:
   . pg_close_stmt offers an alternative way to close a prepared
     statement from the DEALLOCATE sql command in that we can reuse
diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c
index ff56d040494..7661b36c5d3 100644
--- a/ext/pdo_sqlite/pdo_sqlite.c
+++ b/ext/pdo_sqlite/pdo_sqlite.c
@@ -332,6 +332,36 @@ PHP_METHOD(Pdo_Sqlite, openBlob)
 	}
 }

+PHP_METHOD(Pdo_Sqlite, setAuthorizer)
+{
+	zend_fcall_info fci;
+	zend_fcall_info_cache fcc;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_FUNC_NO_TRAMPOLINE_FREE_OR_NULL(fci, fcc)
+	ZEND_PARSE_PARAMETERS_END();
+
+	pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
+	PDO_CONSTRUCT_CHECK_WITH_CLEANUP(free_fcc);
+	pdo_sqlite_db_handle *db_handle = (pdo_sqlite_db_handle *) dbh->driver_data;
+
+	/* Clear previously set callback */
+	if (ZEND_FCC_INITIALIZED(db_handle->authorizer_fcc)) {
+		zend_fcc_dtor(&db_handle->authorizer_fcc);
+	}
+
+	/* Only enable userland authorizer if argument is not NULL */
+	if (ZEND_FCI_INITIALIZED(fci)) {
+		zend_fcc_dup(&db_handle->authorizer_fcc, &fcc);
+	}
+
+	return;
+
+free_fcc:
+	zend_release_fcall_info_cache(&fcc);
+	RETURN_THROWS();
+}
+
 static int php_sqlite_collation_callback(void *context, int string1_len, const void *string1,
 	int string2_len, const void *string2)
 {
@@ -349,7 +379,7 @@ static int php_sqlite_collation_callback(void *context, int string1_len, const v
 	if (!Z_ISUNDEF(retval)) {
 		if (Z_TYPE(retval) != IS_LONG) {
 			zend_string *func_name = get_active_function_or_method_name();
-			zend_type_error("%s(): Return value of the callback must be of type int, %s returned",
+			zend_type_error("%s(): Return value of the collation callback must be of type int, %s returned",
 				ZSTR_VAL(func_name), zend_zval_value_name(&retval));
 			zend_string_release(func_name);
 			zval_ptr_dtor(&retval);
diff --git a/ext/pdo_sqlite/pdo_sqlite.stub.php b/ext/pdo_sqlite/pdo_sqlite.stub.php
index e22c8ce10fc..3832683598e 100644
--- a/ext/pdo_sqlite/pdo_sqlite.stub.php
+++ b/ext/pdo_sqlite/pdo_sqlite.stub.php
@@ -33,6 +33,16 @@ class Sqlite extends \PDO
     /** @cvalue PDO_SQLITE_ATTR_EXTENDED_RESULT_CODES */
     public const int ATTR_EXTENDED_RESULT_CODES = UNKNOWN;

+    /** @cvalue SQLITE_OK */
+    public const int OK = UNKNOWN;
+
+    /* Constants for authorizer return */
+
+    /** @cvalue SQLITE_DENY */
+    public const int DENY = UNKNOWN;
+    /** @cvalue SQLITE_IGNORE */
+    public const int IGNORE = UNKNOWN;
+
     // Registers an aggregating User Defined Function for use in SQL statements
     public function createAggregate(
         string $name,
@@ -63,4 +73,6 @@ public function openBlob(
         ?string $dbname = "main",
         int $flags = \Pdo\Sqlite::OPEN_READONLY
     ) {}
+
+    public function setAuthorizer(?callable $callback): void {}
 }
diff --git a/ext/pdo_sqlite/pdo_sqlite_arginfo.h b/ext/pdo_sqlite/pdo_sqlite_arginfo.h
index 4abbc0bb625..75de256e55c 100644
Binary files a/ext/pdo_sqlite/pdo_sqlite_arginfo.h and b/ext/pdo_sqlite/pdo_sqlite_arginfo.h differ
diff --git a/ext/pdo_sqlite/php_pdo_sqlite_int.h b/ext/pdo_sqlite/php_pdo_sqlite_int.h
index 08d5f877ad5..4a39781f85c 100644
--- a/ext/pdo_sqlite/php_pdo_sqlite_int.h
+++ b/ext/pdo_sqlite/php_pdo_sqlite_int.h
@@ -50,6 +50,7 @@ typedef struct {
 	pdo_sqlite_error_info einfo;
 	struct pdo_sqlite_func *funcs;
 	struct pdo_sqlite_collation *collations;
+	zend_fcall_info_cache authorizer_fcc;
 } pdo_sqlite_db_handle;

 typedef struct {
diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c
index 8a880a3425f..ddf25d4965f 100644
--- a/ext/pdo_sqlite/sqlite_driver.c
+++ b/ext/pdo_sqlite/sqlite_driver.c
@@ -97,6 +97,10 @@ static void pdo_sqlite_cleanup_callbacks(pdo_sqlite_db_handle *H)
 {
 	struct pdo_sqlite_func *func;

+	if (ZEND_FCC_INITIALIZED(H->authorizer_fcc)) {
+		zend_fcc_dtor(&H->authorizer_fcc);
+	}
+
 	while (H->funcs) {
 		func = H->funcs;
 		H->funcs = func->next;
@@ -714,6 +718,10 @@ static void pdo_sqlite_get_gc(pdo_dbh_t *dbh, zend_get_gc_buffer *gc_buffer)
 {
 	pdo_sqlite_db_handle *H = dbh->driver_data;

+	if (ZEND_FCC_INITIALIZED(H->authorizer_fcc)) {
+		zend_get_gc_buffer_add_fcc(gc_buffer, &H->authorizer_fcc);
+	}
+
 	struct pdo_sqlite_func *func = H->funcs;
 	while (func) {
 		if (ZEND_FCC_INITIALIZED(func->func)) {
@@ -784,24 +792,77 @@ static char *make_filename_safe(const char *filename)
 	return estrdup(filename);
 }

-static int authorizer(void *autharg, int access_type, const char *arg3, const char *arg4,
-		const char *arg5, const char *arg6)
+#define ZVAL_NULLABLE_STRING(zv, str) do { \
+	zval *zv_ = zv; \
+	const char *str_ = str; \
+	if (str_) { \
+		ZVAL_STRING(zv_, str_); \
+	} else { \
+		ZVAL_NULL(zv_); \
+	} \
+} while (0)
+
+static int authorizer(void *autharg, int access_type, const char *arg1, const char *arg2,
+		const char *arg3, const char *arg4)
 {
-	char *filename;
-	switch (access_type) {
-		case SQLITE_ATTACH: {
-			filename = make_filename_safe(arg3);
+	if (PG(open_basedir) && *PG(open_basedir)) {
+		if (access_type == SQLITE_ATTACH) {
+			char *filename = make_filename_safe(arg1);
 			if (!filename) {
 				return SQLITE_DENY;
 			}
 			efree(filename);
-			return SQLITE_OK;
 		}
+	}

-		default:
-			/* access allowed */
-			return SQLITE_OK;
+	pdo_sqlite_db_handle *db_obj = autharg;
+
+	/* fallback to access allowed if authorizer callback is not defined */
+	if (!ZEND_FCC_INITIALIZED(db_obj->authorizer_fcc)) {
+		return SQLITE_OK;
+	}
+
+	/* call userland authorizer callback, if set */
+	zval retval;
+	zval argv[5];
+
+	ZVAL_LONG(&argv[0], access_type);
+	ZVAL_NULLABLE_STRING(&argv[1], arg1);
+	ZVAL_NULLABLE_STRING(&argv[2], arg2);
+	ZVAL_NULLABLE_STRING(&argv[3], arg3);
+	ZVAL_NULLABLE_STRING(&argv[4], arg4);
+
+	int authreturn = SQLITE_DENY;
+
+	zend_call_known_fcc(&db_obj->authorizer_fcc, &retval, /* argc */ 5, argv, /* named_params */ NULL);
+	if (Z_ISUNDEF(retval)) {
+		ZEND_ASSERT(EG(exception));
+	} else {
+		if (Z_TYPE(retval) != IS_LONG) {
+			zend_string *func_name = get_active_function_or_method_name();
+			zend_type_error("%s(): Return value of the authorizer callback must be of type int, %s returned",
+				ZSTR_VAL(func_name), zend_zval_value_name(&retval));
+			zend_string_release(func_name);
+		} else {
+			authreturn = Z_LVAL(retval);
+
+			if (authreturn != SQLITE_OK && authreturn != SQLITE_IGNORE && authreturn != SQLITE_DENY) {
+				zend_string *func_name = get_active_function_or_method_name();
+				zend_value_error("%s(): Return value of the authorizer callback must be one of Pdo\\Sqlite::OK, Pdo\\Sqlite::DENY, or Pdo\\Sqlite::IGNORE",
+					ZSTR_VAL(func_name));
+				zend_string_release(func_name);
+				authreturn = SQLITE_DENY;
+			}
+		}
 	}
+
+	zval_ptr_dtor(&retval);
+	zval_ptr_dtor(&argv[1]);
+	zval_ptr_dtor(&argv[2]);
+	zval_ptr_dtor(&argv[3]);
+	zval_ptr_dtor(&argv[4]);
+
+	return authreturn;
 }

 static int pdo_sqlite_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ */
@@ -843,9 +904,7 @@ static int pdo_sqlite_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{
 		goto cleanup;
 	}

-	if (PG(open_basedir) && *PG(open_basedir)) {
-		sqlite3_set_authorizer(H->db, authorizer, NULL);
-	}
+	sqlite3_set_authorizer(H->db, authorizer, H);

 	if (driver_options) {
 		timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, timeout);
diff --git a/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer.phpt b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer.phpt
new file mode 100644
index 00000000000..d1e9039ea1c
--- /dev/null
+++ b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer.phpt
@@ -0,0 +1,101 @@
+--TEST--
+Pdo\Sqlite user authorizer callback
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+
+$db = new Pdo\Sqlite('sqlite::memory:');
+
+$db->setAuthorizer(function (int $action) {
+    if ($action == 21 /* SELECT */) {
+        return Pdo\Sqlite::OK;
+    }
+
+    return Pdo\Sqlite::DENY;
+});
+
+// This query should be accepted
+var_dump($db->query('SELECT 1;'));
+
+try {
+    // This one should fail
+    var_dump($db->exec('CREATE TABLE test (a, b);'));
+} catch (\Exception $e) {
+    echo $e->getMessage() . "\n";
+}
+
+// Test disabling the authorizer
+$db->setAuthorizer(null);
+
+// This should now succeed
+var_dump($db->exec('CREATE TABLE test (a); INSERT INTO test VALUES (42);'));
+var_dump($db->exec('SELECT a FROM test;'));
+
+// Test if we are getting the correct arguments
+$db->setAuthorizer(function (int $action) {
+    $constants = ["COPY", "CREATE_INDEX", "CREATE_TABLE", "CREATE_TEMP_INDEX", "CREATE_TEMP_TABLE", "CREATE_TEMP_TRIGGER", "CREATE_TEMP_VIEW", "CREATE_TRIGGER", "CREATE_VIEW", "DELETE", "DROP_INDEX", "DROP_TABLE", "DROP_TEMP_INDEX", "DROP_TEMP_TABLE", "DROP_TEMP_TRIGGER", "DROP_TEMP_VIEW", "DROP_TRIGGER", "DROP_VIEW", "INSERT", "PRAGMA", "READ", "SELECT", "TRANSACTION", "UPDATE"];
+
+    var_dump($constants[$action], implode(',', array_slice(func_get_args(), 1)));
+    return Pdo\Sqlite::OK;
+});
+
+var_dump($db->exec('SELECT * FROM test WHERE a = 42;'));
+var_dump($db->exec('DROP TABLE test;'));
+
+// Try to return something invalid from the authorizer
+$db->setAuthorizer(function () {
+    return 'FAIL';
+});
+
+try {
+    var_dump($db->query('SELECT 1;'));
+} catch (\Error $e) {
+    echo $e->getMessage() . "\n";
+}
+
+$db->setAuthorizer(function () {
+    return 4200;
+});
+
+try {
+    var_dump($db->query('SELECT 1;'));
+} catch (\Error $e) {
+    echo $e->getMessage() . "\n";
+}
+
+?>
+--EXPECTF--
+object(PDOStatement)#%d (1) {
+  ["queryString"]=>
+  string(9) "SELECT 1;"
+}
+SQLSTATE[HY000]: General error: 23 not authorized
+int(1)
+int(1)
+string(6) "SELECT"
+string(3) ",,,"
+string(4) "READ"
+string(12) "test,a,main,"
+string(4) "READ"
+string(12) "test,a,main,"
+int(1)
+string(6) "DELETE"
+string(20) "sqlite_master,,main,"
+string(10) "DROP_TABLE"
+string(11) "test,,main,"
+string(6) "DELETE"
+string(11) "test,,main,"
+string(6) "DELETE"
+string(20) "sqlite_master,,main,"
+string(4) "READ"
+string(28) "sqlite_master,tbl_name,main,"
+string(4) "READ"
+string(24) "sqlite_master,type,main,"
+string(6) "UPDATE"
+string(28) "sqlite_master,rootpage,main,"
+string(4) "READ"
+string(28) "sqlite_master,rootpage,main,"
+int(1)
+PDO::query(): Return value of the authorizer callback must be of type int, string returned
+PDO::query(): Return value of the authorizer callback must be one of Pdo\Sqlite::OK, Pdo\Sqlite::DENY, or Pdo\Sqlite::IGNORE
diff --git a/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline.phpt b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline.phpt
new file mode 100644
index 00000000000..c93a1f2e34a
--- /dev/null
+++ b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Pdo\Sqlite user authorizer trampoline callback
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+
+class TrampolineTest {
+    public function __call(string $name, array $arguments) {
+        echo 'Trampoline for ', $name, PHP_EOL;
+        if ($arguments[0] == 21 /* SELECT */) {
+            return Pdo\Sqlite::OK;
+        }
+
+        return Pdo\Sqlite::DENY;
+    }
+}
+$o = new TrampolineTest();
+$callback = [$o, 'authorizer'];
+
+$db = new Pdo\Sqlite('sqlite::memory:');
+
+$db->setAuthorizer($callback);
+
+// This query should be accepted
+var_dump($db->query('SELECT 1;'));
+
+try {
+    // This one should fail
+    var_dump($db->query('CREATE TABLE test (a, b);'));
+} catch (\Exception $e) {
+    echo $e->getMessage() . "\n";
+}
+
+?>
+--EXPECTF--
+Trampoline for authorizer
+object(PDOStatement)#%d (1) {
+  ["queryString"]=>
+  string(9) "SELECT 1;"
+}
+Trampoline for authorizer
+SQLSTATE[HY000]: General error: 23 not authorized
diff --git a/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline_no_leak.phpt b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline_no_leak.phpt
new file mode 100644
index 00000000000..84b83877b94
--- /dev/null
+++ b/ext/pdo_sqlite/tests/subclasses/pdosqlite_setauthorizer_trampoline_no_leak.phpt
@@ -0,0 +1,36 @@
+--TEST--
+PdoSqlite::setAuthorizer use F ZPP for trampoline callback and does not leak
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+
+class TrampolineTest {
+    public function __call(string $name, array $arguments) {
+        echo 'Trampoline for ', $name, PHP_EOL;
+        if ($arguments[0] == Pdo\Sqlite::SELECT) {
+            return Pdo\Sqlite::OK;
+        }
+
+        return Pdo\Sqlite::DENY;
+    }
+}
+$o = new TrampolineTest();
+$callback = [$o, 'authorizer'];
+
+echo "Invalid Pdo\Sqlite object:\n";
+$rc = new ReflectionClass(Pdo\Sqlite::class);
+$obj = $rc->newInstanceWithoutConstructor();
+
+try {
+    var_dump($obj->setAuthorizer($callback));
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+DONE
+--EXPECT--
+Invalid Pdo\Sqlite object:
+Error: Pdo\Sqlite object is uninitialized
+DONE