Merge "Minimize property_contexts"
diff --git a/apex/Android.bp b/apex/Android.bp
index bf38860..2194c67 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -47,6 +47,7 @@
     binaries: [
         "fd_server",
         "vm",
+        "compos_key_cmd",
 
         // tools to create composite images
         "mk_cdisk",
diff --git a/compos/Android.bp b/compos/Android.bp
index 858f64c..0cb6894 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -66,6 +66,24 @@
     ],
 }
 
+rust_binary {
+    name: "compos_key_service",
+    srcs: ["src/compos_key_service.rs"],
+    edition: "2018",
+    rustlibs: [
+        "compos_aidl_interface-rust",
+        "android.system.keystore2-V1-rust",
+        "android.hardware.security.keymint-V1-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "liblog_rust",
+        "libring",
+        "libscopeguard",
+    ],
+    prefer_rlib: true,
+    apex_available: ["com.android.compos"],
+}
+
 // TODO(b/190503456) Remove this when vm/virtualizationservice generates payload.img from vm_config
 prebuilt_etc {
     name: "compos_payload_config",
diff --git a/compos/aidl/Android.bp b/compos/aidl/Android.bp
index 3639775..07bec09 100644
--- a/compos/aidl/Android.bp
+++ b/compos/aidl/Android.bp
@@ -15,5 +15,10 @@
                 "com.android.compos",
             ],
         },
+        ndk: {
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
     },
 }
