Skip to main content

vfs/
protocols.rs

1// Copyright 2023 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::common::CreationMode;
6use crate::directory::DirectoryOptions;
7use crate::file::FileOptions;
8use crate::node::NodeOptions;
9use crate::service::ServiceOptions;
10use crate::symlink::SymlinkOptions;
11use flex_fuchsia_io as fio;
12use zx_status::Status;
13
14/// Extends fio::Flags and fio::OpenFlags
15pub trait ProtocolsExt: ToFileOptions + ToNodeOptions + Send + Sync + 'static {
16    /// True if the directory protocol is allowed.
17    fn is_dir_allowed(&self) -> bool;
18
19    /// True if the file protocol is allowed.
20    fn is_file_allowed(&self) -> bool;
21
22    /// True if the symlink protocol is allowed.
23    fn is_symlink_allowed(&self) -> bool;
24
25    /// The creation mode for the connection.
26    fn creation_mode(&self) -> CreationMode;
27
28    /// The rights for the connection.  If None, it means the connection is not for a node based
29    /// protocol.  If the connection is supposed to use the same rights as the parent connection,
30    /// the rights should have been populated.
31    fn rights(&self) -> Option<fio::Operations>;
32
33    /// Convert to directory options.  Returns an error if the request does not permit a directory.
34    fn to_directory_options(&self) -> Result<DirectoryOptions, Status>;
35
36    /// Convert to symlink options.  Returns an error if the request does not permit a symlink.
37    fn to_symlink_options(&self) -> Result<SymlinkOptions, Status>;
38
39    /// Convert to service options.  Returns an error if the request is not valid for a service.
40    fn to_service_options(&self) -> Result<ServiceOptions, Status>;
41
42    /// True if the file should be in append mode.
43    fn is_append(&self) -> bool;
44
45    /// True if the file should be truncated.
46    fn is_truncate(&self) -> bool;
47
48    /// If creating an object, Whether to create a directory.
49    fn create_directory(&self) -> bool;
50
51    /// True if the protocol should be a limited node connection.
52    fn is_node(&self) -> bool;
53
54    /// True if creating as an unnamed temporary object.
55    fn create_unnamed_temporary_in_directory_path(&self) -> bool;
56}
57
58impl ProtocolsExt for fio::OpenFlags {
59    fn is_dir_allowed(&self) -> bool {
60        !self.contains(fio::OpenFlags::NOT_DIRECTORY)
61    }
62
63    fn is_file_allowed(&self) -> bool {
64        !self.contains(fio::OpenFlags::DIRECTORY)
65    }
66
67    fn is_symlink_allowed(&self) -> bool {
68        !self.contains(fio::OpenFlags::DIRECTORY)
69    }
70
71    fn creation_mode(&self) -> CreationMode {
72        if self.contains(fio::OpenFlags::CREATE) {
73            if self.contains(fio::OpenFlags::CREATE_IF_ABSENT) {
74                CreationMode::Always
75            } else {
76                CreationMode::AllowExisting
77            }
78        } else {
79            CreationMode::Never
80        }
81    }
82
83    fn rights(&self) -> Option<fio::Operations> {
84        if self.contains(fio::OpenFlags::CLONE_SAME_RIGHTS) {
85            None
86        } else {
87            let mut rights = fio::Operations::GET_ATTRIBUTES | fio::Operations::CONNECT;
88            if self.contains(fio::OpenFlags::RIGHT_READABLE) {
89                rights |= fio::R_STAR_DIR;
90            }
91            if self.contains(fio::OpenFlags::RIGHT_WRITABLE) {
92                rights |= fio::W_STAR_DIR;
93            }
94            if self.contains(fio::OpenFlags::RIGHT_EXECUTABLE) {
95                rights |= fio::X_STAR_DIR;
96            }
97            Some(rights)
98        }
99    }
100
101    /// Checks flags provided for a new directory connection.  Returns directory options (cleaning
102    /// up some ambiguities) or an error, in case new new connection flags are not permitting the
103    /// connection to be opened.
104    ///
105    /// Changing this function can be dangerous!  Flags operations may have security implications.
106    fn to_directory_options(&self) -> Result<DirectoryOptions, Status> {
107        assert!(!self.intersects(fio::OpenFlags::NODE_REFERENCE));
108
109        let mut flags = *self;
110
111        if flags.intersects(fio::OpenFlags::DIRECTORY) {
112            flags &= !fio::OpenFlags::DIRECTORY;
113        }
114
115        if flags.intersects(fio::OpenFlags::NOT_DIRECTORY) {
116            return Err(Status::NOT_FILE);
117        }
118
119        // Parent connection must check the POSIX flags in `check_child_connection_flags`, so if any
120        // are still present, we expand their respective rights and remove any remaining flags.
121        if flags.intersects(fio::OpenFlags::POSIX_EXECUTABLE) {
122            flags |= fio::OpenFlags::RIGHT_EXECUTABLE;
123        }
124        if flags.intersects(fio::OpenFlags::POSIX_WRITABLE) {
125            flags |= fio::OpenFlags::RIGHT_WRITABLE;
126        }
127        flags &= !(fio::OpenFlags::POSIX_WRITABLE | fio::OpenFlags::POSIX_EXECUTABLE);
128
129        let allowed_flags = fio::OpenFlags::DESCRIBE
130            | fio::OpenFlags::CREATE
131            | fio::OpenFlags::CREATE_IF_ABSENT
132            | fio::OpenFlags::DIRECTORY
133            | fio::OpenFlags::RIGHT_READABLE
134            | fio::OpenFlags::RIGHT_WRITABLE
135            | fio::OpenFlags::RIGHT_EXECUTABLE;
136
137        let prohibited_flags = fio::OpenFlags::APPEND | fio::OpenFlags::TRUNCATE;
138
139        if flags.intersects(prohibited_flags) {
140            return Err(Status::INVALID_ARGS);
141        }
142
143        if flags.intersects(!allowed_flags) {
144            return Err(Status::NOT_SUPPORTED);
145        }
146
147        // Map io1 OpenFlags::RIGHT_* flags to the corresponding set of io2 rights. Using Open1
148        // requires GET_ATTRIBUTES, as this was previously an privileged operation.
149        let mut rights = fio::Rights::GET_ATTRIBUTES;
150        if flags.contains(fio::OpenFlags::RIGHT_READABLE) {
151            rights |= fio::R_STAR_DIR;
152        }
153        if flags.contains(fio::OpenFlags::RIGHT_WRITABLE) {
154            rights |= fio::W_STAR_DIR;
155        }
156        if flags.contains(fio::OpenFlags::RIGHT_EXECUTABLE) {
157            rights |= fio::X_STAR_DIR;
158        }
159
160        Ok(DirectoryOptions { rights })
161    }
162
163    fn to_symlink_options(&self) -> Result<SymlinkOptions, Status> {
164        if self.intersects(fio::OpenFlags::DIRECTORY) {
165            return Err(Status::NOT_DIR);
166        }
167
168        // We allow write and executable access because the client might not know this is a symbolic
169        // link and they want to open the target of the link with write or executable rights.
170        let optional = fio::OpenFlags::NOT_DIRECTORY
171            | fio::OpenFlags::DESCRIBE
172            | fio::OpenFlags::RIGHT_WRITABLE
173            | fio::OpenFlags::RIGHT_EXECUTABLE;
174
175        if *self & !optional != fio::OpenFlags::RIGHT_READABLE {
176            return Err(Status::INVALID_ARGS);
177        }
178
179        let mut rights = fio::Operations::GET_ATTRIBUTES;
180        if self.contains(fio::OpenFlags::RIGHT_READABLE) {
181            rights |= fio::Operations::READ_BYTES;
182        }
183        if self.contains(fio::OpenFlags::RIGHT_WRITABLE) {
184            rights |= fio::Operations::WRITE_BYTES | fio::Operations::UPDATE_ATTRIBUTES;
185        }
186        if self.contains(fio::OpenFlags::RIGHT_EXECUTABLE) {
187            rights |= fio::Operations::EXECUTE;
188        }
189
190        Ok(SymlinkOptions { rights })
191    }
192
193    fn to_service_options(&self) -> Result<ServiceOptions, Status> {
194        if self.intersects(fio::OpenFlags::DIRECTORY) {
195            return Err(Status::NOT_DIR);
196        }
197
198        if self.intersects(!fio::OpenFlags::DESCRIBE.union(fio::OpenFlags::NOT_DIRECTORY)) {
199            return Err(Status::INVALID_ARGS);
200        }
201
202        Ok(ServiceOptions)
203    }
204
205    fn is_append(&self) -> bool {
206        self.contains(fio::OpenFlags::APPEND)
207    }
208
209    fn is_truncate(&self) -> bool {
210        self.contains(fio::OpenFlags::TRUNCATE)
211    }
212
213    fn create_directory(&self) -> bool {
214        self.contains(fio::OpenFlags::DIRECTORY)
215    }
216
217    fn is_node(&self) -> bool {
218        self.contains(fio::OpenFlags::NODE_REFERENCE)
219    }
220
221    fn create_unnamed_temporary_in_directory_path(&self) -> bool {
222        false
223    }
224}
225
226impl ProtocolsExt for fio::Flags {
227    fn is_dir_allowed(&self) -> bool {
228        self.contains(fio::Flags::PROTOCOL_DIRECTORY)
229            || self.intersection(fio::MASK_KNOWN_PROTOCOLS).is_empty()
230    }
231
232    fn is_file_allowed(&self) -> bool {
233        self.contains(fio::Flags::PROTOCOL_FILE)
234            || self.intersection(fio::MASK_KNOWN_PROTOCOLS).is_empty()
235    }
236
237    fn is_symlink_allowed(&self) -> bool {
238        self.contains(fio::Flags::PROTOCOL_SYMLINK)
239            || self.intersection(fio::MASK_KNOWN_PROTOCOLS).is_empty()
240    }
241
242    fn creation_mode(&self) -> CreationMode {
243        #[cfg(fuchsia_api_level_at_least = "HEAD")]
244        {
245            if self.contains(fio::Flags::FLAG_CREATE_AS_UNNAMED_TEMPORARY) {
246                if self.contains(fio::Flags::FLAG_MUST_CREATE) {
247                    return CreationMode::UnlinkableUnnamedTemporary;
248                }
249                return CreationMode::UnnamedTemporary;
250            }
251        }
252        if self.contains(fio::Flags::FLAG_MUST_CREATE) {
253            CreationMode::Always
254        } else if self.contains(fio::Flags::FLAG_MAYBE_CREATE) {
255            CreationMode::AllowExisting
256        } else {
257            CreationMode::Never
258        }
259    }
260
261    fn rights(&self) -> Option<fio::Operations> {
262        Some(flags_to_rights(self))
263    }
264
265    fn to_directory_options(&self) -> Result<DirectoryOptions, Status> {
266        // Verify protocols.
267        if !self.is_dir_allowed() {
268            return Err(if self.is_file_allowed() { Status::NOT_FILE } else { Status::WRONG_TYPE });
269        }
270
271        // Expand the POSIX flags to their respective rights. This is done with the assumption that
272        // the POSIX flags would have been validated prior to calling this. E.g. in the vfs
273        // connection later.
274        let mut updated_flags = *self;
275        if updated_flags.contains(fio::Flags::PERM_INHERIT_WRITE) {
276            updated_flags |=
277                fio::Flags::from_bits_truncate(fio::INHERITED_WRITE_PERMISSIONS.bits());
278        }
279        if updated_flags.contains(fio::Flags::PERM_INHERIT_EXECUTE) {
280            updated_flags |= fio::Flags::PERM_EXECUTE;
281        }
282
283        // Verify that there are no file-related flags.
284        if updated_flags.intersects(fio::Flags::FILE_APPEND | fio::Flags::FILE_TRUNCATE) {
285            return Err(Status::INVALID_ARGS);
286        }
287
288        Ok(DirectoryOptions { rights: flags_to_rights(&updated_flags) })
289    }
290
291    fn to_symlink_options(&self) -> Result<SymlinkOptions, Status> {
292        if !self.is_symlink_allowed() {
293            return Err(Status::WRONG_TYPE);
294        }
295
296        // If is_symlink_allowed() returned true, there must be rights.
297        let rights = self.rights().unwrap();
298        if !rights.contains(fio::Operations::GET_ATTRIBUTES) {
299            return Err(Status::INVALID_ARGS);
300        }
301        Ok(SymlinkOptions { rights })
302    }
303
304    fn to_service_options(&self) -> Result<ServiceOptions, Status> {
305        // This should not be called if fio::Flags::PROTOCOL_NODE was set (`to_node_options` would
306        // be called instead).
307        assert!(!self.contains(fio::Flags::PROTOCOL_NODE));
308        if !self
309            .intersection(fio::MASK_KNOWN_PROTOCOLS)
310            .difference(fio::Flags::PROTOCOL_SERVICE)
311            .is_empty()
312        {
313            return if self.is_dir_allowed() {
314                Err(Status::NOT_DIR)
315            } else if self.is_file_allowed() {
316                Err(Status::NOT_FILE)
317            } else {
318                Err(Status::WRONG_TYPE)
319            };
320        }
321
322        if !self.difference(fio::Flags::PROTOCOL_SERVICE).is_empty() {
323            return Err(Status::INVALID_ARGS);
324        }
325
326        Ok(ServiceOptions)
327    }
328
329    fn is_append(&self) -> bool {
330        self.contains(fio::Flags::FILE_APPEND)
331    }
332
333    fn is_truncate(&self) -> bool {
334        self.contains(fio::Flags::FILE_TRUNCATE)
335    }
336
337    fn create_directory(&self) -> bool {
338        self.contains(fio::Flags::PROTOCOL_DIRECTORY)
339    }
340
341    fn is_node(&self) -> bool {
342        self.contains(fio::Flags::PROTOCOL_NODE)
343    }
344
345    fn create_unnamed_temporary_in_directory_path(&self) -> bool {
346        self.creation_mode() == CreationMode::UnnamedTemporary
347            || self.creation_mode() == CreationMode::UnlinkableUnnamedTemporary
348    }
349}
350
351pub trait ToFileOptions: Send + 'static {
352    fn to_file_options(&self) -> Result<FileOptions, Status>;
353}
354
355impl ToFileOptions for fio::OpenFlags {
356    fn to_file_options(&self) -> Result<FileOptions, Status> {
357        assert!(!self.intersects(fio::OpenFlags::NODE_REFERENCE));
358
359        if self.contains(fio::OpenFlags::DIRECTORY) {
360            return Err(Status::NOT_DIR);
361        }
362
363        // Verify allowed operations/flags this node supports.
364        let flags_without_rights = self.difference(
365            fio::OpenFlags::RIGHT_READABLE
366                | fio::OpenFlags::RIGHT_WRITABLE
367                | fio::OpenFlags::RIGHT_EXECUTABLE,
368        );
369        const ALLOWED_FLAGS: fio::OpenFlags = fio::OpenFlags::DESCRIBE
370            .union(fio::OpenFlags::CREATE)
371            .union(fio::OpenFlags::CREATE_IF_ABSENT)
372            .union(fio::OpenFlags::APPEND)
373            .union(fio::OpenFlags::TRUNCATE)
374            .union(fio::OpenFlags::POSIX_WRITABLE)
375            .union(fio::OpenFlags::POSIX_EXECUTABLE)
376            .union(fio::OpenFlags::NOT_DIRECTORY);
377        if flags_without_rights.intersects(!ALLOWED_FLAGS) {
378            return Err(Status::NOT_SUPPORTED);
379        }
380
381        // Disallow invalid flag combinations.
382        let mut prohibited_flags = fio::OpenFlags::empty();
383        if !self.intersects(fio::OpenFlags::RIGHT_WRITABLE) {
384            prohibited_flags |= fio::OpenFlags::TRUNCATE
385        }
386        if self.intersects(prohibited_flags) {
387            return Err(Status::INVALID_ARGS);
388        }
389
390        Ok(FileOptions {
391            rights: {
392                let mut rights = fio::Operations::GET_ATTRIBUTES;
393                if self.contains(fio::OpenFlags::RIGHT_READABLE) {
394                    rights |= fio::Operations::READ_BYTES;
395                }
396                if self.contains(fio::OpenFlags::RIGHT_WRITABLE) {
397                    rights |= fio::Operations::WRITE_BYTES | fio::Operations::UPDATE_ATTRIBUTES;
398                }
399                if self.contains(fio::OpenFlags::RIGHT_EXECUTABLE) {
400                    rights |= fio::Operations::EXECUTE;
401                }
402                rights
403            },
404            is_append: self.contains(fio::OpenFlags::APPEND),
405            #[cfg(fuchsia_api_level_at_least = "HEAD")]
406            is_linkable: true,
407        })
408    }
409}
410
411impl ToFileOptions for fio::Flags {
412    fn to_file_options(&self) -> Result<FileOptions, Status> {
413        // Verify protocols.
414        if !self.is_file_allowed() {
415            if self.is_dir_allowed() && !self.is_symlink_allowed() {
416                return Err(Status::NOT_DIR);
417            } else {
418                return Err(Status::WRONG_TYPE);
419            }
420        }
421
422        // Verify prohibited flags and disallow invalid flag combinations.
423        if self.contains(fio::Flags::FILE_TRUNCATE) && !self.contains(fio::Flags::PERM_WRITE_BYTES)
424        {
425            return Err(Status::INVALID_ARGS);
426        }
427
428        // Used to remove any non-file flags.
429        const ALLOWED_RIGHTS: fio::Operations = fio::Operations::empty()
430            .union(fio::Operations::GET_ATTRIBUTES)
431            .union(fio::Operations::READ_BYTES)
432            .union(fio::Operations::WRITE_BYTES)
433            .union(fio::Operations::UPDATE_ATTRIBUTES)
434            .union(fio::Operations::EXECUTE);
435
436        Ok(FileOptions {
437            rights: flags_to_rights(self).intersection(ALLOWED_RIGHTS),
438            is_append: self.contains(fio::Flags::FILE_APPEND),
439            #[cfg(fuchsia_api_level_at_least = "HEAD")]
440            is_linkable: !self.contains(
441                fio::Flags::FLAG_CREATE_AS_UNNAMED_TEMPORARY | fio::FLAG_TEMPORARY_AS_NOT_LINKABLE,
442            ),
443        })
444    }
445}
446
447impl ToFileOptions for FileOptions {
448    fn to_file_options(&self) -> Result<FileOptions, Status> {
449        Ok(*self)
450    }
451}
452
453pub trait ToNodeOptions: Send + 'static {
454    fn to_node_options(&self, dirent_type: fio::DirentType) -> Result<NodeOptions, Status>;
455}
456
457impl ToNodeOptions for fio::OpenFlags {
458    fn to_node_options(&self, dirent_type: fio::DirentType) -> Result<NodeOptions, Status> {
459        // Strictly, we shouldn't allow rights to be specified with NODE_REFERENCE, but there's a
460        // CTS pkgdir test that asserts these flags work and fixing that is painful so we preserve
461        // old behaviour (which permitted these flags).
462        let allowed_rights =
463            fio::OPEN_RIGHTS | fio::OpenFlags::POSIX_WRITABLE | fio::OpenFlags::POSIX_EXECUTABLE;
464        if self.intersects(!(fio::OPEN_FLAGS_ALLOWED_WITH_NODE_REFERENCE | allowed_rights)) {
465            Err(Status::INVALID_ARGS)
466        } else if self.contains(fio::OpenFlags::DIRECTORY)
467            && dirent_type != fio::DirentType::Directory
468        {
469            Err(Status::NOT_DIR)
470        } else {
471            Ok(NodeOptions { rights: fio::Operations::GET_ATTRIBUTES })
472        }
473    }
474}
475
476impl ToNodeOptions for fio::Flags {
477    fn to_node_options(&self, dirent_type: fio::DirentType) -> Result<NodeOptions, Status> {
478        // Strictly, we shouldn't allow rights to be specified with PROTOCOL_NODE, but there's a
479        // CTS pkgdir test that asserts these flags work and fixing that is painful so we preserve
480        // old behaviour (which permitted these flags).
481        const ALLOWED_FLAGS: fio::Flags = fio::Flags::FLAG_SEND_REPRESENTATION
482            .union(fio::MASK_KNOWN_PERMISSIONS)
483            .union(fio::MASK_KNOWN_PROTOCOLS);
484
485        if self.intersects(!ALLOWED_FLAGS) {
486            return Err(Status::INVALID_ARGS);
487        }
488
489        // If other `PROTOCOL_*` were were specified along with `PROTOCOL_NODE`, verify that the
490        // target node supports it.
491        if self.intersects(fio::MASK_KNOWN_PROTOCOLS.difference(fio::Flags::PROTOCOL_NODE)) {
492            if dirent_type == fio::DirentType::Directory {
493                if !self.intersects(fio::Flags::PROTOCOL_DIRECTORY) {
494                    if self.intersects(fio::Flags::PROTOCOL_FILE) {
495                        return Err(Status::NOT_FILE);
496                    } else {
497                        return Err(Status::WRONG_TYPE);
498                    }
499                }
500            } else if dirent_type == fio::DirentType::File {
501                if !self.intersects(fio::Flags::PROTOCOL_FILE) {
502                    if self.intersects(fio::Flags::PROTOCOL_DIRECTORY) {
503                        return Err(Status::NOT_DIR);
504                    } else {
505                        return Err(Status::WRONG_TYPE);
506                    }
507                }
508            } else if dirent_type == fio::DirentType::Symlink {
509                if !self.intersects(fio::Flags::PROTOCOL_SYMLINK) {
510                    return Err(Status::WRONG_TYPE);
511                }
512            }
513        }
514
515        Ok(NodeOptions {
516            rights: flags_to_rights(self).intersection(fio::Operations::GET_ATTRIBUTES),
517        })
518    }
519}
520
521impl ToNodeOptions for NodeOptions {
522    fn to_node_options(&self, _dirent_type: fio::DirentType) -> Result<NodeOptions, Status> {
523        Ok(*self)
524    }
525}
526
527fn flags_to_rights(flags: &fio::Flags) -> fio::Rights {
528    fio::Rights::from_bits_truncate(flags.bits())
529}