Skip to main content

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