php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #77977 heap use-after-free via UDF in sqlite
Submitted: 2019-05-06 18:50 UTC Modified: 2020-02-24 09:04 UTC
From: radimre83 at gmail dot com Assigned:
Status: Analyzed Package: SQLite related
PHP Version: 7.3.5 OS: Linux
Private report: No CVE-ID: None
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
Package:
Bug Type:
Summary:
From: radimre83 at gmail dot com
New email:
PHP Version: OS:

 

 [2019-05-06 18:50 UTC] radimre83 at gmail dot com
Description:
------------
Sqlite and PHP both support user defined functions. From sqlite's documentation https://www.sqlite.org/c3ref/create_function.html:

However, such calls must not close the database connection nor finalize or reset the prepared statement in which the function is running. 

PHP does not prevent the user to call close on the db resource which could lead to heap use-after-free.

PHP shall remember if an UDF is being evaluated in the php_sqlite3_db_object structure and refuse close actions in that case.


Test script:
---------------
<?php

$udf_counter = 0;

$db = new SQLite3("main3.db");

$db->createFunction('my_udf', 'my_udf');

$db->exec("CREATE TABLE IF NOT EXISTS t (f TEXT)");
$c = $db->querySingle("SELECT COUNT(*) FROM t");
if($c <= 0) {
  for($i = 0; $i < 1; $i++) {
    $db->exec("INSERT INTO t (f) VALUES ('data$i')");
  }
}

$results = $db->query('SELECT f, my_udf(f) as m FROM t');
while ($row = $results->fetchArray()) {
    var_dump($row);
}

$db->close();

function my_udf($string) {
    global $udf_counter, $db;
    $udf_counter++;

    echo "udf $udf_counter\n";
    $db->close();
    return "x";
}


Expected result:
----------------
Some PHP level warning message complaining about the DB resource being busy and thus close was not permitted.

Actual result:
--------------
A PHP build with address sanitizer:

root@fd7f809a8411:/repo-shared/sqlite#  /build/php-7.3.4/sapi/cli/php udf.php
udf 1

Warning: SQLite3::close(): Unable to close database: 5, unable to close due to unfinalized statements or unfinished backups in /repo-shared/sqlite/udf.php on line 26
udf 2
=================================================================
==8576==ERROR: AddressSanitizer: heap-use-after-free on address 0x63400001ceb8 at pc 0x0000008d155d bp 0x7fff6e11bcb0 sp 0x7fff6e11bca8
READ of size 8 at 0x63400001ceb8 thread T0
    #0 0x8d155c  (/build/php-7.3.4/sapi/cli/php+0x8d155c)
    #1 0x8924a8  (/build/php-7.3.4/sapi/cli/php+0x8924a8)
    #2 0x885b4e  (/build/php-7.3.4/sapi/cli/php+0x885b4e)
    #3 0x9f273d  (/build/php-7.3.4/sapi/cli/php+0x9f273d)
    #4 0x8d4284  (/build/php-7.3.4/sapi/cli/php+0x8d4284)
    #5 0x88ec30  (/build/php-7.3.4/sapi/cli/php+0x88ec30)
    #6 0x1e179e6  (/build/php-7.3.4/sapi/cli/php+0x1e179e6)
    #7 0x1d6b665  (/build/php-7.3.4/sapi/cli/php+0x1d6b665)
    #8 0x1d6bf01  (/build/php-7.3.4/sapi/cli/php+0x1d6bf01)
    #9 0x1be1989  (/build/php-7.3.4/sapi/cli/php+0x1be1989)
    #10 0x1989e1a  (/build/php-7.3.4/sapi/cli/php+0x1989e1a)
    #11 0x1fff164  (/build/php-7.3.4/sapi/cli/php+0x1fff164)
    #12 0x1ffc025  (/build/php-7.3.4/sapi/cli/php+0x1ffc025)
    #13 0x7f48bbfce2e0  (/lib/x86_64-linux-gnu/libc.so.6+0x202e0)
    #14 0x447549  (/build/php-7.3.4/sapi/cli/php+0x447549)

0x63400001ceb8 is located 116408 bytes inside of 120008-byte region [0x634000000800,0x63400001dcc8)
freed by thread T0 here:
    #0 0x4f6cb0  (/build/php-7.3.4/sapi/cli/php+0x4f6cb0)
    #1 0x8c96b1  (/build/php-7.3.4/sapi/cli/php+0x8c96b1)

