Merge "Implement statfs for authfs"
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 00f34b9..a6b1f95 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -4,7 +4,6 @@
 bpfmt = true
 clang_format = true
 jsonlint = true
-google_java_format = true
 pylint3 = true
 rustfmt = true
 xmllint = true
diff --git a/TEST_MAPPING b/TEST_MAPPING
index b07dc3b..87d8e39 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -11,6 +11,9 @@
     },
     {
       "name": "MicrodroidTestApp"
+    },
+    {
+      "name": "art_standalone_dexpreopt_tests"
     }
   ],
   "imports": [
diff --git a/apex/product_packages.mk b/apex/product_packages.mk
index e78d3dc..1a431d5 100644
--- a/apex/product_packages.mk
+++ b/apex/product_packages.mk
@@ -24,5 +24,11 @@
     com.android.virt \
 
 # TODO(b/207336449): Figure out how to get these off /system
-PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST += \
-     system/lib64/libgfxstream_backend.so \
+PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST := \
+    system/lib64/libgfxstream_backend.so \
+    system/framework/oat/%@service-compos.jar@classes.odex \
+    system/framework/oat/%@service-compos.jar@classes.vdex \
+
+PRODUCT_APEX_SYSTEM_SERVER_JARS := com.android.compos:service-compos
+
+PRODUCT_SYSTEM_EXT_PROPERTIES := ro.config.isolated_compilation_enabled=true
diff --git a/compos/aidl/Android.bp b/compos/aidl/Android.bp
index 4d36d3d..7036511 100644
--- a/compos/aidl/Android.bp
+++ b/compos/aidl/Android.bp
@@ -9,6 +9,11 @@
         "com/android/compos/*.aidl",
     ],
     backend: {
+        java: {
+            apex_available: [
+                "com.android.compos",
+            ],
+        },
         rust: {
             enabled: true,
             apex_available: [
diff --git a/compos/apex/Android.bp b/compos/apex/Android.bp
index c68899a..43e75e4 100644
--- a/compos/apex/Android.bp
+++ b/compos/apex/Android.bp
@@ -51,6 +51,8 @@
         "compsvc",
     ],
 
+    systemserverclasspath_fragments: ["com.android.compos-systemserverclasspath-fragment"],
+
     apps: [
         "CompOSPayloadApp",
     ],
@@ -61,6 +63,12 @@
     ],
 }
 
+systemserverclasspath_fragment {
+    name: "com.android.compos-systemserverclasspath-fragment",
+    contents: ["service-compos"],
+    apex_available: ["com.android.compos"],
+}
+
 prebuilt_etc {
     name: "com.android.compos.init.rc",
     src: "composd.rc",
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 508423b..4908f94 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -297,6 +297,12 @@
         log::warn!("VM payload finished, cid = {}, exit code = {}", cid, exit_code);
         Ok(())
     }
+
+    fn onError(&self, cid: i32, error_code: i32, message: &str) -> BinderResult<()> {
+        self.0.set_died();
+        log::warn!("VM error, cid = {}, error code = {}, message = {}", cid, error_code, message,);
+        Ok(())
+    }
 }
 
 fn start_logging(pfd: &ParcelFileDescriptor) -> Result<()> {
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
index 3f431da..c2699ab 100644
--- a/compos/compos_key_cmd/compos_key_cmd.cpp
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -145,6 +145,14 @@
         return ScopedAStatus::ok();
     }
 
