Bundle/Intent傳遞序列化參數暗藏殺機!


前幾天一個朋友跟我說了一個詭異且恐怖的事情,有個人用了幾行代碼就讓他們的app歇菜了。
這勾起了我極大的興趣,於是我親自嘗試了一下。代碼非常簡單,如下:
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.test.test", "com.test.test.MainActivity"));
intent.putExtra("anykey", new Boom());
startActivity(intent);
其中Boom是一個序列化類(serializable),而且extra的key可以是任何值。
com.test.test.MainActivity則是另外一個app中允許外部調起的activity,即MainActivity有一個爲android.intent.action.MAIN的action,否則代碼會報錯。

還需要滿足一個條件,MainActivity代碼中有從intent(getIntent或newIntent的參數)取參數的操作,如
Bundle bundle = intent.getExtras();
if (bundle != null) {
    int sd = bundle.getInt("key");
}


int sd = intent.getIntExtra("key", -1);
注意,不僅僅是getInt,任何類型的都會出問題,而且key不必與之前的anykey一樣!

崩潰日誌如下:
E/AndroidRuntime: FATAL EXCEPTION: main
  Process: xxx, PID: 1688
  java.lang.RuntimeException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = com.example.Boom)
  at android.os.Parcel.readSerializable(Parcel.java:2630)
  at android.os.Parcel.readValue(Parcel.java:2416)
  at android.os.Parcel.readArrayMapInternal(Parcel.java:2732)
  at android.os.BaseBundle.unparcel(BaseBundle.java:271)
  at android.os.BaseBundle.get(BaseBundle.java:364)
  at com.test.test.MainActivity.onNewIntent(MainActivity.java:128)
  ...
  Caused by: java.lang.ClassNotFoundException: com.example.Boom
  at java.lang.Class.classForName(Native Method)
  at java.lang.Class.forName(Class.java:400)
  at android.os.Parcel$2.resolveClass(Parcel.java:2616)
  at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1613)
  at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
  at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772)
  at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
  at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
  at android.os.Parcel.readSerializable(Parcel.java:2624)
  at android.os.Parcel.readValue(Parcel.java:2416)
  at android.os.Parcel.readArrayMapInternal(Parcel.java:2732)
  at android.os.BaseBundle.unparcel(BaseBundle.java:271)
  at android.os.BaseBundle.get(BaseBundle.java:364)
  at com.test.test.MainActivity.onNewIntent(MainActivity.java:128)
  ...
02-27 17:33:33.799 1688-1688/? E/AndroidRuntime: Caused by: java.lang.ClassNotFoundException: Didn't find class "com.example.Boom" on path: DexPathList[...]
  at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:380)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
  ... 27 more
02-27 17:33:33.813 1688-1688/? E/MobclickAgent: onPause called before onResume

可以看到是因爲應用中沒有Boom這個類,反序列化時找不到,那麼既然應用中沒有用到anykey,爲什麼會去做反序列化的操作呢?
查看Intent及Bundle源碼可以發現,那些get函數最終都會調用BaseBundle的相應的get函數。
BaseBundle中不論get函數還是put函數中都會先調用unparcel函數,如:
public int getInt(String key, int defaultValue) {
    unparcel();
    Object o = mMap.get(key);
    if (o == null) {
        return defaultValue;
    }
    try {
        return (Integer) o;
    } catch (ClassCastException e) {
        typeWarning(key, o, "Integer", defaultValue, e);
        return defaultValue;
    }
}

public void putString(@Nullable String key, @Nullable String value) {
    unparcel();
    mMap.put(key, value);
}

unparcel函數我們後面再說,先看看在這兩種函數中存取數據實際上都是在mMap中做的,這是BaseBundle中一個重要的參數,它存儲着Bundle的數據。
那麼這個mMap中的數據又是哪裏來的呢?

下面我們就來看看這個unparcel函數,關鍵源碼如下:

