starnix_core/fs/fuchsia/
remote_volume.rs

1// Copyright 2025 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 crate::fs::fuchsia::{RemoteFs, RemoteNode};
6use crate::task::dynamic_thread_spawner::SpawnRequestBuilder;
7use crate::task::{CurrentTask, LockedAndTask};
8use crate::vfs::{
9    CacheMode, FileSystem, FileSystemHandle, FileSystemOps, FileSystemOptions, FsNodeHandle, FsStr,
10};
11use fidl::endpoints::{DiscoverableProtocolMarker, SynchronousProxy, create_sync_proxy};
12use fidl_fuchsia_fshost::StarnixVolumeProviderMarker;
13use fidl_fuchsia_fxfs::CryptMarker;
14use fidl_fuchsia_hardware_inlineencryption::DeviceMarker as InlineEncryptionDeviceMarker;
15use fidl_fuchsia_io as fio;
16use starnix_crypt::CryptService;
17use starnix_logging::{log_error, log_info};
18use starnix_sync::{FileOpsCore, Locked, Unlocked};
19use starnix_uapi::errors::Errno;
20use starnix_uapi::{errno, from_status_like_fdio, statfs};
21use std::sync::Arc;
22use syncio::{Zxio, zxio_node_attr_has_t, zxio_node_attributes_t};
23
24const CRYPT_THREAD_ROLE: &str = "fuchsia.starnix.remotevol.crypt";
25// `KEY_FILE_PATH` determines where the volume-wide keys for the Starnix volume will live in the
26// container's data storage capability.
27const KEY_FILE_PATH: &str = "key_file";
28
29pub struct RemoteVolume {
30    remotefs: RemoteFs,
31    exposed_dir_proxy: fio::DirectorySynchronousProxy,
32    crypt_service: Arc<CryptService>,
33}
34
35impl RemoteVolume {
36    pub fn remotefs(&self) -> &RemoteFs {
37        &self.remotefs
38    }
39}
40
41impl FileSystemOps for RemoteVolume {
42    fn statfs(
43        &self,
44        locked: &mut Locked<FileOpsCore>,
45        fs: &FileSystem,
46        current_task: &CurrentTask,
47    ) -> Result<statfs, Errno> {
48        self.remotefs.statfs(locked, fs, current_task)
49    }
50
51    fn name(&self) -> &'static FsStr {
52        "remotevol".into()
53    }
54
55    fn uses_external_node_ids(&self) -> bool {
56        self.remotefs.uses_external_node_ids()
57    }
58
59    fn rename(
60        &self,
61        locked: &mut Locked<FileOpsCore>,
62        fs: &FileSystem,
63        current_task: &CurrentTask,
64        old_parent: &FsNodeHandle,
65        old_name: &FsStr,
66        new_parent: &FsNodeHandle,
67        new_name: &FsStr,
68        renamed: &FsNodeHandle,
69        replaced: Option<&FsNodeHandle>,
70    ) -> Result<(), Errno> {
71        self.remotefs.rename(
72            locked,
73            fs,
74            current_task,
75            old_parent,
76            old_name,
77            new_parent,
78            new_name,
79            renamed,
80            replaced,
81        )
82    }
83
84    fn unmount(&self) {
85        let (proxy, server_end) = create_sync_proxy::<fidl_fuchsia_fs::AdminMarker>();
86        if let Err(e) = fdio::service_connect_at(
87            self.exposed_dir_proxy.as_channel(),
88            &format!("svc/{}", fidl_fuchsia_fs::AdminMarker::PROTOCOL_NAME),
89            server_end.into(),
90        ) {
91            log_error!(e:%; "StarnixVolumeProvider.Unmount failed to connect to fuchsia.fs.Admin");
92            return;
93        }
94
95        if let Err(e) = proxy.shutdown(zx::MonotonicInstant::INFINITE) {
96            log_error!(e:%; "StarnixVolumeProvider.Unmount failed at FIDL layer");
97        }
98    }
99
100    fn crypt_service(&self) -> Option<Arc<CryptService>> {
101        Some(self.crypt_service.clone())
102    }
103}
104
105// Key file
106// ========
107//
108// Version 1: No longer supported.
109// Version 2:
110//
111//   +-2-+------- 32 -------+------- 32 -------+
112//   | V |   metadata key   |     data key     |
113//   +---+------------------+------------------+
114//
115// Version 2 includes a 16 bit version which indicates the version of the key file.  The key
116// identifiers used for version 2 key files will use the lblk32 algorithm.
117
118struct VolumeKeys {
119    metadata: [u8; 32],
120    data: [u8; 32],
121}
122
123impl VolumeKeys {
124    // `KEYS_SIZE` is the size of the two keys (the metadata key, and the data key) stored in the
125    // key file.
126    const KEYS_SIZE: usize = 64;
127
128    // Includes 2 bytes for the version.
129    const FILE_SIZE: usize = 2 + Self::KEYS_SIZE;
130
131    const LATEST_VERSION: u16 = 2;
132
133    /// Returns (keys, did_create).
134    fn get_or_create(
135        data: &fio::DirectorySynchronousProxy,
136        key_path: &str,
137    ) -> Result<(Self, bool), Errno> {
138        if let Some(keys) = Self::get(data, key_path)? {
139            Ok((keys, false))
140        } else {
141            log_info!("Creating key file at {key_path}");
142            Ok((Self::create(data, key_path)?, true))
143        }
144    }
145
146    /// Returns None rather than an error if the key file does not exist or is corrupt,
147    /// but returns all other errors (e.g. if the connection to `data` is closed).
148    fn get(data: &fio::DirectorySynchronousProxy, key_path: &str) -> Result<Option<Self>, Errno> {
149        match syncio::directory_read_file(data, key_path, zx::MonotonicInstant::INFINITE) {
150            Ok(bytes) => {
151                if bytes.len() == Self::FILE_SIZE {
152                    if u16::from_le_bytes(bytes[0..2].try_into().unwrap()) != Self::LATEST_VERSION {
153                        return Ok(None);
154                    }
155                    Ok(Some(Self {
156                        metadata: bytes[2..34].try_into().unwrap(),
157                        data: bytes[34..66].try_into().unwrap(),
158                    }))
159                } else {
160                    Ok(None)
161                }
162            }
163            Err(zx::Status::NOT_FOUND) => Ok(None),
164            Err(status) => {
165                log_error!("Failed to read key file: {status:?}");
166                Err(from_status_like_fdio!(status))
167            }
168        }
169    }
170
171    /// Creates a new key file at the latest version, with new random metadata and data keys.
172    fn create(data: &fio::DirectorySynchronousProxy, key_path: &str) -> Result<Self, Errno> {
173        let mut bytes = [0; Self::FILE_SIZE];
174        bytes[..2].copy_from_slice(&Self::LATEST_VERSION.to_le_bytes());
175        zx::cprng_draw(&mut bytes[2..]);
176        let tmp_file = syncio::directory_create_tmp_file(
177            data,
178            fio::PERM_READABLE,
179            zx::MonotonicInstant::INFINITE,
180        )
181        .map_err(|e| {
182            let err = from_status_like_fdio!(e);
183            log_error!("Failed to create tmp file with error: {:?}", err);
184            err
185        })?;
186        tmp_file
187            .write(&bytes, zx::MonotonicInstant::INFINITE)
188            .map_err(|e| {
189                log_error!("FIDL transport error on File.Write {:?}", e);
190                errno!(ENOENT)
191            })?
192            .map_err(|e| {
193                let err = from_status_like_fdio!(zx::Status::from_raw(e));
194                log_error!("File.Write failed with {:?}", err);
195                err
196            })?;
197        tmp_file
198            .sync(zx::MonotonicInstant::INFINITE)
199            .map_err(|e| {
200                log_error!("FIDL transport error on File.Sync {:?}", e);
201                errno!(ENOENT)
202            })?
203            .map_err(|e| {
204                let err = from_status_like_fdio!(zx::Status::from_raw(e));
205                log_error!("File.Sync failed with {:?}", err);
206                err
207            })?;
208        let (status, token) = data.get_token(zx::MonotonicInstant::INFINITE).map_err(|e| {
209            log_error!("transport error on get_token for the data directory, error: {:?}", e);
210            errno!(ENOENT)
211        })?;
212        zx::Status::ok(status).map_err(|e| {
213            let err = from_status_like_fdio!(e);
214            log_error!("Failed to get_token for the data directory, error: {:?}", err);
215            err
216        })?;
217
218        tmp_file
219            .link_into(
220                zx::Event::from(token.ok_or_else(|| errno!(ENOENT))?),
221                key_path,
222                zx::MonotonicInstant::INFINITE,
223            )
224            .map_err(|e| {
225                log_error!("FIDL transport error on File.LinkInto {:?}", e);
226                errno!(EIO)
227            })?
228            .map_err(|e| {
229                let err = from_status_like_fdio!(zx::Status::from_raw(e));
230                log_error!("File.LinkInto failed with {:?}", err);
231                err
232            })?;
233        Ok(Self {
234            metadata: bytes[2..34].try_into().unwrap(),
235            data: bytes[34..].try_into().unwrap(),
236        })
237    }
238}
239
240pub fn new_remote_vol(
241    locked: &mut Locked<Unlocked>,
242    current_task: &CurrentTask,
243    options: FileSystemOptions,
244) -> Result<FileSystemHandle, Errno> {
245    let kernel = current_task.kernel();
246    // TODO(https://fxbug.dev/460156877): Starnix cannot handle multiple volumes.
247    let volume_provider = current_task
248        .kernel()
249        .connect_to_protocol_at_container_svc::<StarnixVolumeProviderMarker>()
250        .map_err(|_| errno!(ENOENT))?
251        .into_sync_proxy();
252
253    let (crypt_client_end, crypt_proxy) = fidl::endpoints::create_endpoints::<CryptMarker>();
254
255    let key_location = match options.params.get(FsStr::new(b"keylocation")) {
256        Some(path) => str::from_utf8(path.as_bytes()).map_err(|_| errno!(EINVAL))?,
257        None => {
258            // TODO(https://fxbug.dev/460156877): Starnix cannot handle unencrypted volumes.
259            log_error!(
260                "TODO(b/460156877): Starnix is unable to mount remote volumes without encryption. \
261                Encrypted volumes should specify a keylocation in the mount flags."
262            );
263            return Err(errno!(EINVAL));
264        }
265    };
266
267    let open_flags =
268        fio::PERM_READABLE | fio::Flags::FLAG_MAYBE_CREATE | fio::Flags::PROTOCOL_DIRECTORY;
269    let (root, subdir) = kernel.open_ns_dir(key_location, open_flags)?;
270
271    let open_rights = fio::PERM_READABLE | fio::PERM_WRITABLE;
272    let subdir = if subdir.is_empty() { ".".to_string() } else { subdir };
273    let key_location_proxy = syncio::directory_open_directory_async(&root, &subdir, open_rights)
274        .map_err(|e| errno!(EIO, format!("Failed to open proxy for keylocation: {e}")))?;
275
276    let (keys, created_key_file) = VolumeKeys::get_or_create(&key_location_proxy, KEY_FILE_PATH)?;
277
278    // Attempt to connect to the inline encryption device if mount options specify inline crypt
279    let inline_encryption_provider = if options.params.get(FsStr::new(b"inlinecrypt")).is_some() {
280        match current_task
281            .kernel()
282            .connect_to_protocol_at_container_svc::<InlineEncryptionDeviceMarker>()
283        {
284            Ok(client_end) => Some(client_end.into_sync_proxy()),
285            Err(error) => {
286                log_error!(error:?; "Error connecting to inline encryption device");
287                return Err(error);
288            }
289        }
290    } else {
291        None
292    };
293    let crypt_service =
294        Arc::new(CryptService::new(&keys.metadata, &keys.data, inline_encryption_provider));
295
296    let (exposed_dir_client_end, exposed_dir_server) =
297        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
298
299    {
300        let crypt_service = Arc::clone(&crypt_service);
301        let closure = async move |_: LockedAndTask<'_>| {
302            if let Err(e) = crypt_service.handle_connection(crypt_proxy.into_stream()).await {
303                log_error!("Error while handling a Crypt request {e}");
304            }
305        };
306        let req = SpawnRequestBuilder::new()
307            .with_debug_name("remote-volume-crypt")
308            .with_role(CRYPT_THREAD_ROLE)
309            .with_async_closure(closure)
310            .build();
311        kernel.kthreads.spawner().spawn_from_request(req);
312    }
313
314    let mode = if created_key_file {
315        fidl_fuchsia_fshost::MountMode::AlwaysCreate
316    } else {
317        fidl_fuchsia_fshost::MountMode::MaybeCreate
318    };
319    let guid = volume_provider
320        .mount(crypt_client_end, mode, exposed_dir_server, zx::MonotonicInstant::INFINITE)
321        .map_err(|e| {
322            log_error!("FIDL transport error on StarnixVolumeProvider.Mount {:?}", e);
323            errno!(ENOENT)
324        })?
325        .map_err(|e| {
326            let error = from_status_like_fdio!(zx::Status::from_raw(e));
327            log_error!(error:?; "StarnixVolumeProvider.Mount failed");
328            error
329        })?;
330
331    crypt_service.set_uuid(guid);
332
333    let exposed_dir_proxy = exposed_dir_client_end.into_sync_proxy();
334
335    let root = syncio::directory_open_directory_async(
336        &exposed_dir_proxy,
337        "root",
338        fio::PERM_READABLE | fio::PERM_WRITABLE,
339    )
340    .map_err(|e| errno!(EIO, format!("Failed to open root: {e}")))?;
341
342    let rights = fio::PERM_READABLE | fio::PERM_WRITABLE;
343
344    let (client_end, server_end) = zx::Channel::create();
345    let remotefs = RemoteFs::new(root.into_channel(), server_end, rights)?;
346    let mut attrs = zxio_node_attributes_t {
347        has: zxio_node_attr_has_t { id: true, ..Default::default() },
348        ..Default::default()
349    };
350    let (remote_node, node_id) =
351        match Zxio::create_with_on_representation(client_end.into(), Some(&mut attrs)) {
352            Err(status) => return Err(from_status_like_fdio!(status)),
353            Ok(zxio) => (RemoteNode::new(zxio), attrs.id),
354        };
355
356    let use_remote_ids = remotefs.use_remote_ids();
357    let remotevol = RemoteVolume { remotefs, exposed_dir_proxy, crypt_service };
358    let fs = FileSystem::new(
359        locked,
360        kernel,
361        CacheMode::Cached(kernel.fs_cache_config()),
362        remotevol,
363        options,
364    )?;
365    if use_remote_ids {
366        fs.create_root(node_id, remote_node);
367    } else {
368        let root_ino = fs.allocate_ino();
369        fs.create_root(root_ino, remote_node);
370    }
371    Ok(fs)
372}