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_insecure_inline_encryption_server(
90        self: &Arc<Self>,
91        server: ServerEnd<DeviceMarker>,
92        uuid: [u8; 16],
93    ) -> impl Future<Output = ()> + Send {
94        let this = self.clone();
95        this.serve_insecure_inline_encryption(server.into_stream(), uuid)
96    }
97
98    /// Evict key slot for software ciphers.
99    pub fn evict_key_slot(&self, slot: u8) -> Result<(), zx::Status> {
100        self.server.session_manager().interface().fscrypt_keys().evict_key(slot)
101    }
102
103    /// Implements software-fallback for fuchsia_hardware_inlineencryption.ProgramKey. There is a
104    /// maximum of 256 keyslots. Insert keyslot at the next available slot.
105    fn program_key(&self, xts_key: &[u8; 64]) -> Result<u8, zx::Status> {
106        self.server.session_manager().interface().fscrypt_keys().program_key(xts_key)
107    }
108
109    pub async fn serve_insecure_inline_encryption(
110        self: Arc<Self>,
111        mut requests: DeviceRequestStream,
112        uuid: [u8; 16],
113    ) {
114        while let Some(Ok(request)) = requests.next().await {
115            match request {
116                DeviceRequest::ProgramKey { wrapped_key, data_unit_size: _, responder } => {
117                    responder
118                        .send(
119                            self.program_key(&fscrypt::to_xts_key(&wrapped_key, uuid))
120                                .map_err(zx::Status::into_raw),
121                        )
122                        .unwrap_or_else(|e| {
123                            log::error!("failed to send ProgramKey response. error: {:?}", e);
124                        });
125                }
126                DeviceRequest::DeriveRawSecret { mut wrapped_key, responder } => {
127                    // Swap the nibbles.
128                    for b in &mut wrapped_key {
129                        *b = *b >> 4 | *b << 4;
130                    }
131                    responder.send(Ok(&wrapped_key)).unwrap();
132                }
133            }
134        }
135    }
136}
137
138/// The initial contents of the VMO.  This also determines the size of the block device.
139pub enum InitialContents<'a> {
140    /// An empty VMO will be created with capacity for this many *blocks*.
141    FromCapacity(u64),
142    /// A VMO is created with capacity for this many *blocks* and the buffer's contents copied into
143    /// it.
144    FromCapacityAndBuffer(u64, &'a [u8]),
145    /// A VMO is created which is exactly large enough for the initial contents (rounded up to block
146    /// size), and the buffer's contents copied into it.
147    FromBuffer(&'a [u8]),
148    /// The provided VMO is used.  If its size is not block-aligned, the data will be truncated.
149    FromVmo(zx::Vmo),
150}
151
152pub struct VmoBackedServerOptions<'a> {
153    /// NB: `block_count` is ignored as that comes from `initial_contents`.
154    pub info: DeviceInfo,
155    pub block_size: u32,
156    pub initial_contents: InitialContents<'a>,
157    #[cfg(feature = "for-testing")]
158    pub observer: Option<Box<dyn Observer>>,
159    /// Enables write tracking so [`Observer::flush`] and [`Observer::barrier`] will be provided
160    /// with [`WriteCache`]. Note that this is expensive.
161    #[cfg(feature = "for-testing")]
162    pub write_tracking: bool,
163    /// If set, each operation will be delayed by a random duration <= this value, which is useful
164    /// for testing race conditions due to out-of-order block requests.
165    #[cfg(feature = "for-testing")]
166    pub max_jitter_usec: Option<u64>,
167}
168
169impl Default for VmoBackedServerOptions<'_> {
170    fn default() -> Self {
171        VmoBackedServerOptions {
172            info: DeviceInfo::Block(BlockInfo {
173                device_flags: fblock::DeviceFlag::empty(),
174                block_count: 0,
175                max_transfer_blocks: None,
176            }),
177            block_size: 512,
178            initial_contents: InitialContents::FromCapacity(0),
179            #[cfg(feature = "for-testing")]
180            observer: None,
181            #[cfg(feature = "for-testing")]
182            write_tracking: false,
183            #[cfg(feature = "for-testing")]
184            max_jitter_usec: None,
185        }
186    }
187}
188
189impl VmoBackedServerOptions<'_> {
190    pub fn build(self) -> Result<VmoBackedServer, Error> {
191        let (data, block_count) = match self.initial_contents {
192            InitialContents::FromCapacity(block_count) => {
193                (zx::Vmo::create(block_count * self.block_size as u64)?, block_count)
194            }
195            InitialContents::FromCapacityAndBuffer(block_count, buf) => {
196                let needed =
197                    buf.len()
198                        .checked_next_multiple_of(self.block_size as usize)
199                        .ok_or_else(|| anyhow!("Invalid buffer size"))? as u64
200                        / self.block_size as u64;
201                if needed > block_count {
202                    return Err(anyhow!("Not enough capacity: {needed} vs {block_count}"));
203                }
204                let vmo = zx::Vmo::create(block_count * self.block_size as u64)?;
205                if !buf.is_empty() {
206                    vmo.write(buf, 0)?;
207                }
208                (vmo, block_count)
209            }
210            InitialContents::FromBuffer(buf) => {
211                let block_count =
212                    buf.len()
213                        .checked_next_multiple_of(self.block_size as usize)
214                        .ok_or_else(|| anyhow!("Invalid buffer size"))? as u64
215                        / self.block_size as u64;
216                let vmo = zx::Vmo::create(block_count * self.block_size as u64)?;
217                if !buf.is_empty() {
218                    vmo.write(buf, 0)?;
219                }
220                (vmo, block_count)
221            }
222            InitialContents::FromVmo(vmo) => {
223                let size = vmo.get_size()?;
224                let block_count = size / self.block_size as u64;
225                (vmo, block_count)
226            }
227        };
228
229        let info = match self.info {
230            DeviceInfo::Block(mut info) => {
231                info.block_count = block_count;
232                DeviceInfo::Block(info)
233            }
234            DeviceInfo::Partition(mut info) => {
235                info.block_range = Some(0..block_count);
236                DeviceInfo::Partition(info)
237            }
238        };
239        Ok(VmoBackedServer {
240            server: BlockServer::new(
241                self.block_size,
242                Arc::new(Data {
243                    info,
244                    block_size: self.block_size,
245                    data,
246                    #[cfg(feature = "for-testing")]
247                    observer: self.observer,
248                    #[cfg(feature = "for-testing")]
249                    write_cache: self
250                        .write_tracking
251                        .then(|| fuchsia_sync::Mutex::new(WriteCache::new(self.block_size as u64))),
252                    #[cfg(feature = "for-testing")]
253                    max_jitter_usec: self.max_jitter_usec,
254                    #[cfg(feature = "for-testing")]
255                    fscrypt_keys: fuchsia_sync::Mutex::new(FscryptKeys::new()),
256                }),
257            ),
258        })
259    }
260}
261
262/// Implements `BlockConnector` to vend connections to a VmoBackedServer.
263pub struct VmoBackedServerConnector {
264    scope: fuchsia_async::ScopeHandle,
265    server: Arc<VmoBackedServer>,
266}
267
268impl VmoBackedServerConnector {
269    /// New connections will served on the current scope.
270    pub fn new(server: Arc<VmoBackedServer>) -> Self {
271        Self { scope: fuchsia_async::Scope::current(), server }
272    }
273
274    /// New connections will served on the provided scope.
275    pub fn new_with_scope(server: Arc<VmoBackedServer>, scope: fuchsia_async::ScopeHandle) -> Self {
276        Self { scope, server }
277    }
278}
279
280impl BlockConnector for VmoBackedServerConnector {
281    fn connect_channel_to_block(
282        &self,
283        server_end: ServerEnd<fblock::BlockMarker>,
284    ) -> Result<(), Error> {
285        let server = self.server.clone();
286        let _ = self.scope.spawn(async move {
287            let _ = server.serve(server_end.into_stream()).await;
288        });
289        Ok(())
290    }
291}
292
293pub trait BlockClient: FromClient {}
294
295impl BlockClient for fblock::BlockProxy {}
296impl BlockClient for fblock::BlockSynchronousProxy {}
297impl BlockClient for ClientEnd<fblock::BlockMarker> {}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use block_server::async_interface::Interface;
303    use block_server::{InlineCryptoOptions, ReadOptions, WriteOptions};
304
305    #[fuchsia::test]
306    async fn test_program_and_evict_key_slot() {
307        let block_size = 4096;
308        let server =
309            VmoBackedServer::new(100, block_size, &[]).expect("Failed to create VmoBackedServer");
310
311        let key = [0xaa; 64];
312        let slot = server.program_key(&key).expect("program_key failed");
313        assert_eq!(slot, 0);
314
315        // Use the internal interface to avoid FIDL complexity for this test.
316        let block_interface = server.server.session_manager().interface();
317        // Verify that we can write and read using the programmed key.
318        let vmo = Arc::new(zx::Vmo::create(block_size as u64).expect("Vmo::create failed"));
319        let original_data = vec![0xbb; block_size as usize];
320        vmo.write(&original_data, 0).expect("Vmo::write failed");
321        let write_opts = WriteOptions {
322            inline_crypto: InlineCryptoOptions::enabled(slot, 0),
323            ..Default::default()
324        };
325        block_interface.write(0, 1, &vmo, 0, write_opts, None).await.expect("write failed");
326
327        // Verify we can read it back.
328        let vmo_read = Arc::new(zx::Vmo::create(block_size as u64).expect("Vmo::create failed"));
329        let read_opts = ReadOptions {
330            inline_crypto: InlineCryptoOptions::enabled(slot, 0),
331            ..Default::default()
332        };
333        block_interface.read(0, 1, &vmo_read, 0, read_opts, None).await.expect("read failed");
334        let mut read_data = vec![0u8; block_size as usize];
335        vmo_read.read(&mut read_data, 0).expect("Vmo::read failed");
336        assert_eq!(read_data, original_data);
337
338        server.evict_key_slot(slot).expect("evict_key_slot failed");
339        assert_eq!(server.evict_key_slot(slot), Err(zx::Status::INVALID_ARGS));
340
341        // Writing and reading from file after the key has been evicted should fail.
342        assert_eq!(
343            block_interface.read(0, 1, &vmo_read, 0, read_opts, None).await,
344            Err(zx::Status::IO)
345        );
346
347        assert_eq!(
348            block_interface.write(0, 1, &vmo, 0, write_opts, None).await,
349            Err(zx::Status::IO)
350        );
351    }
352
353    #[fuchsia::test]
354    async fn test_program_key_out_of_slots() {
355        let server = VmoBackedServer::new(100, 512, &[]).expect("Failed to create VmoBackedServer");
356
357        let key = [0xaa; 64];
358        for expected_slot in 0..=u8::MAX {
359            let slot = server.program_key(&key).expect("program_key failed");
360            assert_eq!(slot, expected_slot);
361        }
362        assert_eq!(server.program_key(&key), Err(zx::Status::NO_RESOURCES));
363    }
364}