previously allocated by thread T0 here:
    #0 0x4f6e68  (/build/php-7.3.4/sapi/cli/php+0x4f6e68)
    #1 0xd5a9af  (/build/php-7.3.4/sapi/cli/php+0xd5a9af)

SUMMARY: AddressSanitizer: heap-use-after-free (/build/php-7.3.4/sapi/cli/php+0x8d155c)
Shadow bytes around the buggy address:
  0x0c687fffb980: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c687fffb990: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c687fffb9a0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c687fffb9b0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c687fffb9c0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
=>0x0c687fffb9d0: fd fd fd fd fd fd fd[fd]fd fd fd fd fd fd fd fd
  0x0c687fffb9e0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c687fffb9f0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c687fffba00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c687fffba10: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c687fffba20: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==8576==ABORTING



Latest PHP with typical config options:

root@fd7f809a8411:/repo-shared/sqlite#  /build/php-7.3.5/sapi/cli/php udf.php
udf 1

Warning: SQLite3::close(): Unable to close database: 5, unable to close due to unfinalized statements or unfinished backups in /repo-shared/sqlite/udf.php on line 26
udf 2
php: /build/php-7.3.5/ext/sqlite3/libsqlite/sqlite3.c:73966: sqlite3VdbeMemSetStr: Assertion `(pMem->flags & MEM_RowSet)==0' failed.
Aborted (core dumped)
root@fd7f809a8411:/repo-shared/sqlite# gdb /build/php-7.3.5/sapi/cli/php core
GNU gdb (Debian 7.12-6) 7.12.0.20161007-git
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /build/php-7.3.5/sapi/cli/php...done.
[New LWP 8577]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `/build/php-7.3.5/sapi/cli/php udf.php'.
Program terminated with signal SIGABRT, Aborted.
#0  0x00007f3a128e7fff in raise () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) bt
#0  0x00007f3a128e7fff in raise () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x00007f3a128e942a in abort () from /lib/x86_64-linux-gnu/libc.so.6
#2  0x00007f3a128e0e67 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#3  0x00007f3a128e0f12 in __assert_fail () from /lib/x86_64-linux-gnu/libc.so.6
#4  0x00007f3a14b12948 in sqlite3VdbeMemSetStr (pMem=0x7f3a167c4aa8, z=0x7f3a1664f958 "x", n=1, enc=1 '\001', xDel=0xffffffffffffffff) at /build/php-7.3.5/ext/sqlite3/libsqlite/sqlite3.c:73966
#5  0x00007f3a14b1e1bc in setResultStrOrError (pCtx=0x7f3a167c7988, z=0x7f3a1664f958 "x", n=1, enc=1 '\001', xDel=0xffffffffffffffff) at /build/php-7.3.5/ext/sqlite3/libsqlite/sqlite3.c:79998
#6  0x00007f3a14b1e51d in sqlite3_result_text (pCtx=0x7f3a167c7988, z=0x7f3a1664f958 "x", n=1, xDel=0xffffffffffffffff) at /build/php-7.3.5/ext/sqlite3/libsqlite/sqlite3.c:80095
#7  0x00007f3a14acaee9 in sqlite3_do_callback (fc=0x7f3a0b86e4c8, cb=0x7f3a0b86e498, argc=1, argv=0x7f3a167c79b8, context=0x7f3a167c7988, is_agg=0) at /build/php-7.3.5/ext/sqlite3/sqlite3.c:808
#8  0x00007f3a14acb041 in php_sqlite3_callback_func (context=0x7f3a167c7988, argc=1, argv=0x7f3a167c79b8) at /build/php-7.3.5/ext/sqlite3/sqlite3.c:839
#9  0x00007f3a14b32775 in sqlite3VdbeExec (p=0x7f3a167c5d68) at /build/php-7.3.5/ext/sqlite3/libsqlite/sqlite3.c:89021
#10 0x00007f3a14b1eb79 in sqlite3Step (p=0x7f3a167c5d68) at /build/php-7.3.5/ext/sqlite3/libsqlite/sqlite3.c:80302
#11 0x00007f3a14b1eeb1 in sqlite3_step (pStmt=0x7f3a167c5d68) at /build/php-7.3.5/ext/sqlite3/libsqlite/sqlite3.c:80365
#12 0x00007f3a14acde2c in zim_sqlite3result_fetchArray (execute_data=0x7f3a0b81f200, return_value=0x7f3a0b81f1d0) at /build/php-7.3.5/ext/sqlite3/sqlite3.c:1813
#13 0x00007f3a15008a33 in ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER () at /build/php-7.3.5/Zend/zend_vm_execute.h:1102
#14 0x00007f3a1506fd04 in execute_ex (ex=0x7f3a0b81f030) at /build/php-7.3.5/Zend/zend_vm_execute.h:55489
#15 0x00007f3a150752f1 in zend_execute (op_array=0x7f3a0b879300, return_value=0x0) at /build/php-7.3.5/Zend/zend_vm_execute.h:60881
#16 0x00007f3a14fa3f6f in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /build/php-7.3.5/Zend/zend.c:1568
#17 0x00007f3a14f12bfa in php_execute_script (primary_file=0x7ffcba7103b0) at /build/php-7.3.5/main/main.c:2630
#18 0x00007f3a15077e2f in do_cli (argc=2, argv=0x7f3a16615710) at /build/php-7.3.5/sapi/cli/php_cli.c:997
#19 0x00007f3a15078da2 in main (argc=2, argv=0x7f3a16615710) at /build/php-7.3.5/sapi/cli/php_cli.c:1389


