| /* |
| * 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. |
| */ |
| |
| // `dm` module implements part of the `device-mapper` ioctl interfaces. It currently supports |
| // creation and deletion of the mapper device. It doesn't support other operations like querying |
| // the status of the mapper device. And there's no plan to extend the support unless it is |
| // required. |
| // |
| // Why in-house development? [`devicemapper`](https://crates.io/crates/devicemapper) is a public |
| // Rust implementation of the device mapper APIs. However, it doesn't provide any abstraction for |
| // the target-specific tables. User has to manually craft the table. Ironically, the library |
| // provides a lot of APIs for the features that are not required for `apkdmverity` such as listing |
| // the device mapper block devices that are currently listed in the kernel. Size is an important |
| // criteria for Microdroid. |
| |
| //! A library to create device mapper spec & issue ioctls. |
| |
| #![allow(missing_docs)] |
| #![cfg_attr(test, allow(unused))] |
| |
| use anyhow::{Context, Result}; |
| use data_model::DataInit; |
| use std::fs::{File, OpenOptions}; |
| use std::io::Write; |
| use std::mem::size_of; |
| use std::os::unix::io::AsRawFd; |
| use std::path::{Path, PathBuf}; |
| |
| /// Exposes DmCryptTarget & related builder |
| pub mod crypt; |
| /// Expose util functions |
| pub mod util; |
| /// Exposes the DmVerityTarget & related builder |
| pub mod verity; |
| // Expose loopdevice |
| pub mod loopdevice; |
| |
| mod sys; |
| use crypt::DmCryptTarget; |
| use sys::*; |
| use util::*; |
| use verity::DmVerityTarget; |
| |
| nix::ioctl_readwrite!(_dm_dev_create, DM_IOCTL, Cmd::DM_DEV_CREATE, DmIoctl); |
| nix::ioctl_readwrite!(_dm_dev_suspend, DM_IOCTL, Cmd::DM_DEV_SUSPEND, DmIoctl); |
| nix::ioctl_readwrite!(_dm_table_load, DM_IOCTL, Cmd::DM_TABLE_LOAD, DmIoctl); |
| nix::ioctl_readwrite!(_dm_dev_remove, DM_IOCTL, Cmd::DM_DEV_REMOVE, DmIoctl); |
| |
| /// Create a new (mapper) device |
| fn dm_dev_create(dm: &DeviceMapper, ioctl: *mut DmIoctl) -> Result<i32> { |
| // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the |
| // state of this process in any way. |
| Ok(unsafe { _dm_dev_create(dm.0.as_raw_fd(), ioctl) }?) |
| } |
| |
| fn dm_dev_suspend(dm: &DeviceMapper, ioctl: *mut DmIoctl) -> Result<i32> { |
| // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the |
| // state of this process in any way. |
| Ok(unsafe { _dm_dev_suspend(dm.0.as_raw_fd(), ioctl) }?) |
| } |
| |
| fn dm_table_load(dm: &DeviceMapper, ioctl: *mut DmIoctl) -> Result<i32> { |
| // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the |
| // state of this process in any way. |
| Ok(unsafe { _dm_table_load(dm.0.as_raw_fd(), ioctl) }?) |
| } |
| |
| fn dm_dev_remove(dm: &DeviceMapper, ioctl: *mut DmIoctl) -> Result<i32> { |
| // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the |
| // state of this process in any way. |
| Ok(unsafe { _dm_dev_remove(dm.0.as_raw_fd(), ioctl) }?) |
| } |
| |
| // `DmTargetSpec` is the header of the data structure for a device-mapper target. When doing the |
| // ioctl, one of more `DmTargetSpec` (and its body) are appened to the `DmIoctl` struct. |
| #[repr(C)] |
| #[derive(Copy, Clone)] |
| struct DmTargetSpec { |
| sector_start: u64, |
| length: u64, // number of 512 sectors |
| status: i32, |
| next: u32, |
| target_type: [u8; DM_MAX_TYPE_NAME], |
| } |
| |
| // SAFETY: C struct is safe to be initialized from raw data |
| unsafe impl DataInit for DmTargetSpec {} |
| |
| impl DmTargetSpec { |
| fn new(target_type: &str) -> Result<Self> { |
| // safe because the size of the array is the same as the size of the struct |
| let mut spec: Self = *DataInit::from_mut_slice(&mut [0; size_of::<Self>()]).unwrap(); |
| spec.target_type.as_mut().write_all(target_type.as_bytes())?; |
| Ok(spec) |
| } |
| } |
| |
| impl DmIoctl { |
| fn new(name: &str) -> Result<DmIoctl> { |
| // safe because the size of the array is the same as the size of the struct |
| let mut data: Self = *DataInit::from_mut_slice(&mut [0; size_of::<Self>()]).unwrap(); |
| data.version[0] = DM_VERSION_MAJOR; |
| data.version[1] = DM_VERSION_MINOR; |
| data.version[2] = DM_VERSION_PATCHLEVEL; |
| data.data_size = size_of::<Self>() as u32; |
| data.data_start = 0; |
| data.name.as_mut().write_all(name.as_bytes())?; |
| Ok(data) |
| } |
| |
| fn set_uuid(&mut self, uuid: &str) -> Result<()> { |
| let mut dst = self.uuid.as_mut(); |
| dst.fill(0); |
| dst.write_all(uuid.as_bytes())?; |
| Ok(()) |
| } |
| } |
| |
| /// `DeviceMapper` is the entry point for the device mapper framework. It essentially is a file |
| /// handle to "/dev/mapper/control". |
| pub struct DeviceMapper(File); |
| |
| #[cfg(not(target_os = "android"))] |
| const MAPPER_CONTROL: &str = "/dev/mapper/control"; |
| #[cfg(not(target_os = "android"))] |
| const MAPPER_DEV_ROOT: &str = "/dev/mapper"; |
| |
| #[cfg(target_os = "android")] |
| const MAPPER_CONTROL: &str = "/dev/device-mapper"; |
| #[cfg(target_os = "android")] |
| const MAPPER_DEV_ROOT: &str = "/dev/block/mapper"; |
| |
| impl DeviceMapper { |
| /// Constructs a new `DeviceMapper` entrypoint. This is essentially the same as opening |
| /// "/dev/mapper/control". |
| pub fn new() -> Result<DeviceMapper> { |
| let f = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .open(MAPPER_CONTROL) |
| .context(format!("failed to open {}", MAPPER_CONTROL))?; |
| Ok(DeviceMapper(f)) |
| } |
| |
| /// Creates a (crypt) device and configure it according to the `target` specification. |
| /// The path to the generated device is "/dev/mapper/<name>". |
| pub fn create_crypt_device(&self, name: &str, target: &DmCryptTarget) -> Result<PathBuf> { |
| self.create_device(name, target.as_slice(), uuid("crypto".as_bytes())?, true) |
| } |
| |
| /// Creates a (verity) device and configure it according to the `target` specification. |
| /// The path to the generated device is "/dev/mapper/<name>". |
| pub fn create_verity_device(&self, name: &str, target: &DmVerityTarget) -> Result<PathBuf> { |
| self.create_device(name, target.as_slice(), uuid("apkver".as_bytes())?, false) |
| } |
| |
| /// Removes a mapper device. |
| pub fn delete_device_deferred(&self, name: &str) -> Result<()> { |
| let mut data = DmIoctl::new(name)?; |
| data.flags |= Flag::DM_DEFERRED_REMOVE; |
| dm_dev_remove(self, &mut data) |
| .context(format!("failed to remove device with name {}", &name))?; |
| Ok(()) |
| } |
| |
| fn create_device( |
| &self, |
| name: &str, |
| target: &[u8], |
| uid: String, |
| writable: bool, |
| ) -> Result<PathBuf> { |
| // Step 1: create an empty device |
| let mut data = DmIoctl::new(name)?; |
| data.set_uuid(&uid)?; |
| dm_dev_create(self, &mut data) |
| .context(format!("failed to create an empty device with name {}", &name))?; |
| |
| // Step 2: load table onto the device |
| let payload_size = size_of::<DmIoctl>() + target.len(); |
| |
| let mut data = DmIoctl::new(name)?; |
| data.data_size = payload_size as u32; |
| data.data_start = size_of::<DmIoctl>() as u32; |
| data.target_count = 1; |
| |
| if !writable { |
| data.flags |= Flag::DM_READONLY_FLAG; |
| } |
| |
| let mut payload = Vec::with_capacity(payload_size); |
| payload.extend_from_slice(data.as_slice()); |
| payload.extend_from_slice(target); |
| dm_table_load(self, payload.as_mut_ptr() as *mut DmIoctl) |
| .context("failed to load table")?; |
| |
| // Step 3: activate the device (note: the term 'suspend' might be misleading, but it |
| // actually activates the table. See include/uapi/linux/dm-ioctl.h |
| let mut data = DmIoctl::new(name)?; |
| dm_dev_suspend(self, &mut data).context("failed to activate")?; |
| |
| // Step 4: wait unti the device is created and return the device path |
| let path = Path::new(MAPPER_DEV_ROOT).join(name); |
| wait_for_path(&path)?; |
| Ok(path) |
| } |
| } |
| |
| /// Used to derive a UUID that uniquely identifies a device mapper device when creating it. |
| fn uuid(node_id: &[u8]) -> Result<String> { |
| use std::time::{SystemTime, UNIX_EPOCH}; |
| use uuid::v1::{Context, Timestamp}; |
| use uuid::Uuid; |
| |
| let context = Context::new(0); |
| let now = SystemTime::now().duration_since(UNIX_EPOCH)?; |
| let ts = Timestamp::from_unix(context, now.as_secs(), now.subsec_nanos()); |
| let uuid = Uuid::new_v1(ts, node_id.try_into()?); |
| Ok(String::from(uuid.hyphenated().encode_lower(&mut Uuid::encode_buffer()))) |
| } |
| |
| #[cfg(test)] |
| ignorabletest::test_main!(tests::all_tests()); |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crypt::{CipherType, DmCryptTargetBuilder}; |
| use ignorabletest::{list_tests, test}; |
| use rustutils::system_properties; |
| use std::fs::{read, File, OpenOptions}; |
| use std::io::Write; |
| |
| // Just a logical set of keys to make testing easy. This has no real meaning. |
| struct KeySet<'a> { |
| cipher: CipherType, |
| key: &'a [u8], |
| different_key: &'a [u8], |
| } |
| |
| const KEY_SET_XTS: KeySet = KeySet { |
| cipher: CipherType::AES256XTS, |
| key: b"sixtyfourbyteslongsentencearerarebutletsgiveitatrycantbethathard", |
| different_key: b"drahtahtebtnacyrtatievigsteltuberareraecnetnesgnolsetybruofytxis", |
| }; |
| const KEY_SET_HCTR2: KeySet = KeySet { |
| cipher: CipherType::AES256HCTR2, |
| key: b"thirtytwobyteslongreallylongword", |
| different_key: b"drowgnolyllaergnolsetybowtytriht", |
| }; |
| |
| list_tests! {all_tests: [ |
| mapping_again_keeps_data_xts, |
| mapping_again_keeps_data_hctr2, |
| data_inaccessible_with_diff_key_xts, |
| data_inaccessible_with_diff_key_hctr2, |
| ]} |
| |
| // Create a file in given temp directory with given size |
| fn prepare_tmpfile(test_dir: &Path, filename: &str, sz: u64) -> PathBuf { |
| let filepath = test_dir.join(filename); |
| let f = File::create(&filepath).unwrap(); |
| f.set_len(sz).unwrap(); |
| filepath |
| } |
| |
| fn write_to_dev(path: &Path, data: &[u8]) { |
| let mut f = OpenOptions::new().read(true).write(true).open(path).unwrap(); |
| f.write_all(data).unwrap(); |
| } |
| |
| // TODO(b/250880499): delete_device() doesn't really delete it even without DM_DEFERRED_REMOVE. |
| // Hence, we have to create a new device with a different name for each test. Retrying |
| // the test on same machine without reboot will also fail. |
| fn delete_device(dm: &DeviceMapper, name: &str) -> Result<()> { |
| dm.delete_device_deferred(name)?; |
| wait_for_path_disappears(Path::new(MAPPER_DEV_ROOT).join(name))?; |
| Ok(()) |
| } |
| |
| fn is_hctr2_supported() -> bool { |
| // hctr2 is NOT enabled in kernel 5.10 or lower. We run Microdroid tests on kernel versions |
| // 5.10 or above & therefore, we don't really care to skip test on other versions. |
| if let Some(version) = system_properties::read("ro.kernel.version") |
| .expect("Unable to read system property ro.kernel.version") |
| { |
| version != "5.10" |
| } else { |
| panic!("Could not read property: kernel.version!!"); |
| } |
| } |
| |
| test!(mapping_again_keeps_data_xts); |
| fn mapping_again_keeps_data_xts() { |
| mapping_again_keeps_data(&KEY_SET_XTS, "name1"); |
| } |
| |
| test!(mapping_again_keeps_data_hctr2, ignore_if: !is_hctr2_supported()); |
| fn mapping_again_keeps_data_hctr2() { |
| mapping_again_keeps_data(&KEY_SET_HCTR2, "name2"); |
| } |
| |
| test!(data_inaccessible_with_diff_key_xts); |
| fn data_inaccessible_with_diff_key_xts() { |
| data_inaccessible_with_diff_key(&KEY_SET_XTS, "name3"); |
| } |
| |
| test!(data_inaccessible_with_diff_key_hctr2, ignore_if: !is_hctr2_supported()); |
| fn data_inaccessible_with_diff_key_hctr2() { |
| data_inaccessible_with_diff_key(&KEY_SET_HCTR2, "name4"); |
| } |
| |
| fn mapping_again_keeps_data(keyset: &KeySet, device: &str) { |
| // This test creates 2 different crypt devices using same key backed by same data_device |
| // -> Write data on dev1 -> Check the data is visible & same on dev2 |
| let dm = DeviceMapper::new().unwrap(); |
| let inputimg = include_bytes!("../testdata/rand8k"); |
| let sz = inputimg.len() as u64; |
| |
| let test_dir = tempfile::TempDir::new().unwrap(); |
| let backing_file = prepare_tmpfile(test_dir.path(), "storage", sz); |
| let data_device = loopdevice::attach( |
| backing_file, |
| 0, |
| sz, |
| /*direct_io*/ true, |
| /*writable*/ true, |
| ) |
| .unwrap(); |
| let device_diff = device.to_owned() + "_diff"; |
| |
| scopeguard::defer! { |
| loopdevice::detach(&data_device).unwrap(); |
| _ = delete_device(&dm, device); |
| _ = delete_device(&dm, &device_diff); |
| } |
| |
| let target = DmCryptTargetBuilder::default() |
| .data_device(&data_device, sz) |
| .cipher(keyset.cipher) |
| .key(keyset.key) |
| .build() |
| .unwrap(); |
| |
| let mut crypt_device = dm.create_crypt_device(device, &target).unwrap(); |
| write_to_dev(&crypt_device, inputimg); |
| |
| // Recreate another device using same target spec & check if the content is the same |
| crypt_device = dm.create_crypt_device(&device_diff, &target).unwrap(); |
| |
| let crypt = read(crypt_device).unwrap(); |
| assert_eq!(inputimg.len(), crypt.len()); // fail early if the size doesn't match |
| assert_eq!(inputimg, crypt.as_slice()); |
| } |
| |
| fn data_inaccessible_with_diff_key(keyset: &KeySet, device: &str) { |
| // This test creates 2 different crypt devices using different keys backed |
| // by same data_device -> Write data on dev1 -> Check the data is visible but not the same on dev2 |
| let dm = DeviceMapper::new().unwrap(); |
| let inputimg = include_bytes!("../testdata/rand8k"); |
| let sz = inputimg.len() as u64; |
| |
| let test_dir = tempfile::TempDir::new().unwrap(); |
| let backing_file = prepare_tmpfile(test_dir.path(), "storage", sz); |
| let data_device = loopdevice::attach( |
| backing_file, |
| 0, |
| sz, |
| /*direct_io*/ true, |
| /*writable*/ true, |
| ) |
| .unwrap(); |
| let device_diff = device.to_owned() + "_diff"; |
| scopeguard::defer! { |
| loopdevice::detach(&data_device).unwrap(); |
| _ = delete_device(&dm, device); |
| _ = delete_device(&dm, &device_diff); |
| } |
| |
| let target = DmCryptTargetBuilder::default() |
| .data_device(&data_device, sz) |
| .cipher(keyset.cipher) |
| .key(keyset.key) |
| .build() |
| .unwrap(); |
| let target2 = DmCryptTargetBuilder::default() |
| .data_device(&data_device, sz) |
| .cipher(keyset.cipher) |
| .key(keyset.different_key) |
| .build() |
| .unwrap(); |
| |
| let mut crypt_device = dm.create_crypt_device(device, &target).unwrap(); |
| |
| write_to_dev(&crypt_device, inputimg); |
| |
| // Recreate the crypt device again diff key & check if the content is changed |
| crypt_device = dm.create_crypt_device(&device_diff, &target2).unwrap(); |
| let crypt = read(crypt_device).unwrap(); |
| assert_ne!(inputimg, crypt.as_slice()); |
| } |
| } |