/* package */ synchronized void unparcel() {
    synchronized (this) {
        ...

        ArrayMap<String, Object> map = mMap;
        if (map == null) {
            map = new ArrayMap<>(N);
        } else {
            map.erase();
            map.ensureCapacity(N);
        }
        try {
            mParcelledData.readArrayMapInternal(map, N, mClassLoader);
        } catch (BadParcelableException e) {
            if (sShouldDefuse) {
                Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
                map.erase();
            } else {
                throw e;
            }
        } finally {
            mMap = map;
            mParcelledData.recycle();
            mParcelledData = null;
        }
        if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this))
                + " final map: " + mMap);
    }
}

這裏面涉及到了Bundle中的兩個重要參數mMap和mParcelledData,mMap我們上面說過,另外一個mParcelledData則是一個Parcel對象。它是怎麼來的呢?
這要從activity的啓動過程來說,參見探索startActivity流程及在Activity間是如何傳遞Intent的。在這篇文章的最後,我們看到在ActivityManagerNative中onTransact函數中處理binder接收的消息,其中就有這麼一行:
Bundle options = data.readInt() != 0
  ? Bundle.CREATOR.createFromParcel(data) : null;

這行的作用就是從binder的消息中解析出傳送過來的Bundle數據,繼續看來Bundle.CREATOR:
public static final Parcelable.Creator<Bundle> CREATOR =
    new Parcelable.Creator<Bundle>() {
    @Override
    public Bundle createFromParcel(Parcel in) {
        return in.readBundle();
    }

    @Override
    public Bundle[] newArray(int size) {
        return new Bundle[size];
    }
};

createFromParcel函數其實就是調用來Parcel的readBundle函數,代碼如下:
public final Bundle readBundle() {
    return readBundle(null);
}

public final Bundle readBundle(ClassLoader loader) {
    int length = readInt();
    ...
    final Bundle bundle = new Bundle(this, length);
    ...
    return bundle;
}

通過Bundle的構造函數來新建了一個對象,這個構造函數則調用了父類BaseBundle對應的構造函數如下:

BaseBundle(Parcel parcelledData, int length) {
    readFromParcelInner(parcelledData, length);
}

private void readFromParcelInner(Parcel parcel, int length) {
    ...

    Parcel p = Parcel.obtain();
    p.setDataPosition(0);
    p.appendFrom(parcel, offset, length);
    if (DEBUG) Log.d(TAG, "Retrieving "  + Integer.toHexString(System.identityHashCode(this))
            + ": " + length + " bundle bytes starting at " + offset);
    p.setDataPosition(0);

    mParcelledData = p;
}

這樣我們就在readFromParcelInner函數中找到了mParcelledData的來源,它實際上就是傳送過來的Bundle序列化後的數據。
那麼就有了另外一個疑問,既然傳送過來的只有mParcelledData,那麼mMap中其實是空的,那麼get函數怎麼取到值的?
這就是爲什麼每個get和put函數都先調用unparcel函數的原因。繼續觀察上面的unparcel函數,我們發現“mParcelledData.readArrayMapInternal(map, N, mClassLoader);”這句代碼,調用了Parcel的readArrayMapInternal函數,並且傳入了map,這個map後面會賦值給mMap,所以實際上兩者是一致的。函數的源碼如下:
/* package */ void readArrayMapInternal(ArrayMap outVal, int N,
    ClassLoader loader) {
    if (DEBUG_ARRAY_MAP) {
        RuntimeException here =  new RuntimeException("here");
        here.fillInStackTrace();
        Log.d(TAG, "Reading " + N + " ArrayMap entries", here);
    }
    int startPos;
    while (N > 0) {
        if (DEBUG_ARRAY_MAP) startPos = dataPosition();
        String key = readString();
        Object value = readValue(loader);
        if (DEBUG_ARRAY_MAP) Log.d(TAG, "  Read #" + (N-1) + " "
                + (dataPosition()-startPos) + " bytes: key=0x"
                + Integer.toHexString((key != null ? key.hashCode() : 0)) + " " + key);
        outVal.append(key, value);
        N--;
    }
    outVal.validate();
}

在這個函數裏就可以比較明顯的看出來,從Parcel中分別讀取出key和value,然後put進map中。這樣就解決了之前的疑惑,unparcel函數的作用實際上是預處理,提前將序列化的數據反序列化並放入mMap中,然後Bundle再從mMap中存取數據。

我們越來越接近真相了!讀取value用的是readValue函數,代碼如下:

