ART: Check for duplicate classes when loading oat files

Oat files are usually produced standalone, and the compilers take
advantage of any information they get. It is thus possible that
when compile-time and runtime class-path are not the same, classes
are resolved differently and optimized code is incorrect.

This is a very conservative check, scanning the complete class tables
of dex files. In case any duplicate class is found, the new oat file
will be rejected and the original dex files will be used in interpreted
mode.

A possible refinement to this is actual tracking of the compile-time
class-path instead. That is however significantly complicated by the
DexFile API and the non-standard uses it allows.

An alternative for both optimized code and correct resolution is
native multidex. Apps should switch to multidex and benefit from
the optimization as well as the shift of all compile time to install
time. Split APKs are currently compiled separately, but it is a goal
to change that install flow to simulated multidex.

Change-Id: Ib9e0db5091e060e3bb2c0e5e6c007430becbfc21
diff --git a/runtime/class_linker.cc b/runtime/class_linker.cc
index dc8bf2a..07790b8 100644
--- a/runtime/class_linker.cc
+++ b/runtime/class_linker.cc
@@ -31,6 +31,7 @@
 #include "base/scoped_flock.h"
 #include "base/stl_util.h"
 #include "base/unix_file/fd_file.h"
+#include "base/value_object.h"
 #include "class_linker-inl.h"
 #include "compiler_callbacks.h"
 #include "debugger.h"
@@ -81,6 +82,10 @@
 
 static constexpr bool kSanityCheckObjects = kIsDebugBuild;
 
+// Do a simple class redefinition check in OpenDexFilesFromOat. This is a conservative check to
+// avoid problems with compile-time class-path != runtime class-path.
+static constexpr bool kCheckForDexCollisions = true;
+
 static void ThrowNoClassDefFoundError(const char* fmt, ...)
     __attribute__((__format__(__printf__, 1, 2)))
     SHARED_LOCKS_REQUIRED(Locks::mutator_lock_);
@@ -712,6 +717,179 @@
   return *oat_file;
 }
 
