StringBlock incremental hardening default values
When a string cannot be retrieved from a StringBlock due to pages of
the StringPool missing, rather than throwing an
IndexOutOfBoundsException, compute a default value rather that
abides by the API contacts of the functions that use the StringBlock.
IndexOutOfBoundsExceptions are not really documented as a possible
exception that can be thrown from APIs that use StringBlock because
it indicates that the binary resource table was built incorrectly.
Returning default values is much less likely to break applications
that fetch the resources of incrementally installed applications than
throwing IndexOutOfBoundsExceptions where they would normally not be
expected.
Bug: 188174746
Test: atest ResourcesHardeningTest
Change-Id: I58fe754fb446fefc031ddba19e290830fdd6d015
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index f3783e4..224ff14c 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -334,13 +334,14 @@
}
}
+ @Nullable
CharSequence getStringFromPool(int idx) {
if (mStringBlock == null) {
return null;
}
synchronized (this) {
- return mStringBlock.get(idx);
+ return mStringBlock.getSequence(idx);
}
}
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index 13433dc..d50404e 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -550,7 +550,9 @@
outValue.changingConfigurations);
if (outValue.type == TypedValue.TYPE_STRING) {
- outValue.string = getPooledStringForCookie(cookie, outValue.data);
+ if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
+ return false;
+ }
}
return true;
}
@@ -731,7 +733,9 @@
outValue.changingConfigurations);
if (outValue.type == TypedValue.TYPE_STRING) {
- outValue.string = getPooledStringForCookie(cookie, outValue.data);
+ if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
+ return false;
+ }
}
return true;
}
@@ -833,6 +837,7 @@
}
}
+ @Nullable
CharSequence getPooledStringForCookie(int cookie, int id) {
// Cookies map to ApkAssets starting at 1.
return getApkAssets()[cookie - 1].getStringFromPool(id);
diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java
index 8f3f77b..5bc235f 100644
--- a/core/java/android/content/res/StringBlock.java
+++ b/core/java/android/content/res/StringBlock.java
@@ -16,6 +16,8 @@
package android.content.res;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Color;
import android.graphics.Paint;
@@ -86,8 +88,19 @@
+ ": " + nativeGetSize(mNative));
}
+ /**
+ * @deprecated use {@link #getSequence(int)} which can return null when a string cannot be found
+ * due to incremental installation.
+ */
+ @Deprecated
@UnsupportedAppUsage
public CharSequence get(int idx) {
+ CharSequence seq = getSequence(idx);
+ return seq == null ? "" : seq;
+ }
+
+ @Nullable
+ public CharSequence getSequence(int idx) {
synchronized (this) {
if (mStrings != null) {
CharSequence res = mStrings[idx];
@@ -108,6 +121,9 @@
}
}
String str = nativeGetString(mNative, idx);
+ if (str == null) {
+ return null;
+ }
CharSequence res = str;
int[] style = nativeGetStyle(mNative, idx);
if (localLOGV) Log.v(TAG, "Got string: " + str);
@@ -133,6 +149,9 @@
}
String styleTag = nativeGetString(mNative, styleId);
+ if (styleTag == null) {
+ return null;
+ }
if (styleTag.equals("b")) {
mStyleIDs.boldId = styleId;
@@ -161,8 +180,10 @@
res = applyStyles(str, style, mStyleIDs);
}
- if (mStrings != null) mStrings[idx] = res;
- else mSparseStrings.put(idx, res);
+ if (res != null) {
+ if (mStrings != null) mStrings[idx] = res;
+ else mSparseStrings.put(idx, res);
+ }
return res;
}
}
@@ -203,6 +224,7 @@
private int marqueeId = -1;
}
+ @Nullable
private CharSequence applyStyles(String str, int[] style, StyleIDs ids) {
if (style.length == 0)
return str;
@@ -260,6 +282,9 @@
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
} else {
String tag = nativeGetString(mNative, type);
+ if (tag == null) {
+ return null;
+ }
if (tag.startsWith("font;")) {
String sub;
diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java
index 2a349e9..d6f55d6 100644
--- a/core/java/android/content/res/TypedArray.java
+++ b/core/java/android/content/res/TypedArray.java
@@ -1376,6 +1376,7 @@
return true;
}
+ @Nullable
private CharSequence loadStringValueAt(int index) {
final int[] data = mData;
final int cookie = data[index + STYLE_ASSET_COOKIE];
diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java
index b0291ce..53dfed3 100644
--- a/core/java/android/content/res/XmlBlock.java
+++ b/core/java/android/content/res/XmlBlock.java
@@ -19,6 +19,7 @@
import static android.content.res.Resources.ID_NULL;
import android.annotation.AnyRes;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
@@ -161,9 +162,10 @@
public int getDepth() {
return mDepth;
}
+ @Nullable
public String getText() {
int id = nativeGetText(mParseState);
- return id >= 0 ? mStrings.get(id).toString() : null;
+ return id >= 0 ? getSequenceString(mStrings.getSequence(id)) : null;
}
public int getLineNumber() {
return nativeGetLineNumber(mParseState);
@@ -189,25 +191,29 @@
}
return chars;
}
+ @Nullable
public String getNamespace() {
int id = nativeGetNamespace(mParseState);
- return id >= 0 ? mStrings.get(id).toString() : "";
+ return id >= 0 ? getSequenceString(mStrings.getSequence(id)) : "";
}
+ @Nullable
public String getName() {
int id = nativeGetName(mParseState);
- return id >= 0 ? mStrings.get(id).toString() : null;
+ return id >= 0 ? getSequenceString(mStrings.getSequence(id)) : null;
}
+ @NonNull
public String getAttributeNamespace(int index) {
int id = nativeGetAttributeNamespace(mParseState, index);
if (DEBUG) System.out.println("getAttributeNamespace of " + index + " = " + id);
- if (id >= 0) return mStrings.get(id).toString();
+ if (id >= 0) return getSequenceString(mStrings.getSequence(id));
else if (id == -1) return "";
throw new IndexOutOfBoundsException(String.valueOf(index));
}
+ @NonNull
public String getAttributeName(int index) {
int id = nativeGetAttributeName(mParseState, index);
if (DEBUG) System.out.println("getAttributeName of " + index + " = " + id);
- if (id >= 0) return mStrings.get(id).toString();
+ if (id >= 0) return getSequenceString(mStrings.getSequence(id));
throw new IndexOutOfBoundsException(String.valueOf(index));
}
public String getAttributePrefix(int index) {
@@ -220,10 +226,11 @@
public int getAttributeCount() {
return mEventType == START_TAG ? nativeGetAttributeCount(mParseState) : -1;
}
+ @NonNull
public String getAttributeValue(int index) {
int id = nativeGetAttributeStringValue(mParseState, index);
if (DEBUG) System.out.println("getAttributeValue of " + index + " = " + id);
- if (id >= 0) return mStrings.get(id).toString();
+ if (id >= 0) return getSequenceString(mStrings.getSequence(id));
// May be some other type... check and try to convert if so.
int t = nativeGetAttributeDataType(mParseState, index);
@@ -390,7 +397,7 @@
int v = nativeGetAttributeData(mParseState, idx);
if (t == TypedValue.TYPE_STRING) {
return XmlUtils.convertValueToList(
- mStrings.get(v), options, defaultValue);
+ mStrings.getSequence(v), options, defaultValue);
}
return v;
}
@@ -444,14 +451,15 @@
}
throw new RuntimeException("not a float!");
}
-
+ @Nullable
public String getIdAttribute() {
int id = nativeGetIdAttribute(mParseState);
- return id >= 0 ? mStrings.get(id).toString() : null;
+ return id >= 0 ? getSequenceString(mStrings.getSequence(id)) : null;
}
+ @Nullable
public String getClassAttribute() {
int id = nativeGetClassAttribute(mParseState);
- return id >= 0 ? mStrings.get(id).toString() : null;
+ return id >= 0 ? getSequenceString(mStrings.getSequence(id)) : null;
}
public int getIdAttributeResourceValue(int defaultValue) {
@@ -463,6 +471,17 @@
return nativeGetStyleAttribute(mParseState);
}
+ private String getSequenceString(@Nullable CharSequence str) {
+ if (str == null) {
+ // A value of null retrieved from a StringPool indicates that retrieval of the
+ // string failed due to incremental installation. The presence of all the XmlBlock
+ // data is verified when it is created, so this exception must not be possible.
+ throw new IllegalStateException("Retrieving a string from the StringPool of an"
+ + " XmlBlock should never fail");
+ }
+ return str.toString();
+ }
+
public void close() {
synchronized (mBlock) {
if (mParseState != 0) {
@@ -472,13 +491,14 @@
}
}
}
-
+
protected void finalize() throws Throwable {
close();
}
+ @Nullable
/*package*/ final CharSequence getPooledString(int id) {
- return mStrings.get(id);
+ return mStrings.getSequence(id);
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
diff --git a/core/jni/android_util_StringBlock.cpp b/core/jni/android_util_StringBlock.cpp
index d94e084..dd7df26 100644
--- a/core/jni/android_util_StringBlock.cpp
+++ b/core/jni/android_util_StringBlock.cpp
@@ -72,7 +72,7 @@
ResStringPool* osb = reinterpret_cast<ResStringPool*>(token);
if (osb == NULL) {
jniThrowNullPointerException(env, NULL);
- return 0;
+ return NULL;
}
if (auto str8 = osb->string8At(idx); str8.has_value()) {
@@ -80,9 +80,11 @@
}
auto str = osb->stringAt(idx);
- if (UNLIKELY(!str.has_value())) {
+ if (IsIOError(str)) {
+ return NULL;
+ } else if (UNLIKELY(!str.has_value())) {
jniThrowException(env, "java/lang/IndexOutOfBoundsException", NULL);
- return 0;
+ return NULL;
}
return env->NewString((const jchar*)str->data(), str->size());