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