+class DexFileAndClassPair : ValueObject {
+ public:
+  DexFileAndClassPair(const DexFile* dex_file, size_t current_class_index, bool from_loaded_oat)
+     : cached_descriptor_(GetClassDescriptor(dex_file, current_class_index)),
+       dex_file_(dex_file),
+       current_class_index_(current_class_index),
+       from_loaded_oat_(from_loaded_oat) {}
+
+  DexFileAndClassPair(const DexFileAndClassPair&) = default;
+
+  DexFileAndClassPair& operator=(const DexFileAndClassPair& rhs) {
+    cached_descriptor_ = rhs.cached_descriptor_;
+    dex_file_ = rhs.dex_file_;
+    current_class_index_ = rhs.current_class_index_;
+    from_loaded_oat_ = rhs.from_loaded_oat_;
+    return *this;
+  }
+
+  const char* GetCachedDescriptor() const {
+    return cached_descriptor_;
+  }
+
+  bool operator<(const DexFileAndClassPair& rhs) const {
+    const char* lhsDescriptor = cached_descriptor_;
+    const char* rhsDescriptor = rhs.cached_descriptor_;
+    int cmp = strcmp(lhsDescriptor, rhsDescriptor);
+    if (cmp != 0) {
+      return cmp > 0;
+    }
+    return dex_file_ < rhs.dex_file_;
+  }
+
+  bool DexFileHasMoreClasses() const {
+    return current_class_index_ + 1 < dex_file_->NumClassDefs();
+  }
+
+  DexFileAndClassPair GetNext() const {
+    return DexFileAndClassPair(dex_file_, current_class_index_ + 1, from_loaded_oat_);
+  }
+
+  size_t GetCurrentClassIndex() const {
+    return current_class_index_;
+  }
+
+  bool FromLoadedOat() const {
+    return from_loaded_oat_;
+  }
+
+  const DexFile* GetDexFile() const {
+    return dex_file_;
+  }
+
+ private:
+  static const char* GetClassDescriptor(const DexFile* dex_file, size_t index) {
+    const DexFile::ClassDef& class_def = dex_file->GetClassDef(static_cast<uint16_t>(index));
+    return dex_file->StringByTypeIdx(class_def.class_idx_);
+  }
+
+  const char* cached_descriptor_;
+  const DexFile* dex_file_;
+  size_t current_class_index_;
+  bool from_loaded_oat_;  // We only need to compare mismatches between what we load now
+                          // and what was loaded before. Any old duplicates must have been
+                          // OK, and any new "internal" duplicates are as well (they must
+                          // be from multidex, which resolves correctly).
+};
+
+static void AddDexFilesFromOat(const OatFile* oat_file, bool already_loaded,
+                               std::priority_queue<DexFileAndClassPair>* heap) {
+  const std::vector<const OatDexFile*>& oat_dex_files = oat_file->GetOatDexFiles();
+  for (const OatDexFile* oat_dex_file : oat_dex_files) {
+    std::string error;
+    std::unique_ptr<const DexFile> dex_file = oat_dex_file->OpenDexFile(&error);
+    if (dex_file.get() == nullptr) {
+      LOG(WARNING) << "Could not create dex file from oat file: " << error;
+    } else {
+      if (dex_file->NumClassDefs() > 0U) {
+        heap->emplace(dex_file.release(), 0U, already_loaded);
+      }
+    }
+  }
+}
+
+static void AddNext(const DexFileAndClassPair& original,
+                    std::priority_queue<DexFileAndClassPair>* heap) {
+  if (original.DexFileHasMoreClasses()) {
+    heap->push(original.GetNext());
+  } else {
+    // Need to delete the dex file.
+    delete original.GetDexFile();
+  }
+}
+
+static void FreeDexFilesInHeap(std::priority_queue<DexFileAndClassPair>* heap) {
+  while (!heap->empty()) {
+    delete heap->top().GetDexFile();
+    heap->pop();
+  }
+}
+
+bool ClassLinker::HasCollisions(const OatFile* oat_file, std::string* error_msg) {
+  if (!kCheckForDexCollisions) {
+    return false;
+  }
+
+  // Dex files are registered late - once a class is actually being loaded. We have to compare
+  // against the open oat files.
+  ReaderMutexLock mu(Thread::Current(), dex_lock_);
+
+  std::priority_queue<DexFileAndClassPair> heap;
+
+  // Add dex files from already loaded oat files, but skip boot.
+  {
+    // To grab the boot oat, look at the dex files in the boot classpath.
+    const OatFile* boot_oat = nullptr;
+    if (!boot_class_path_.empty()) {
+      const DexFile* boot_dex_file = boot_class_path_[0];
+      // Is it from an oat file?
+      if (boot_dex_file->GetOatDexFile() != nullptr) {
+        boot_oat = boot_dex_file->GetOatDexFile()->GetOatFile();
+      }
+    }
+
+    for (const OatFile* loaded_oat_file : oat_files_) {
+      if (loaded_oat_file == boot_oat) {
+        continue;
+      }
+      AddDexFilesFromOat(loaded_oat_file, true, &heap);
+    }
+  }
+
+  if (heap.empty()) {
+    // No other oat files, return early.
+    return false;
+  }
+
+  // Add dex files from the oat file to check.
+  AddDexFilesFromOat(oat_file, false, &heap);
+
+  // Now drain the heap.
+  while (!heap.empty()) {
+    DexFileAndClassPair compare_pop = heap.top();
+    heap.pop();
+
+    // Compare against the following elements.
+    while (!heap.empty()) {
+      DexFileAndClassPair top = heap.top();
+
+      if (strcmp(compare_pop.GetCachedDescriptor(), top.GetCachedDescriptor()) == 0) {
+        // Same descriptor. Check whether it's crossing old-oat-files to new-oat-files.
+        if (compare_pop.FromLoadedOat() != top.FromLoadedOat()) {
+          *error_msg =
+              StringPrintf("Found duplicated class when checking oat files: '%s' in %s and %s",
+                           compare_pop.GetCachedDescriptor(),
+                           compare_pop.GetDexFile()->GetLocation().c_str(),
+                           top.GetDexFile()->GetLocation().c_str());
+          FreeDexFilesInHeap(&heap);
+          return true;
+        }
+        // Pop it.
+        heap.pop();
+        AddNext(top, &heap);
+      } else {
+        // Something else. Done here.
+        break;
+      }
+    }
+    AddNext(compare_pop, &heap);
+  }
+
+  return false;
+}
+
 std::vector<std::unique_ptr<const DexFile>> ClassLinker::OpenDexFilesFromOat(
     const char* dex_location, const char* oat_location,
     std::vector<std::string>* error_msgs) {
@@ -757,8 +935,14 @@
     // Get the oat file on disk.
     std::unique_ptr<OatFile> oat_file = oat_file_assistant.GetBestOatFile();
     if (oat_file.get() != nullptr) {
-      source_oat_file = oat_file.release();
-      RegisterOatFile(source_oat_file);
+      // Take the file only if it has no collisions.
+      if (!HasCollisions(oat_file.get(), &error_msg)) {
+        source_oat_file = oat_file.release();
+        RegisterOatFile(source_oat_file);
+      } else {
+        LOG(WARNING) << "Found duplicate classes, falling back to interpreter mode (if enabled):";
+        LOG(WARNING) << error_msg;
+      }
     }
   }
 
