Skip to main content

vmo_blob/
lib.rs

1// Copyright 2023 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 fidl_fuchsia_io as fio;
7use log::error;
8use std::sync::{Arc, OnceLock};
9use vfs::directory::entry::{EntryInfo, GetEntryInfo};
10use vfs::file::{File, FileOptions, GetVmo, SyncMode};
11use vfs::immutable_attributes;
12
13/// Mimics the c++ blobfs block size.
14const BLOCK_SIZE: u64 = 8192;
15static VMEX_RESOURCE: OnceLock<zx::Resource> = OnceLock::new();
16
17/// Attempt to initialize the vmex resource. Without a vmex, attempts to get the backing memory
18/// of a blob with executable rights will fail with NOT_SUPPORTED.
19pub fn init_vmex_resource(vmex: zx::Resource) -> Result<(), Error> {
20    VMEX_RESOURCE.set(vmex).map_err(|_| anyhow!(zx::Status::ALREADY_BOUND))
21}
22
23/// `VmoBlob` is a wrapper around the fuchsia.io/File protocol. Represents an immutable blob on
24/// Fxfs. Clients will use this library to read and execute blobs.
25pub struct VmoBlob {
26    vmo: zx::Vmo,
27    root_hash: [u8; 32],
28}
29
30impl VmoBlob {
31    pub fn new(vmo: zx::Vmo, root_hash: [u8; 32]) -> Arc<Self> {
32        Arc::new(Self { vmo, root_hash })
33    }
34
35    pub fn root_hash(&self) -> &[u8; 32] {
36        &self.root_hash
37    }
38}
39
40impl GetVmo for VmoBlob {
41    fn get_vmo(&self) -> &zx::Vmo {
42        &self.vmo
43    }
44}
45
46impl GetEntryInfo for VmoBlob {
47    fn entry_info(&self) -> EntryInfo {
48        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)
49    }
50}
51
52impl vfs::node::Node for VmoBlob {
53    async fn get_attributes(
54        &self,
55        requested_attributes: fio::NodeAttributesQuery,
56    ) -> Result<fio::NodeAttributes2, zx::Status> {
57        let content_size = self.get_size().await?;
58        Ok(immutable_attributes!(
59            requested_attributes,
60            Immutable {
61                protocols: fio::NodeProtocolKinds::FILE,
62                abilities: fio::Operations::GET_ATTRIBUTES
63                    | fio::Operations::READ_BYTES
64                    | fio::Operations::EXECUTE,
65                content_size: content_size,
66                // TODO(https://fxbug.dev/295550170): Get storage_size from fxblob.
67                storage_size: content_size.div_ceil(BLOCK_SIZE) * BLOCK_SIZE,
68                root_hash: self.root_hash.to_vec(),
69            }
70        ))
71    }
72}
73
74/// Implement VFS trait so blobs can be accessed as files.
75impl File for VmoBlob {
76    fn executable(&self) -> bool {
77        true
78    }
79
80    async fn open_file(&self, _options: &FileOptions) -> Result<(), zx::Status> {
81        Ok(())
82    }
83
84    async fn truncate(&self, _length: u64) -> Result<(), zx::Status> {
85        Err(zx::Status::ACCESS_DENIED)
86    }
87
88    async fn get_size(&self) -> Result<u64, zx::Status> {
89        self.vmo.get_content_size()
90    }
91
92    async fn get_backing_memory(&self, flags: fio::VmoFlags) -> Result<zx::Vmo, zx::Status> {
93        // We do not support exact/duplicate sharing mode.
94        if flags.contains(fio::VmoFlags::SHARED_BUFFER) {
95            error!("get_backing_memory does not support exact sharing mode!");
96            return Err(zx::Status::NOT_SUPPORTED);
97        }
98        // We only support the combination of WRITE when a private COW clone is explicitly
99        // specified. This implicitly restricts any mmap call that attempts to use MAP_SHARED +
100        // PROT_WRITE.
101        if flags.contains(fio::VmoFlags::WRITE) && !flags.contains(fio::VmoFlags::PRIVATE_CLONE) {
102            error!("get_buffer only supports VmoFlags::WRITE with VmoFlags::PRIVATE_CLONE!");
103            return Err(zx::Status::NOT_SUPPORTED);
104        }
105
106        // If the VMO we return is not going to be written to then we can return a REFERENCE instead
107        // of a SNAPSHOT child, which has a more efficient kernel representation.
108        let use_reference = !flags.contains(fio::VmoFlags::WRITE);
109        let child_options = if use_reference {
110            zx::VmoChildOptions::REFERENCE | zx::VmoChildOptions::NO_WRITE
111        } else {
112            zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE
113        };
114        let child_size = if use_reference { 0 } else { self.vmo.get_content_size()? };
115        let mut child_vmo = self.vmo.create_child(child_options, 0, child_size)?;
116
117        if flags.contains(fio::VmoFlags::EXECUTE) {
118            // TODO(https://fxbug.dev/293606235): Filter out other flags.
119            child_vmo = child_vmo
120                .replace_as_executable(VMEX_RESOURCE.get().ok_or(zx::Status::NOT_SUPPORTED)?)?;
121        }
122
123        Ok(child_vmo)
124    }
125
126    async fn update_attributes(
127        &self,
128        _attributes: fio::MutableNodeAttributes,
129    ) -> Result<(), zx::Status> {
130        Err(zx::Status::NOT_SUPPORTED)
131    }
132
133    async fn sync(&self, _mode: SyncMode) -> Result<(), zx::Status> {
134        Ok(())
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use vfs::node::Node;
142
143    fn create_test_blob(content: &[u8]) -> Arc<VmoBlob> {
144        let vmo = zx::Vmo::create(content.len().max(4096) as u64).unwrap();
145        vmo.set_content_size(&(content.len() as u64)).unwrap();
146        vmo.write(content, 0).unwrap();
147        VmoBlob::new(vmo, [0xabu8; 32])
148    }
149
150    #[fuchsia::test]
151    async fn test_vmo_blob_node_and_file_traits() {
152        let blob = create_test_blob(b"hello vmo blob");
153
154        assert_eq!(blob.get_vmo().get_content_size().unwrap(), 14);
155        assert_eq!(blob.entry_info(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File));
156        assert!(blob.executable());
157        assert_eq!(blob.open_file(&FileOptions::default()).await, Ok(()));
158        assert_eq!(blob.truncate(0).await, Err(zx::Status::ACCESS_DENIED));
159        assert_eq!(blob.get_size().await, Ok(14));
160        assert_eq!(
161            blob.update_attributes(fio::MutableNodeAttributes::default()).await,
162            Err(zx::Status::NOT_SUPPORTED)
163        );
164        assert_eq!(blob.sync(SyncMode::Normal).await, Ok(()));
165
166        let attrs = blob.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap();
167        assert_eq!(attrs.immutable_attributes.protocols, Some(fio::NodeProtocolKinds::FILE));
168        assert_eq!(attrs.immutable_attributes.content_size, Some(14));
169        assert_eq!(attrs.immutable_attributes.storage_size, Some(BLOCK_SIZE));
170        assert_eq!(attrs.immutable_attributes.root_hash, Some(vec![0xabu8; 32]));
171    }
172
173    #[fuchsia::test]
174    async fn test_get_backing_memory_and_vmex() {
175        let blob = create_test_blob(b"backing memory test");
176
177        // Shared buffer is not supported.
178        assert_eq!(
179            blob.get_backing_memory(fio::VmoFlags::SHARED_BUFFER).await.unwrap_err(),
180            zx::Status::NOT_SUPPORTED
181        );
182
183        // Write without PRIVATE_CLONE is not supported.
184        assert_eq!(
185            blob.get_backing_memory(fio::VmoFlags::WRITE).await.unwrap_err(),
186            zx::Status::NOT_SUPPORTED
187        );
188
189        // Read-only reference child VMO.
190        let ro_vmo = blob.get_backing_memory(fio::VmoFlags::READ).await.unwrap();
191        let mut buf = [0u8; 19];
192        ro_vmo.read(&mut buf, 0).unwrap();
193        assert_eq!(&buf, b"backing memory test");
194
195        // Writable private clone child VMO.
196        let rw_vmo = blob
197            .get_backing_memory(
198                fio::VmoFlags::READ | fio::VmoFlags::WRITE | fio::VmoFlags::PRIVATE_CLONE,
199            )
200            .await
201            .unwrap();
202        rw_vmo.write(b"MODIFIED", 0).unwrap();
203
204        // Execute without valid VMEX fails.
205        assert!(
206            blob.get_backing_memory(fio::VmoFlags::READ | fio::VmoFlags::EXECUTE).await.is_err()
207        );
208
209        // Initializing VMEX once succeeds, and a second time returns ALREADY_BOUND.
210        let _ = init_vmex_resource(zx::Resource::from(zx::NullableHandle::invalid()));
211        assert!(init_vmex_resource(zx::Resource::from(zx::NullableHandle::invalid())).is_err());
212    }
213}