Merge changes Iab565e85,Ic48be15a

* changes:
  Support unlink and rmdir
  Track inode reference count from handles
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
index 76ff06f..27c7275 100644
--- a/compos/compos_key_cmd/compos_key_cmd.cpp
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -152,9 +152,13 @@
 
     ::ndk::ScopedAStatus onError(int32_t in_cid, int32_t in_error_code,
                                  const std::string& in_message) override {
-        // For now, just log the error as onDied() will follow.
         LOG(WARNING) << "VM error! cid = " << in_cid << ", error_code = " << in_error_code
                      << ", message = " << in_message;
+        {
+            std::unique_lock lock(mMutex);
+            mDied = true;
+        }
+        mCv.notify_all();
         return ScopedAStatus::ok();
     }
 
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
index 685d60c..6cfd0ba 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
@@ -18,8 +18,11 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.app.job.JobInfo;
 import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
 import android.app.job.JobService;
+import android.content.ComponentName;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -28,6 +31,7 @@
 import android.system.composd.IIsolatedCompilationService;
 import android.util.Log;
 
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 /**
@@ -37,35 +41,66 @@
  */
 public class IsolatedCompilationJobService extends JobService {
     private static final String TAG = IsolatedCompilationJobService.class.getName();
+    private static final int DAILY_JOB_ID = 5132250;
+    private static final int STAGED_APEX_JOB_ID = 5132251;
 
     private final AtomicReference<CompilationJob> mCurrentJob = new AtomicReference<>();
 
+    static void scheduleDailyJob(JobScheduler scheduler) {
+        // TODO(b/205296305) Remove this
+        ComponentName serviceName =
+                new ComponentName("android", IsolatedCompilationJobService.class.getName());
+
+        int result = scheduler.schedule(new JobInfo.Builder(DAILY_JOB_ID, serviceName)
+                .setRequiresDeviceIdle(true)
+                .setRequiresCharging(true)
+                .setPeriodic(TimeUnit.DAYS.toMillis(1))
+                .build());
+        if (result != JobScheduler.RESULT_SUCCESS) {
+            Log.e(TAG, "Failed to schedule daily job");
+        }
+    }
+
+    static void scheduleStagedApexJob(JobScheduler scheduler) {
+        ComponentName serviceName =
+                new ComponentName("android", IsolatedCompilationJobService.class.getName());
+
+        int result = scheduler.schedule(new JobInfo.Builder(STAGED_APEX_JOB_ID, serviceName)
+                // Wait in case more APEXes are staged
+                .setMinimumLatency(TimeUnit.MINUTES.toMillis(60))
+                // We consume CPU, battery, and storage
+                .setRequiresDeviceIdle(true)
+                .setRequiresBatteryNotLow(true)
+                .setRequiresStorageNotLow(true)
+                .build());
+        if (result != JobScheduler.RESULT_SUCCESS) {
+            Log.e(TAG, "Failed to schedule staged APEX job");
+        }
+    }
+
+    static boolean isStagedApexJobScheduled(JobScheduler scheduler) {
+        return scheduler.getPendingJob(STAGED_APEX_JOB_ID) != null;
+    }
+
     @Override
     public boolean onStartJob(JobParameters params) {
-        Log.i(TAG, "starting job");
+        int jobId = params.getJobId();
 
-        CompilationJob oldJob = mCurrentJob.getAndSet(null);
-        if (oldJob != null) {
-            // This should probably never happen, but just in case
-            oldJob.stop();
-        }
+        Log.i(TAG, "Starting job " + jobId);
 
         // This function (and onStopJob) are only ever called on the main thread, so we don't have
         // to worry about two starts at once, or start and stop happening at once. But onCompletion
         // can be called on any thread, so we need to be careful with that.
 
-        CompilationCallback callback = new CompilationCallback() {
-            @Override
-            public void onSuccess() {
-                onCompletion(params, true);
-            }
+        CompilationJob oldJob = mCurrentJob.get();
+        if (oldJob != null) {
+            // We're already running a job, give up on this one
+            Log.w(TAG, "Another job is in progress, skipping");
+            return false;  // Already finished
+        }
 
-            @Override
-            public void onFailure() {
-                onCompletion(params, false);
-            }
-        };
-        CompilationJob newJob = new CompilationJob(callback);
+        CompilationJob newJob = new CompilationJob(IsolatedCompilationJobService.this::onCompletion,
+                params);
         mCurrentJob.set(newJob);
 
         // This can take some time - we need to start up a VM - so we do it on a separate
@@ -75,9 +110,10 @@
             @Override
             public void run() {
                 try {
-                    newJob.start();
+                    newJob.start(jobId);
                 } catch (RuntimeException e) {
                     Log.e(TAG, "Starting CompilationJob failed", e);
+                    mCurrentJob.set(null);
                     newJob.stop(); // Just in case it managed to start before failure
                     jobFinished(params, /*wantReschedule=*/ false);
                 }
@@ -112,23 +148,23 @@
     }
 
     interface CompilationCallback {
-        void onSuccess();
-
-        void onFailure();
+        void onCompletion(JobParameters params, boolean succeeded);
     }
 
     static class CompilationJob extends ICompilationTaskCallback.Stub
             implements IBinder.DeathRecipient {
         private final AtomicReference<ICompilationTask> mTask = new AtomicReference<>();
         private final CompilationCallback mCallback;
+        private final JobParameters mParams;
         private volatile boolean mStopRequested = false;
         private volatile boolean mCanceled = false;
 
-        CompilationJob(CompilationCallback callback) {
+        CompilationJob(CompilationCallback callback, JobParameters params) {
             mCallback = requireNonNull(callback);
+            mParams = params;
         }
 
-        void start() {
+        void start(int jobId) {
             IBinder binder = ServiceManager.waitForService("android.system.composd");
             IIsolatedCompilationService composd =
                     IIsolatedCompilationService.Stub.asInterface(binder);
@@ -138,8 +174,12 @@
             }
 
             try {
-                // TODO(b/205296305) Call startStagedApexCompile instead
-                ICompilationTask composTask = composd.startTestCompile(this);
+                ICompilationTask composTask;
+                if (jobId == DAILY_JOB_ID) {
+                    composTask = composd.startTestCompile(this);
+                } else {
+                    composTask = composd.startStagedApexCompile(this);
+                }
                 mTask.set(composTask);
                 composTask.asBinder().linkToDeath(this, 0);
             } catch (RemoteException e) {
@@ -180,17 +220,18 @@
 
         @Override
         public void onSuccess() {
-            mTask.set(null);
-            if (!mCanceled) {
-                mCallback.onSuccess();
-            }
+            onCompletion(true);
         }
 
         @Override
         public void onFailure() {
+            onCompletion(false);
+        }
+
+        private void onCompletion(boolean succeeded) {
             mTask.set(null);
             if (!mCanceled) {
-                mCallback.onFailure();
+                mCallback.onCompletion(mParams, succeeded);
             }
         }
     }
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationService.java b/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
index cbc3371..6918572 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
@@ -17,17 +17,20 @@
 package com.android.server.compos;
 
 import android.annotation.NonNull;
-import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
-import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ApexStagedEvent;
+import android.content.pm.IPackageManagerNative;
+import android.content.pm.IStagedApexObserver;
+import android.content.pm.StagedApexInfo;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.provider.DeviceConfig;
 import android.util.Log;
 
 import com.android.server.SystemService;
 
 import java.io.File;
-import java.util.concurrent.TimeUnit;
 
 /**
  * A system service responsible for performing Isolated Compilation (compiling boot & system server
@@ -37,8 +40,6 @@
  */
 public class IsolatedCompilationService extends SystemService {
     private static final String TAG = IsolatedCompilationService.class.getName();
-    private static final int JOB_ID = 5132250;
-    private static final long JOB_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
 
     public IsolatedCompilationService(@NonNull Context context) {
         super(context);
@@ -59,24 +60,15 @@
             return;
         }
 
-        ComponentName serviceName =
-                new ComponentName("android", IsolatedCompilationJobService.class.getName());
 
         JobScheduler scheduler = getContext().getSystemService(JobScheduler.class);
         if (scheduler == null) {
             Log.e(TAG, "No scheduler");
             return;
         }
-        int result =
-                scheduler.schedule(
-                        new JobInfo.Builder(JOB_ID, serviceName)
-                                .setRequiresDeviceIdle(true)
-                                .setRequiresCharging(true)
-                                .setPeriodic(JOB_PERIOD_MILLIS)
-                                .build());
-        if (result != JobScheduler.RESULT_SUCCESS) {
-            Log.e(TAG, "Failed to schedule job");
-        }
+
+        IsolatedCompilationJobService.scheduleDailyJob(scheduler);
+        StagedApexObserver.registerForStagedApexUpdates(scheduler);
     }
 
     private static boolean isIsolatedCompilationSupported() {
@@ -94,4 +86,66 @@
 
         return true;
     }
+
+    private static class StagedApexObserver extends IStagedApexObserver.Stub {
+        private final JobScheduler mScheduler;
+        private final IPackageManagerNative mPackageNative;
+
+        static void registerForStagedApexUpdates(JobScheduler scheduler) {
+            final IPackageManagerNative packageNative = IPackageManagerNative.Stub.asInterface(
+                    ServiceManager.getService("package_native"));
+            if (packageNative == null) {
+                Log.e(TAG, "No IPackageManagerNative");
+                return;
+            }
+
+            StagedApexObserver observer = new StagedApexObserver(scheduler, packageNative);
+            try {
+                packageNative.registerStagedApexObserver(observer);
+                // In the unlikely event that an APEX has been staged before we get here, we may
+                // have to schedule compilation immediately.
+                observer.checkModules(packageNative.getStagedApexModuleNames());
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to initialize observer", e);
+            }
+        }
+
+        private StagedApexObserver(JobScheduler scheduler,
+                IPackageManagerNative packageNative) {
+            mScheduler = scheduler;
+            mPackageNative = packageNative;
+        }
+
+        @Override
+        public void onApexStaged(ApexStagedEvent event) {
+            Log.d(TAG, "onApexStaged");
+            checkModules(event.stagedApexModuleNames);
+        }
+
+        void checkModules(String[] moduleNames) {
+            if (IsolatedCompilationJobService.isStagedApexJobScheduled(mScheduler)) {
+                Log.d(TAG, "Job already scheduled");
+                // We're going to run anyway, we don't need to check this update
+                return;
+            }
+            boolean needCompilation = false;
+            for (String moduleName : moduleNames) {
+                try {
+                    StagedApexInfo apexInfo = mPackageNative.getStagedApexInfo(moduleName);
+                    if (apexInfo != null && (apexInfo.hasBootClassPathJars
+                            || apexInfo.hasDex2OatBootClassPathJars
+                            || apexInfo.hasSystemServerClassPathJars)) {
+                        Log.i(TAG, "Classpath affecting module updated: " + moduleName);
+                        needCompilation = true;
+                        break;
+                    }
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Failed to get getStagedApexInfo for " + moduleName);
+                }
+            }
+            if (needCompilation) {
+                IsolatedCompilationJobService.scheduleStagedApexJob(mScheduler);
+            }
+        }
+    }
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index 9dbed64..2ddaf30 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -33,7 +33,12 @@
 public interface VirtualMachineCallback {
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({ERROR_UNKNOWN, ERROR_PAYLOAD_VERIFICATION_FAILED, ERROR_PAYLOAD_CHANGED})
+    @IntDef({
+        ERROR_UNKNOWN,
+        ERROR_PAYLOAD_VERIFICATION_FAILED,
+        ERROR_PAYLOAD_CHANGED,
+        ERROR_PAYLOAD_INVALID_CONFIG
+    })
     @interface ErrorCode {}
 
     /** Error code for all other errors not listed below. */
@@ -48,6 +53,9 @@
     /** Error code indicating that the payload is verified, but has changed since the last boot. */
     int ERROR_PAYLOAD_CHANGED = 2;
 
+    /** Error code indicating that the payload config is invalid. */
+    int ERROR_PAYLOAD_INVALID_CONFIG = 3;
+
     /** Called when the payload starts in the VM. */
     void onPayloadStarted(@NonNull VirtualMachine vm, @Nullable ParcelFileDescriptor stream);
 
diff --git a/microdroid/keymint/Android.bp b/microdroid/keymint/Android.bp
index a0bbaf4..7915ada 100644
--- a/microdroid/keymint/Android.bp
+++ b/microdroid/keymint/Android.bp
@@ -14,8 +14,10 @@
         "-Wall",
         "-Wextra",
     ],
+    defaults: [
+        "keymint_use_latest_hal_aidl_ndk_shared",
+    ],
     shared_libs: [
-        "android.hardware.security.keymint-V1-ndk",
         "lib_android_keymaster_keymint_utils",
         "libbase",
         "libbinder_ndk",
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index f427966..c69d875 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -24,6 +24,7 @@
         "libmicrodroid_metadata",
         "libmicrodroid_payload_config",
         "libnix",
+        "libonce_cell",
         "libprotobuf",
         "libring",
         "librustutils",
diff --git a/microdroid_manager/src/instance.rs b/microdroid_manager/src/instance.rs
index 47230e3..8ba6f51 100644
--- a/microdroid_manager/src/instance.rs
+++ b/microdroid_manager/src/instance.rs
@@ -315,6 +315,7 @@
 pub struct MicrodroidData {
     pub apk_data: ApkData,
     pub apex_data: Vec<ApexData>,
+    pub bootconfig: Box<[u8]>,
 }
 
 #[derive(Debug, Serialize, Deserialize, PartialEq)]
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 99ebc51..efe6126 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -27,6 +27,7 @@
 use log::{error, info, warn};
 use microdroid_metadata::{write_metadata, Metadata};
 use microdroid_payload_config::{Task, TaskType, VmPayloadConfig};
+use once_cell::sync::OnceCell;
 use payload::{get_apex_data_from_payload, load_metadata, to_metadata};
 use rustutils::system_properties;
 use rustutils::system_properties::PropertyWatcher;
@@ -39,7 +40,7 @@
 use vsock::VsockStream;
 
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
-    ERROR_PAYLOAD_CHANGED, ERROR_PAYLOAD_VERIFICATION_FAILED, ERROR_UNKNOWN, VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT, IVirtualMachineService,
+    ERROR_PAYLOAD_CHANGED, ERROR_PAYLOAD_VERIFICATION_FAILED, ERROR_PAYLOAD_INVALID_CONFIG, ERROR_UNKNOWN, VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT, IVirtualMachineService,
 };
 
 const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -66,6 +67,8 @@
     PayloadChanged(String),
     #[error("Payload verification has failed: {0}")]
     PayloadVerificationFailed(String),
+    #[error("Payload config is invalid: {0}")]
+    InvalidConfig(String),
 }
 
 fn translate_error(err: &Error) -> (i32, String) {
@@ -75,6 +78,7 @@
             MicrodroidError::PayloadVerificationFailed(msg) => {
                 (ERROR_PAYLOAD_VERIFICATION_FAILED, msg.to_string())
             }
+            MicrodroidError::InvalidConfig(msg) => (ERROR_PAYLOAD_INVALID_CONFIG, msg.to_string()),
         }
     } else {
         (ERROR_UNKNOWN, err.to_string())
@@ -112,16 +116,27 @@
     info!("started.");
 
     let service = get_vms_rpc_binder().context("cannot connect to VirtualMachineService")?;
-    if let Err(err) = try_start_payload(&service) {
-        let (error_code, message) = translate_error(&err);
-        service.notifyError(error_code, &message)?;
-        Err(err)
-    } else {
-        Ok(())
+    match try_run_payload(&service) {
+        Ok(code) => {
+            info!("notifying payload finished");
+            service.notifyPayloadFinished(code)?;
+            if code == 0 {
+                info!("task successfully finished");
+            } else {
+                error!("task exited with exit code: {}", code);
+            }
+            Ok(())
+        }
+        Err(err) => {
+            error!("task terminated: {:?}", err);
+            let (error_code, message) = translate_error(&err);
+            service.notifyError(error_code, &message)?;
+            Err(err)
+        }
     }
 }
 
-fn try_start_payload(service: &Strong<dyn IVirtualMachineService>) -> Result<()> {
+fn try_run_payload(service: &Strong<dyn IVirtualMachineService>) -> Result<i32> {
     let metadata = load_metadata().context("Failed to load payload metadata")?;
 
     let mut instance = InstanceDisk::new().context("Failed to load instance.img")?;
@@ -151,27 +166,26 @@
     )
     .context("Failed to run zipfuse")?;
 
-    if !metadata.payload_config_path.is_empty() {
-        let config = load_config(Path::new(&metadata.payload_config_path))?;
+    ensure!(
+        !metadata.payload_config_path.is_empty(),
+        MicrodroidError::InvalidConfig("No payload_config_path in metadata".to_string())
+    );
+    let config = load_config(Path::new(&metadata.payload_config_path))?;
 
-        let fake_secret = "This is a placeholder for a value that is derived from the images that are loaded in the VM.";
-        if let Err(err) = rustutils::system_properties::write("ro.vmsecret.keymint", fake_secret) {
-            warn!("failed to set ro.vmsecret.keymint: {}", err);
-        }
-
-        // Wait until apex config is done. (e.g. linker configuration for apexes)
-        // TODO(jooyung): wait until sys.boot_completed?
-        wait_for_apex_config_done()?;
-
-        if let Some(main_task) = &config.task {
-            exec_task(main_task, service).map_err(|e| {
-                error!("failed to execute task: {}", e);
-                e
-            })?;
-        }
+    let fake_secret = "This is a placeholder for a value that is derived from the images that are loaded in the VM.";
+    if let Err(err) = rustutils::system_properties::write("ro.vmsecret.keymint", fake_secret) {
+        warn!("failed to set ro.vmsecret.keymint: {}", err);
     }
 
-    Ok(())
+    // Wait until apex config is done. (e.g. linker configuration for apexes)
+    // TODO(jooyung): wait until sys.boot_completed?
+    wait_for_apex_config_done()?;
+
+    ensure!(
+        config.task.is_some(),
+        MicrodroidError::InvalidConfig("No task in VM config".to_string())
+    );
+    exec_task(&config.task.unwrap(), service)
 }
 
 struct ApkDmverityArgument<'a> {
@@ -215,6 +229,13 @@
 ) -> Result<MicrodroidData> {
     let start_time = SystemTime::now();
 
+    if let Some(saved_bootconfig) = saved_data.map(|d| &d.bootconfig) {
+        ensure!(
+            saved_bootconfig.as_ref() == get_bootconfig()?.as_slice(),
+            MicrodroidError::PayloadChanged(String::from("Bootconfig has changed."))
+        );
+    }
+
     let root_hash = saved_data.map(|d| &d.apk_data.root_hash);
     let root_hash_from_idsig = get_apk_root_hash_from_idsig()?;
     let root_hash_trustful = root_hash == Some(&root_hash_from_idsig);
@@ -275,6 +296,7 @@
     Ok(MicrodroidData {
         apk_data: ApkData { root_hash: root_hash_from_idsig, pubkey: apk_pubkey },
         apex_data: apex_data_from_payload,
+        bootconfig: get_bootconfig()?.clone().into_boxed_slice(),
     })
 }
 
@@ -297,6 +319,13 @@
     Ok(idsig.hashing_info.raw_root_hash)
 }
 
+fn get_bootconfig() -> Result<&'static Vec<u8>> {
+    static VAL: OnceCell<Vec<u8>> = OnceCell::new();
+    VAL.get_or_try_init(|| {
+        fs::read("/proc/bootconfig").context("Failed to read bootconfig")
+    })
+}
+
 fn load_config(path: &Path) -> Result<VmPayloadConfig> {
     info!("loading config from {:?}...", path);
     let file = ioutil::wait_for_file(path, WAIT_TIMEOUT)?;
@@ -305,7 +334,7 @@
 
 /// Executes the given task. Stdout of the task is piped into the vsock stream to the
 /// virtualizationservice in the host side.
-fn exec_task(task: &Task, service: &Strong<dyn IVirtualMachineService>) -> Result<()> {
+fn exec_task(task: &Task, service: &Strong<dyn IVirtualMachineService>) -> Result<i32> {
     info!("executing main task {:?}...", task);
     let mut command = build_command(task)?;
 
@@ -319,19 +348,7 @@
     }
 
     let exit_status = command.spawn()?.wait()?;
-    if let Some(code) = exit_status.code() {
-        info!("notifying payload finished");
-        service.notifyPayloadFinished(code)?;
-
-        if code == 0 {
-            info!("task successfully finished");
-        } else {
-            error!("task exited with exit code: {}", code);
-        }
-    } else {
-        error!("task terminated: {}", exit_status);
-    }
-    Ok(())
+    exit_status.code().ok_or_else(|| anyhow!("Failed to get exit_code from the paylaod."))
 }
 
 fn build_command(task: &Task) -> Result<Command> {
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 61c3edc..0e99745 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -15,14 +15,21 @@
  */
 package com.android.microdroid.test;
 
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeNoException;
+import static org.junit.Assume.assumeThat;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineConfig.DebugLevel;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
 
@@ -36,6 +43,9 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
@@ -148,4 +158,65 @@
                 };
         listener.runToFinish(mInner.mVm);
     }
+
+    @Test
+    public void changingDebugLevelInvalidatesVmIdentity()
+            throws VirtualMachineException, InterruptedException, IOException {
+        assumeThat("Skip on Cuttlefish. b/195765441",
+                android.os.Build.DEVICE, is(not("vsoc_x86_64")));
+
+        VirtualMachineConfig.Builder builder =
+                new VirtualMachineConfig.Builder(mInner.mContext, "assets/vm_config.json");
+        VirtualMachineConfig normalConfig = builder.debugLevel(DebugLevel.NONE).build();
+        mInner.mVm = mInner.mVmm.getOrCreate("test_vm", normalConfig);
+        VmEventListener listener =
+                new VmEventListener() {
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        // TODO(b/208639280): remove this sleep. For now, we need to wait for a few
+                        // seconds so that crosvm can actually persist instance.img.
+                        try {
+                            Thread.sleep(30 * 1000);
+                        } catch (InterruptedException e) { }
+                        forceStop(vm);
+                    }
+                };
+        listener.runToFinish(mInner.mVm);
+
+        // Launch the same VM with different debug level. The Java API prohibits this (thankfully).
+        // For testing, we do that by creating another VM with debug level, and copy the config file
+        // from the new VM directory to the old VM directory.
+        VirtualMachineConfig debugConfig = builder.debugLevel(DebugLevel.FULL).build();
+        VirtualMachine newVm  = mInner.mVmm.getOrCreate("test_debug_vm", debugConfig);
+        File vmRoot = new File(mInner.mContext.getFilesDir(), "vm");
+        File newVmConfig = new File(new File(vmRoot, "test_debug_vm"), "config.xml");
+        File oldVmConfig = new File(new File(vmRoot, "test_vm"), "config.xml");
+        Files.copy(newVmConfig.toPath(), oldVmConfig.toPath(), REPLACE_EXISTING);
+        newVm.delete();
+        mInner.mVm = mInner.mVmm.get("test_vm"); // re-load with the copied-in config file.
+        listener =
+                new VmEventListener() {
+                    private boolean mPayloadStarted = false;
+                    private boolean mErrorOccurred = false;
+
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
+                        mPayloadStarted = true;
+                        forceStop(vm);
+                    }
+
+                    @Override
+                    public void onError(VirtualMachine vm, int errorCode, String message) {
+                        mErrorOccurred = true;
+                        forceStop(vm);
+                    }
+
+                    @Override
+                    public void onDied(VirtualMachine vm) {
+                        assertFalse(mPayloadStarted);
+                        assertTrue(mErrorOccurred);
+                    }
+                };
+        listener.runToFinish(mInner.mVm);
+    }
 }
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index 97f6ca3..1a16f2a 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -64,4 +64,9 @@
      * Error code indicating that the payload is verified, but has changed since the last boot.
      */
     const int ERROR_PAYLOAD_CHANGED = 2;
+
+    /**
+     * Error code indicating that the payload config is invalid.
+     */
+    const int ERROR_PAYLOAD_INVALID_CONFIG = 3;
 }