diff options
author | Weston Carvalho <westoncarvalho@google.com> | 2023-09-28 18:20:03 -0500 |
---|---|---|
committer | Weston Carvalho <westoncarvalho@google.com> | 2024-04-09 13:58:24 -0500 |
commit | 170ebeddc8b51c2b54df475ef1b0fd84d92a3095 (patch) | |
tree | 682b1b3c1dac45e38f656ce6abd5685e767fc958 | |
parent | df16cc4ff58d60e171a64c320f601236541850e4 (diff) | |
download | storage-170ebeddc8b51c2b54df475ef1b0fd84d92a3095.tar.gz |
Translate some of the unittests from `storage-unittest` into Rust tests
calling the AIDL service.
Tested: ./build-root/build-qemu-generic-arm64-test-debug/run --headless --boot-test "com.android.trusty.rust.storage_unittest_aidl.test"
Bug: 300673823
Change-Id: Ib75e3afb3c179418da936c8e529e57b7bcb9efd8
-rw-r--r-- | PREUPLOAD.cfg | 2 | ||||
-rw-r--r-- | build-config-usertests | 2 | ||||
l--------- | rustfmt.toml | 1 | ||||
-rw-r--r-- | test/storage-unittest-aidl/lib.rs | 69 | ||||
-rw-r--r-- | test/storage-unittest-aidl/manifest.json | 9 | ||||
-rw-r--r-- | test/storage-unittest-aidl/rules.mk | 39 | ||||
-rw-r--r-- | test/storage-unittest-aidl/unittests/helpers.rs | 154 | ||||
-rw-r--r-- | test/storage-unittest-aidl/unittests/mod.rs | 203 | ||||
-rw-r--r-- | usertests-inc.mk | 6 |
9 files changed, 484 insertions, 1 deletions
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 587b0de..20c50ba 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -2,6 +2,8 @@ clang_format = true commit_msg_bug_field = true commit_msg_changeid_field = true +rustfmt = true [Builtin Hooks Options] clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp +rustfmt = --config-path=rustfmt.toml
\ No newline at end of file diff --git a/build-config-usertests b/build-config-usertests index 45d99f5..568ad2f 100644 --- a/build-config-usertests +++ b/build-config-usertests @@ -13,8 +13,8 @@ # limitations under the License. # This file lists userspace tests - [ + porttest("com.android.trusty.rust.storage_unittest_aidl.test", timeout=(60 * 30)), # userspace tests using storage available at early boot needs( [ diff --git a/rustfmt.toml b/rustfmt.toml new file mode 120000 index 0000000..3956e6b --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +../../base/rustfmt.toml
\ No newline at end of file diff --git a/test/storage-unittest-aidl/lib.rs b/test/storage-unittest-aidl/lib.rs new file mode 100644 index 0000000..6eb0e47 --- /dev/null +++ b/test/storage-unittest-aidl/lib.rs @@ -0,0 +1,69 @@ +#![feature(c_str_literals)] + +#[cfg(test)] +mod unittests; + +#[cfg(test)] +mod tests { + use crate::unittests; + use android_hardware_security_see_storage::aidl::android::hardware::security::see::storage::{ + FileAvailability::FileAvailability, FileIntegrity::FileIntegrity, + FileProperties::FileProperties, ISecureStorage::ISecureStorage, + IStorageSession::IStorageSession, + }; + use binder::{Status, StatusCode, Strong}; + use core::ffi::CStr; + use rpcbinder::RpcSession; + use test::assert_ok; + + //This line is needed in order to run the unit tests in Trusty + test::init!(); + + fn connect() -> Result<Strong<dyn ISecureStorage>, StatusCode> { + const STORAGE_AIDL_PORT_NAME: &CStr = c"com.android.hardware.security.see.storage"; + + RpcSession::new().setup_trusty_client(STORAGE_AIDL_PORT_NAME) + } + + fn start_session(properties: &FileProperties) -> Result<Strong<dyn IStorageSession>, Status> { + connect()?.startSession(properties) + } + + #[test] + fn ping() { + use binder::IBinder as _; + + let secure_storage = assert_ok!(connect()); + assert_ok!(secure_storage.as_binder().ping_binder()); + } + + mod tp { + use super::*; + const TP: &'static FileProperties = &FileProperties { + integrity: FileIntegrity::TAMPER_PROOF_AT_REST, + availability: FileAvailability::AFTER_USERDATA, + persistent: false, + }; + + #[test] + fn create_delete() { + let ss = assert_ok!(start_session(TP)); + unittests::create_delete(&*ss); + } + #[test] + fn create_move_delete() { + let ss = assert_ok!(start_session(TP)); + unittests::create_move_delete(&*ss); + } + #[test] + fn file_list() { + let ss = assert_ok!(start_session(TP)); + unittests::file_list(&*ss); + } + #[test] + fn write_read_sequential() { + let ss = assert_ok!(start_session(TP)); + unittests::write_read_sequential(&*ss); + } + } +} diff --git a/test/storage-unittest-aidl/manifest.json b/test/storage-unittest-aidl/manifest.json new file mode 100644 index 0000000..6a4b307 --- /dev/null +++ b/test/storage-unittest-aidl/manifest.json @@ -0,0 +1,9 @@ +{ + "app_name": "storage_unittest_aidl_lib", + "uuid": "c0ee7860-b88c-4903-ae0f-4f3e2c8a5638", + "min_heap": 65536, + "min_stack": 32768, + "mgmt_flags": { + "non_critical_app": true + } +}
\ No newline at end of file diff --git a/test/storage-unittest-aidl/rules.mk b/test/storage-unittest-aidl/rules.mk new file mode 100644 index 0000000..4b245b6 --- /dev/null +++ b/test/storage-unittest-aidl/rules.mk @@ -0,0 +1,39 @@ +# Copyright (C) 2023 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. + +LOCAL_DIR := $(GET_LOCAL_DIR) + +MODULE := $(LOCAL_DIR) + +MANIFEST := $(LOCAL_DIR)/manifest.json + +MODULE_SRCS += \ + $(LOCAL_DIR)/lib.rs \ + +MODULE_CRATE_NAME := storage_unittest_aidl + +MODULE_LIBRARY_DEPS += \ + $(call FIND_CRATE,libc) \ + $(call FIND_CRATE,log) \ + frameworks/native/libs/binder/trusty/rust \ + frameworks/native/libs/binder/trusty/rust/binder_rpc_server \ + frameworks/native/libs/binder/trusty/rust/rpcbinder \ + trusty/user/base/interface/secure_storage/rust \ + trusty/user/base/lib/trusty-std \ + trusty/user/base/lib/trusty-log \ + trusty/user/base/lib/tipc/rust \ + +MODULE_RUST_TESTS := true + +include make/library.mk diff --git a/test/storage-unittest-aidl/unittests/helpers.rs b/test/storage-unittest-aidl/unittests/helpers.rs new file mode 100644 index 0000000..3eb227b --- /dev/null +++ b/test/storage-unittest-aidl/unittests/helpers.rs @@ -0,0 +1,154 @@ +use android_hardware_security_see_storage::aidl::android::hardware::security::see::storage::{ + DeleteOptions::DeleteOptions, IFile::IFile, ISecureStorage as SecureStorage, + IStorageSession::IStorageSession, ReadIntegrity::ReadIntegrity, +}; +use binder::ExceptionCode; + +pub(crate) enum Exists { + Must, + MustNot, + Unknown, +} + +pub(crate) fn ensure_deleted( + ss: &(impl IStorageSession + ?Sized), + fname: &str, + expectation: Exists, +) -> Result<(), String> { + const DEFAULT_DELETE: &'static DeleteOptions = &DeleteOptions { + readIntegrity: ReadIntegrity::NO_TAMPER, + allowWritesDuringAbUpdate: false, + }; + + // Try to delete file + let rc = ss.deleteFile(fname, DEFAULT_DELETE); + match rc { + Ok(()) => { + if let Exists::MustNot = expectation { + return Err(format!( + "deleteFile succeeded, but the file (name: {}) shouldn't have exisited", + fname + )); + } + } + Err(e) + if e.exception_code() == ExceptionCode::SERVICE_SPECIFIC + && e.service_specific_error() == SecureStorage::ERR_NOT_FOUND => + { + if let Exists::Must = expectation { + return Err(format!( + "deleteFile failed (for {}) with ERR_NOT_FOUND, but the file should have existed", + fname + )); + } + ss.commitChanges().map_err(|e| format!("commitChanges failed with: {}", e))?; + return Ok(()); + } + Err(e) => { + return Err(format!("deleteFile failed (for {}) with unexpected error: {}", fname, e)) + } + }; + ss.commitChanges().map_err(|e| format!("commitChanges failed with: {}", e))?; + + // If delete succeeded, try again to make sure it doesn't exist now + ensure_deleted(ss, fname, Exists::MustNot) + .map_err(|s| format!("while ensuring non-existence, {}", s)) +} + +fn check_pattern32(offset: usize, buf: &[u8]) -> Result<(), String> { + const U32_SIZE: usize = std::mem::size_of::<u32>(); + let mut pattern: u32 = (offset / U32_SIZE).try_into().unwrap(); + + let mut chunks = buf.chunks_exact(U32_SIZE); + for chunk in &mut chunks { + let actual = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + if actual != pattern { + return Err(format!("Expected to read {}, but found {}", pattern, actual)); + } + pattern += 1; + } + + let rem = chunks.remainder(); + for byte in rem { + if *byte != 0 { + return Err(format!( + "Expected unpatterned portion of read to be zeroed; found {:x?}", + rem + )); + } + } + Ok(()) +} + +fn fill_pattern32(offset: usize, buf: &mut [u8]) { + const U32_SIZE: usize = std::mem::size_of::<u32>(); + let mut pattern: u32 = (offset / U32_SIZE).try_into().unwrap(); + + for chunk in buf.chunks_exact_mut(U32_SIZE) { + let bytes = pattern.to_ne_bytes(); + for i in 0..U32_SIZE { + chunk[i] = bytes[i]; + } + pattern += 1; + } +} + +fn check_valid_size(chunk_len: usize) -> Result<(), String> { + if chunk_len % std::mem::size_of::<u32>() != 0 { + return Err(format!("Chunk size ({}) not 32-bit aligned.", chunk_len)); + } + Ok(()) +} + +pub(crate) fn write_pattern( + file: &dyn IFile, + offset: usize, + chunks: usize, + chunk_len: usize, +) -> Result<(), String> { + check_valid_size(chunk_len)?; + let mut buf = vec![0; chunk_len]; + + for i in 0..chunks { + let chunk_offset = offset + i * chunk_len; + fill_pattern32(chunk_offset, &mut buf); + let written = file + .write(chunk_offset.try_into().unwrap(), &buf) + .map_err(|e| format!("Encountered error calling write (chunk {}): {}", i, e))?; + if written != chunk_len.try_into().unwrap() { + return Err(format!( + "Wrote {} bytes to chunk {}, but expected to write {}", + written, i, chunk_len, + )); + } + } + + Ok(()) +} + +pub(crate) fn read_pattern( + file: &dyn IFile, + offset: usize, + chunks: usize, + chunk_len: usize, +) -> Result<(), String> { + check_valid_size(chunk_len)?; + + for i in 0..chunks { + let chunk_offset = offset + i * chunk_len; + let read = file + .read(chunk_len.try_into().unwrap(), chunk_offset.try_into().unwrap()) + .map_err(|e| format!("Encountered error calling read (chunk {}): {}", i, e))?; + if read.len() != chunk_len { + return Err(format!( + "Read {} bytes from chunk {}, but expected to read {}", + read.len(), + i, + chunk_len, + )); + } + check_pattern32(chunk_offset, &*read).map_err(|e| format!("For chunk {}: {}", i, e))?; + } + + Ok(()) +} diff --git a/test/storage-unittest-aidl/unittests/mod.rs b/test/storage-unittest-aidl/unittests/mod.rs new file mode 100644 index 0000000..23d4627 --- /dev/null +++ b/test/storage-unittest-aidl/unittests/mod.rs @@ -0,0 +1,203 @@ +use android_hardware_security_see_storage::aidl::android::hardware::security::see::storage::{ + CreationMode::CreationMode, FileMode::FileMode, ISecureStorage as SecureStorage, + IStorageSession::IStorageSession, OpenOptions::OpenOptions, ReadIntegrity::ReadIntegrity, +}; +use binder::ExceptionCode; +use test::{assert_ok, expect, fail}; + +mod helpers; +use helpers::{ensure_deleted, Exists}; + +const CREATE_EXCLUSIVE: &'static OpenOptions = &OpenOptions { + createMode: CreationMode::CREATE_EXCLUSIVE, + accessMode: FileMode::READ_WRITE, + readIntegrity: ReadIntegrity::NO_TAMPER, + truncateOnOpen: true, + allowWritesDuringAbUpdate: false, +}; + +const NO_CREATE: &'static OpenOptions = &OpenOptions { + createMode: CreationMode::NO_CREATE, + accessMode: FileMode::READ_WRITE, + readIntegrity: ReadIntegrity::NO_TAMPER, + truncateOnOpen: false, + allowWritesDuringAbUpdate: false, +}; + +pub(crate) fn create_delete(ss: &(impl IStorageSession + ?Sized)) { + let fname = "test_create_delete_file"; + assert_ok!(ensure_deleted(ss, fname, Exists::Unknown)); + + { + // Create file + let _file = assert_ok!(ss.openFile(fname, CREATE_EXCLUSIVE)); + assert_ok!(ss.commitChanges()); + + // Try to create file again + match ss.openFile(fname, CREATE_EXCLUSIVE) { + Ok(_) => fail!("openFile (on existing file) unexpectedly succeeded"), + Err(e) + if e.exception_code() == ExceptionCode::SERVICE_SPECIFIC + && e.service_specific_error() == SecureStorage::ERR_ALREADY_EXISTS => + { + () + } + Err(e) => fail!("openFile (on existing file) failed with: {}", e), + }; + assert_ok!(ss.commitChanges()); + + // File closes + } + + assert_ok!(ensure_deleted(ss, fname, Exists::Must)); +} + +pub(crate) fn create_move_delete(ss: &(impl IStorageSession + ?Sized)) { + let fname1 = "test_create_move_delete_1_file"; + let fname2 = "test_create_move_delete_2_file"; + + assert_ok!(ensure_deleted(ss, fname1, Exists::Unknown)); + assert_ok!(ensure_deleted(ss, fname2, Exists::Unknown)); + + { + // Create file + let file = assert_ok!(ss.openFile(fname1, CREATE_EXCLUSIVE)); + assert_ok!(ss.commitChanges()); + + // Move fname1 -> fname2 + assert_ok!(file.rename(fname2, CreationMode::CREATE_EXCLUSIVE)); + assert_ok!(ss.commitChanges()); + + // Try to create fname2, with file still alive + match ss.openFile(fname2, CREATE_EXCLUSIVE) { + Ok(_) => fail!("openFile unexpectedly succeeded"), + Err(e) + if e.exception_code() == ExceptionCode::SERVICE_SPECIFIC + && e.service_specific_error() == SecureStorage::ERR_ALREADY_EXISTS => + { + () + } + Err(e) => fail!("openFile failed with unexpected error: {}", e), + }; + assert_ok!(ss.commitChanges()); + + // file closes + } + + // Try to create fname2, now that file is closed + match ss.openFile(fname2, CREATE_EXCLUSIVE) { + Ok(_) => fail!("openFile unexpectedly succeeded"), + Err(e) + if e.exception_code() == ExceptionCode::SERVICE_SPECIFIC + && e.service_specific_error() == SecureStorage::ERR_ALREADY_EXISTS => + { + () + } + Err(e) => fail!("openFile failed with unexpected error: {}", e), + }; + assert_ok!(ss.commitChanges()); + + { + // Recreate fname1 + let file = assert_ok!(ss.openFile(fname1, CREATE_EXCLUSIVE)); + assert_ok!(ss.commitChanges()); + + // Move doesn't work now that fname2 exists + match file.rename(fname2, CreationMode::CREATE_EXCLUSIVE) { + Ok(_) => panic!("rename unexpectedly succeeded"), + Err(e) + if e.exception_code() == ExceptionCode::SERVICE_SPECIFIC + && e.service_specific_error() == SecureStorage::ERR_ALREADY_EXISTS => + { + () + } + Err(e) => panic!("rename failed with unexpected error: {}", e), + }; + + // file closes + } + + assert_ok!(ensure_deleted(ss, fname1, Exists::Must)); + assert_ok!(ensure_deleted(ss, fname2, Exists::Must)); +} + +pub(crate) fn file_list(ss: &(impl IStorageSession + ?Sized)) { + use core::array::from_fn; + use std::collections::HashSet; + + let fnames: [String; 100] = from_fn(|i| format!("test_file_list_{:02}_file", i)); + for fname in &fnames { + assert_ok!(ensure_deleted(ss, fname, Exists::Unknown), "file: {}", fname); + } + + { + let dir = assert_ok!(ss.openDir("", ReadIntegrity::NO_TAMPER)); + let filenames = assert_ok!(dir.readNextFilenames(0)); + expect!(filenames.is_empty(), "Found unexpected files: {:?}", filenames); + } + + // Create, commit, and close file #0 + { + let _file = assert_ok!(ss.openFile(&fnames[0], CREATE_EXCLUSIVE)); + assert_ok!(ss.commitChanges()); + } + // Create and close (don't commit) other files + for fname in &fnames[1..] { + let _file = assert_ok!(ss.openFile(fname, CREATE_EXCLUSIVE), "for fname {}", fname); + } + + let mut read_file_names = HashSet::new(); + { + let dir = assert_ok!(ss.openDir("", ReadIntegrity::NO_TAMPER)); + let mut filenames = assert_ok!(dir.readNextFilenames(0)); + while !filenames.is_empty() { + for filename in filenames { + let existed = read_file_names.replace(filename); + if let Some(previous) = existed { + fail!("NextFilenameResult returned file more than once: {}", previous); + } + } + filenames = assert_ok!(dir.readNextFilenames(0)); + } + } + assert_ok!(ss.commitChanges()); + + // Clean up + for fname in &fnames { + assert_ok!(ensure_deleted(ss, fname, Exists::Must), "file: {}", fname); + } + + let expected = fnames.into_iter().collect::<HashSet<_>>(); + let missing = expected.difference(&read_file_names).collect::<Vec<_>>(); + let extra = read_file_names.difference(&expected).collect::<Vec<_>>(); + + expect!(missing.is_empty(), "Did not find the following expected files: {:?}", missing); + expect!(extra.is_empty(), "Found the following unexpected files: {:?}", extra); +} + +pub(crate) fn write_read_sequential(ss: &(impl IStorageSession + ?Sized)) { + use helpers::{read_pattern, write_pattern}; + + let fname = "test_write_read_sequential"; + let chunks = 32; + let chunk_len = 2048; + + assert_ok!(ensure_deleted(ss, fname, Exists::Unknown)); + + { + let file = assert_ok!(ss.openFile(fname, CREATE_EXCLUSIVE)); + assert_ok!(ss.commitChanges()); + + assert_ok!(write_pattern(&*file, 0, chunks, chunk_len)); + assert_ok!(ss.commitChanges()); + + assert_ok!(read_pattern(&*file, 0, chunks, chunk_len)); + } // file closes + + { + let file = assert_ok!(ss.openFile(fname, NO_CREATE)); + assert_ok!(read_pattern(&*file, 0, chunks, chunk_len)); + } // file closes + + assert_ok!(ensure_deleted(ss, fname, Exists::Must)); +} diff --git a/usertests-inc.mk b/usertests-inc.mk index 4fc661d..95be4c3 100644 --- a/usertests-inc.mk +++ b/usertests-inc.mk @@ -16,3 +16,9 @@ TRUSTY_USER_TESTS += \ trusty/user/app/storage/test/storage-unittest \ trusty/user/app/storage/test/storage-benchmark + +ifeq (true,$(call TOBOOL,$(STORAGE_AIDL_ENABLED))) +TRUSTY_RUST_USER_TESTS += \ + trusty/user/app/storage/test/storage-unittest-aidl \ + +endif |