public final Object readValue(ClassLoader loader) {
    int type = readInt();
    switch (type) {
    case VAL_NULL:
        return null;
    case VAL_STRING:
        return readString();
    case VAL_INTEGER:
        return readInt();
    ...
    case VAL_SERIALIZABLE:
        return readSerializable(loader);
    ...
    default:
        int off = dataPosition() - 4;
        throw new RuntimeException(
            "Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off);
    }
}

根據不同的類型調用不同的函數來獲得value,這裏我們只關注Serializable這個類型,readSerializable代碼如下:

private final Serializable readSerializable(final ClassLoader loader) {
    String name = readString();
    ...
    try {
        ObjectInputStream ois = new ObjectInputStream(bais) {
            @Override
            protected Class<?> resolveClass(ObjectStreamClass osClass)
                    throws IOException, ClassNotFoundException {
                if (loader != null) {
                    Class<?> c = Class.forName(osClass.getName(), false, loader);
                    if (c != null) {
                        return c;
                    }
                }
                return super.resolveClass(osClass);
            }
        };
        return (Serializable) ois.readObject();
    } catch (IOException ioe) {
        throw new RuntimeException("Parcelable encountered " +
            "IOException reading a Serializable object (name = " + name +
            ")", ioe);
    } catch (ClassNotFoundException cnfe) {
        throw new RuntimeException("Parcelable encountered " +
            "ClassNotFoundException reading a Serializable object (name = "
            + name + ")", cnfe);
    }
}

我們終於找到了最開始的崩潰錯誤的源頭,在這裏反序列化時需要根據類名去找到Class對象,這時就出問題了,因爲通過上面我們知道,unparcel函數預處理時會將mParcelledData中所有的數據都解析出來,這時當解析到最開始的Boom類時,由於在本App中並不存在這個類,所以無法找到這個類,這樣就出問題了。這樣也解釋了爲什麼任意key都會出問題。



上面我們只說到了序列化的一種:serializable,我們知道在Android中還有另外一種推薦的序列化:Parcelable。
那麼Parcelable會出現這種crash麼,經測試也會出現這樣的問題,但是報出的錯誤是不同的:
E/AndroidRuntime: FATAL EXCEPTION: main
  Process: com.huichongzi.locationmocker, PID: 30769
  java.lang.RuntimeException: Unable to start activity ComponentInfo{com.test.test/com.test.test.MainActivity}: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.Boom
  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2369)
  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2431)
  ...
  Caused by: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.bennu.testapp.Boom
  at android.os.Parcel.readParcelableCreator(Parcel.java:2295)
  at android.os.Parcel.readParcelable(Parcel.java:2245)
  at android.os.Parcel.readValue(Parcel.java:2152)
  at android.os.Parcel.readArrayMapInternal(Parcel.java:2485)
  at android.os.BaseBundle.unparcel(BaseBundle.java:221)
  at android.os.BaseBundle.get(BaseBundle.java:280)
  at com.test.test.MainActivity.onCreate(MainActivity.java:142)
  ...

情況其實與serializable差不多,差別在readValue函數這一步調用了另外一個函數readParcelable,源碼如下:

public final <T extends Parcelable> T readParcelable(ClassLoader loader) {
    Parcelable.Creator<?> creator = readParcelableCreator(loader);
    if (creator == null) {
        return null;
    }
    if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
      Parcelable.ClassLoaderCreator<?> classLoaderCreator =
          (Parcelable.ClassLoaderCreator<?>) creator;
      return (T) classLoaderCreator.createFromParcel(this, loader);
    }
    return (T) creator.createFromParcel(this);
}

