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