+    ::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;
+        return ScopedAStatus::ok();
+    }
+
     ::ndk::ScopedAStatus onDied(int32_t in_cid) override {
         LOG(WARNING) << "VM died! cid = " << in_cid;
         {
@@ -173,8 +181,12 @@
 
 class TargetVm {
 public:
-    TargetVm(int cid, const std::string& logFile, const std::string& instanceImageFile)
-          : mCid(cid), mLogFile(logFile), mInstanceImageFile(instanceImageFile) {}
+    TargetVm(int cid, const std::string& logFile, const std::string& instanceImageFile,
+             bool debuggable)
+          : mCid(cid),
+            mLogFile(logFile),
+            mInstanceImageFile(instanceImageFile),
+            mDebuggable(debuggable) {}
 
     // Returns 0 if we are to connect to a local service, otherwise the CID of
     // either an existing VM or a VM we have started, depending on the command
@@ -236,7 +248,8 @@
         appConfig.idsig = std::move(idsigFd);
         appConfig.instanceImage = std::move(instanceFd);
         appConfig.configPath = kConfigFilePath;
-        appConfig.debugLevel = VirtualMachineAppConfig::DebugLevel::FULL;
+        appConfig.debugLevel = mDebuggable ? VirtualMachineAppConfig::DebugLevel::FULL
+                                           : VirtualMachineAppConfig::DebugLevel::NONE;
         appConfig.memoryMib = 0; // Use default
 
         LOG(INFO) << "Starting VM";
@@ -279,6 +292,7 @@
     const int mCid;
     const std::string mLogFile;
     const std::string mInstanceImageFile;
+    const bool mDebuggable;
     std::shared_ptr<Callback> mCallback;
     std::shared_ptr<IVirtualMachine> mVm;
 };
@@ -524,8 +538,18 @@
     int cid = 0;
     std::string imageFile;
     std::string logFile;
+    bool debuggable = false;
 
-    while (argc >= 3) {
+    for (;;) {
+        if (argc >= 2) {
+            if (argv[1] == "--debug"sv) {
+                debuggable = true;
+                argc -= 1;
+                argv += 1;
+                continue;
+            }
+        }
+        if (argc < 3) break;
         if (argv[1] == "--cid"sv) {
             cid = atoi(argv[2]);
             if (cid == 0) {
@@ -543,7 +567,7 @@
         argv += 2;
     }
 
-    TargetVm vm(cid, logFile, imageFile);
+    TargetVm vm(cid, logFile, imageFile, debuggable);
 
     if (argc == 4 && argv[1] == "generate"sv) {
         auto result = generate(vm, argv[2], argv[3]);
@@ -599,8 +623,9 @@
                   << "    <filename>.signature\n"
                   << "  make-instance <image file> Create an empty instance image file for a VM.\n"
                   << "\n"
-                  << "OPTIONS: --log <log file> (--cid <cid> | --start <image file>)\n"
+                  << "OPTIONS: --log <log file> --debug (--cid <cid> | --start <image file>)\n"
                   << "  Specify --log to write VM log to a file rather than stdout.\n"
+                  << "  Specify --debug with --start to make the VM fully debuggable.\n"
                   << "  Specify --cid to connect to a VM rather than the host.\n"
                   << "  Specify --start to start a VM from the given instance image file and\n "
                   << "    connect to that.\n";
diff --git a/compos/composd/aidl/Android.bp b/compos/composd/aidl/Android.bp
index 62c1b40..376313b 100644
--- a/compos/composd/aidl/Android.bp
+++ b/compos/composd/aidl/Android.bp
@@ -9,7 +9,7 @@
     unstable: true,
     backend: {
         java: {
-            apex_available: ["//apex_available:platform"],
+            apex_available: ["com.android.compos"],
         },
         rust: {
             enabled: true,
diff --git a/compos/composd/aidl/android/system/composd/ICompilationTask.aidl b/compos/composd/aidl/android/system/composd/ICompilationTask.aidl
index ae03fcc..c1da0a5 100644
--- a/compos/composd/aidl/android/system/composd/ICompilationTask.aidl
+++ b/compos/composd/aidl/android/system/composd/ICompilationTask.aidl
@@ -23,5 +23,5 @@
      * Attempt to cancel compilation. If successful compilation will end and no further success or
      * failed callbacks will be received (although any in flight may still be delivered).
      */
-    void cancel();
+    oneway void cancel();
 }
diff --git a/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl b/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
index a9d41b8..b334d8b 100644
--- a/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
+++ b/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
@@ -19,7 +19,7 @@
  * Interface to be implemented by clients of IIsolatedCompilationService to be notified when a
  * requested compilation task completes.
  */
-interface ICompilationTaskCallback {
+oneway interface ICompilationTaskCallback {
     /**
      * Called if a compilation task has ended successfully, generating all the required artifacts.
      */
diff --git a/compos/service/Android.bp b/compos/service/Android.bp
new file mode 100644
index 0000000..6270c9a
--- /dev/null
+++ b/compos/service/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2021 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.
+
+java_library {
+    name: "service-compos",
+    srcs: [
+        "java/**/*.java",
+    ],
+    defaults: ["framework-system-server-module-defaults"],
+    permitted_packages: [
+        "com.android.server.compos",
+        "com.android.compos",
+        "android.system.composd",
+    ],
+    static_libs: [
+        "android.system.composd-java",
+    ],
+    apex_available: [
+        "com.android.compos",
+    ],
+    // Access to SystemService, ServiceManager#waitForService etc
+    libs: ["services"],
+    sdk_version: "",
+    platform_apis: true,
+    installable: true,
+}
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
new file mode 100644
index 0000000..2aacc2d
--- /dev/null
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2021 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.
+ */
+
+package com.android.server.compos;
+
+import static java.util.Objects.requireNonNull;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.composd.ICompilationTask;
+import android.system.composd.ICompilationTaskCallback;
+import android.system.composd.IIsolatedCompilationService;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A job scheduler service responsible for performing Isolated Compilation when scheduled.
+ *
+ * @hide
+ */
+public class IsolatedCompilationJobService extends JobService {
+    private static final String TAG = IsolatedCompilationJobService.class.getName();
+
+    private final AtomicReference<CompilationJob> mCurrentJob = new AtomicReference<>();
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        Log.i(TAG, "starting job");
+
+        CompilationJob oldJob = mCurrentJob.getAndSet(null);
+        if (oldJob != null) {
+            // This should probably never happen, but just in case
+            oldJob.stop();
+        }
+
+        // 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);
+            }
+
+            @Override
+            public void onFailure() {
+                onCompletion(params, false);
+            }
+        };
+        CompilationJob newJob = new CompilationJob(callback);
+        mCurrentJob.set(newJob);
+
+        try {
+            // This can take some time - we need to start up a VM - so we do it on a separate
+            // thread. This thread exits as soon as the compilation Ttsk has been started (or
+            // there's a failure), and then compilation continues in composd and the VM.
+            new Thread("IsolatedCompilationJob_starter") {
+                @Override
+                public void run() {
+                    newJob.start();
+                }
+            }.start();
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Starting CompilationJob failed", e);
+            return false; // We're finished
+        }
+        return true; // Job is running in the background
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        CompilationJob job = mCurrentJob.getAndSet(null);
+        if (job == null) {
+            return false; // No need to reschedule, we'd finished
+        } else {
+            job.stop();
+            return true; // We didn't get to finish, please re-schedule
+        }
+    }
+
+    void onCompletion(JobParameters params, boolean succeeded) {
+        Log.i(TAG, "onCompletion, succeeded=" + succeeded);
+
+        CompilationJob job = mCurrentJob.getAndSet(null);
+        if (job == null) {
+            // No need to call jobFinished if we've been told to stop.
+            return;
+        }
+        // On success we don't need to reschedule.
+        // On failure we could reschedule, but that could just use a lot of resources and still
+        // fail; instead we just let odsign do compilation on reboot if necessary.
+        jobFinished(params, /*wantReschedule=*/ false);
+    }
+
+    interface CompilationCallback {
+        void onSuccess();
+
+        void onFailure();
+    }
+
+    static class CompilationJob extends ICompilationTaskCallback.Stub
+            implements IBinder.DeathRecipient {
+        private final AtomicReference<ICompilationTask> mTask = new AtomicReference<>();
+        private final CompilationCallback mCallback;
+        private volatile boolean mStopRequested = false;
+        private volatile boolean mCanceled = false;
+
+        CompilationJob(CompilationCallback callback) {
+            mCallback = requireNonNull(callback);
+        }
+
+        void start() {
+            IBinder binder = ServiceManager.waitForService("android.system.composd");
+            IIsolatedCompilationService composd =
+                    IIsolatedCompilationService.Stub.asInterface(binder);
+
+            if (composd == null) {
+                throw new IllegalStateException("Unable to find composd service");
+            }
+
+            try {
+                ICompilationTask composTask = composd.startTestCompile(this);
+                mTask.set(composTask);
+                composTask.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+
+            if (mStopRequested) {
+                // We were asked to stop while we were starting the task. We need to
+                // cancel it now, since we couldn't before.
+                cancelTask();
+            }
+        }
+
+        void stop() {
+            mStopRequested = true;
+            cancelTask();
+        }
+
+        private void cancelTask() {
+            ICompilationTask task = mTask.getAndSet(null);
+            if (task != null) {
+                mCanceled = true;
+                Log.i(TAG, "Cancelling task");
+                try {
+                    task.cancel();
+                } catch (RuntimeException | RemoteException e) {
+                    // If canceling failed we'll assume it means that the task has already failed;
+                    // there's nothing else we can do anyway.
+                    Log.w(TAG, "Failed to cancel CompilationTask", e);
+                }
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            onFailure();
+        }
+
+        @Override
+        public void onSuccess() {
+            mTask.set(null);
+            if (!mCanceled) {
+                mCallback.onSuccess();
+            }
+        }
+
+        @Override
+        public void onFailure() {
+            mTask.set(null);
+            if (!mCanceled) {
+                mCallback.onFailure();
+            }
+        }
+    }
+}
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationService.java b/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
new file mode 100644
index 0000000..cbc3371
--- /dev/null
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2021 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.
+ */
+
+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.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
+ * classpath JARs in a protected VM) when appropriate.
+ *
+ * @hide
+ */
+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);
+    }
+
+    @Override
+    public void onStart() {
+        // Note that our binder service is exposed directly from native code in composd, so
+        // we don't need to do anything here.
+    }
+
+    @Override
+    public void onBootPhase(/* @BootPhase */ int phase) {
+        if (phase != PHASE_BOOT_COMPLETED) return;
+
+        if (!isIsolatedCompilationSupported()) {
+            Log.i(TAG, "Isolated compilation not supported, not scheduling job");
+            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");
+        }
+    }
+
+    private static boolean isIsolatedCompilationSupported() {
+        // Check that the relevant experiment is enabled on this device
+        // TODO - Remove this once we are ready for wider use.
+        if (!DeviceConfig.getBoolean(
+                "virtualization_framework_native", "isolated_compilation_enabled", false)) {
+            return false;
+        }
+
+        // Check that KVM is enabled on the device
+        if (!new File("/dev/kvm").exists()) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/compos/verify_key/verify_key.rs b/compos/verify_key/verify_key.rs
index 0a9d36b..945acb4 100644
--- a/compos/verify_key/verify_key.rs
+++ b/compos/verify_key/verify_key.rs
@@ -22,7 +22,7 @@
 use compos_common::compos_client::{VmInstance, VmParameters};
 use compos_common::{
     COMPOS_DATA_ROOT, CURRENT_INSTANCE_DIR, INSTANCE_IMAGE_FILE, PENDING_INSTANCE_DIR,
-    PRIVATE_KEY_BLOB_FILE, PUBLIC_KEY_FILE,
+    PRIVATE_KEY_BLOB_FILE, PUBLIC_KEY_FILE, TEST_INSTANCE_DIR,
 };
 use std::fs::{self, File};
 use std::io::Read;
@@ -30,41 +30,54 @@
 
 const MAX_FILE_SIZE_BYTES: u64 = 8 * 1024;
 
-fn main() -> Result<()> {
+fn main() {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("compos_verify_key")
             .with_min_level(log::Level::Info),
     );
 
+    if let Err(e) = try_main() {
+        log::error!("{:?}", e);
+        std::process::exit(-1)
+    }
+}
+
+fn try_main() -> Result<()> {
     let matches = clap::App::new("compos_verify_key")
         .arg(
             clap::Arg::with_name("instance")
                 .long("instance")
                 .takes_value(true)
                 .required(true)
-                .possible_values(&["pending", "current"]),
+                .possible_values(&["pending", "current", "test"]),
         )
+        .arg(clap::Arg::with_name("debug").long("debug"))
         .get_matches();
-    let do_pending = matches.value_of("instance").unwrap() == "pending";
 
-    let instance_dir: PathBuf =
-        [COMPOS_DATA_ROOT, if do_pending { PENDING_INSTANCE_DIR } else { CURRENT_INSTANCE_DIR }]
-            .iter()
-            .collect();
+    let debug_mode = matches.is_present("debug");
+    let (promote_if_valid, instance_dir) = match matches.value_of("instance").unwrap() {
+        "pending" => (true, PENDING_INSTANCE_DIR),
+        "current" => (false, CURRENT_INSTANCE_DIR),
+        "test" => (false, TEST_INSTANCE_DIR),
+        _ => unreachable!("Unexpected instance name"),
+    };
+
+    let instance_dir: PathBuf = [COMPOS_DATA_ROOT, instance_dir].iter().collect();
 
     if !instance_dir.is_dir() {
-        bail!("{} is not a directory", instance_dir.display());
+        bail!("{:?} is not a directory", instance_dir);
     }
 
     // We need to start the thread pool to be able to receive Binder callbacks
     ProcessState::start_thread_pool();
 
-    let result = verify(&instance_dir).and_then(|_| {
-        if do_pending {
-            // If the pending instance is ok, then it must actually match the current system state,
+    let result = verify(debug_mode, &instance_dir).and_then(|_| {
+        log::info!("Verified {:?}", instance_dir);
+        if promote_if_valid {
+            // If the instance is ok, then it must actually match the current system state,
             // so we promote it to current.
-            log::info!("Promoting pending to current");
+            log::info!("Promoting to current");
             promote_to_current(&instance_dir)
         } else {
             Ok(())
@@ -73,7 +86,7 @@
 
     if result.is_err() {
         // This is best efforts, and we still want to report the original error as our result
-        log::info!("Removing {}", instance_dir.display());
+        log::info!("Removing {:?}", instance_dir);
         if let Err(e) = fs::remove_dir_all(&instance_dir) {
             log::warn!("Failed to remove directory: {}", e);
         }
@@ -82,7 +95,7 @@
     result
 }
 
-fn verify(instance_dir: &Path) -> Result<()> {
+fn verify(debug_mode: bool, instance_dir: &Path) -> Result<()> {
     let blob = instance_dir.join(PRIVATE_KEY_BLOB_FILE);
     let public_key = instance_dir.join(PUBLIC_KEY_FILE);
     let instance_image = instance_dir.join(INSTANCE_IMAGE_FILE);
@@ -93,7 +106,7 @@
 
     let virtualization_service = VmInstance::connect_to_virtualization_service()?;
     let vm_instance =
-        VmInstance::start(&*virtualization_service, instance_image, &VmParameters::default())?;
+        VmInstance::start(&*virtualization_service, instance_image, &VmParameters { debug_mode })?;
     let service = vm_instance.get_service()?;
 
     let result = service.verifySigningKey(&blob, &public_key).context("Verifying signing key")?;
@@ -105,6 +118,17 @@
     Ok(())
 }
 
+fn promote_to_current(instance_dir: &Path) -> Result<()> {
+    let current_dir: PathBuf = [COMPOS_DATA_ROOT, CURRENT_INSTANCE_DIR].iter().collect();
+
+    // This may fail if the directory doesn't exist - which is fine, we only care about the rename
+    // succeeding.
+    let _ = fs::remove_dir_all(&current_dir);
+
+    fs::rename(&instance_dir, &current_dir).context("Unable to promote instance to current")?;
+    Ok(())
+}
+
 fn read_small_file(file: PathBuf) -> Result<Vec<u8>> {
     let mut file = File::open(file)?;
     if file.metadata()?.len() > MAX_FILE_SIZE_BYTES {
@@ -114,15 +138,3 @@
     file.read_to_end(&mut data)?;
     Ok(data)
 }
-
-fn promote_to_current(instance_dir: &Path) -> Result<()> {
-    let current_dir: PathBuf = [COMPOS_DATA_ROOT, CURRENT_INSTANCE_DIR].iter().collect();
-
-    // This may fail if the directory doesn't exist - which is fine, we only care about the rename
-    // succeeding.
-    let _ = fs::remove_dir_all(&current_dir);
-
-    fs::rename(&instance_dir, &current_dir)
-        .context("Unable to promote pending instance to current")?;
-    Ok(())
-}
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index 15d9046..1a0e14d 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -263,6 +263,17 @@
                         }
 
                         @Override
+                        public void onError(VirtualMachine vm, int errorCode, String message) {
+                            // This check doesn't 100% prevent race condition, but is fine for demo.
+                            if (!mService.isShutdown()) {
+                                mPayloadOutput.postValue(
+                                        String.format(
+                                                "(Error occurred. code: %d, message: %s)",
+                                                errorCode, message));
+                            }
+                        }
+
+                        @Override
                         public void onDied(VirtualMachine vm) {
                             mService.shutdownNow();
                             mStatus.postValue(VirtualMachine.Status.STOPPED);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index d04da0e..c2a897b 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -401,6 +401,16 @@
                         }
 
                         @Override
+                        public void onError(int cid, int errorCode, String message) {
+                            final VirtualMachineCallback cb = mCallback;
+                            if (cb == null) {
+                                return;
+                            }
+                            mCallbackExecutor.execute(
+                                    () -> cb.onError(VirtualMachine.this, errorCode, message));
+                        }
+
+                        @Override
                         public void onDied(int cid) {
                             final VirtualMachineCallback cb = mCallback;
                             if (cb == null) {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index 988acd7..9dbed64 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -16,10 +16,14 @@
 
 package android.system.virtualmachine;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.ParcelFileDescriptor;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Callback interface to get notified with the events from the virtual machine. The methods are
  * executed on a binder thread. Implementations can make blocking calls in the methods.
@@ -27,6 +31,22 @@
  * @hide
  */
 public interface VirtualMachineCallback {
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ERROR_UNKNOWN, ERROR_PAYLOAD_VERIFICATION_FAILED, ERROR_PAYLOAD_CHANGED})
+    @interface ErrorCode {}
+
+    /** Error code for all other errors not listed below. */
+    int ERROR_UNKNOWN = 0;
+
+    /**
+     * Error code indicating that the payload can't be verified due to various reasons (e.g invalid
+     * merkle tree, invalid formats, etc).
+     */
+    int ERROR_PAYLOAD_VERIFICATION_FAILED = 1;
+
+    /** Error code indicating that the payload is verified, but has changed since the last boot. */
+    int ERROR_PAYLOAD_CHANGED = 2;
 
     /** Called when the payload starts in the VM. */
     void onPayloadStarted(@NonNull VirtualMachine vm, @Nullable ParcelFileDescriptor stream);
@@ -37,6 +57,9 @@
     /** Called when the payload has finished in the VM. */
     void onPayloadFinished(@NonNull VirtualMachine vm, int exitCode);
 
+    /** Called when an error occurs in the VM. */
+    void onError(@NonNull VirtualMachine vm, @ErrorCode int errorCode, @NonNull String message);
+
     /** Called when the VM died. */
     void onDied(@NonNull VirtualMachine vm);
 }
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index 721f9fa..f427966 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -30,6 +30,7 @@
         "libserde",
         "libserde_cbor",
         "libserde_json",
+        "libthiserror",
         "libuuid",
         "libvsock",
         "librand",
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index f666294..2e6fa36 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -19,7 +19,7 @@
 mod payload;
 
 use crate::instance::{ApkData, InstanceDisk, MicrodroidData, RootHash};
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{anyhow, bail, ensure, Context, Error, Result};
 use apkverify::{get_public_key_der, verify};
 use binder::unstable_api::{new_spibinder, AIBinder};
 use binder::{FromIBinder, Strong};
@@ -39,7 +39,7 @@
 use vsock::VsockStream;
 
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
-    VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT, IVirtualMachineService,
+    ERROR_PAYLOAD_CHANGED, ERROR_PAYLOAD_VERIFICATION_FAILED, ERROR_UNKNOWN, VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT, IVirtualMachineService,
 };
 
 const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -51,6 +51,27 @@
 const APEX_CONFIG_DONE_PROP: &str = "apex_config.done";
 const LOGD_ENABLED_PROP: &str = "ro.boot.logd.enabled";
 
+#[derive(thiserror::Error, Debug)]
+enum MicrodroidError {
+    #[error("Payload has changed: {0}")]
+    PayloadChanged(String),
+    #[error("Payload verification has failed: {0}")]
+    PayloadVerificationFailed(String),
+}
+
+fn translate_error(err: &Error) -> (i32, String) {
+    if let Some(e) = err.downcast_ref::<MicrodroidError>() {
+        match e {
+            MicrodroidError::PayloadChanged(msg) => (ERROR_PAYLOAD_CHANGED, msg.to_string()),
+            MicrodroidError::PayloadVerificationFailed(msg) => {
+                (ERROR_PAYLOAD_VERIFICATION_FAILED, msg.to_string())
+            }
+        }
+    } else {
+        (ERROR_UNKNOWN, err.to_string())
+    }
+}
+
 fn get_vms_rpc_binder() -> Result<Strong<dyn IVirtualMachineService>> {
     // SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership can be
     // safely taken by new_spibinder.
@@ -81,6 +102,17 @@
     kernlog::init()?;
     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(())
+    }
+}
+
+fn try_start_payload(service: &Strong<dyn IVirtualMachineService>) -> Result<()> {
     let metadata = load_metadata().context("Failed to load payload metadata")?;
 
     let mut instance = InstanceDisk::new().context("Failed to load instance.img")?;
@@ -90,11 +122,13 @@
     let verified_data =
         verify_payload(&metadata, saved_data.as_ref()).context("Payload verification failed")?;
     if let Some(saved_data) = saved_data {
-        if saved_data == verified_data {
-            info!("Saved data is verified.");
-        } else {
-            bail!("Detected an update of the payload which isn't supported yet.");
-        }
+        ensure!(
+            saved_data == verified_data,
+            MicrodroidError::PayloadChanged(String::from(
+                "Detected an update of the payload which isn't supported yet."
+            ))
+        );
+        info!("Saved data is verified.");
     } else {
         info!("Saving verified data.");
         instance.write_microdroid_data(&verified_data).context("Failed to write identity data")?;
@@ -103,7 +137,6 @@
     // Before reading a file from the APK, start zipfuse
     system_properties::write("ctl.start", "zipfuse")?;
 
-    let service = get_vms_rpc_binder().expect("cannot connect to VirtualMachineService");
     if !metadata.payload_config_path.is_empty() {
         let config = load_config(Path::new(&metadata.payload_config_path))?;
 
@@ -117,7 +150,7 @@
         wait_for_apex_config_done()?;
 
         if let Some(main_task) = &config.task {
-            exec_task(main_task, &service).map_err(|e| {
+            exec_task(main_task, service).map_err(|e| {
                 error!("failed to execute task: {}", e);
                 e
             })?;
@@ -156,7 +189,10 @@
     let apex_data_from_payload = get_apex_data_from_payload(metadata)?;
     if let Some(saved_data) = saved_data.map(|d| &d.apex_data) {
         // We don't support APEX updates. (assuming that update will change root digest)
-        ensure!(saved_data == &apex_data_from_payload, "APEX payloads has changed.");
+        ensure!(
+            saved_data == &apex_data_from_payload,
+            MicrodroidError::PayloadChanged(String::from("APEXes have changed."))
+        );
         let apex_metadata = to_metadata(&apex_data_from_payload);
         // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
         // metadata instead of the default one (/dev/block/by-name/payload-metadata)
@@ -178,7 +214,10 @@
     // of the VM or APK was updated in the host.
     // TODO(jooyung): consider multithreading to make this faster
     let apk_pubkey = if !root_hash_trustful {
-        verify(DM_MOUNTED_APK_PATH).context(format!("failed to verify {}", DM_MOUNTED_APK_PATH))?
+        verify(DM_MOUNTED_APK_PATH).context(MicrodroidError::PayloadVerificationFailed(format!(
+            "failed to verify {}",
+            DM_MOUNTED_APK_PATH
+        )))?
     } else {
         get_public_key_der(DM_MOUNTED_APK_PATH)?
     };
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 e0d6cc1..61c3edc 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -111,6 +111,9 @@
         public void onPayloadFinished(VirtualMachine vm, int exitCode) {}
 
         @Override
+        public void onError(VirtualMachine vm, int errorCode, String message) {}
+
+        @Override
         public void onDied(VirtualMachine vm) {}
     }
 
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 18d8ade..37350ff 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -36,9 +36,10 @@
         "libmicrodroid_payload_config",
         "libonce_cell",
         "librustutils",
+        "libselinux_bindgen",
+        "libserde",
         "libserde_json",
         "libserde_xml_rs",
-        "libserde",
         "libshared_child",
         "libvmconfig",
         "libzip",
@@ -48,6 +49,7 @@
     ],
     shared_libs: [
         "libbinder_rpc_unstable",
+        "libselinux",
     ],
 }
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
index 15354a3..d7f90a1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
@@ -42,6 +42,11 @@
     void onPayloadFinished(int cid, int exitCode);
 
     /**
+     * Called when an error occurs in the VM.
+     */
+    void onError(int cid, int errorCode, in String message);
+
+    /**
      * Called when the VM dies.
      *
      * Note that this will not be called if the VirtualizationService itself dies, so you should
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl
index b1aebfd..d85b3c1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl
@@ -44,5 +44,5 @@
     /**
      * The VM has died.
      */
-    DEAD = 5,
+    DEAD = 6,
 }
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index 8611898..97f6ca3 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -43,4 +43,25 @@
      * Notifies that the payload has finished.
      */
     void notifyPayloadFinished(int exitCode);
+
+    /**
+     * Notifies that an error has occurred. See the ERROR_* constants.
+     */
+    void notifyError(int errorCode, in String message);
+
+    /**
+     * Error code for all other errors not listed below.
+     */
+    const int ERROR_UNKNOWN = 0;
+
+    /**
+     * Error code indicating that the payload can't be verified due to various reasons (e.g invalid
+     * merkle tree, invalid formats, etc).
+     */
+    const int ERROR_PAYLOAD_VERIFICATION_FAILED = 1;
+
+    /**
+     * Error code indicating that the payload is verified, but has changed since the last boot.
+     */
+    const int ERROR_PAYLOAD_CHANGED = 2;
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 5d64684..af420f6 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -18,6 +18,7 @@
 use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmInstance, VmState};
 use crate::payload::add_microdroid_images;
 use crate::{Cid, FIRST_GUEST_CID, SYSPROP_LAST_CID};
+use crate::selinux::{SeContext, getfilecon};
 use ::binder::unstable_api::AsNative;
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
@@ -25,6 +26,7 @@
     IVirtualMachine::{BnVirtualMachine, IVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
     IVirtualizationService::IVirtualizationService,
+    Partition::Partition,
     PartitionType::PartitionType,
     VirtualMachineAppConfig::DebugLevel::DebugLevel,
     VirtualMachineAppConfig::VirtualMachineAppConfig,
@@ -169,6 +171,8 @@
             }
         }
 
+        let is_app_config = matches!(config, VirtualMachineConfig::AppConfig(_));
+
         let config = match config {
             VirtualMachineConfig::AppConfig(config) => BorrowedOrOwned::Owned(
                 load_app_config(config, &temporary_directory).map_err(|e| {
@@ -183,6 +187,25 @@
         };
         let config = config.as_ref();
 
+        // Check if partition images are labeled incorrectly. This is to prevent random images
+        // which are not protected by the Android Verified Boot (e.g. bits downloaded by apps) from
+        // being loaded in a pVM.  Specifically, for images in the raw config, nothing is allowed
+        // to be labeled as app_data_file. For images in the app config, nothing but the instance
+        // partition is allowed to be labeled as such.
+        config
+            .disks
+            .iter()
+            .flat_map(|disk| disk.partitions.iter())
+            .filter(|partition| {
+                if is_app_config {
+                    partition.label != "vm-instance"
+                } else {
+                    true // all partitions are checked
+                }
+            })
+            .try_for_each(check_label_for_partition)
+            .map_err(|e| new_binder_exception(ExceptionCode::SERVICE_SPECIFIC, e.to_string()))?;
+
         let zero_filler_path = temporary_directory.join("zero.img");
         write_zero_filler(&zero_filler_path).map_err(|e| {
             error!("Failed to make composite image: {}", e);
@@ -606,6 +629,16 @@
     check_permission("android.permission.MANAGE_VIRTUAL_MACHINE")
 }
 
+/// Check if a partition has selinux labels that are not allowed
+fn check_label_for_partition(partition: &Partition) -> Result<()> {
+    let ctx = getfilecon(partition.image.as_ref().unwrap().as_ref())?;
+    if ctx == SeContext::new("u:object_r:app_data_file:s0").unwrap() {
+        Err(anyhow!("Partition {} shouldn't be labeled as {}", &partition.label, ctx))
+    } else {
+        Ok(())
+    }
+}
+
 /// Implementation of the AIDL `IVirtualMachine` interface. Used as a handle to a VM.
 #[derive(Debug)]
 struct VirtualMachine {
@@ -714,6 +747,16 @@
         }
     }
 
+    /// Call all registered callbacks to say that the VM encountered an error.
+    pub fn notify_error(&self, cid: Cid, error_code: i32, message: &str) {
+        let callbacks = &*self.0.lock().unwrap();
+        for callback in callbacks {
+            if let Err(e) = callback.onError(cid as i32, error_code, message) {
+                error!("Error notifying error event from VM CID {}: {}", cid, e);
+            }
+        }
+    }
+
     /// Call all registered callbacks to say that the VM has died.
     pub fn callback_on_died(&self, cid: Cid) {
         let callbacks = &*self.0.lock().unwrap();
@@ -879,10 +922,10 @@
             vm.callbacks.notify_payload_started(cid, stream);
             Ok(())
         } else {
-            error!("notifyPayloadStarted is called from an unknown cid {}", cid);
+            error!("notifyPayloadStarted is called from an unknown CID {}", cid);
             Err(new_binder_exception(
                 ExceptionCode::SERVICE_SPECIFIC,
-                format!("cannot find a VM with cid {}", cid),
+                format!("cannot find a VM with CID {}", cid),
             ))
         }
     }
@@ -896,10 +939,10 @@
             vm.callbacks.notify_payload_ready(cid);
             Ok(())
         } else {
-            error!("notifyPayloadReady is called from an unknown cid {}", cid);
+            error!("notifyPayloadReady is called from an unknown CID {}", cid);
             Err(new_binder_exception(
                 ExceptionCode::SERVICE_SPECIFIC,
-                format!("cannot find a VM with cid {}", cid),
+                format!("cannot find a VM with CID {}", cid),
             ))
         }
     }
@@ -913,10 +956,27 @@
             vm.callbacks.notify_payload_finished(cid, exit_code);
             Ok(())
         } else {
-            error!("notifyPayloadFinished is called from an unknown cid {}", cid);
+            error!("notifyPayloadFinished is called from an unknown CID {}", cid);
             Err(new_binder_exception(
                 ExceptionCode::SERVICE_SPECIFIC,
-                format!("cannot find a VM with cid {}", cid),
+                format!("cannot find a VM with CID {}", cid),
+            ))
+        }
+    }
+
+    fn notifyError(&self, error_code: i32, message: &str) -> binder::Result<()> {
+        let cid = self.cid;
+        if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
+            info!("VM having CID {} encountered an error", cid);
+            vm.update_payload_state(PayloadState::Finished)
+                .map_err(|e| new_binder_exception(ExceptionCode::ILLEGAL_STATE, e.to_string()))?;
+            vm.callbacks.notify_error(cid, error_code, message);
+            Ok(())
+        } else {
+            error!("notifyPayloadStarted is called from an unknown CID {}", cid);
+            Err(new_binder_exception(
+                ExceptionCode::SERVICE_SPECIFIC,
+                format!("cannot find a VM with CID {}", cid),
             ))
         }
     }
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 0e1e974..69ae076 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -18,6 +18,7 @@
 mod composite;
 mod crosvm;
 mod payload;
+mod selinux;
 
 use crate::aidl::{VirtualizationService, BINDER_SERVICE_IDENTIFIER, TEMPORARY_DIRECTORY};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
diff --git a/virtualizationservice/src/selinux.rs b/virtualizationservice/src/selinux.rs
new file mode 100644
index 0000000..e450dee
--- /dev/null
+++ b/virtualizationservice/src/selinux.rs
@@ -0,0 +1,104 @@
+// Copyright 2021, 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.
+
+//! Wrapper to libselinux
+
+use anyhow::{anyhow, Context, Result};
+use std::ffi::{CStr, CString};
+use std::fmt;
+use std::fs::File;
+use std::io;
+use std::ops::Deref;
+use std::os::raw::c_char;
+use std::os::unix::io::AsRawFd;
+use std::ptr;
+
+// Partially copied from system/security/keystore2/selinux/src/lib.rs
+/// SeContext represents an SELinux context string. It can take ownership of a raw
+/// s-string as allocated by `getcon` or `selabel_lookup`. In this case it uses
+/// `freecon` to free the resources when dropped. In its second variant it stores
+/// an `std::ffi::CString` that can be initialized from a Rust string slice.
+#[derive(Debug)]
+pub enum SeContext {
+    /// Wraps a raw context c-string as returned by libselinux.
+    Raw(*mut ::std::os::raw::c_char),
+    /// Stores a context string as `std::ffi::CString`.
+    CString(CString),
+}
+
+impl PartialEq for SeContext {
+    fn eq(&self, other: &Self) -> bool {
+        // We dereference both and thereby delegate the comparison
+        // to `CStr`'s implementation of `PartialEq`.
+        **self == **other
+    }
+}
+
+impl Eq for SeContext {}
+
+impl fmt::Display for SeContext {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.to_str().unwrap_or("Invalid context"))
+    }
+}
+
+impl Drop for SeContext {
+    fn drop(&mut self) {
+        if let Self::Raw(p) = self {
+            // SAFETY: SeContext::Raw is created only with a pointer that is set by libselinux and
+            // has to be freed with freecon.
+            unsafe { selinux_bindgen::freecon(*p) };
+        }
+    }
+}
+
+impl Deref for SeContext {
+    type Target = CStr;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            // SAFETY: the non-owned C string pointed by `p` is guaranteed to be valid (non-null
+            // and shorter than i32::MAX). It is freed when SeContext is dropped.
+            Self::Raw(p) => unsafe { CStr::from_ptr(*p) },
+            Self::CString(cstr) => cstr,
+        }
+    }
+}
+
+impl SeContext {
+    /// Initializes the `SeContext::CString` variant from a Rust string slice.
+    pub fn new(con: &str) -> Result<Self> {
+        Ok(Self::CString(
+            CString::new(con)
+                .with_context(|| format!("Failed to create SeContext with \"{}\"", con))?,
+        ))
+    }
+}
+
+pub fn getfilecon(file: &File) -> Result<SeContext> {
+    let fd = file.as_raw_fd();
+    let mut con: *mut c_char = ptr::null_mut();
+    // SAFETY: the returned pointer `con` is wrapped in SeContext::Raw which is freed with
+    // `freecon` when it is dropped.
+    match unsafe { selinux_bindgen::fgetfilecon(fd, &mut con) } {
+        1.. => {
+            if !con.is_null() {
+                Ok(SeContext::Raw(con))
+            } else {
+                Err(anyhow!("fgetfilecon returned a NULL context"))
+            }
+        }
+        _ => Err(anyhow!(io::Error::last_os_error())).context("fgetfilecon failed"),
+    }
+}
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 1cd51a1..7f5f9fc 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -271,6 +271,11 @@
         Ok(())
     }
 
+    fn onError(&self, _cid: i32, error_code: i32, message: &str) -> BinderResult<()> {
+        eprintln!("VM encountered an error: code={}, message={}", error_code, message);
+        Ok(())
+    }
+
     fn onDied(&self, _cid: i32) -> BinderResult<()> {
         // No need to explicitly report the event to the user (e.g. via println!) because this
         // callback is registered only when the vm tool is invoked as interactive mode (e.g. not