Patches

prevent-uaf (last revision 2019-05-08 09:25 UTC by cmb@php.net)

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2019-05-07 18:25 UTC] cmb@php.net
-Assigned To: +Assigned To: cmb
 [2019-05-08 09:24 UTC] cmb@php.net
-Status: Assigned +Status: Analyzed -Type: Security +Type: Bug
 [2019-05-08 09:24 UTC] cmb@php.net
Thanks for reporting this issue!

This is, however, not a security issue, since closing a database
connection in a callback function never makes sense, and PHP is
not suppossed to shield from *all* silly programmer mistakes.

Anyhow, actually you're hitting bug #64531 here, which causes
my_udf() to be called twice even though there is only a single row
in the result set.  During the first call, the statement is not
yet registered, so isn't closed when SQLite3::close() is called,
which causes libsqlite3 to keep the zombie connection.  If you'd
call SQLite3::close() only for the second call to my_udf(),
everything would be fine.  As I already stated in the other
ticket, there's no general solution for that, so we may consider
to apply a workaround for this ticket, which would prevent the
UAF, but likely causes memory leaks.
 [2019-05-08 09:25 UTC] cmb@php.net
The following patch has been added/updated:

Patch Name: prevent-uaf
Revision:   1557307512
URL:        https://bugs.php.net/patch-display.php?bug=77977&patch=prevent-uaf&revision=1557307512
 [2019-11-11 13:17 UTC] cmb@php.net
-Assigned To: cmb +Assigned To:
 [2019-11-11 13:17 UTC] cmb@php.net
Unassigning, since a clean fix for this issue is blocked by bug
#64531, and the workaround is dubious.
 [2020-02-24 09:04 UTC] cmb@php.net
Correction: this is not really related to the mentioned bug, but
rather to the documented[1] fact that:

| An application-defined function is permitted to call other
| SQLite interfaces. However, such calls must not close the database
| connection nor finalize or reset the prepared statement in which
| the function is running.

We could prevent the test script crashing by calling
sqlite3_close_v2() in SQLite3::close(), but since the former
never fails, the return value of the latter would then be
meaningless.  We might be able to set a flag on the connection
object to prevent calling sqlite3_close(), but I don't think it's
possible to catch illegal manipulation of the executing statement,
which can crash the application as well, e.g.

<?php

function my_udf($string) {
    global $udf_counter, $db, $stmt;
    $udf_counter++;

    echo "udf $udf_counter\n";
    $stmt->reset();
    return "x";
}

$udf_counter = 0;

$db = new SQLite3(":memory:");

$db->createFunction('my_udf', 'my_udf');

$db->exec("CREATE TABLE IF NOT EXISTS t (f TEXT)");
$c = $db->querySingle("SELECT COUNT(*) FROM t");
if ($c <= 0) {
    for ($i = 0; $i < 1; $i++) {
        $db->exec("INSERT INTO t (f) VALUES ('data$i')");
    }
}

$stmt = $db->prepare('SELECT f, my_udf(f) as m FROM t');
$results = $stmt->execute();
while ($row = $results->fetchArray()) {
    var_dump($row);
}

$db->close();
echo "done\n";
?>

So this looks like a documentation issue.  Any thoughts about this?

[1] <https://sqlite.org/c3ref/create_function.html>
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Mon Oct 07 11:01:28 2024 UTC