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