Home | History | Annotate | Download | only in rawdex
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package dexfuzz.rawdex;
     18 
     19 import dexfuzz.Log;
     20 
     21 import java.io.IOException;
     22 import java.util.ArrayList;
     23 import java.util.HashMap;
     24 import java.util.List;
     25 import java.util.Map;
     26 
     27 /**
     28  * This class allows the recording of both Offsettable items (that is, items that can be
     29  * referred to by an offset somewhere else in the file - RawDexObjects) and Offsets.
     30  * The idea in a nutshell is that for every Offsettable item we read, we remember
     31  * its original position in the file using a map, and the order in which the Offsettables were
     32  * written out. We also remember every Offset we read in, and its value. Then, after reading
     33  * the whole file, we use the map to find the Offsettable it pointed at.
     34  * Then, as we write out the file, for every Offsettable we write out, we record its new position,
     35  * using the order we collected earlier. For every Offset we write out, we look at its Offsettable
     36  * to see where it was written. If it hasn't been written yet, then we write out a blank value
     37  * for the time being, remember where that blank value was written, and put the Offset into a
     38  * table for patching once everything has been written out.
     39  * There are some variables (index_after_map_list, restore_point) used for remembering certain
     40  * points to jump forward and back to, because we cannot read and write the file out in exactly
     41  * the same order.
     42  * TODO: Perhaps it makes more sense to just reorder the offsettable_table once it's been read,
     43  * in preparation for the order in which the file is written out?
     44  * Finally, we provide methods for adding new Offsettable items into the right place in the order
     45  * table.
     46  */
     47 public class OffsetTracker {
     48   /**
     49    * A map from the original offset in the input DEX file to
     50    * the Offsettable it points to. (That Offsettable will contain
     51    * the actual item, and later on the new offset for the item when
     52    * the item is written out.
     53    */
     54   private Map<Integer, Offsettable> offsettableMap;
     55 
     56   /**
     57    * A table of all Offsettables. We need to ensure we write out
     58    * all items in the same order we read them in, to make sure we update
     59    * the Offsettable.new_position field with the correct value wrt to
     60    * the original_position field.
     61    */
     62   private List<Offsettable> offsettableTable;
     63 
     64   /**
     65    * A table of all offsets that is populated as we read in the DEX file.
     66    * As the end, we find the correct Offsettable for the Offset in the above
     67    * map, and associate them.
     68    */
     69   private List<Offset> needsAssociationTable;
     70 
     71   /**
     72    * A table of all offsets that we could not write out an updated offset for
     73    * as we write out a DEX file. Will be checked after writing is complete,
     74    * to allow specific patching of each offset's location as at that point
     75    * all Offsettables will have been updated with their new position.
     76    */
     77   private List<Offset> needsUpdateTable;
     78 
     79   /**
     80    * Tracks how far we are through the offsettable_table as we write out the file.
     81    */
     82   private int offsettableTableIdx;
     83 
     84   /**
     85    * Because we write in a slightly different order to how we read
     86    * (why? to read, we read the header, then the map list, and then use the map
     87    *  list to read everything else.
     88    *  when we write, we write the header, and then we cannot write the map list
     89    *  because we don't know where it will go yet, so we write everything else first)
     90    * so: we remember the position in the offsettable_table after we read the map list,
     91    * so we can skip there after we write out the header.
     92    */
     93   private int indexAfterMapList;
     94 
     95   /**
     96    * Related to index_after_map_list, this is the index we save when we're jumping back to
     97    * write the map list.
     98    */
     99   private int restorePoint;
    100 
    101   /**
    102    * Create a new OffsetTracker. Should persist between parsing a DEX file, and outputting
    103    * the mutated DEX file.
    104    */
    105   public OffsetTracker() {
    106     offsettableMap = new HashMap<Integer,Offsettable>();
    107     offsettableTable = new ArrayList<Offsettable>();
    108     needsAssociationTable = new ArrayList<Offset>();
    109     needsUpdateTable = new ArrayList<Offset>();
    110   }
    111 
    112   /**
    113    * Lookup an Item by the offset it had in the input DEX file.
    114    * @param offset The offset in the input DEX file.
    115    * @return The corresponding Item.
    116    */
    117   public RawDexObject getItemByOffset(int offset) {
    118     return offsettableMap.get(offset).getItem();
    119   }
    120 
    121   /**
    122    * As Items are read in, they call this function once they have word-aligned the file pointer,
    123    * to record their position and themselves into an Offsettable object, that will be tracked.
    124    * @param file Used for recording position into the new Offsettable.
    125    * @param item Used for recording the relevant Item into the new Offsettable.
    126    */
    127   public void getNewOffsettable(DexRandomAccessFile file, RawDexObject item) throws IOException {
    128     Offsettable offsettable = new Offsettable(item, false);
    129     offsettable.setOriginalPosition((int) file.getFilePointer());
    130     offsettableMap.put(offsettable.getOriginalPosition(), offsettable);
    131     offsettableTable.add(offsettable);
    132   }
    133 
    134   /**
    135    * As Items read in Offsets, they call this function with the offset they originally
    136    * read from the file, to allow later association with an Offsettable.
    137    * @param originalOffset The original offset read from the input DEX file.
    138    * @return An Offset that will later be associated with an Offsettable.
    139    */
    140   public Offset getNewOffset(int originalOffset) throws IOException {
    141     Offset offset = new Offset(false);
    142     offset.setOriginalOffset(originalOffset);
    143     needsAssociationTable.add(offset);
    144     return offset;
    145   }
    146 
    147   /**
    148    * Only MapItem should call this method, when the MapItem that points to the header
    149    * is read.
    150    */
    151   public Offset getNewHeaderOffset(int originalOffset) throws IOException {
    152     Offset offset = new Offset(true);
    153     offset.setOriginalOffset(originalOffset);
    154     needsAssociationTable.add(offset);
    155     return offset;
    156   }
    157 
    158   /**
    159    * Call this after reading, to associate Offsets with Offsettables.
    160    */
    161   public void associateOffsets() {
    162     for (Offset offset : needsAssociationTable) {
    163       if (offset.getOriginalOffset() == 0 && !(offset.pointsAtHeader())) {
    164         offset.setPointsAtNull();
    165       } else {
    166         offset.pointTo(offsettableMap.get(offset.getOriginalOffset()));
    167         if (!offset.pointsToSomething()) {
    168           Log.error(String.format("Couldn't find original offset 0x%x!",
    169               offset.getOriginalOffset()));
    170         }
    171       }
    172     }
    173     needsAssociationTable.clear();
    174   }
    175 
    176   /**
    177    * As Items are written out into the output DEX file, this function is called
    178    * to update the next Offsettable with the file pointer's current position.
    179    * This should allow the tracking of new offset locations.
    180    * This also requires that reading and writing of all items happens in the same order
    181    * (with the exception of the map list, see above)
    182    * @param file Used for recording the new position.
    183    */
    184   public void updatePositionOfNextOffsettable(DexRandomAccessFile file) throws IOException {
    185     if (offsettableTableIdx == offsettableTable.size()) {
    186       Log.errorAndQuit("Not all created Offsettable items have been added to the "
    187           + "Offsettable Table!");
    188     }
    189     Offsettable offsettable = offsettableTable.get(offsettableTableIdx);
    190     offsettable.setNewPosition((int) file.getFilePointer());
    191     offsettableTableIdx++;
    192   }
    193 
    194   /**
    195    * As Items are written out, any writing out of an offset must call this function, passing
    196    * in the relevant offset. This function will write out the offset, if the associated
    197    * Offsettable has been updated with its new position, or else will write out a null value, and
    198    * the Offset will be stored for writing after all Items have been written, and all
    199    * Offsettables MUST have been updated.
    200    * @param offset The offset received from getNewOffset().
    201    * @param file Used for writing out to the file.
    202    * @param useUleb128 Whether or not the offset should be written in UINT or ULEB128 form.
    203    */
    204   public void tryToWriteOffset(Offset offset, DexRandomAccessFile file, boolean useUleb128)
    205       throws IOException {
    206     if (!offset.isNewOffset() && (!offset.pointsToSomething())) {
    207       if (useUleb128) {
    208         file.writeUleb128(0);
    209       } else {
    210         file.writeUInt(0);
    211       }
    212       return;
    213     }
    214 
    215     if (offset.readyForWriting()) {
    216       if (useUleb128) {
    217         file.writeUleb128(offset.getNewPositionOfItem());
    218       } else {
    219         file.writeUInt(offset.getNewPositionOfItem());
    220       }
    221     } else {
    222       offset.setOutputLocation((int) file.getFilePointer());
    223       if (useUleb128) {
    224         file.writeLargestUleb128(offset.getOriginalOffset());
    225         offset.setUsesUleb128();
    226       } else {
    227         file.writeUInt(offset.getOriginalOffset());
    228       }
    229       needsUpdateTable.add(offset);
    230     }
    231   }
    232 
    233   /**
    234    * This is called after all writing has finished, to write out any Offsets
    235    * that could not be written out during the original writing phase, because their
    236    * associated Offsettables hadn't had their new positions calculated yet.
    237    * @param file Used for writing out to the file.
    238    */
    239   public void updateOffsets(DexRandomAccessFile file) throws IOException {
    240     if (offsettableTableIdx != offsettableTable.size()) {
    241       Log.errorAndQuit("Being asked to update dangling offsets but the "
    242           + "correct number of offsettables has not been written out!");
    243     }
    244     for (Offset offset : needsUpdateTable) {
    245       file.seek(offset.getOutputLocation());
    246       if (offset.usesUleb128()) {
    247         file.writeLargestUleb128(offset.getNewPositionOfItem());
    248       } else {
    249         file.writeUInt(offset.getNewPositionOfItem());
    250       }
    251     }
    252     needsUpdateTable.clear();
    253   }
    254 
    255   /**
    256    * Called after writing out the header, to skip to after the map list.
    257    */
    258   public void skipToAfterMapList() {
    259     offsettableTableIdx = indexAfterMapList;
    260   }
    261 
    262   /**
    263    * Called once the map list needs to be written out, to set the
    264    * offsettable table index back to the right place.
    265    */
    266   public void goBackToMapList() {
    267     restorePoint = offsettableTableIdx;
    268     offsettableTableIdx = (indexAfterMapList - 1);
    269   }
    270 
    271   /**
    272    * Called once the map list has been written out, to set the
    273    * offsettable table index back to where it was before.
    274    */
    275   public void goBackToPreviousPoint() {
    276     if (offsettableTableIdx != indexAfterMapList) {
    277       Log.errorAndQuit("Being asked to go to the point before the MapList was written out,"
    278           + " but we're not in the right place.");
    279     }
    280     offsettableTableIdx = restorePoint;
    281   }
    282 
    283   /**
    284    * Called after reading in the map list, to remember the point to be skipped
    285    * to later.
    286    */
    287   public void rememberPointAfterMapList() {
    288     indexAfterMapList = offsettableTable.size();
    289   }
    290 
    291   private void updateHeaderOffsetIfValid(Offset offset, Offsettable previousFirst,
    292       Offsettable newFirst, String offsetName) {
    293     if (offset.pointsToThisOffsettable(previousFirst)) {
    294       offset.pointToNew(newFirst);
    295     } else {
    296       Log.errorAndQuit("Header " + offsetName + " offset not pointing at first element?");
    297     }
    298   }
    299 
    300   private void addTypeListsToMapFile(RawDexFile rawDexFile, Offsettable typeListOffsettable) {
    301     // Create a MapItem for the TypeLists
    302     MapItem typeListMapItem = new MapItem();
    303     typeListMapItem.offset = new Offset(false);
    304     typeListMapItem.offset.pointToNew(typeListOffsettable);
    305     typeListMapItem.type = MapItem.TYPE_TYPE_LIST;
    306     typeListMapItem.size = 1;
    307 
    308     // Insert into the MapList.
    309     // (first, find the MapItem that points to StringDataItems...)
    310     int idx = 0;
    311     for (MapItem mapItem : rawDexFile.mapList.mapItems) {
    312       if (mapItem.type == MapItem.TYPE_STRING_DATA_ITEM) {
    313         break;
    314       }
    315       idx++;
    316     }
    317     // (now insert the TypeList MapItem just before the StringDataItem one...)
    318     rawDexFile.mapList.mapItems.add(idx, typeListMapItem);
    319   }
    320 
    321   private void addFieldIdsToHeaderAndMapFile(RawDexFile rawDexFile,
    322       Offsettable fieldOffsettable) {
    323     // Add the field IDs to the header.
    324     rawDexFile.header.fieldIdsOff.unsetNullAndPointTo(fieldOffsettable);
    325     rawDexFile.header.fieldIdsSize = 1;
    326 
    327     // Create a MapItem for the field IDs.
    328     MapItem fieldMapItem = new MapItem();
    329     fieldMapItem.offset = new Offset(false);
    330     fieldMapItem.offset.pointToNew(fieldOffsettable);
    331     fieldMapItem.type = MapItem.TYPE_FIELD_ID_ITEM;
    332     fieldMapItem.size = 1;
    333 
    334     // Insert into the MapList.
    335     // (first, find the MapItem that points to MethodIdItems...)
    336     int idx = 0;
    337     for (MapItem mapItem : rawDexFile.mapList.mapItems) {
    338       if (mapItem.type == MapItem.TYPE_METHOD_ID_ITEM) {
    339         break;
    340       }
    341       idx++;
    342     }
    343     // (now insert the FieldIdItem MapItem just before the MethodIdItem one...)
    344     rawDexFile.mapList.mapItems.add(idx, fieldMapItem);
    345   }
    346 
    347 
    348   private void updateOffsetsInHeaderAndMapFile(RawDexFile rawDexFile,
    349       Offsettable newFirstOffsettable) {
    350     Offsettable prevFirstOffsettable = null;
    351     for (int i = 0; i < offsettableTable.size(); i++) {
    352       if (offsettableTable.get(i) == newFirstOffsettable) {
    353         prevFirstOffsettable = offsettableTable.get(i + 1);
    354         break;
    355       }
    356     }
    357     if (prevFirstOffsettable == null) {
    358       Log.errorAndQuit("When calling updateMapListOffsets, could not find new "
    359           + "first offsettable?");
    360     }
    361 
    362     // Based on the type of the item we just added, check the relevant Offset in the header
    363     // and if it pointed at the prev_first_offsettable, make it point at the new one.
    364     // NB: if it isn't pointing at the prev one, something is wrong.
    365     HeaderItem header = rawDexFile.header;
    366     if (newFirstOffsettable.getItem() instanceof StringIdItem) {
    367       updateHeaderOffsetIfValid(header.stringIdsOff, prevFirstOffsettable,
    368           newFirstOffsettable, "StringID");
    369     } else if (newFirstOffsettable.getItem() instanceof TypeIdItem) {
    370       updateHeaderOffsetIfValid(header.typeIdsOff, prevFirstOffsettable,
    371           newFirstOffsettable, "TypeID");
    372     } else if (newFirstOffsettable.getItem() instanceof ProtoIdItem) {
    373       updateHeaderOffsetIfValid(header.protoIdsOff, prevFirstOffsettable,
    374           newFirstOffsettable, "ProtoID");
    375     } else if (newFirstOffsettable.getItem() instanceof FieldIdItem) {
    376       updateHeaderOffsetIfValid(header.fieldIdsOff, prevFirstOffsettable,
    377           newFirstOffsettable, "FieldID");
    378     } else if (newFirstOffsettable.getItem() instanceof MethodIdItem) {
    379       updateHeaderOffsetIfValid(header.methodIdsOff, prevFirstOffsettable,
    380           newFirstOffsettable, "MethodID");
    381     } else if (newFirstOffsettable.getItem() instanceof ClassDefItem) {
    382       updateHeaderOffsetIfValid(header.classDefsOff, prevFirstOffsettable,
    383           newFirstOffsettable, "ClassDef");
    384     }
    385 
    386     // Now iterate through the MapList's MapItems, and see if their Offsets pointed at the
    387     // prev_first_offsettable, and if so, make them now point at the new_first_offsettable.
    388     for (MapItem mapItem : rawDexFile.mapList.mapItems) {
    389       if (mapItem.offset.pointsToThisOffsettable(prevFirstOffsettable)) {
    390         Log.info("Updating offset in MapItem (type: " + mapItem.type + ") after "
    391             + "we called insertNewOffsettableAsFirstOfType()");
    392         mapItem.offset.pointToNew(newFirstOffsettable);
    393       }
    394     }
    395   }
    396 
    397   private void insertOffsettableAt(int idx, Offsettable offsettable) {
    398     offsettableTable.add(idx, offsettable);
    399     if (indexAfterMapList > idx) {
    400       indexAfterMapList++;
    401     }
    402     if (restorePoint > idx) {
    403       restorePoint++;
    404     }
    405   }
    406 
    407   /**
    408    * If we're creating our first TypeList, then IdCreator has to call this method to
    409    * ensure it gets put into the correct place in the offsettable table.
    410    * This assumes TypeLists always come before StringDatas.
    411    */
    412   public Offsettable insertNewOffsettableAsFirstEverTypeList(RawDexObject item,
    413       RawDexFile rawDexFile) {
    414     // We find the first StringDataItem, the type lists will come before this.
    415     Log.info("Calling insertNewOffsettableAsFirstEverTypeList()");
    416     for (int i = 0; i < offsettableTable.size(); i++) {
    417       if (offsettableTable.get(i).getItem() instanceof StringDataItem) {
    418         Offsettable offsettable = new Offsettable(item, true);
    419         insertOffsettableAt(i, offsettable);
    420         addTypeListsToMapFile(rawDexFile, offsettable);
    421         return offsettable;
    422       }
    423     }
    424     Log.errorAndQuit("Could not find any StringDataItems to insert the type list before.");
    425     return null;
    426   }
    427 
    428   /**
    429    * If we're creating our first FieldId, then IdCreator has to call this method to
    430    * ensure it gets put into the correct place in the offsettable table.
    431    * This assumes FieldIds always come before MethodIds.
    432    */
    433   public Offsettable insertNewOffsettableAsFirstEverField(RawDexObject item,
    434       RawDexFile rawDexFile) {
    435     // We find the first MethodIdItem, the fields will come before this.
    436     Log.info("Calling insertNewOffsettableAsFirstEverField()");
    437     for (int i = 0; i < offsettableTable.size(); i++) {
    438       if (offsettableTable.get(i).getItem() instanceof MethodIdItem) {
    439         Offsettable offsettable = new Offsettable(item, true);
    440         insertOffsettableAt(i, offsettable);
    441         addFieldIdsToHeaderAndMapFile(rawDexFile, offsettable);
    442         return offsettable;
    443       }
    444     }
    445     Log.errorAndQuit("Could not find any MethodIdItems to insert the field before.");
    446     return null;
    447   }
    448 
    449   /**
    450    * If we're creating a new Item (such as FieldId, MethodId) that is placed into the
    451    * first position of the relevant ID table, then IdCreator has to call this method to
    452    * ensure it gets put into the correct place in the offsettable table.
    453    */
    454   public Offsettable insertNewOffsettableAsFirstOfType(RawDexObject item,
    455       RawDexFile rawDexFile) {
    456     Log.debug("Calling insertNewOffsettableAsFirstOfType()");
    457     int index = getOffsettableIndexForFirstItemType(item);
    458     if (index == -1) {
    459       Log.errorAndQuit("Could not find any object of class: " + item.getClass());
    460     }
    461     Offsettable offsettable = new Offsettable(item, true);
    462     insertOffsettableAt(index, offsettable);
    463     updateOffsetsInHeaderAndMapFile(rawDexFile, offsettable);
    464     return offsettable;
    465   }
    466 
    467   /**
    468    * IdCreator has to call this method when it creates a new IdItem, to make sure it
    469    * gets put into the correct place in the offsettable table. IdCreator should
    470    * provide the IdItem that should come before this new IdItem.
    471    */
    472   public Offsettable insertNewOffsettableAfter(RawDexObject item, RawDexObject itemBefore) {
    473     Log.debug("Calling insertNewOffsettableAfter()");
    474     int index = getOffsettableIndexForItem(itemBefore);
    475     if (index == -1) {
    476       Log.errorAndQuit("Did not find specified 'after' object in offsettable table.");
    477     }
    478     Offsettable offsettable = new Offsettable(item, true);
    479     insertOffsettableAt(index + 1, offsettable);
    480     return offsettable;
    481   }
    482 
    483   private int getOffsettableIndexForFirstItemType(RawDexObject item) {
    484     Class<?> itemClass = item.getClass();
    485     for (int i = 0; i < offsettableTable.size(); i++) {
    486       if (offsettableTable.get(i).getItem().getClass().equals(itemClass)) {
    487         return i;
    488       }
    489     }
    490     return -1;
    491   }
    492 
    493   private int getOffsettableIndexForItem(RawDexObject item) {
    494     for (int i = 0; i < offsettableTable.size(); i++) {
    495       if (offsettableTable.get(i).getItem() == item) {
    496         return i;
    497       }
    498     }
    499     return -1;
    500   }
    501 
    502   /**
    503    * Given a RawDexObject, get the Offsettable that contains it.
    504    */
    505   public Offsettable getOffsettableForItem(RawDexObject item) {
    506     for (int i = 0; i < offsettableTable.size(); i++) {
    507       if (offsettableTable.get(i).getItem() == item) {
    508         return offsettableTable.get(i);
    509       }
    510     }
    511     return null;
    512   }
    513 }
    514