diff --git a/compos/aidl/com/android/compos/CompOsKeyData.aidl b/compos/aidl/com/android/compos/CompOsKeyData.aidl
new file mode 100644
index 0000000..381ec0d
--- /dev/null
+++ b/compos/aidl/com/android/compos/CompOsKeyData.aidl
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+package com.android.compos;
+
+/** {@hide} */
+parcelable CompOsKeyData {
+    /**
+     * Self-signed certificate (X.509 DER) containing the public key.
+     */
+    byte[] certificate;
+
+    /**
+     * Opaque encrypted blob containing the private key and related metadata.
+     */
+    byte[] keyBlob;
+}
diff --git a/compos/aidl/com/android/compos/ICompOsKeyService.aidl b/compos/aidl/com/android/compos/ICompOsKeyService.aidl
new file mode 100644
index 0000000..2ddae58
--- /dev/null
+++ b/compos/aidl/com/android/compos/ICompOsKeyService.aidl
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+package com.android.compos;
+
+import com.android.compos.CompOsKeyData;
+
+/** {@hide} */
+interface ICompOsKeyService {
+    /**
+     * Generate a new public/private key pair suitable for signing CompOs output files.
+     *
+     * @return a certificate for the public key and the encrypted private key
+     */
+    CompOsKeyData generateSigningKey();
+
+    /**
+     * Check that the supplied encrypted private key is valid for signing CompOs output files, and
+     * corresponds to the public key.
+     *
+     * @param keyBlob The encrypted blob containing the private key, as returned by
+     *                generateSigningKey().
+     * @param publicKey The public key, as a DER encoded RSAPublicKey (RFC 3447 Appendix-A.1.1).
+     * @return whether the inputs are valid and correspond to each other.
+     */
+    boolean verifySigningKey(in byte[] keyBlob, in byte[] publicKey);
+}
diff --git a/compos/apex/Android.bp b/compos/apex/Android.bp
index 3a8f601..95463d0 100644
--- a/compos/apex/Android.bp
+++ b/compos/apex/Android.bp
@@ -36,6 +36,7 @@
     updatable: false,
 
     binaries: [
+        "compos_key_service",
         "compsvc",
         "compsvc_worker",
         "pvm_exec",
diff --git a/compos/compos_key_cmd/Android.bp b/compos/compos_key_cmd/Android.bp
new file mode 100644
index 0000000..e03dfdf
--- /dev/null
+++ b/compos/compos_key_cmd/Android.bp
@@ -0,0 +1,16 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_binary {
+    name: "compos_key_cmd",
+    srcs: ["compos_key_cmd.cpp"],
+    apex_available: ["com.android.virt"],
+
+    shared_libs: [
+        "compos_aidl_interface-ndk_platform",
+        "libbase",
+        "libbinder_ndk",
+        "libcrypto",
+    ],
+}
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
new file mode 100644
index 0000000..d98dac5
--- /dev/null
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+
+#include <aidl/com/android/compos/ICompOsKeyService.h>
+#include <android-base/file.h>
+#include <android-base/result.h>
+#include <android/binder_auto_utils.h>
+#include <android/binder_manager.h>
+#include <openssl/evp.h>
+#include <openssl/mem.h>
+#include <openssl/rsa.h>
+#include <openssl/x509.h>
+
+#include <iostream>
+#include <string>
+
+using android::base::Error;
+using android::base::Result;
+
+using aidl::com::android::compos::CompOsKeyData;
+using aidl::com::android::compos::ICompOsKeyService;
+
+static bool writeBytesToFile(const std::vector<uint8_t>& bytes, const std::string& path) {
+    std::string str(bytes.begin(), bytes.end());
+    return android::base::WriteStringToFile(str, path);
+}
+
+static Result<std::vector<uint8_t>> readBytesFromFile(const std::string& path) {
+    std::string str;
+    if (!android::base::ReadFileToString(path, &str)) {
+        return Error() << "Failed to read " << path;
+    }
+    return std::vector<uint8_t>(str.begin(), str.end());
+}
+
+static Result<std::vector<uint8_t>> extractRsaPublicKey(
+        const std::vector<uint8_t>& der_certificate) {
+    auto data = der_certificate.data();
+    bssl::UniquePtr<X509> x509(d2i_X509(nullptr, &data, der_certificate.size()));
+    if (!x509) {
+        return Error() << "Failed to parse certificate";
+    }
+    if (data != der_certificate.data() + der_certificate.size()) {
+        return Error() << "Certificate has unexpected trailing data";
+    }
+
+    bssl::UniquePtr<EVP_PKEY> pkey(X509_get_pubkey(x509.get()));
+    if (EVP_PKEY_base_id(pkey.get()) != EVP_PKEY_RSA) {
+        return Error() << "Subject key is not RSA";
+    }
+    RSA* rsa = EVP_PKEY_get0_RSA(pkey.get());
+    if (!rsa) {
+        return Error() << "Failed to extract RSA key";
+    }
+
+    uint8_t* out = nullptr;
+    int size = i2d_RSAPublicKey(rsa, &out);
+    if (size < 0 || !out) {
+        return Error() << "Failed to convert to RSAPublicKey";
+    }
+
+    bssl::UniquePtr<uint8_t> buffer(out);
+    std::vector<uint8_t> result(out, out + size);
+    return result;
+}
+
+static Result<void> generate(const std::string& blob_file, const std::string& public_key_file) {
+    ndk::SpAIBinder binder(AServiceManager_getService("android.system.composkeyservice"));
+    auto service = ICompOsKeyService::fromBinder(binder);
+    if (!service) {
+        return Error() << "No service";
+    }
+
+    CompOsKeyData key_data;
+    auto status = service->generateSigningKey(&key_data);
+    if (!status.isOk()) {
+        return Error() << "Failed to generate key: " << status.getDescription();
+    }
+
+    auto public_key = extractRsaPublicKey(key_data.certificate);
+    if (!public_key.ok()) {
+        return Error() << "Failed to extract public key from cert: " << public_key.error();
+    }
+    if (!writeBytesToFile(key_data.keyBlob, blob_file)) {
+        return Error() << "Failed to write keyBlob to " << blob_file;
+    }
+
+    if (!writeBytesToFile(public_key.value(), public_key_file)) {
+        return Error() << "Failed to write public key to " << public_key_file;
+    }
+
+    return {};
+}
+
+static Result<bool> verify(const std::string& blob_file, const std::string& public_key_file) {
+    ndk::SpAIBinder binder(AServiceManager_getService("android.system.composkeyservice"));
+    auto service = ICompOsKeyService::fromBinder(binder);
+    if (!service) {
+        return Error() << "No service";
+    }
+
+    auto blob = readBytesFromFile(blob_file);
+    if (!blob.ok()) {
+        return blob.error();
+    }
+
+    auto public_key = readBytesFromFile(public_key_file);
+    if (!public_key.ok()) {
+        return public_key.error();
+    }
+
+    bool result = false;
+    auto status = service->verifySigningKey(blob.value(), public_key.value(), &result);
+    if (!status.isOk()) {
+        return Error() << "Failed to verify key: " << status.getDescription();
+    }
+
+    return result;
+}
+
+int main(int argc, char** argv) {
+    if (argc == 4 && std::string(argv[1]) == "--generate") {
+        auto result = generate(argv[2], argv[3]);
+        if (result.ok()) {
+            return 0;
+        } else {
+            std::cerr << result.error() << '\n';
+        }
+    } else if (argc == 4 && std::string(argv[1]) == "--verify") {
+        auto result = verify(argv[2], argv[3]);
+        if (result.ok()) {
+            if (result.value()) {
+                std::cerr << "Key files are valid.\n";
+                return 0;
+            } else {
+                std::cerr << "Key files are not valid.\n";
+            }
+        } else {
+            std::cerr << result.error() << '\n';
+        }
+    } else {
+        std::cerr << "Usage: \n"
+                  << "  --generate <blob file> <public key file> Generate new key pair and "
+                     "write\n"
+                  << "    the private key blob and public key to the specified files.\n "
+                  << "  --verify <blob file> <public key file> Verify that the content of the\n"
+                  << "    specified private key blob and public key files are valid.\n ";
+    }
+    return 1;
+}
diff --git a/compos/src/compos_key_service.rs b/compos/src/compos_key_service.rs
new file mode 100644
index 0000000..97fd855
--- /dev/null
+++ b/compos/src/compos_key_service.rs
@@ -0,0 +1,196 @@
+// 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.
+
+//! Provides a binder service for key generation & verification for CompOs. We assume we have
+//! access to Keystore in the VM, but not persistent storage; instead the host stores the key
+//! on our behalf via this service.
+
+use android_hardware_security_keymint::aidl::android::hardware::security::keymint::{
+    Algorithm::Algorithm, Digest::Digest, KeyParameter::KeyParameter,
+    KeyParameterValue::KeyParameterValue, KeyPurpose::KeyPurpose, PaddingMode::PaddingMode,
+    SecurityLevel::SecurityLevel, Tag::Tag,
+};
+use android_system_keystore2::aidl::android::system::keystore2::{
+    Domain::Domain, IKeystoreSecurityLevel::IKeystoreSecurityLevel,
+    IKeystoreService::IKeystoreService, KeyDescriptor::KeyDescriptor,
+};
+use anyhow::{anyhow, Context, Result};
+use compos_aidl_interface::aidl::com::android::compos::{
+    CompOsKeyData::CompOsKeyData,
+    ICompOsKeyService::{BnCompOsKeyService, ICompOsKeyService},
+};
+use compos_aidl_interface::binder::{
+    self, add_service, get_interface, BinderFeatures, ExceptionCode, Interface, ProcessState,
+    Status, Strong,
+};
+use log::{info, warn, Level};
+use ring::rand::{SecureRandom, SystemRandom};
+use ring::signature;
+use scopeguard::ScopeGuard;
+use std::ffi::CString;
+use std::sync::Mutex;
+
+const LOG_TAG: &str = "CompOsKeyService";
+const OUR_SERVICE_NAME: &str = "android.system.composkeyservice";
+
+const KEYSTORE_SERVICE_NAME: &str = "android.system.keystore2.IKeystoreService/default";
+const COMPOS_NAMESPACE: i64 = 101;
+const PURPOSE_SIGN: KeyParameter =
+    KeyParameter { tag: Tag::PURPOSE, value: KeyParameterValue::KeyPurpose(KeyPurpose::SIGN) };
+const ALGORITHM: KeyParameter =
+    KeyParameter { tag: Tag::ALGORITHM, value: KeyParameterValue::Algorithm(Algorithm::RSA) };
+const PADDING: KeyParameter = KeyParameter {
+    tag: Tag::PADDING,
+    value: KeyParameterValue::PaddingMode(PaddingMode::RSA_PKCS1_1_5_SIGN),
+};
+const DIGEST: KeyParameter =
+    KeyParameter { tag: Tag::DIGEST, value: KeyParameterValue::Digest(Digest::SHA_2_256) };
+const KEY_SIZE: KeyParameter =
+    KeyParameter { tag: Tag::KEY_SIZE, value: KeyParameterValue::Integer(2048) };
+const EXPONENT: KeyParameter =
+    KeyParameter { tag: Tag::RSA_PUBLIC_EXPONENT, value: KeyParameterValue::LongInteger(65537) };
+const NO_AUTH_REQUIRED: KeyParameter =
+    KeyParameter { tag: Tag::NO_AUTH_REQUIRED, value: KeyParameterValue::BoolValue(true) };
+
+const KEY_DESCRIPTOR: KeyDescriptor =
+    KeyDescriptor { domain: Domain::BLOB, nspace: COMPOS_NAMESPACE, alias: None, blob: None };
+
+struct CompOsKeyService {
+    random: SystemRandom,
+    state: Mutex<State>,
+}
+
+struct State {
+    security_level: Strong<dyn IKeystoreSecurityLevel>,
+}
+
+impl Interface for CompOsKeyService {}
+
+impl ICompOsKeyService for CompOsKeyService {
+    fn generateSigningKey(&self) -> binder::Result<CompOsKeyData> {
+        self.do_generate()
+            .map_err(|e| new_binder_exception(ExceptionCode::ILLEGAL_STATE, e.to_string()))
+    }
+
+    fn verifySigningKey(&self, key_blob: &[u8], public_key: &[u8]) -> binder::Result<bool> {
+        Ok(if let Err(e) = self.do_verify(key_blob, public_key) {
+            warn!("Signing key verification failed: {}", e.to_string());
+            false
+        } else {
+            true
+        })
+    }
+}
+
+/// Constructs a new Binder error `Status` with the given `ExceptionCode` and message.
+fn new_binder_exception<T: AsRef<str>>(exception: ExceptionCode, message: T) -> Status {
+    Status::new_exception(exception, CString::new(message.as_ref()).ok().as_deref())
+}
+
+impl CompOsKeyService {
+    fn new(keystore_service: &Strong<dyn IKeystoreService>) -> Self {
+        Self {
+            random: SystemRandom::new(),
+            state: Mutex::new(State {
+                security_level: keystore_service
+                    .getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT)
+                    .unwrap(),
+            }),
+        }
+    }
+
+    fn security_level(&self) -> Strong<dyn IKeystoreSecurityLevel> {
+        // We need the Mutex because Strong<_> isn't sync. But we don't need to keep it locked
+        // to make the call, once we've cloned the pointer.
+        self.state.lock().unwrap().security_level.clone()
+    }
+
+    fn do_generate(&self) -> Result<CompOsKeyData> {
+        let key_parameters =
+            [PURPOSE_SIGN, ALGORITHM, PADDING, DIGEST, KEY_SIZE, EXPONENT, NO_AUTH_REQUIRED];
+        let attestation_key = None;
+        let flags = 0;
+        let entropy = [];
+
+        let key_metadata = self
+            .security_level()
+            .generateKey(&KEY_DESCRIPTOR, attestation_key, &key_parameters, flags, &entropy)
+            .context("Generating key failed")?;
+
+        if let (Some(certificate), Some(blob)) = (key_metadata.certificate, key_metadata.key.blob) {
+            Ok(CompOsKeyData { certificate, keyBlob: blob })
+        } else {
+            Err(anyhow!("Missing cert or blob"))
+        }
+    }
+
+    fn do_verify(&self, key_blob: &[u8], public_key: &[u8]) -> Result<()> {
+        let mut data = [0u8; 32];
+        self.random.fill(&mut data).context("No random data")?;
+
+        let signature = self.sign(key_blob, &data)?;
+
+        let public_key =
+            signature::UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, public_key);
+        public_key.verify(&data, &signature).context("Signature verification failed")?;
+
+        Ok(())
+    }
+
+    fn sign(&self, key_blob: &[u8], data: &[u8]) -> Result<Vec<u8>> {
+        let key_descriptor = KeyDescriptor { blob: Some(key_blob.to_vec()), ..KEY_DESCRIPTOR };
+        let operation_parameters = [PURPOSE_SIGN, ALGORITHM, PADDING, DIGEST];
+        let forced = false;
+
+        let response = self
+            .security_level()
+            .createOperation(&key_descriptor, &operation_parameters, forced)
+            .context("Creating key failed")?;
+        let operation = scopeguard::guard(
+            response.iOperation.ok_or_else(|| anyhow!("No operation created"))?,
+            |op| op.abort().unwrap_or_default(),
+        );
+
+        if response.operationChallenge.is_some() {
+            return Err(anyhow!("Key requires user authorization"));
+        }
+
+        let signature = operation.finish(Some(&data), None).context("Signing failed")?;
+        // Operation has finished, we're no longer responsible for aborting it
+        ScopeGuard::into_inner(operation);
+
+        signature.ok_or_else(|| anyhow!("No signature returned"))
+    }
+}
+
+fn main() -> Result<()> {
+    android_logger::init_once(
+        android_logger::Config::default().with_tag(LOG_TAG).with_min_level(Level::Trace),
+    );
+
+    // We need to start the thread pool for Binder to work properly.
+    ProcessState::start_thread_pool();
+
+    let keystore_service = get_interface::<dyn IKeystoreService>(KEYSTORE_SERVICE_NAME)
+        .context("No Keystore service")?;
+    let service = CompOsKeyService::new(&keystore_service);
+    let service = BnCompOsKeyService::new_binder(service, BinderFeatures::default());
+
+    add_service(OUR_SERVICE_NAME, service.as_binder()).context("Adding service failed")?;
+    info!("It's alive!");
+
+    ProcessState::join_thread_pool();
+
+    Ok(())
+}
diff --git a/launcher/Android.bp b/launcher/Android.bp
index 2c3f093..93cae96 100644
--- a/launcher/Android.bp
+++ b/launcher/Android.bp
@@ -5,5 +5,8 @@
 cc_binary {
     name: "microdroid_launcher",
     srcs: ["main.cpp"],
-    shared_libs: ["libdl"],
+    shared_libs: [
+        "libdl",
+        "libdl_android",
+    ],
 }
