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