diff --git a/runtime/class_linker.h b/runtime/class_linker.h
index 1bd9f0a..57989b2 100644
--- a/runtime/class_linker.h
+++ b/runtime/class_linker.h
@@ -663,6 +663,9 @@
   //       a recreation with a custom string.
   void ThrowEarlierClassFailure(mirror::Class* c) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_);
 
+  // Check for duplicate class definitions of the given oat file against all open oat files.
+  bool HasCollisions(const OatFile* oat_file, std::string* error_msg) LOCKS_EXCLUDED(dex_lock_);
+
   std::vector<const DexFile*> boot_class_path_;
   std::vector<std::unique_ptr<const DexFile>> opened_dex_files_;
 
diff --git a/test/138-duplicate-classes-check/build b/test/138-duplicate-classes-check/build
new file mode 100755
index 0000000..7ddc81d
--- /dev/null
+++ b/test/138-duplicate-classes-check/build
@@ -0,0 +1,31 @@
+#!/bin/bash
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Stop if something fails.
+set -e
+
+mkdir classes
+${JAVAC} -d classes `find src -name '*.java'`
+
+mkdir classes-ex
+${JAVAC} -d classes-ex `find src-ex -name '*.java'`
+
+if [ ${NEED_DEX} = "true" ]; then
+  ${DX} -JXmx256m --debug --dex --dump-to=classes.lst --output=classes.dex --dump-width=1000 classes
+  zip $TEST_NAME.jar classes.dex
+  ${DX} -JXmx256m --debug --dex --dump-to=classes-ex.lst --output=classes.dex --dump-width=1000 classes-ex
+  zip ${TEST_NAME}-ex.jar classes.dex
+fi
diff --git a/test/138-duplicate-classes-check/expected.txt b/test/138-duplicate-classes-check/expected.txt
new file mode 100644
index 0000000..b2f7f08
--- /dev/null
+++ b/test/138-duplicate-classes-check/expected.txt
@@ -0,0 +1,2 @@
+10
+10
diff --git a/test/138-duplicate-classes-check/info.txt b/test/138-duplicate-classes-check/info.txt
new file mode 100644
index 0000000..22a66a2
--- /dev/null
+++ b/test/138-duplicate-classes-check/info.txt
@@ -0,0 +1 @@
+Check whether a duplicate class is detected.
diff --git a/test/138-duplicate-classes-check/src-ex/A.java b/test/138-duplicate-classes-check/src-ex/A.java
new file mode 100644
index 0000000..8e52cb3
--- /dev/null
+++ b/test/138-duplicate-classes-check/src-ex/A.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class A {
+    public volatile int i;
+
+    public A() {
+      i = 10;
+    }
+}
diff --git a/test/138-duplicate-classes-check/src-ex/TestEx.java b/test/138-duplicate-classes-check/src-ex/TestEx.java
new file mode 100644
index 0000000..87558fa
--- /dev/null
+++ b/test/138-duplicate-classes-check/src-ex/TestEx.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class TestEx {
+    public static void test() {
+        System.out.println(new A().i);
+    }
+}
diff --git a/test/138-duplicate-classes-check/src/A.java b/test/138-duplicate-classes-check/src/A.java
new file mode 100644
index 0000000..e1773e5
--- /dev/null
+++ b/test/138-duplicate-classes-check/src/A.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class A {
+    // Object fields add padding in the Foo class object layout. Therefore the field 'i' should
+    // be at a different offset compared to the A class from the ex DEX file.
+    public final Object anObject = null;
+    public final Object anotherObject = null;
+    // Use volatile to defeat inlining of the constructor + load-elimination.
+    public volatile int i;
+
+    public A() {
+      i = 10;
+    }
+}
diff --git a/test/138-duplicate-classes-check/src/FancyLoader.java b/test/138-duplicate-classes-check/src/FancyLoader.java
new file mode 100644
index 0000000..03ec948
--- /dev/null
+++ b/test/138-duplicate-classes-check/src/FancyLoader.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * A class loader with atypical behavior: we try to load a private
+ * class implementation before asking the system or boot loader.  This
+ * is used to create multiple classes with identical names in a single VM.
+ *
+ * If DexFile is available, we use that; if not, we assume we're not in
+ * Dalvik and instantiate the class with defineClass().
+ *
+ * The location of the DEX files and class data is dependent upon the
+ * test framework.
+ */
+public class FancyLoader extends ClassLoader {
+    /* this is where the "alternate" .class files live */
+    static final String CLASS_PATH = "classes-ex/";
+
+    /* this is the "alternate" DEX/Jar file */
+    static final String DEX_FILE = System.getenv("DEX_LOCATION") +
+            "/138-duplicate-classes-check-ex.jar";
+
+    /* on Dalvik, this is a DexFile; otherwise, it's null */
+    private Class mDexClass;
+
+    private Object mDexFile;
+
+    /**
+     * Construct FancyLoader, grabbing a reference to the DexFile class
+     * if we're running under Dalvik.
+     */
+    public FancyLoader(ClassLoader parent) {
+        super(parent);
+
+        try {
+            mDexClass = parent.loadClass("dalvik.system.DexFile");
+        } catch (ClassNotFoundException cnfe) {
+            // ignore -- not running Dalvik
+        }
+    }
+
+    /**
+     * Finds the class with the specified binary name.
+     *
+     * We search for a file in CLASS_PATH or pull an entry from DEX_FILE.
+     * If we don't find a match, we throw an exception.
+     */
+    protected Class<?> findClass(String name) throws ClassNotFoundException
+    {
+        if (mDexClass != null) {
+            return findClassDalvik(name);
+        } else {
+            return findClassNonDalvik(name);
+        }
+    }
+
+    /**
+     * Finds the class with the specified binary name, from a DEX file.
+     */
+    private Class<?> findClassDalvik(String name)
+        throws ClassNotFoundException {
+
+        if (mDexFile == null) {
+            synchronized (FancyLoader.class) {
+                Constructor ctor;
+                /*
+                 * Construct a DexFile object through reflection.
+                 */
+                try {
+                    ctor = mDexClass.getConstructor(new Class[] {String.class});
+                } catch (NoSuchMethodException nsme) {
+                    throw new ClassNotFoundException("getConstructor failed",
+                        nsme);
+                }
+
+                try {
+                    mDexFile = ctor.newInstance(DEX_FILE);
+                } catch (InstantiationException ie) {
+                    throw new ClassNotFoundException("newInstance failed", ie);
+                } catch (IllegalAccessException iae) {
+                    throw new ClassNotFoundException("newInstance failed", iae);
+                } catch (InvocationTargetException ite) {
+                    throw new ClassNotFoundException("newInstance failed", ite);
+                }
+            }
+        }
+
+        /*
+         * Call DexFile.loadClass(String, ClassLoader).
+         */
+        Method meth;
+
+        try {
+            meth = mDexClass.getMethod("loadClass",
+                    new Class[] { String.class, ClassLoader.class });
+        } catch (NoSuchMethodException nsme) {
+            throw new ClassNotFoundException("getMethod failed", nsme);
+        }
+
+        try {
+            meth.invoke(mDexFile, name, this);
+        } catch (IllegalAccessException iae) {
+            throw new ClassNotFoundException("loadClass failed", iae);
+        } catch (InvocationTargetException ite) {
+            throw new ClassNotFoundException("loadClass failed",
+                ite.getCause());
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds the class with the specified binary name, from .class files.
+     */
+    private Class<?> findClassNonDalvik(String name)
+        throws ClassNotFoundException {
+
+        String pathName = CLASS_PATH + name + ".class";
+        //System.out.println("--- Fancy: looking for " + pathName);
+
+        File path = new File(pathName);
+        RandomAccessFile raf;
+
+        try {
+            raf = new RandomAccessFile(path, "r");
+        } catch (FileNotFoundException fnfe) {
+            throw new ClassNotFoundException("Not found: " + pathName);
+        }
+
+        /* read the entire file in */
+        byte[] fileData;
+        try {
+            fileData = new byte[(int) raf.length()];
+            raf.readFully(fileData);
+        } catch (IOException ioe) {
+            throw new ClassNotFoundException("Read error: " + pathName);
+        } finally {
+            try {
+                raf.close();
+            } catch (IOException ioe) {
+                // drop
+            }
+        }
+
+        /* create the class */
+        //System.out.println("--- Fancy: defining " + name);
+        try {
+            return defineClass(name, fileData, 0, fileData.length);
+        } catch (Throwable th) {
+            throw new ClassNotFoundException("defineClass failed", th);
+        }
+    }
+
+    /**
+     * Load a class.
+     *
+     * Normally a class loader wouldn't override this, but we want our
+     * version of the class to take precedence over an already-loaded
+     * version.
+     *
+     * We still want the system classes (e.g. java.lang.Object) from the
+     * bootstrap class loader.
+     */
+    protected Class<?> loadClass(String name, boolean resolve)
+        throws ClassNotFoundException
+    {
+        Class res;
+
+        /*
+         * 1. Invoke findLoadedClass(String) to check if the class has
+         * already been loaded.
+         *
+         * This doesn't change.
+         */
+        res = findLoadedClass(name);
+        if (res != null) {
+            System.out.println("FancyLoader.loadClass: "
+                + name + " already loaded");
+            if (resolve)
+                resolveClass(res);
+            return res;
+        }
+
+        /*
+         * 3. Invoke the findClass(String) method to find the class.
+         */
+        try {
+            res = findClass(name);
+            if (resolve)
+                resolveClass(res);
+        }
+        catch (ClassNotFoundException e) {
+            // we couldn't find it, so eat the exception and keep going
+        }
+
+        /*
+         * 2. Invoke the loadClass method on the parent class loader.  If
+         * the parent loader is null the class loader built-in to the
+         * virtual machine is used, instead.
+         *
+         * (Since we're not in java.lang, we can't actually invoke the
+         * parent's loadClass() method, but we passed our parent to the
+         * super-class which can take care of it for us.)
+         */
+        res = super.loadClass(name, resolve);   // returns class or throws
+        return res;
+    }
+}
diff --git a/test/138-duplicate-classes-check/src/Main.java b/test/138-duplicate-classes-check/src/Main.java
new file mode 100644
index 0000000..a9b5bb0
--- /dev/null
+++ b/test/138-duplicate-classes-check/src/Main.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File;
+import java.lang.reflect.Method;
+
+/**
+ * Structural hazard test.
+ */
+public class Main {
+    public static void main(String[] args) {
+        new Main().run();
+    }
+
+    private void run() {
+        System.out.println(new A().i);
+
+        // Now run the class from the -ex file.
+
+        FancyLoader loader = new FancyLoader(getClass().getClassLoader());
+
+        try {
+            Class testEx = loader.loadClass("TestEx");
+            Method test = testEx.getDeclaredMethod("test");
+            test.invoke(null);
+        } catch (Exception exc) {
+            exc.printStackTrace();
+        }
+    }
+}
diff --git a/test/138-duplicate-classes-check2/build b/test/138-duplicate-classes-check2/build
new file mode 100755
index 0000000..abcbbb8
--- /dev/null
+++ b/test/138-duplicate-classes-check2/build
@@ -0,0 +1,32 @@
+#!/bin/bash
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Stop if something fails.
+set -e
+
+mkdir classes
+${JAVAC} -d classes `find src -name '*.java'`
+
+mkdir classes-ex
+${JAVAC} -d classes-ex `find src-ex -name '*.java'`
+rm classes-ex/A.class
+
+if [ ${NEED_DEX} = "true" ]; then
+  ${DX} -JXmx256m --debug --dex --dump-to=classes.lst --output=classes.dex --dump-width=1000 classes
+  zip $TEST_NAME.jar classes.dex
+  ${DX} -JXmx256m --debug --dex --dump-to=classes-ex.lst --output=classes.dex --dump-width=1000 classes-ex
+  zip ${TEST_NAME}-ex.jar classes.dex
+fi
diff --git a/test/138-duplicate-classes-check2/expected.txt b/test/138-duplicate-classes-check2/expected.txt
new file mode 100644
index 0000000..b2f7f08
--- /dev/null
+++ b/test/138-duplicate-classes-check2/expected.txt
@@ -0,0 +1,2 @@
+10
+10
diff --git a/test/138-duplicate-classes-check2/info.txt b/test/138-duplicate-classes-check2/info.txt
new file mode 100644
index 0000000..7100122
--- /dev/null
+++ b/test/138-duplicate-classes-check2/info.txt
@@ -0,0 +1,2 @@
+Check whether a duplicate class is not detected, even though we compiled against one (but removed
+it before creating the dex file).
diff --git a/test/138-duplicate-classes-check2/run b/test/138-duplicate-classes-check2/run
new file mode 100755
index 0000000..8494ad9
--- /dev/null
+++ b/test/138-duplicate-classes-check2/run
@@ -0,0 +1,19 @@
+#!/bin/bash
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# We want to run as no-dex-file-fallback to confirm that even though the -ex file has a symbolic
+# reference to A, there's no class-def, so we don't detect a collision.
+exec ${RUN} --runtime-option -Xno-dex-file-fallback "${@}"
diff --git a/test/138-duplicate-classes-check2/src-ex/A.java b/test/138-duplicate-classes-check2/src-ex/A.java
new file mode 100644
index 0000000..8e52cb3
--- /dev/null
+++ b/test/138-duplicate-classes-check2/src-ex/A.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class A {
+    public volatile int i;
+
+    public A() {
+      i = 10;
+    }
+}
diff --git a/test/138-duplicate-classes-check2/src-ex/TestEx.java b/test/138-duplicate-classes-check2/src-ex/TestEx.java
new file mode 100644
index 0000000..87558fa
--- /dev/null
+++ b/test/138-duplicate-classes-check2/src-ex/TestEx.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class TestEx {
+    public static void test() {
+        System.out.println(new A().i);
+    }
+}
diff --git a/test/138-duplicate-classes-check2/src/A.java b/test/138-duplicate-classes-check2/src/A.java
new file mode 100644
index 0000000..e1773e5
--- /dev/null
+++ b/test/138-duplicate-classes-check2/src/A.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class A {
+    // Object fields add padding in the Foo class object layout. Therefore the field 'i' should
+    // be at a different offset compared to the A class from the ex DEX file.
+    public final Object anObject = null;
+    public final Object anotherObject = null;
+    // Use volatile to defeat inlining of the constructor + load-elimination.
+    public volatile int i;
+
+    public A() {
+      i = 10;
+    }
+}
diff --git a/test/138-duplicate-classes-check2/src/FancyLoader.java b/test/138-duplicate-classes-check2/src/FancyLoader.java
new file mode 100644
index 0000000..7e2bb08
--- /dev/null
+++ b/test/138-duplicate-classes-check2/src/FancyLoader.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * A class loader with atypical behavior: we try to load a private
+ * class implementation before asking the system or boot loader.  This
+ * is used to create multiple classes with identical names in a single VM.
+ *
+ * If DexFile is available, we use that; if not, we assume we're not in
+ * Dalvik and instantiate the class with defineClass().
+ *
+ * The location of the DEX files and class data is dependent upon the
+ * test framework.
+ */
+public class FancyLoader extends ClassLoader {
+    /* this is where the "alternate" .class files live */
+    static final String CLASS_PATH = "classes-ex/";
+
+    /* this is the "alternate" DEX/Jar file */
+    static final String DEX_FILE = System.getenv("DEX_LOCATION") +
+            "/138-duplicate-classes-check2-ex.jar";
+
+    /* on Dalvik, this is a DexFile; otherwise, it's null */
+    private Class mDexClass;
+
+    private Object mDexFile;
+
+    /**
+     * Construct FancyLoader, grabbing a reference to the DexFile class
+     * if we're running under Dalvik.
+     */
+    public FancyLoader(ClassLoader parent) {
+        super(parent);
+
+        try {
+            mDexClass = parent.loadClass("dalvik.system.DexFile");
+        } catch (ClassNotFoundException cnfe) {
+            // ignore -- not running Dalvik
+        }
+    }
+
+    /**
+     * Finds the class with the specified binary name.
+     *
+     * We search for a file in CLASS_PATH or pull an entry from DEX_FILE.
+     * If we don't find a match, we throw an exception.
+     */
+    protected Class<?> findClass(String name) throws ClassNotFoundException
+    {
+        if (mDexClass != null) {
+            return findClassDalvik(name);
+        } else {
+            return findClassNonDalvik(name);
+        }
+    }
+
+    /**
+     * Finds the class with the specified binary name, from a DEX file.
+     */
+    private Class<?> findClassDalvik(String name)
+        throws ClassNotFoundException {
+
+        if (mDexFile == null) {
+            synchronized (FancyLoader.class) {
+                Constructor ctor;
+                /*
+                 * Construct a DexFile object through reflection.
+                 */
+                try {
+                    ctor = mDexClass.getConstructor(new Class[] {String.class});
+                } catch (NoSuchMethodException nsme) {
+                    throw new ClassNotFoundException("getConstructor failed",
+                        nsme);
+                }
+
+                try {
+                    mDexFile = ctor.newInstance(DEX_FILE);
+                } catch (InstantiationException ie) {
+                    throw new ClassNotFoundException("newInstance failed", ie);
+                } catch (IllegalAccessException iae) {
+                    throw new ClassNotFoundException("newInstance failed", iae);
+                } catch (InvocationTargetException ite) {
+                    throw new ClassNotFoundException("newInstance failed", ite);
+                }
+            }
+        }
+
+        /*
+         * Call DexFile.loadClass(String, ClassLoader).
+         */
+        Method meth;
+
+        try {
+            meth = mDexClass.getMethod("loadClass",
+                    new Class[] { String.class, ClassLoader.class });
+        } catch (NoSuchMethodException nsme) {
+            throw new ClassNotFoundException("getMethod failed", nsme);
+        }
+
+        try {
+            meth.invoke(mDexFile, name, this);
+        } catch (IllegalAccessException iae) {
+            throw new ClassNotFoundException("loadClass failed", iae);
+        } catch (InvocationTargetException ite) {
+            throw new ClassNotFoundException("loadClass failed",
+                ite.getCause());
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds the class with the specified binary name, from .class files.
+     */
+    private Class<?> findClassNonDalvik(String name)
+        throws ClassNotFoundException {
+
+        String pathName = CLASS_PATH + name + ".class";
+        //System.out.println("--- Fancy: looking for " + pathName);
+
+        File path = new File(pathName);
+        RandomAccessFile raf;
+
+        try {
+            raf = new RandomAccessFile(path, "r");
+        } catch (FileNotFoundException fnfe) {
+            throw new ClassNotFoundException("Not found: " + pathName);
+        }
+
+        /* read the entire file in */
+        byte[] fileData;
+        try {
+            fileData = new byte[(int) raf.length()];
+            raf.readFully(fileData);
+        } catch (IOException ioe) {
+            throw new ClassNotFoundException("Read error: " + pathName);
+        } finally {
+            try {
+                raf.close();
+            } catch (IOException ioe) {
+                // drop
+            }
+        }
+
+        /* create the class */
+        //System.out.println("--- Fancy: defining " + name);
+        try {
+            return defineClass(name, fileData, 0, fileData.length);
+        } catch (Throwable th) {
+            throw new ClassNotFoundException("defineClass failed", th);
+        }
+    }
+
+    /**
+     * Load a class.
+     *
+     * Normally a class loader wouldn't override this, but we want our
+     * version of the class to take precedence over an already-loaded
+     * version.
+     *
+     * We still want the system classes (e.g. java.lang.Object) from the
+     * bootstrap class loader.
+     */
+    protected Class<?> loadClass(String name, boolean resolve)
+        throws ClassNotFoundException
+    {
+        Class res;
+
+        /*
+         * 1. Invoke findLoadedClass(String) to check if the class has
+         * already been loaded.
+         *
+         * This doesn't change.
+         */
+        res = findLoadedClass(name);
+        if (res != null) {
+            System.out.println("FancyLoader.loadClass: "
+                + name + " already loaded");
+            if (resolve)
+                resolveClass(res);
+            return res;
+        }
+
+        /*
+         * 3. Invoke the findClass(String) method to find the class.
+         */
+        try {
+            res = findClass(name);
+            if (resolve)
+                resolveClass(res);
+        }
+        catch (ClassNotFoundException e) {
+            // we couldn't find it, so eat the exception and keep going
+        }
+
+        /*
+         * 2. Invoke the loadClass method on the parent class loader.  If
+         * the parent loader is null the class loader built-in to the
+         * virtual machine is used, instead.
+         *
+         * (Since we're not in java.lang, we can't actually invoke the
+         * parent's loadClass() method, but we passed our parent to the
+         * super-class which can take care of it for us.)
+         */
+        res = super.loadClass(name, resolve);   // returns class or throws
+        return res;
+    }
+}
diff --git a/test/138-duplicate-classes-check2/src/Main.java b/test/138-duplicate-classes-check2/src/Main.java
new file mode 100644
index 0000000..a9b5bb0
--- /dev/null
+++ b/test/138-duplicate-classes-check2/src/Main.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File;
+import java.lang.reflect.Method;
+
+/**
+ * Structural hazard test.
+ */
+public class Main {
+    public static void main(String[] args) {
+        new Main().run();
+    }
+
+    private void run() {
+        System.out.println(new A().i);
+
+        // Now run the class from the -ex file.
+
+        FancyLoader loader = new FancyLoader(getClass().getClassLoader());
+
+        try {
+            Class testEx = loader.loadClass("TestEx");
+            Method test = testEx.getDeclaredMethod("test");
+            test.invoke(null);
+        } catch (Exception exc) {
+            exc.printStackTrace();
+        }
+    }
+}
diff --git a/test/Android.run-test.mk b/test/Android.run-test.mk
index 731c040..e311401 100644
--- a/test/Android.run-test.mk
+++ b/test/Android.run-test.mk
@@ -95,7 +95,7 @@
   RELOCATE_TYPES += no-relocate
 endif
 ifeq ($(ART_TEST_RUN_TEST_RELOCATE_NO_PATCHOAT),true)
-  RELOCATE_TYPES := relocate-npatchoat
+  RELOCATE_TYPES += relocate-npatchoat
 endif
 TRACE_TYPES := ntrace
 ifeq ($(ART_TEST_TRACE),true)
@@ -257,7 +257,12 @@
   116-nodex2oat \
   117-nopatchoat \
   118-noimage-dex2oat \
-  119-noimage-patchoat
+  119-noimage-patchoat \
+  138-duplicate-classes-check2
+
+# This test fails without an image.
+TEST_ART_BROKEN_NO_IMAGE_RUN_TESTS := \
+  138-duplicate-classes-check
 
 ifneq (,$(filter no-dex2oat,$(PREBUILD_TYPES)))
   ART_TEST_KNOWN_BROKEN += $(call all-run-test-names,$(TARGET_TYPES),$(RUN_TYPES),no-dex2oat, \
@@ -270,6 +275,9 @@
   ART_TEST_KNOWN_BROKEN += $(call all-run-test-names,$(TARGET_TYPES),$(RUN_TYPES),$(PREBUILD_TYPES), \
       $(COMPILER_TYPES), $(RELOCATE_TYPES),$(TRACE_TYPES),$(GC_TYPES),$(JNI_TYPES),no-image, \
       $(PICTEST_TYPES), $(DEBUGGABLE_TYPES), $(TEST_ART_BROKEN_FALLBACK_RUN_TESTS),$(ALL_ADDRESS_SIZES))
+  ART_TEST_KNOWN_BROKEN += $(call all-run-test-names,$(TARGET_TYPES),$(RUN_TYPES),$(PREBUILD_TYPES), \
+      $(COMPILER_TYPES), $(RELOCATE_TYPES),$(TRACE_TYPES),$(GC_TYPES),$(JNI_TYPES),no-image, \
+      $(PICTEST_TYPES), $(DEBUGGABLE_TYPES), $(TEST_ART_BROKEN_NO_IMAGE_RUN_TESTS),$(ALL_ADDRESS_SIZES))
 endif
 
 ifneq (,$(filter relocate-npatchoat,$(RELOCATE_TYPES)))