diff --git a/launcher/main.cpp b/launcher/main.cpp
index fc9477d..4ecef3f 100644
--- a/launcher/main.cpp
+++ b/launcher/main.cpp
@@ -18,6 +18,24 @@
 
 #include <cstdlib>
 #include <iostream>
+#include <string>
+
+#include <android/dlext.h>
+
+extern "C" {
+enum {
+    ANDROID_NAMESPACE_TYPE_REGULAR = 0,
+    ANDROID_NAMESPACE_TYPE_ISOLATED = 1,
+    ANDROID_NAMESPACE_TYPE_SHARED = 2,
+};
+
+extern struct android_namespace_t* android_create_namespace(
+        const char* name, const char* ld_library_path, const char* default_library_path,
+        uint64_t type, const char* permitted_when_isolated_path,
+        struct android_namespace_t* parent);
+} // extern "C"
+
+static void* load(const std::string& libname);
 
 int main(int argc, char* argv[]) {
     if (argc < 2) {
@@ -27,7 +45,7 @@
     }
 
     const char* libname = argv[1];
-    void* handle = dlopen(libname, RTLD_NOW);
+    void* handle = load(libname);
     if (handle == nullptr) {
         std::cerr << "Failed to load " << libname << ": " << dlerror() << "\n";
         return EXIT_FAILURE;
@@ -42,3 +60,31 @@
 
     return entry(argc - 1, argv + 1);
 }
+
+// Create a new linker namespace whose search path is set to the directory of the library. Then
+// load it from there. Returns the handle to the loaded library if successful. Returns nullptr
+// if failed.
+void* load(const std::string& libname) {
+    // Parent as nullptr means the default namespace
+    android_namespace_t* parent = nullptr;
+    // The search paths of the new namespace are inherited from the parent namespace.
+    const uint64_t type = ANDROID_NAMESPACE_TYPE_SHARED;
+    // The directory of the library is appended to the search paths
+    const std::string libdir = libname.substr(0, libname.find_last_of("/"));
+    const char* ld_library_path = libdir.c_str();
+    const char* default_library_path = libdir.c_str();
+
+    android_namespace_t* new_ns = nullptr;
+    new_ns = android_create_namespace("microdroid_app", ld_library_path, default_library_path, type,
+                                      /* permitted_when_isolated_path */ nullptr, parent);
+    if (new_ns == nullptr) {
+        std::cerr << "Failed to create linker namespace: " << dlerror() << "\n";
+        return nullptr;
+    }
+
+    const android_dlextinfo info = {
+            .flags = ANDROID_DLEXT_USE_NAMESPACE,
+            .library_namespace = new_ns,
+    };
+    return android_dlopen_ext(libname.c_str(), RTLD_NOW, &info);
+}
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index e9eb2ef..f942349 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -76,8 +76,6 @@
         "cgroups.json",
         "public.libraries.android.txt",
 
-        "android.system.keystore2-V1-ndk_platform",
-
         // TODO(b/185767624): remove hidl after full keymint support
         "hwservicemanager",
 
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 54541c0..2457797 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -97,6 +97,7 @@
         // Check if the command in vm_config.json was executed by examining the side effect of the
         // command
         assertThat(runOnMicrodroid("getprop", "debug.microdroid.app.run"), is("true"));
+        assertThat(runOnMicrodroid("getprop", "debug.microdroid.app.sublib.run"), is("true"));
 
         // Manually execute the library and check the output
         final String microdroidLauncher = "system/bin/microdroid_launcher";
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index f72d616..fc5681e 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -13,14 +13,20 @@
 // TODO(jiyong): make this a binary, not a shared library
 cc_library_shared {
     name: "MicrodroidTestNativeLib",
-    srcs: ["src/native/*.cpp"],
+    srcs: ["src/native/testbinary.cpp"],
     shared_libs: [
         "android.system.keystore2-V1-ndk_platform",
         "libbase",
         "libbinder_ndk",
+        "MicrodroidTestNativeLibSub",
     ],
 }
 
+cc_library_shared {
+    name: "MicrodroidTestNativeLibSub",
+    srcs: ["src/native/testlib.cpp"],
+}
+
 genrule {
     name: "MicrodroidTestApp.signed",
     out: [
@@ -28,12 +34,12 @@
         "MicrodroidTestApp.apk.idsig",
     ],
     srcs: [":MicrodroidTestApp"],
-    tools:["apksigner"],
+    tools: ["apksigner"],
     tool_files: ["test.keystore"],
     cmd: "$(location apksigner) sign " +
-         "--ks $(location test.keystore) " +
-         "--ks-pass=pass:testkey --key-pass=pass:testkey " +
-         "--in $(in) " +
-         "--out $(genDir)/MicrodroidTestApp.apk",
-         // $(genDir)/MicrodroidTestApp.apk.idsig is generated implicitly
+        "--ks $(location test.keystore) " +
+        "--ks-pass=pass:testkey --key-pass=pass:testkey " +
+        "--in $(in) " +
+        "--out $(genDir)/MicrodroidTestApp.apk",
+    // $(genDir)/MicrodroidTestApp.apk.idsig is generated implicitly
 }
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 5510ae1..20519cd 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -38,6 +38,8 @@
 using android::base::Error;
 using android::base::Result;
 
+extern void testlib_sub();
+
 namespace {
 
 Result<void> test_keystore() {
@@ -192,6 +194,7 @@
             printf(" ");
         }
     }
+    testlib_sub();
     printf("\n");
 
     __system_property_set("debug.microdroid.app.run", "true");
diff --git a/tests/testapk/src/native/testlib.cpp b/tests/testapk/src/native/testlib.cpp
new file mode 100644
index 0000000..792c6c8
--- /dev/null
+++ b/tests/testapk/src/native/testlib.cpp
@@ -0,0 +1,5 @@
+#include <sys/system_properties.h>
+
+void testlib_sub() {
+    __system_property_set("debug.microdroid.app.sublib.run", "true");
+}