Exploit-DB #46299
Description: macOS < 10.14.3 / iOS < 12.1.3 XNU - 'vm_map_copy' Optimization which Requires Atomicity isn't Atomic
Type: dos
Platform: multiple
Date: 2019-01-31
Author: Google Security Research

Exploit:
/*
vm_map_copyin_internal in vm_map.c converts a region of a vm_map into "copied in" form, constructing a vm_map_copy
structure representing the copied memory which can then be mapped into another vm_map (or the same one.)

The function contains a while loop which walks through each of the vm_map_entry structures which make up the
region to be copied and tries to append a "copy" of each in turn to a vm_map_copy structure.

Under certain circumstances the copy operation can be optimized, here's a code snippet describing one such optimization:

        
         //  Attempt non-blocking copy-on-write optimizations.
         
        
        if (src_destroy &&
            (src_object == VM_OBJECT_NULL ||
             (src_object->internal &&
              src_object->copy_strategy == MEMORY_OBJECT_COPY_SYMMETRIC &&
              !map_share))) {
                 /*
                  * If we are destroying the source, and the object
                  * is internal, we can move the object reference
                  * from the source to the copy.  The copy is
                  * copy-on-write only if the source is.
                  * We make another reference to the object, because
                  * destroying the source entry will deallocate it.
                  
                 vm_object_reference(src_object);
                 
                 /*
                  * Copy is always unwired.  vm_map_copy_entry
                  * set its wired count to zero.
                  
                 
                 goto CopySuccessful;
            

This optimization will apply if the vm_map_entry represents anonymous memory and the semantics of the copy
will cause that memory to be deallocated from the source map. In this case, as the comment describes, we can just
"move" the entry to the target map.

The issue is that this move is not performed atomically - the vm_map_entry which we want to move will only be removed
from the source map after we have copied all the entries representing the region we want to copy and the while(true) loop
is done:

    }   // end while(true)
    
    /*
     * If the source should be destroyed, do it now, since the
     * copy was successful.
     
    if (src_destroy) {
        (void) vm_map_delete(
                             src_map,
                             vm_map_trunc_page(src_addr,
                                               VM_MAP_PAGE_MASK(src_map)),
                             src_end,
                             ((src_map == kernel_map) ?
                              VM_MAP_REMOVE_KUNWIRE :
                              VM_MAP_NO_FLAGS),
                             VM_MAP_NULL);


The cause of the lack of atomicity is two-fold:

Firstly: in the while loop the vm_map's lock gets dropped and retaken:

        /*
         *  Create a new address map entry to hold the result.
         *  Fill in the fields from the appropriate source entries.
         *  We must unlock the source map to do this if we need
         *  to allocate a map entry.
         
        if (new_entry == VM_MAP_ENTRY_NULL) {
            version.main_timestamp = src_map->timestamp;
            vm_map_unlock(src_map);
            
            new_entry = vm_map_copy_entry_create(copy, !copy->cpy_hdr.entries_pageable);
            
            vm_map_lock(src_map);
            if ((version.main_timestamp + 1) != src_map->timestamp) {
                if (!vm_map_lookup_entry(src_map, src_start,
                                         &tmp_entry)) {
                    RETURN(KERN_INVALID_ADDRESS);
                }
                if (!tmp_entry->is_sub_map)
                    vm_map_clip_start(src_map, tmp_entry, src_start);
                continue; /* restart w/ new tmp_entry 
            }
        }

Here, each time they allocate a new entry structure they have to drop and retake the lock.
Secondly: the check and bailout there aren't sufficient to ensure atomicity of the "entry move" optimization:

The check "if ((version.main_timestamp + 1) != src_map->timestamp) {" tries to detect whether another thread
took the lock while we dropped it; if it did then they try to bailout and lookup the entry again.

The problem is that just checking whether there is still an entry covering the address we're currently trying to
copy isn't sufficient, since the continue statement just goes back up to the start of the while loop. It doesn't
invalidate all the entries we've already appended to the vm_map_copy, even though while we had dropped the lock
another thread could have come in and also started an "optimized entry move" for the same entry that we're in the process
of moving.

Note that this lock dropping and reacquiring pattern seems quite pervasive; this isn't the only place in the code where this
happens; a proper fix for such issues will require some refactoring.

This PoC demonstrates the issue by sending two mach messages in parallel, one to ourselves and one to our parent process.

Those messages contain out-of-line regions built up of alternating memory object mappings and anonymous memory:

                      _ _ _ _ _ _ _ _ _ _ _ _ 
  <................../..>                    \
  <                 {   >                     }
  < +---+---+---+---{--->---+---+---+---+---+ }
  < | A | O | A | O { A > O | A | O | A | O | }
  < +---+---+---+---{--->---+---+---+---+---+ }
  <.................{...>                     }
           ^         \_ _ _ _ _ _ _ _ _ _ _ _/
           |                    ^
  OOL desc for msg_a            |
                          OOL desc for msg_b    
          

The two messages overlap by one anonymous memory entry, appearing at the end of msg_a and the start of msg_b. The locking issue
means that if we win the race this entry will have the move optimization applied twice, leading to it appearing in two vm_map_copys.