Skip to main content

test_vmo_backed_block_server/
lib.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Error, anyhow};
6use block_server::async_interface::SessionManager;
7use block_server::{BlockInfo, BlockServer, DeviceInfo};
8#[cfg(feature = "for-testing")]
9use fidl::endpoints::RequestStream;
10use fidl::endpoints::{ClientEnd, FromClient, ServerEnd};
11#[cfg(feature = "for-testing")]
12use fidl_fuchsia_hardware_inlineencryption::{DeviceMarker, DeviceRequest, DeviceRequestStream};
13use fidl_fuchsia_storage_block as fblock;
14use fs_management::filesystem::BlockConnector;
15#[cfg(feature = "for-testing")]
16use futures::StreamExt;
17use std::sync::Arc;
18
19#[cfg(not(feature = "for-testing"))]
20mod data;
21#[cfg(not(feature = "for-testing"))]
22use data::Data;
23
24#[cfg(feature = "for-testing")]
25mod data_for_testing;
26#[cfg(feature = "for-testing")]
27use data_for_testing::{Data, FscryptKeys};
28#[cfg(feature = "for-testing")]
29pub use data_for_testing::{Observer, WriteAction, WriteCache};
30
31/// A local server backed by a VMO.
32pub struct VmoBackedServer {
33    server: BlockServer<SessionManager<Data>>,
34}
35
36impl VmoBackedServer {
37    /// Handles `requests`.  The future will resolve when the stream terminates.
38    pub async fn serve(&self, requests: fblock::BlockRequestStream) -> Result<(), Error> {
39        let res = self.server.handle_requests(requests).await;
40
41        #[cfg(feature = "for-testing")]
42        self.server.session_manager().interface().client_closed()?;
43
44        res
45    }
46
47    pub fn new(block_count: u64, block_size: u32, initial_content: &[u8]) -> Result<Self, Error> {
48        VmoBackedServerOptions {
49            block_size,
50            initial_contents: InitialContents::FromCapacityAndBuffer(block_count, initial_content),
51            ..Default::default()
52        }
53        .build()
54    }
55
56    pub fn from_vmo(block_size: u32, vmo: zx::Vmo) -> Result<Self, Error> {
57        VmoBackedServerOptions {
58            block_size,
59            initial_contents: InitialContents::FromVmo(vmo),
60            ..Default::default()
61        }
62        .build()
63    }
64}
65
66#[cfg(feature = "for-testing")]
67impl VmoBackedServer {
68    pub fn from_file(block_size: u32, path: &str) -> Self {
69        let contents = std::fs::read(path).expect("Failed to read file");
70        VmoBackedServerOptions {
71            block_size,
72            initial_contents: InitialContents::FromBuffer(&contents),
73            ..Default::default()
74        }
75        .build()
76        .expect("Failed to create VmoBackedServer.")
77    }
78
79    pub fn connect<R: BlockClient>(self: &Arc<Self>) -> R {
80        let (client, server) = fidl::endpoints::create_endpoints::<R::Protocol>();
81        let this = self.clone();
82        fuchsia_async::Task::spawn(async move {
83            let _ = this.serve(server.into_stream().cast_stream()).await;
84        })
85        .detach();
86        R::from_client(client)
87    }
88
89    pub fn connect_mapper(self: &Arc<Self>) -> fblock::MapperProxy {
90        let (proxy, server) = fidl::endpoints::create_proxy::<fblock::MapperMarker>();
91        let this = self.clone();
92        fuchsia_async::Task::spawn(async move {
93            let _ = this.server.handle_mapper_requests(server.into_stream()).await;
94        })
95        .detach();
96        proxy
97    }
98
99    pub fn connect_insecure_inline_encryption_server(
100        self: &Arc<Self>,
101        server: ServerEnd<DeviceMarker>,
102        uuid: [u8; 16],
103    ) -> impl Future<Output = ()> + Send {
104        let this = self.clone();
105        this.serve_insecure_inline_encryption(server.into_stream(), uuid)
106    }
107
108    /// Evict key slot for software ciphers.
109    pub fn evict_key_slot(&self, slot: u8) -> Result<(), zx::Status> {
110        self.server.session_manager().interface().fscrypt_keys().evict_key(slot)
111    }
112
113    /// Implements software-fallback for fuchsia_hardware_inlineencryption.ProgramKey. There is a
114    /// maximum of 256 keyslots. Insert keyslot at the next available slot.
115    fn program_key(&self, xts_key: &[u8; 64]) -> Result<u8, zx::Status> {
116        self.server.session_manager().interface().fscrypt_keys().program_key(xts_key)
117    }
118
119    pub async fn serve_insecure_inline_encryption(
120        self: Arc<Self>,
121        mut requests: DeviceRequestStream,
122        uuid: [u8; 16],
123    ) {
124        while let Some(Ok(request)) = requests.next().await {
125            match request {
126                DeviceRequest::ProgramKey { wrapped_key, data_unit_size: _, responder } => {
127                    let res = self
128                        .program_key(&fscrypt::to_xts_key(&wrapped_key, uuid))
129                        .and_then(|hw_slot| self.server.register_key_slot(hw_slot));
130                    responder.send(res.map_err(zx::Status::into_raw)).unwrap_or_else(|e| {
131                        log::error!("failed to send ProgramKey response. error: {:?}", e);
132                    });
133                }
134                DeviceRequest::DeriveRawSecret { mut wrapped_key, responder } => {
135                    // Swap the nibbles.
136                    for b in &mut wrapped_key {
137                        *b = *b >> 4 | *b << 4;
138                    }
139                    responder.send(Ok(&wrapped_key)).unwrap();
140                }
141            }
142        }
143    }
144}
145
146/// The initial contents of the VMO.  This also determines the size of the block device.
147pub enum InitialContents<'a> {
148    /// An empty VMO will be created with capacity for this many *blocks*.
149    FromCapacity(u64),
150    /// A VMO is created with capacity for this many *blocks* and the buffer's contents copied into
151    /// it.
152    FromCapacityAndBuffer(u64, &'a [u8]),
153    /// A VMO is created which is exactly large enough for the initial contents (rounded up to block
154    /// size), and the buffer's contents copied into it.
155    FromBuffer(&'a [u8]),
156    /// The provided VMO is used.  If its size is not block-aligned, the data will be truncated.
157    FromVmo(zx::Vmo),
158}
159
160pub struct VmoBackedServerOptions<'a> {
161    /// NB: `block_count` is ignored as that comes from `initial_contents`.
162    pub info: DeviceInfo,
163    pub block_size: u32,
164    pub initial_contents: InitialContents<'a>,
165    #[cfg(feature = "for-testing")]
166    pub observer: Option<Box<dyn Observer>>,
167    /// Enables write tracking so [`Observer::flush`] and [`Observer::barrier`] will be provided
168    /// with [`WriteCache`]. Note that this is expensive.
169    #[cfg(feature = "for-testing")]
170    pub write_tracking: bool,
171    /// If set, each operation will be delayed by a random duration <= this value, which is useful
172    /// for testing race conditions due to out-of-order block requests.
173    #[cfg(feature = "for-testing")]
174    pub max_jitter_usec: Option<u64>,
175}
176
177impl Default for VmoBackedServerOptions<'_> {
178    fn default() -> Self {
179        VmoBackedServerOptions {
180            info: DeviceInfo::Block(BlockInfo {
181                device_flags: fblock::DeviceFlag::empty(),
182                block_count: 0,
183                max_transfer_blocks: None,
184            }),
185            block_size: 512,
186            initial_contents: InitialContents::FromCapacity(0),
187            #[cfg(feature = "for-testing")]
188            observer: None,
189            #[cfg(feature = "for-testing")]
190            write_tracking: false,
191            #[cfg(feature = "for-testing")]
192            max_jitter_usec: None,
193        }
194    }
195}
196
197impl VmoBackedServerOptions<'_> {
198    pub fn build(self) -> Result<VmoBackedServer, Error> {
199        let (data, block_count) = match self.initial_contents {
200            InitialContents::FromCapacity(block_count) => {
201                (zx::Vmo::create(block_count * self.block_size as u64)?, block_count)
202            }
203            InitialContents::FromCapacityAndBuffer(block_count, buf) => {
204                let needed =
205                    buf.len()
206                        .checked_next_multiple_of(self.block_size as usize)
207                        .ok_or_else(|| anyhow!("Invalid buffer size"))? as u64
208                        / self.block_size as u64;
209                if needed > block_count {
210                    return Err(anyhow!("Not enough capacity: {needed} vs {block_count}"));
211                }
212                let vmo = zx::Vmo::create(block_count * self.block_size as u64)?;
213                if !buf.is_empty() {
214                    vmo.write(buf, 0)?;
215                }
216                (vmo, block_count)
217            }
218            InitialContents::FromBuffer(buf) => {
219                let block_count =
220                    buf.len()
221                        .checked_next_multiple_of(self.block_size as usize)
222                        .ok_or_else(|| anyhow!("Invalid buffer size"))? as u64
223                        / self.block_size as u64;
224                let vmo = zx::Vmo::create(block_count * self.block_size as u64)?;
225                if !buf.is_empty() {
226                    vmo.write(buf, 0)?;
227                }
228                (vmo, block_count)
229            }
230            InitialContents::FromVmo(vmo) => {
231                let size = vmo.get_size()?;
232                let block_count = size / self.block_size as u64;
233                (vmo, block_count)
234            }
235        };
236
237        let info = match self.info {
238            DeviceInfo::Block(mut info) => {
239                info.block_count = block_count;
240                DeviceInfo::Block(info)
241            }
242            DeviceInfo::Partition(mut info) => {
243                info.block_count = block_count;
244                DeviceInfo::Partition(info)
245            }
246            DeviceInfo::Volume(info) => DeviceInfo::Volume(info),
247        };
248        Ok(VmoBackedServer {
249            server: BlockServer::new(
250                self.block_size,
251                Arc::new(Data {
252                    info,
253                    block_size: self.block_size,
254                    data,
255                    #[cfg(feature = "for-testing")]
256                    observer: self.observer,
257                    #[cfg(feature = "for-testing")]
258                    write_cache: self
259                        .write_tracking
260                        .then(|| fuchsia_sync::Mutex::new(WriteCache::new(self.block_size as u64))),
261                    #[cfg(feature = "for-testing")]
262                    max_jitter_usec: self.max_jitter_usec,
263                    #[cfg(feature = "for-testing")]
264                    fscrypt_keys: fuchsia_sync::Mutex::new(FscryptKeys::new()),
265                }),
266            ),
267        })
268    }
269}
270
271/// Implements `BlockConnector` to vend connections to a VmoBackedServer.
272pub struct VmoBackedServerConnector {
273    scope: fuchsia_async::ScopeHandle,
274    server: Arc<VmoBackedServer>,
275}
276
277impl VmoBackedServerConnector {
278    /// New connections will served on the current scope.
279    pub fn new(server: Arc<VmoBackedServer>) -> Self {
280        Self { scope: fuchsia_async::Scope::current(), server }
281    }
282
283    /// New connections will served on the provided scope.
284    pub fn new_with_scope(server: Arc<VmoBackedServer>, scope: fuchsia_async::ScopeHandle) -> Self {
285        Self { scope, server }
286    }
287}
288
289impl BlockConnector for VmoBackedServerConnector {
290    fn connect_channel_to_block(
291        &self,
292        server_end: ServerEnd<fblock::BlockMarker>,
293    ) -> Result<(), Error> {
294        let server = self.server.clone();
295        let _ = self.scope.spawn(async move {
296            let _ = server.serve(server_end.into_stream()).await;
297        });
298        Ok(())
299    }
300}
301
302pub trait BlockClient: FromClient {}
303
304impl BlockClient for fblock::BlockProxy {}
305impl BlockClient for fblock::BlockSynchronousProxy {}
306impl BlockClient for ClientEnd<fblock::BlockMarker> {}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use block_server::async_interface::Interface;
312    use block_server::{InlineCryptoOptions, ReadOptions, WriteOptions};
313
314    #[fuchsia::test]
315    async fn test_program_and_evict_key_slot() {
316        let block_size = 4096;
317        let server =
318            VmoBackedServer::new(100, block_size, &[]).expect("Failed to create VmoBackedServer");
319
320        let key = [0xaa; 64];
321        let slot = server.program_key(&key).expect("program_key failed");
322        assert_eq!(slot, 0);
323
324        // Use the internal interface to avoid FIDL complexity for this test.
325        let block_interface = server.server.session_manager().interface();
326        // Verify that we can write and read using the programmed key.
327        let vmo = Arc::new(zx::Vmo::create(block_size as u64).expect("Vmo::create failed"));
328        let original_data = vec![0xbb; block_size as usize];
329        vmo.write(&original_data, 0).expect("Vmo::write failed");
330        let write_opts = WriteOptions {
331            inline_crypto: InlineCryptoOptions::enabled(slot, 0),
332            ..Default::default()
333        };
334        block_interface.write(0, 1, &vmo, 0, write_opts, None).await.expect("write failed");
335
336        // Verify we can read it back.
337        let vmo_read = Arc::new(zx::Vmo::create(block_size as u64).expect("Vmo::create failed"));
338        let read_opts = ReadOptions {
339            inline_crypto: InlineCryptoOptions::enabled(slot, 0),
340            ..Default::default()
341        };
342        block_interface.read(0, 1, &vmo_read, 0, read_opts, None).await.expect("read failed");
343        let mut read_data = vec![0u8; block_size as usize];
344        vmo_read.read(&mut read_data, 0).expect("Vmo::read failed");
345        assert_eq!(read_data, original_data);
346
347        server.evict_key_slot(slot).expect("evict_key_slot failed");
348        assert_eq!(server.evict_key_slot(slot), Err(zx::Status::INVALID_ARGS));
349
350        // Writing and reading from file after the key has been evicted should fail.
351        assert_eq!(
352            block_interface.read(0, 1, &vmo_read, 0, read_opts, None).await,
353            Err(zx::Status::IO)
354        );
355
356        assert_eq!(
357            block_interface.write(0, 1, &vmo, 0, write_opts, None).await,
358            Err(zx::Status::IO)
359        );
360    }
361
362    #[fuchsia::test]
363    async fn test_program_key_out_of_slots() {
364        let server = VmoBackedServer::new(100, 512, &[]).expect("Failed to create VmoBackedServer");
365
366        let key = [0xaa; 64];
367        for expected_slot in 0..=u8::MAX {
368            let slot = server.program_key(&key).expect("program_key failed");
369            assert_eq!(slot, expected_slot);
370        }
371        assert_eq!(server.program_key(&key), Err(zx::Status::NO_RESOURCES));
372    }
373}