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    let (crypt_client_end, crypt_proxy) = fidl::endpoints::create_endpoints::<CryptMarker>();
268
269    let key_location = match options.params.get(FsStr::new(b"keylocation")) {
270        Some(path) => str::from_utf8(path.as_bytes()).map_err(|_| errno!(EINVAL))?,
271        None => {
272            // TODO(https://fxbug.dev/460156877): Starnix cannot handle unencrypted volumes.
273            log_error!(
274                "TODO(b/460156877): Starnix is unable to mount remote volumes without encryption. \
275                Encrypted volumes should specify a keylocation in the mount flags."
276            );
277            return Err(errno!(EINVAL));
278        }
279    };
280
281    let open_flags =
282        fio::PERM_READABLE | fio::Flags::FLAG_MAYBE_CREATE | fio::Flags::PROTOCOL_DIRECTORY;
283    let (root, subdir) = kernel.open_ns_dir(key_location, open_flags)?;
284
285    let open_rights = fio::PERM_READABLE | fio::PERM_WRITABLE;
286    let subdir = if subdir.is_empty() { ".".to_string() } else { subdir };
287    let key_location_proxy = syncio::directory_open_directory_async(&root, &subdir, open_rights)
288        .map_err(|e| errno!(EIO, format!("Failed to open proxy for keylocation: {e}")))?;
289
290    let (keys, created_key_file) = VolumeKeys::get_or_create(&key_location_proxy, KEY_FILE_PATH)?;
291
292    // Attempt to connect to the inline encryption device if mount options specify inline crypt
293    let inline_encryption_provider = if options.params.get(FsStr::new(b"inlinecrypt")).is_some() {
294        match current_task
295            .kernel()
296            .connect_to_protocol_at_container_svc::<InlineEncryptionDeviceMarker>()
297        {
298            Ok(client_end) => Some(client_end.into_sync_proxy()),
299            Err(error) => {
300                log_error!(error:?; "Error connecting to inline encryption device");
301                return Err(error);
302            }
303        }
304    } else {
305        None
306    };
307    let crypt_service =
308        Arc::new(CryptService::new(&keys.metadata, &keys.data, inline_encryption_provider));
309
310    let (exposed_dir_client_end, exposed_dir_server) =
311        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
312
313    {
314        let crypt_service = Arc::clone(&crypt_service);
315        let closure = async move |_: LockedAndTask<'_>| {
316            if let Err(e) = crypt_service.handle_connection(crypt_proxy.into_stream()).await {
317                log_error!("Error while handling a Crypt request {e}");
318            }
319        };
320        let req = SpawnRequestBuilder::new()
321            .with_debug_name("remote-volume-crypt")
322            .with_role(CRYPT_THREAD_ROLE)
323            .with_async_closure(closure)
324            .build();
325        kernel.kthreads.spawner().spawn_from_request(req);
326    }
327
328    let mode = if created_key_file {
329        fidl_fuchsia_fshost::MountMode::AlwaysCreate
330    } else {
331        fidl_fuchsia_fshost::MountMode::MaybeCreate
332    };
333    let guid = volume_provider
334        .mount(crypt_client_end, mode, exposed_dir_server, zx::MonotonicInstant::INFINITE)
335        .map_err(|e| {
336            log_error!("FIDL transport error on StarnixVolumeProvider.Mount {:?}", e);
337            errno!(ENOENT)
338        })?
339        .map_err(|e| {
340            let error = from_status_like_fdio!(zx::Status::from_raw(e));
341            log_error!(error:?; "StarnixVolumeProvider.Mount failed");
342            error
343        })?;
344
345    crypt_service.set_uuid(guid);
346
347    let exposed_dir_proxy = exposed_dir_client_end.into_sync_proxy();
348
349    let root = syncio::directory_open_directory_async(
350        &exposed_dir_proxy,
351        "root",
352        fio::PERM_READABLE | fio::PERM_WRITABLE,
353    )
354    .map_err(|e| errno!(EIO, format!("Failed to open root: {e}")))?;
355
356    let rights = fio::PERM_READABLE | fio::PERM_WRITABLE;
357
358    let (remotefs, root_node, info, node_id) = RemoteFs::new(root.into_channel(), rights)?;
359
360    let use_remote_ids = remotefs.use_remote_ids();
361    let remotevol = RemoteVolume { remotefs, exposed_dir_proxy, crypt_service };
362    let fs = FileSystem::new(
363        locked,
364        kernel,
365        CacheMode::Cached(kernel.fs_cache_config()),
366        remotevol,
367        options,
368    )?;
369
370    if use_remote_ids {
371        fs.create_root_with_info(node_id, root_node, info);
372    } else {
373        let root_ino = fs.allocate_ino();
374        fs.create_root_with_info(root_ino, root_node, info);
375    }
376
377    Ok(fs)
378}