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, RemoteNode};
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_io as fio;
15use starnix_crypt::CryptService;
16use starnix_logging::{log_error, log_info};
17use starnix_sync::{FileOpsCore, Locked, Unlocked};
18use starnix_uapi::errors::Errno;
19use starnix_uapi::{errno, from_status_like_fdio, statfs};
20use std::sync::Arc;
21use syncio::{Zxio, zxio_node_attr_has_t, zxio_node_attributes_t};
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(
42        &self,
43        locked: &mut Locked<FileOpsCore>,
44        fs: &FileSystem,
45        current_task: &CurrentTask,
46    ) -> Result<statfs, Errno> {
47        self.remotefs.statfs(locked, fs, current_task)
48    }
49
50    fn name(&self) -> &'static FsStr {
51        "remotevol".into()
52    }
53
54    fn uses_external_node_ids(&self) -> bool {
55        self.remotefs.uses_external_node_ids()
56    }
57
58    fn rename(
59        &self,
60        locked: &mut Locked<FileOpsCore>,
61        fs: &FileSystem,
62        current_task: &CurrentTask,
63        old_parent: &FsNodeHandle,
64        old_name: &FsStr,
65        new_parent: &FsNodeHandle,
66        new_name: &FsStr,
67        renamed: &FsNodeHandle,
68        replaced: Option<&FsNodeHandle>,
69    ) -> Result<(), Errno> {
70        self.remotefs.rename(
71            locked,
72            fs,
73            current_task,
74            old_parent,
75            old_name,
76            new_parent,
77            new_name,
78            renamed,
79            replaced,
80        )
81    }
82
83    fn unmount(&self) {
84        let (proxy, server_end) = create_sync_proxy::<fidl_fuchsia_fs::AdminMarker>();
85        if let Err(e) = fdio::service_connect_at(
86            self.exposed_dir_proxy.as_channel(),
87            &format!("svc/{}", fidl_fuchsia_fs::AdminMarker::PROTOCOL_NAME),
88            server_end.into(),
89        ) {
90            log_error!(e:%; "StarnixVolumeProvider.Unmount failed to connect to fuchsia.fs.Admin");
91            return;
92        }
93
94        if let Err(e) = proxy.shutdown(zx::MonotonicInstant::INFINITE) {
95            log_error!(e:%; "StarnixVolumeProvider.Unmount failed at FIDL layer");
96        }
97    }
98
99    fn crypt_service(&self) -> Option<Arc<CryptService>> {
100        Some(self.crypt_service.clone())
101    }
102}
103
104// Key file
105// ========
106//
107// Version 1: No longer supported.
108// Version 2:
109//
110//   +-2-+------- 32 -------+------- 32 -------+
111//   | V |   metadata key   |     data key     |
112//   +---+------------------+------------------+
113//
114// Version 2 includes a 16 bit version which indicates the version of the key file.  The key
115// identifiers used for version 2 key files will use the lblk32 algorithm.
116
117struct VolumeKeys {
118    metadata: [u8; 32],
119    data: [u8; 32],
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        if let Some(keys) = Self::get(data, key_path)? {
138            Ok((keys, false))
139        } else {
140            log_info!("Creating key file at {key_path}");
141            Ok((Self::create(data, key_path)?, true))
142        }
143    }
144
145    /// Returns None rather than an error if the key file does not exist or is corrupt,
146    /// but returns all other errors (e.g. if the connection to `data` is closed).
147    fn get(data: &fio::DirectorySynchronousProxy, key_path: &str) -> Result<Option<Self>, Errno> {
148        match syncio::directory_read_file(data, key_path, zx::MonotonicInstant::INFINITE) {
149            Ok(bytes) => {
150                if bytes.len() == Self::FILE_SIZE {
151                    if u16::from_le_bytes(bytes[0..2].try_into().unwrap()) != Self::LATEST_VERSION {
152                        return Ok(None);
153                    }
154                    Ok(Some(Self {
155                        metadata: bytes[2..34].try_into().unwrap(),
156                        data: bytes[34..66].try_into().unwrap(),
157                    }))
158                } else {
159                    Ok(None)
160                }
161            }
162            Err(zx::Status::NOT_FOUND) => Ok(None),
163            Err(status) => {
164                log_error!("Failed to read key file: {status:?}");
165                Err(from_status_like_fdio!(status))
166            }
167        }
168    }
169
170    /// Creates a new key file at the latest version, with new random metadata and data keys.
171    fn create(data: &fio::DirectorySynchronousProxy, key_path: &str) -> Result<Self, Errno> {
172        let mut bytes = [0; Self::FILE_SIZE];
173        bytes[..2].copy_from_slice(&Self::LATEST_VERSION.to_le_bytes());
174        zx::cprng_draw(&mut bytes[2..]);
175        let tmp_file = syncio::directory_create_tmp_file(
176            data,
177            fio::PERM_READABLE,
178            zx::MonotonicInstant::INFINITE,
179        )
180        .map_err(|e| {
181            let err = from_status_like_fdio!(e);
182            log_error!("Failed to create tmp file with error: {:?}", err);
183            err
184        })?;
185        tmp_file
186            .write(&bytes, zx::MonotonicInstant::INFINITE)
187            .map_err(|e| {
188                log_error!("FIDL transport error on File.Write {:?}", e);
189                errno!(ENOENT)
190            })?
191            .map_err(|e| {
192                let err = from_status_like_fdio!(zx::Status::from_raw(e));
193                log_error!("File.Write failed with {:?}", err);
194                err
195            })?;
196        tmp_file
197            .sync(zx::MonotonicInstant::INFINITE)
198            .map_err(|e| {
199                log_error!("FIDL transport error on File.Sync {:?}", e);
200                errno!(ENOENT)
201            })?
202            .map_err(|e| {
203                let err = from_status_like_fdio!(zx::Status::from_raw(e));
204                log_error!("File.Sync failed with {:?}", err);
205                err
206            })?;
207        let (status, token) = data.get_token(zx::MonotonicInstant::INFINITE).map_err(|e| {
208            log_error!("transport error on get_token for the data directory, error: {:?}", e);
209            errno!(ENOENT)
210        })?;
211        zx::Status::ok(status).map_err(|e| {
212            let err = from_status_like_fdio!(e);
213            log_error!("Failed to get_token for the data directory, error: {:?}", err);
214            err
215        })?;
216
217        tmp_file
218            .link_into(
219                zx::Event::from(token.ok_or_else(|| errno!(ENOENT))?),
220                key_path,
221                zx::MonotonicInstant::INFINITE,
222            )
223            .map_err(|e| {
224                log_error!("FIDL transport error on File.LinkInto {:?}", e);
225                errno!(EIO)
226            })?
227            .map_err(|e| {
228                let err = from_status_like_fdio!(zx::Status::from_raw(e));
229                log_error!("File.LinkInto failed with {:?}", err);
230                err
231            })?;
232        Ok(Self {
233            metadata: bytes[2..34].try_into().unwrap(),
234            data: bytes[34..].try_into().unwrap(),
235        })
236    }
237}
238
239pub fn new_remote_vol(
240    locked: &mut Locked<Unlocked>,
241    current_task: &CurrentTask,
242    options: FileSystemOptions,
243) -> Result<FileSystemHandle, Errno> {
244    let kernel = current_task.kernel();
245    // TODO(https://fxbug.dev/460156877): Starnix cannot handle multiple volumes.
246    let volume_provider = current_task
247        .kernel()
248        .connect_to_protocol_at_container_svc::<StarnixVolumeProviderMarker>()
249        .map_err(|_| errno!(ENOENT))?
250        .into_sync_proxy();
251
252    let (crypt_client_end, crypt_proxy) = fidl::endpoints::create_endpoints::<CryptMarker>();
253
254    let key_location = match options.params.get(FsStr::new(b"keylocation")) {
255        Some(path) => str::from_utf8(path.as_bytes()).map_err(|_| errno!(EINVAL))?,
256        None => {
257            // TODO(https://fxbug.dev/460156877): Starnix cannot handle unencrypted volumes.
258            log_error!(
259                "TODO(b/460156877): Starnix is unable to mount remote volumes without encryption. \
260                Encrypted volumes should specify a keylocation in the mount flags."
261            );
262            return Err(errno!(EINVAL));
263        }
264    };
265
266    let open_flags =
267        fio::PERM_READABLE | fio::Flags::FLAG_MAYBE_CREATE | fio::Flags::PROTOCOL_DIRECTORY;
268    let (root, subdir) = kernel.open_ns_dir(key_location, open_flags)?;
269
270    let open_rights = fio::PERM_READABLE | fio::PERM_WRITABLE;
271    let subdir = if subdir.is_empty() { ".".to_string() } else { subdir };
272    let key_location_proxy = syncio::directory_open_directory_async(&root, &subdir, open_rights)
273        .map_err(|e| errno!(EIO, format!("Failed to open proxy for keylocation: {e}")))?;
274
275    let (keys, created_key_file) = VolumeKeys::get_or_create(&key_location_proxy, KEY_FILE_PATH)?;
276
277    // Attempt to connect to the inline encryption device if mount options specify inline crypt
278    let inline_encryption_provider = match options.params.get(FsStr::new(b"inlinecrypt")) {
279        Some(_) => {
280            let (inline_encryption_provider, inline_encryption_server) =
281                fidl::endpoints::create_sync_proxy();
282            // TODO(https://fxbug.dev/477720373): Use component routing instead.
283            match volume_provider
284                .connect_to_inline_encryption(
285                    inline_encryption_server,
286                    zx::MonotonicInstant::INFINITE,
287                )
288                .map_err(|e| {
289                    log_error!(
290                        "FIDL error on StarnixVolumeProvider.ConnectToInlineEncryption {:?}",
291                        e
292                    );
293                    errno!(EIO)
294                })? {
295                Ok(()) => Some(inline_encryption_provider),
296                Err(e) => {
297                    let error = from_status_like_fdio!(zx::Status::from_raw(e));
298                    log_error!(error:?; "Error while connecting to inline encryption");
299                    return Err(error);
300                }
301            }
302        }
303        None => None,
304    };
305    let crypt_service =
306        Arc::new(CryptService::new(&keys.metadata, &keys.data, inline_encryption_provider));
307
308    let (exposed_dir_client_end, exposed_dir_server) =
309        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
310
311    {
312        let crypt_service = Arc::clone(&crypt_service);
313        let closure = async move |_: LockedAndTask<'_>| {
314            if let Err(e) = crypt_service.handle_connection(crypt_proxy.into_stream()).await {
315                log_error!("Error while handling a Crypt request {e}");
316            }
317        };
318        let req = SpawnRequestBuilder::new()
319            .with_debug_name("remote-volume-crypt")
320            .with_role(CRYPT_THREAD_ROLE)
321            .with_async_closure(closure)
322            .build();
323        kernel.kthreads.spawner().spawn_from_request(req);
324    }
325
326    let mode = if created_key_file {
327        fidl_fuchsia_fshost::MountMode::AlwaysCreate
328    } else {
329        fidl_fuchsia_fshost::MountMode::MaybeCreate
330    };
331    let guid = volume_provider
332        .mount(crypt_client_end, mode, exposed_dir_server, zx::MonotonicInstant::INFINITE)
333        .map_err(|e| {
334            log_error!("FIDL transport error on StarnixVolumeProvider.Mount {:?}", e);
335            errno!(ENOENT)
336        })?
337        .map_err(|e| {
338            let error = from_status_like_fdio!(zx::Status::from_raw(e));
339            log_error!(error:?; "StarnixVolumeProvider.Mount failed");
340            error
341        })?;
342
343    crypt_service.set_uuid(guid);
344
345    let exposed_dir_proxy = exposed_dir_client_end.into_sync_proxy();
346
347    let root = syncio::directory_open_directory_async(
348        &exposed_dir_proxy,
349        "root",
350        fio::PERM_READABLE | fio::PERM_WRITABLE,
351    )
352    .map_err(|e| errno!(EIO, format!("Failed to open root: {e}")))?;
353
354    let rights = fio::PERM_READABLE | fio::PERM_WRITABLE;
355
356    let (client_end, server_end) = zx::Channel::create();
357    let remotefs = RemoteFs::new(root.into_channel(), server_end)?;
358    let mut attrs = zxio_node_attributes_t {
359        has: zxio_node_attr_has_t { id: true, ..Default::default() },
360        ..Default::default()
361    };
362    let (remote_node, node_id) =
363        match Zxio::create_with_on_representation(client_end.into(), Some(&mut attrs)) {
364            Err(status) => return Err(from_status_like_fdio!(status)),
365            Ok(zxio) => (RemoteNode::new(zxio, rights), attrs.id),
366        };
367
368    let use_remote_ids = remotefs.use_remote_ids();
369    let remotevol = RemoteVolume { remotefs, exposed_dir_proxy, crypt_service };
370    let fs = FileSystem::new(
371        locked,
372        kernel,
373        CacheMode::Cached(kernel.fs_cache_config()),
374        remotevol,
375        options,
376    )?;
377    if use_remote_ids {
378        fs.create_root(node_id, remote_node);
379    } else {
380        let root_ino = fs.allocate_ino();
381        fs.create_root(root_ino, remote_node);
382    }
383    Ok(fs)
384}