/** @hide */
public final Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) {
    String name = readString();
    if (name == null) {
        return null;
    }
    Parcelable.Creator<?> creator;
    synchronized (mCreators) {
        HashMap<String,Parcelable.Creator<?>> map = mCreators.get(loader);
        if (map == null) {
            map = new HashMap<>();
            mCreators.put(loader, map);
        }
        creator = map.get(name);
        if (creator == null) {
            try {
                ClassLoader parcelableClassLoader =
                        (loader == null ? getClass().getClassLoader() : loader);
                Class<?> parcelableClass = Class.forName(name, false /* initialize */,
                        parcelableClassLoader);
                if (!Parcelable.class.isAssignableFrom(parcelableClass)) {
                    throw new BadParcelableException("Parcelable protocol requires that the "
                            + "class implements Parcelable");
                }
                Field f = parcelableClass.getField("CREATOR");
                if ((f.getModifiers() & Modifier.STATIC) == 0) {
                    throw new BadParcelableException("Parcelable protocol requires "
                            + "the CREATOR object to be static on class " + name);
                }
                Class<?> creatorType = f.getType();
                if (!Parcelable.Creator.class.isAssignableFrom(creatorType)) {
                    throw new BadParcelableException("Parcelable protocol requires a "
                            + "Parcelable.Creator object called "
                            + "CREATOR on class " + name);
                }
                creator = (Parcelable.Creator<?>) f.get(null);
            }
            catch (IllegalAccessException e) {
                Log.e(TAG, "Illegal access when unmarshalling: " + name, e);
                throw new BadParcelableException(
                        "IllegalAccessException when unmarshalling: " + name);
            }
            catch (ClassNotFoundException e) {
                Log.e(TAG, "Class not found when unmarshalling: " + name, e);
                throw new BadParcelableException(
                        "ClassNotFoundException when unmarshalling: " + name);
            }
            catch (NoSuchFieldException e) {
                throw new BadParcelableException("Parcelable protocol requires a "
                        + "Parcelable.Creator object called "
                        + "CREATOR on class " + name);
            }
            if (creator == null) {
                throw new BadParcelableException("Parcelable protocol requires a "
                        + "non-null Parcelable.Creator object called "
                        + "CREATOR on class " + name);
            }
            map.put(name, creator);
        }
    }
    return creator;
}

在readParcelable函數中調用readParcelableCreator函數來解析數據,在這個函數中就可以看到同樣需要查找class來反序列化,而不同的是對Expection沒有直接拋出,而是包裝成BadParcelableException拋出的,這也是爲什麼crash信息有區別。

你以爲這樣就結束了?還沒有!
讓我們回到之前的unparcel函數,看看最後部分的代碼:

/* package */ synchronized void unparcel() {
    synchronized (this) {
        ...

        try {
            mParcelledData.readArrayMapInternal(map, N, mClassLoader);
        } catch (BadParcelableException e) {
            if (sShouldDefuse) {
                Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
                map.erase();
            } else {
                throw e;
            }
        } finally {
            mMap = map;
            mParcelledData.recycle();
            mParcelledData = null;
        }
        if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this))
                + " final map: " + mMap);
    }
}

可以看到mParcelledData.readArrayMapInternal是在一個try-catch中的,而是catch部分又catch了BadParcelableException,這裏就有了一個小彩蛋:當sShouldDefuse爲true時,這個錯誤就被吞掉了,而爲false時繼續拋出。

那麼這個sShouldDefuse的值怎麼來的?
在BaseBundle中sShouldDefuse默認是false,但是有一個函數可以設值,如下:
/**
* Set global variable indicating that any Bundles parsed in this process
* should be "defused." That is, any {@link BadParcelableException}
* encountered will be suppressed and logged, leaving an empty Bundle
* instead of crashing.
*
* @hide
*/
public static void setShouldDefuse(boolean shouldDefuse) {
    sShouldDefuse = shouldDefuse;
}

這個函數是static的,但是是隱藏的,所以我們不能直接使用。通過這個函數的註釋我們可以知道,當設爲true的時候,會吞掉所有BadParcelableException錯誤,這時會返回一個空的Bundle代替crash。

根據網上相關android framwork層源碼來看,高版本的android系統中默認將其設置爲true,應該是google做的一步優化。具體那個版本開始的還有待調查。



經過測試發現,不論serializable還是Parcelable在部分華爲手機上並不會crash,估計是華爲系統對此進行了優化,將問題直接吞掉了。
serializable的情況,android各個版本(8.0未測試)都還存在這個問題。
Parcelable則像上面說的,高版本已經處理了,具體那個版本還需要調查一下。

目前想到的解決方法是,在對外的Activity中如果獲取bundle數據,try-catch一下。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章