Keystore 2.0: Add run_as to keystore2_test_utils

The run_as function allows a test with sufficient privileges to run a
closure as different identity given by a tuple of UID, GID, and SELinux
context. This is infrastructure in preparation for the keystore2 vts
test.

Test: keystore2_test_utils_test
Bug: 182508302
Change-Id: Ic1923028e5bc4ca4b1112e34669d52687450fd14
diff --git a/keystore2/Android.bp b/keystore2/Android.bp
index 9c3ce6e..7c4f61b 100644
--- a/keystore2/Android.bp
+++ b/keystore2/Android.bp
@@ -74,8 +74,32 @@
     crate_name: "keystore2_test_utils",
     srcs: ["test_utils/lib.rs"],
     rustlibs: [
+        "libkeystore2_selinux",
         "liblog_rust",
+        "libnix",
         "librand",
+        "libserde",
+        "libserde_cbor",
+    ],
+}
+
+rust_test {
+    name: "keystore2_test_utils_test",
+    srcs: ["test_utils/lib.rs"],
+    test_suites: ["general-tests"],
+    // TODO Remove custom test_config and enable the following two lines when
+    //      b/200602232 was resolved.
+    // require_root: true,
+    // auto_gen_config: true,
+    test_config: "test_utils/AndroidTest.xml",
+    compile_multilib: "first",
+    rustlibs: [
+        "libkeystore2_selinux",
+        "liblog_rust",
+        "libnix",
+        "librand",
+        "libserde",
+        "libserde_cbor",
     ],
 }
 
diff --git a/keystore2/TEST_MAPPING b/keystore2/TEST_MAPPING
index 16b6f85..127ff1e 100644
--- a/keystore2/TEST_MAPPING
+++ b/keystore2/TEST_MAPPING
@@ -10,6 +10,9 @@
       "name": "keystore2_test"
     },
     {
+      "name": "keystore2_test_utils_test"
+    },
+    {
       "name": "CtsIdentityTestCases"
     }
   ]
diff --git a/keystore2/selinux/src/lib.rs b/keystore2/selinux/src/lib.rs
index cf6dfd3..902e9a4 100644
--- a/keystore2/selinux/src/lib.rs
+++ b/keystore2/selinux/src/lib.rs
@@ -321,6 +321,18 @@
     }
 }
 
