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