Add tests for Rust VM Payload

Make sure we exercise the Rust wrapper by having a test payload using
it.

API tweaks in the process:
- Add a module for restricted functions to make them more obvious.
- Remove a bogus generic parameter.

Test tweaks in the process:
- Test retrieving VM secrets in more places, it's not a restricted
  operation unlike CDIs etc.

Note that attestation-related APIs are exercised by
VmAttestationTestApp, so aren't covered here.

Bug: 340857915
Test: atest MicrodroidTests
Change-Id: I8f4166ffea5db17381875c83119c592d6be48296
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index 5f5dc1c..52635ad 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -72,7 +72,7 @@
 impl IAttestationService for AttestationService {
     fn requestAttestationForTesting(&self) -> binder::Result<()> {
         const CHALLENGE: &[u8] = &[0xaa; 32];
-        let res = vm_payload::request_attestation_for_testing(CHALLENGE)
+        let res = vm_payload::restricted::request_attestation_for_testing(CHALLENGE)
             .with_log()
             .or_service_specific_exception(-1)?;
         *self.res.lock().unwrap() = Some(res);
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index e02db39..98ef092 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -20,8 +20,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
-import static org.junit.Assume.assumeTrue;
 import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.Instrumentation;
 import android.app.UiAutomation;
@@ -548,6 +548,7 @@
         public int mFileMode;
         public int mMountFlags;
         public String mConsoleInput;
+        public byte[] mInstanceSecret;
 
         public void assertNoException() {
             if (mException != null) {
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 471aea7..e32ff88 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -55,6 +55,7 @@
         "MicrodroidExitNativeLib",
         "MicrodroidPrivateLinkingNativeLib",
         "MicrodroidCrashNativeLib",
+        "libmicrodroid_testlib_rust",
         "libvm_attestation_test_payload",
     ],
     min_sdk_version: "33",
@@ -166,3 +167,22 @@
     header_libs: ["vm_payload_headers"],
     stl: "libc++_static",
 }
+
+// A payload written in Rust, using the Rust wrapper for the VM payload API.
+rust_ffi_shared {
+    name: "libmicrodroid_testlib_rust",
+    crate_name: "microdroid_testlib_rust",
+    defaults: ["avf_build_flags_rust"],
+    prefer_rlib: true,
+    srcs: ["src/native/testbinary.rs"],
+    compile_multilib: "both",
+    rustlibs: [
+        "com.android.microdroid.testservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libavflog",
+        "libcstr",
+        "liblog_rust",
+        "libvm_payload_rs",
+    ],
+}
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 4141903..4d0f5eb 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -197,6 +197,7 @@
                             tr.mSublibRunProp = ts.readProperty("debug.microdroid.app.sublib.run");
                             tr.mApkContentsPath = ts.getApkContentsPath();
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+                            tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
                         });
         testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
@@ -204,6 +205,7 @@
         assertThat(testResults.mSublibRunProp).isEqualTo("true");
         assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
         assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+        assertThat(testResults.mInstanceSecret).hasLength(32);
     }
 
     @Test
@@ -2363,6 +2365,63 @@
         runVmTestService(TAG, vm, (ts, tr) -> {}).assertNoException();
     }
 
+    @Test
+    public void createAndRunRustVm() throws Exception {
+        // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+        // We're testing the same functionality as in other tests, the only difference is
+        // that the payload is written in Rust.
+
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mAddInteger = ts.addInteger(37, 73);
+                            tr.mApkContentsPath = ts.getApkContentsPath();
+                            tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+                            tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
+        assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+        assertThat(testResults.mInstanceSecret).hasLength(32);
+    }
+
+    @Test
+    public void createAndRunRustVmWithEncryptedStorage() throws Exception {
+        // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+        // We're testing the same functionality as in other tests, the only difference is
+        // that the payload is written in Rust.
+
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> tr.mEncryptedStoragePath = ts.getEncryptedStoragePath());
+        testResults.assertNoException();
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
+    }
+
     private VirtualMachineConfig buildVmConfigWithVendor(File vendorDiskImage) throws Exception {
         return buildVmConfigWithVendor(vendorDiskImage, "MicrodroidTestNativeLib.so");
     }
diff --git a/tests/testapk/src/native/testbinary.rs b/tests/testapk/src/native/testbinary.rs
new file mode 100644
index 0000000..85b411e
--- /dev/null
+++ b/tests/testapk/src/native/testbinary.rs
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2024 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.
+ */
+
+//! A VM payload that exists to allow testing of the Rust wrapper for the VM payload APIs.
+
+use anyhow::Result;
+use com_android_microdroid_testservice::{
+    aidl::com::android::microdroid::testservice::{
+        IAppCallback::IAppCallback,
+        ITestService::{BnTestService, ITestService, PORT},
+    },
+    binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong},
+};
+use cstr::cstr;
+use log::{error, info};
+use std::panic;
+use std::process::exit;
+use std::string::String;
+use std::vec::Vec;
+
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("microdroid_testlib_rust")
+            .with_max_level(log::LevelFilter::Debug),
+    );
+    // Redirect panic messages to logcat.
+    panic::set_hook(Box::new(|panic_info| {
+        error!("{panic_info}");
+    }));
+    if let Err(e) = try_main() {
+        error!("failed with {:?}", e);
+        exit(1);
+    }
+}
+
+fn try_main() -> Result<()> {
+    info!("Welcome to the Rust test binary");
+
+    vm_payload::run_single_vsock_service(TestService::new_binder(), PORT.try_into()?)
+}
+
+struct TestService {}
+
+impl Interface for TestService {}
+
+impl TestService {
+    fn new_binder() -> Strong<dyn ITestService> {
+        BnTestService::new_binder(TestService {}, BinderFeatures::default())
+    }
+}
+
+impl ITestService for TestService {
+    fn quit(&self) -> BinderResult<()> {
+        exit(0)
+    }
+
+    fn addInteger(&self, a: i32, b: i32) -> BinderResult<i32> {
+        a.checked_add(b).ok_or_else(|| Status::new_exception(ExceptionCode::ILLEGAL_ARGUMENT, None))
+    }
+
+    fn getApkContentsPath(&self) -> BinderResult<String> {
+        Ok(vm_payload::apk_contents_path().to_string_lossy().to_string())
+    }
+
+    fn getEncryptedStoragePath(&self) -> BinderResult<String> {
+        Ok(vm_payload::encrypted_storage_path()
+            .map(|p| p.to_string_lossy().to_string())
+            .unwrap_or("".to_string()))
+    }
+
+    fn insecurelyExposeVmInstanceSecret(&self) -> BinderResult<Vec<u8>> {
+        let mut secret = vec![0u8; 32];
+        vm_payload::get_vm_instance_secret(b"identifier", secret.as_mut_slice());
+        Ok(secret)
+    }
+
+    // Everything below here is unimplemented. Implementations may be added as needed.
+
+    fn readProperty(&self, _: &str) -> BinderResult<String> {
+        unimplemented()
+    }
+    fn insecurelyExposeAttestationCdi(&self) -> BinderResult<Vec<u8>> {
+        unimplemented()
+    }
+    fn getBcc(&self) -> BinderResult<Vec<u8>> {
+        unimplemented()
+    }
+    fn runEchoReverseServer(&self) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn getEffectiveCapabilities(&self) -> BinderResult<Vec<String>> {
+        unimplemented()
+    }
+    fn getUid(&self) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn writeToFile(&self, _: &str, _: &str) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn readFromFile(&self, _: &str) -> BinderResult<String> {
+        unimplemented()
+    }
+    fn getFilePermissions(&self, _: &str) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn getMountFlags(&self, _: &str) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn requestCallback(&self, _: &Strong<dyn IAppCallback + 'static>) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn readLineFromConsole(&self) -> BinderResult<String> {
+        unimplemented()
+    }
+}
+
+fn unimplemented<T>() -> BinderResult<T> {
+    let message = cstr!("Got a call to an unimplemented ITestService method in testbinary.rs");
+    error!("{message:?}");
+    Err(Status::new_exception(ExceptionCode::UNSUPPORTED_OPERATION, Some(message)))
+}
diff --git a/vm_payload/wrapper/lib.rs b/vm_payload/wrapper/lib.rs
index bc26802..d3f03d7 100644
--- a/vm_payload/wrapper/lib.rs
+++ b/vm_payload/wrapper/lib.rs
@@ -22,9 +22,7 @@
 
 mod attestation;
 
-pub use attestation::{
-    request_attestation, request_attestation_for_testing, AttestationError, AttestationResult,
-};
+pub use attestation::{request_attestation, AttestationError, AttestationResult};
 use binder::unstable_api::AsNative;
 use binder::{FromIBinder, Strong};
 use std::ffi::{c_void, CStr, OsStr};
@@ -36,6 +34,16 @@
     AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer,
 };
 
+/// The functions declared here are restricted to VMs created with a config file;
+/// they will fail, or panic, if called in other VMs. The ability to create such VMs
+/// requires the android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission, and is
+/// therefore not available to privileged or third party apps.
+///
+/// These functions can be used by tests, if the permission is granted via shell.
+pub mod restricted {
+    pub use crate::attestation::request_attestation_for_testing;
+}
+
 /// Marks the main function of the VM payload.
 ///
 /// When the VM is run, this function is called. If it returns, the VM ends normally with a 0 exit
@@ -171,7 +179,7 @@
 ///
 /// The secret is returned in [`secret`], truncated to its size, which must be between
 /// 1 and 32 bytes (inclusive) or the function will panic.
-pub fn get_vm_instance_secret<const N: usize>(identifier: &[u8], secret: &mut [u8]) {
+pub fn get_vm_instance_secret(identifier: &[u8], secret: &mut [u8]) {
     let secret_size = secret.len();
     assert!((1..=32).contains(&secret_size), "VM instance secrets can be up to 32 bytes long");