+/// Safe wrapper around setcon.
+pub fn setcon(target: &CStr) -> std::io::Result<()> {
+    // SAFETY: `setcon` takes a const char* and only performs read accesses on it
+    // using strdup and strcmp. `setcon` does not retain a pointer to `target`
+    // and `target` outlives the call to `setcon`.
+    if unsafe { selinux::setcon(target.as_ptr()) } != 0 {
+        Err(std::io::Error::last_os_error())
+    } else {
+        Ok(())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/keystore2/test_utils/AndroidTest.xml b/keystore2/test_utils/AndroidTest.xml
new file mode 100644
index 0000000..24e277a
--- /dev/null
+++ b/keystore2/test_utils/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<configuration description="Config to run keystore2_test_utils_test device tests.">
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option
+            name="push"
+            value="keystore2_test_utils_test->/data/local/tmp/keystore2_test_utils_test"
+        />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+        <option name="test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="keystore2_test_utils_test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/keystore2/test_utils/lib.rs b/keystore2/test_utils/lib.rs
index 627af20..a355544 100644
--- a/keystore2/test_utils/lib.rs
+++ b/keystore2/test_utils/lib.rs
@@ -19,6 +19,8 @@
 use std::path::{Path, PathBuf};
 use std::{env::temp_dir, ops::Deref};
 
+pub mod run_as;
+
 /// Represents the lifecycle of a temporary directory for testing.
 #[derive(Debug)]
 pub struct TempDir {
diff --git a/keystore2/test_utils/run_as.rs b/keystore2/test_utils/run_as.rs
new file mode 100644
index 0000000..d42303d
--- /dev/null
+++ b/keystore2/test_utils/run_as.rs
@@ -0,0 +1,191 @@
+// 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.
+
+//! This module is intended for testing access control enforcement of services such as keystore2,
+//! by assuming various identities with varying levels of privilege. Consequently, appropriate
+//! privileges are required, or the attempt will fail causing a panic.
+//! The `run_as` module provides the function `run_as`, which takes a UID, GID, an SELinux
+//! context, and a closure. The return type of the closure, which is also the return type of
+//! `run_as`, must implement `serde::Serialize` and `serde::Deserialize`.
+//! `run_as` forks, transitions to the given identity, and executes the closure in the newly
+//! forked process. If the closure returns, i.e., does not panic, the forked process exits with
+//! a status of `0`, and the return value is serialized and sent through a pipe to the parent where
+//! it gets deserialized and returned. The STDIO is not changed and the parent's panic handler
+//! remains unchanged. So if the closure panics, the panic message is printed on the parent's STDERR
+//! and the exit status is set to a non `0` value. The latter causes the parent to panic as well,
+//! and if run in a test context, the test to fail.
+
+use keystore2_selinux as selinux;
+use nix::sys::wait::{waitpid, WaitStatus};
+use nix::unistd::{
+    close, fork, pipe as nix_pipe, read as nix_read, setgid, setuid, write as nix_write,
+    ForkResult, Gid, Uid,
+};
+use serde::{de::DeserializeOwned, Serialize};
+use std::os::unix::io::RawFd;
+
+fn transition(se_context: selinux::Context, uid: Uid, gid: Gid) {
+    setgid(gid).expect("Failed to set GID. This test might need more privileges.");
+    setuid(uid).expect("Failed to set UID. This test might need more privileges.");
+
+    selinux::setcon(&se_context)
+        .expect("Failed to set SELinux context. This test might need more privileges.");
+}
+
+/// PipeReader is a simple wrapper around raw pipe file descriptors.
+/// It takes ownership of the file descriptor and closes it on drop. It provides `read_all`, which
+/// reads from the pipe into an expending vector, until no more data can be read.
+struct PipeReader(RawFd);
+
+impl PipeReader {
+    pub fn read_all(&self) -> Result<Vec<u8>, nix::Error> {
+        let mut buffer = [0u8; 128];
+        let mut result = Vec::<u8>::new();
+        loop {
+            let bytes = nix_read(self.0, &mut buffer)?;
+            if bytes == 0 {
+                return Ok(result);
+            }
+            result.extend_from_slice(&buffer[0..bytes]);
+        }
+    }
+}
+
+impl Drop for PipeReader {
+    fn drop(&mut self) {
+        close(self.0).expect("Failed to close reader pipe fd.");
+    }
+}
+
+/// PipeWriter is a simple wrapper around raw pipe file descriptors.
+/// It takes ownership of the file descriptor and closes it on drop. It provides `write`, which
+/// writes the given buffer into the pipe, returning the number of bytes written.
+struct PipeWriter(RawFd);
+
+impl PipeWriter {
+    pub fn write(&self, data: &[u8]) -> Result<usize, nix::Error> {
+        nix_write(self.0, data)
+    }
+}
+
+impl Drop for PipeWriter {
+    fn drop(&mut self) {
+        close(self.0).expect("Failed to close writer pipe fd.");
+    }
+}
+
+fn pipe() -> Result<(PipeReader, PipeWriter), nix::Error> {
+    let (read_fd, write_fd) = nix_pipe()?;
+    Ok((PipeReader(read_fd), PipeWriter(write_fd)))
+}
+
+/// Run the given closure in a new process running with the new identity given as
+/// `uid`, `gid`, and `se_context`.
+pub fn run_as<F, R>(se_context: &str, uid: Uid, gid: Gid, f: F) -> R
+where
+    R: Serialize + DeserializeOwned,
+    F: 'static + Send + FnOnce() -> R,
+{
+    let se_context =
+        selinux::Context::new(se_context).expect("Unable to construct selinux::Context.");
+    let (reader, writer) = pipe().expect("Failed to create pipe.");
+
+    match unsafe { fork() } {
+        Ok(ForkResult::Parent { child, .. }) => {
+            drop(writer);
+            let status = waitpid(child, None).expect("Failed while waiting for child.");
+            if let WaitStatus::Exited(_, 0) = status {
+                // Child exited successfully.
+                // Read the result from the pipe.
+                let serialized_result =
+                    reader.read_all().expect("Failed to read result from child.");
+
+                // Deserialize the result and return it.
+                serde_cbor::from_slice(&serialized_result).expect("Failed to deserialize result.")
+            } else {
+                panic!("Child did not exit as expected {:?}", status);
+            }
+        }
+        Ok(ForkResult::Child) => {
+            // This will panic on error or insufficient privileges.
+            transition(se_context, uid, gid);
+
+            // Run the closure.
+            let result = f();
+
+            // Serialize the result of the closure.
+            let vec = serde_cbor::to_vec(&result).expect("Result serialization failed");
+
+            // Send the result to the parent using the pipe.
+            writer.write(&vec).expect("Failed to send serialized result to parent.");
+
+            // Set exit status to `0`.
+            std::process::exit(0);
+        }
+        Err(errno) => {
+            panic!("Failed to fork: {:?}", errno);
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use keystore2_selinux as selinux;
+    use nix::unistd::{getgid, getuid};
+    use serde::{Deserialize, Serialize};
+
+    /// This test checks that the closure does not produce an exit status of `0` when run inside a
+    /// test and the closure panics. This would mask test failures as success.
+    #[test]
+    #[should_panic]
+    fn test_run_as_panics_on_closure_panic() {
+        run_as(selinux::getcon().unwrap().to_str().unwrap(), getuid(), getgid(), || {
+            panic!("Closure must panic.")
+        });
+    }
+
+    static TARGET_UID: Uid = Uid::from_raw(10020);
+    static TARGET_GID: Gid = Gid::from_raw(10020);
+    static TARGET_CTX: &str = "u:r:untrusted_app:s0:c91,c256,c10,c20";
+
+    /// Tests that the closure is running as the target identity.
+    #[test]
+    fn test_transition_to_untrusted_app() {
+        run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || {
+            assert_eq!(TARGET_UID, getuid());
+            assert_eq!(TARGET_GID, getgid());
+            assert_eq!(TARGET_CTX, selinux::getcon().unwrap().to_str().unwrap());
+        });
+    }
+
+    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+    struct SomeResult {
+        a: u32,
+        b: u64,
+        c: String,
+    }
+
+    #[test]
+    fn test_serialized_result() {
+        let test_result = SomeResult {
+            a: 5,
+            b: 0xffffffffffffffff,
+            c: "supercalifragilisticexpialidocious".to_owned(),
+        };
+        let test_result_clone = test_result.clone();
+        let result = run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || test_result_clone);
+        assert_eq!(test_result, result);
+    }
+}