Uploaded image for project: 'PHP Driver: Extension'
  1. PHP Driver: Extension
  2. PHPC-732

Possible mongoc_client_t use-after-free with generators

    • Type: Icon: Bug Bug
    • Resolution: Done
    • Priority: Icon: Critical - P2 Critical - P2
    • 1.2.0
    • Affects Version/s: 1.1.8
    • Component/s: None
    • None
    • Environment:
      PHP 5.5.37 (possible other PHP 5.x, but not PHP 7)

      mongo-php-adapter uses generators to wrap a MongoDB\Driver\Cursor and implement the legacy driver's MongoCursor API:

      On PHP 5.x, this can lead to use-after-free segfaults with mongoc_client_t. Tracing revealed that MongoDB\Driver\Manager may still be freed before MongoDB\Driver\Cursor, which was something previously addressed in PHPC-671; however, that issue did not involve generators.

      As with PHPC-671, we should also confirm if a use-after-free crash can be triggered with just a Server object.


      From jwage:

      I was able to reproduce the segfault locally with this script:

      <?php
      
      use Symfony\Component\ClassLoader\ApcClassLoader;
      
      $loader = require_once __DIR__.'/opensky/bootstrap.php.cache';
      $loader = new ApcClassLoader('devo', $loader);
      $loader->register(true);
      
      $mongo = new \MongoClient('mongodb://localhost:27017');
      
      $db = $mongo->selectDB('opensky_devo');
      
      $collection = $db->selectCollection('sellables');
      
      $cursor = $collection->find([], []);
      $cursor->batchSize(2);
      $cursor->limit(3);
      
      $cursor->next();
      

      If I call $cursor->next() exactly 4 times (2 batches) then it does not crash.

      Here is the backtrace:

      #0  mongoc_set_id_cmp (a_=0x7fff842d13a0, b_=0xeb8) at /var/tmp/mongodb/src/libmongoc/src/mongoc/mongoc-set.c:49
              a = 0x7fff842d13a0
              b = 0xeb8
      #1  0x0000003572033f80 in bsearch (key=0x7fff842d13a0, base=0xb38, nmemb=<value optimized out>, size=16, compar=0x7f7f99d8b800 <mongoc_set_id_cmp>) at bsearch.c:38
              l = <value optimized out>
              u = 113
              idx = 56
              p = 0xeb8
              comparison = <value optimized out>
      #2  0x00007f7f99d8baf2 in mongoc_set_get (set=<value optimized out>, id=<value optimized out>) at /var/tmp/mongodb/src/libmongoc/src/mongoc/mongoc-set.c:114
              ptr = <value optimized out>
              key = {id = 1, item = 0x14d6e00}
      #3  0x00007f7f99d93817 in mongoc_topology_description_server_by_id (description=<value optimized out>, id=1, error=0x0) at /var/tmp/mongodb/src/libmongoc/src/mongoc/mongoc-topology-description.c:557
              sd = <value optimized out>
              __func__ = "mongoc_topology_description_server_by_id"
      #4  0x00007f7f99d914db in mongoc_topology_server_by_id (topology=0x7f7f8c7f0c40, id=1, error=0x0) at /var/tmp/mongodb/src/libmongoc/src/mongoc/mongoc-topology.c:571
              sd = <value optimized out>
      #5  0x00007f7f99d777c5 in mongoc_cluster_stream_for_server (cluster=0x7f7f8c7f5cc0, server_id=1, reconnect_ok=false, error=0x0) at /var/tmp/mongodb/src/libmongoc/src/mongoc/mongoc-cluster.c:1418
              topology = <value optimized out>
              sd = <value optimized out>
              server_stream = 0x0
              __func__ = "mongoc_cluster_stream_for_server"
      #6  0x00007f7f99d71f8e in _mongoc_client_kill_cursor (client=0x7f7f8c7f5ca8, server_id=1, cursor_id=584888672639, db=0x7fff842d15e0 "opensky_devo", collection=0x7f7f8c837535 "sellables")
          at /var/tmp/mongodb/src/libmongoc/src/mongoc/mongoc-client.c:1282
              server_stream = <value optimized out>
              __func__ = "_mongoc_client_kill_cursor"
      #7  0x00007f7f99d7e0db in _mongoc_cursor_destroy (cursor=0x7f7f8c8373e8) at /var/tmp/mongodb/src/libmongoc/src/mongoc/mongoc-cursor.c:273
              db = "opensky_devo\000\000\000\000\340\001\000\000\000\000\000\000Ҧ[\000\000\000\000\000\320\004\000\000\000\000\000\000Ҧ[\000\000\000\000\000\250~\223\001\000\000\000\000Pʐ\001\000\000\000\000\020\002\000\000\000\000\000\000Ҧ[\000\000\000\000\000X\000\000\000\000\000\000\000Ҧ[\000\000\000\000\000\220\000\000\000\000\000\000\000Ҧ[\000\000\000\000\000X%\200\214\177\177\000\000\026\000\000\000\000\000\000"
              __func__ = "_mongoc_cursor_destroy"
      #8  0x00007f7f99d7e198 in mongoc_cursor_destroy (cursor=0x7f7f8c8373e8) at /var/tmp/mongodb/src/libmongoc/src/mongoc/mongoc-cursor.c:249
      ---Type <return> to continue, or q <return> to quit---
              __func__ = "mongoc_cursor_destroy"
      #9  0x00007f7f99d9ebe2 in php_phongo_cursor_free (cursor=0x7f7f8c844a20) at /var/tmp/mongodb/php_phongo.c:2142
      No locals.
      #10 0x00007f7f99d50a41 in php_phongo_cursor_free_object (object=0x7f7f8c844a20) at /var/tmp/mongodb/src/MongoDB/Cursor.c:226
              intern = 0x7f7f8c844a20
      #11 0x000000000060bbac in zend_objects_store_free_object_storage ()
      No symbol table info available.
      #12 0x00000000005d4493 in ?? ()
      No symbol table info available.
      #13 0x00000000005e2652 in ?? ()
      No symbol table info available.
      #14 0x00000000005820d1 in php_request_shutdown ()
      No symbol table info available.
      #15 0x0000000000693b8f in ?? ()
      No symbol table info available.
      #16 0x0000000000694f98 in ?? ()
      No symbol table info available.
      #17 0x000000357201ed5d in __libc_start_main (main=0x694910, argc=2, ubp_av=0x7fff842d2fd8, init=<value optimized out>, fini=<value optimized out>, rtld_fini=<value optimized out>, stack_end=0x7fff842d2fc8)
          at libc-start.c:226
              result = <value optimized out>
              unwind_buf = {cancel_jmp_buf = {{jmp_buf = {0, -4001403079134328888, 4331088, 140735410941904, 0, 0, 4001203369062123464, -4029933203728152632}, mask_was_saved = 0}}, priv = {pad = {0x0, 0x0, 0x0, 0x1},
                  data = {prev = 0x0, cleanup = 0x0, canceltype = 0}}}
              not_first_call = <value optimized out>
      #18 0x0000000000421679 in _start ()
      No symbol table info available.
      

      The following script reproduces a crash and may be useful for creating a test case:

      function wrapCursor(MongoDB\Driver\Cursor $cursor)
      {
          foreach ($cursor as $key => $value) {
              yield $key => $value;
          }
      }
      
      $manager = new MongoDB\Driver\Manager(STANDALONE);
      
      $bulk = new MongoDB\Driver\BulkWrite();
      $bulk->insert(['_id' => 1]);
      $bulk->insert(['_id' => 2]);
      $bulk->insert(['_id' => 3]);
      $manager->executeBulkWrite(NS, $bulk);
      
      $cursor = $manager->executeQuery(NS, new MongoDB\Driver\Query([], ['batchSize' => 2]));
      $generator = wrapCursor($cursor);
      
      foreach ($generator as $value) {
          exit(0);
      }
      

      At present, the work-around for this crash is to ensure that the cursor is exhausted (i.e. all batches are fetched from the server), which will prevent libmongoc from killing it. For PHPC-671, a crash could be avoided by unsetting the cursor before the manager instance, but that may not apply if the cursor is within a generator.

            Assignee:
            jmikola@mongodb.com Jeremy Mikola
            Reporter:
            jmikola@mongodb.com Jeremy Mikola
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: