serve_processargs/
namespace.rs

1// Use of this source code is governed by a BSD-style license that can be
2// found in the LICENSE file.
3
4use cm_types::{NamespacePath, Path, RelativePath};
5use fidl::endpoints::ClientEnd;
6use futures::channel::mpsc::{UnboundedSender, unbounded};
7use namespace::{Entry as NamespaceEntry, EntryError, Namespace, NamespaceError, Tree};
8use router_error::Explain;
9use sandbox::{Capability, Dict, RemotableCapability, RouterResponse};
10use thiserror::Error;
11use vfs::directory::entry::serve_directory;
12use vfs::execution_scope::ExecutionScope;
13use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
14
15/// A builder object for assembling a program's incoming namespace.
16pub struct NamespaceBuilder {
17    /// Mapping from namespace path to capabilities that can be turned into `Directory`.
18    entries: Tree<Capability>,
19
20    /// Path-not-found errors are sent here.
21    not_found: UnboundedSender<String>,
22
23    /// Scope in which the namespace vfs executes.
24    ///
25    /// This can be used to terminate the vfs.
26    namespace_scope: ExecutionScope,
27}
28
29#[derive(Error, Debug, Clone)]
30pub enum BuildNamespaceError {
31    #[error(transparent)]
32    NamespaceError(#[from] NamespaceError),
33
34    #[error(
35        "while installing capabilities within the namespace entry `{path}`, \
36        failed to convert the namespace entry to Directory: {err}"
37    )]
38    Conversion {
39        path: NamespacePath,
40        #[source]
41        err: sandbox::ConversionError,
42    },
43
44    #[error("unable to serve `{path}` after converting to directory: {err}")]
45    Serve {
46        path: NamespacePath,
47        #[source]
48        err: fidl::Status,
49    },
50}
51
52impl NamespaceBuilder {
53    pub fn new(namespace_scope: ExecutionScope, not_found: UnboundedSender<String>) -> Self {
54        return NamespaceBuilder { entries: Default::default(), not_found, namespace_scope };
55    }
56
57    /// Add a capability `cap` at `path`. As a result, the framework will create a
58    /// namespace entry at the parent directory of `path`.
59    pub fn add_object(
60        self: &mut Self,
61        cap: Capability,
62        path: &Path,
63    ) -> Result<(), BuildNamespaceError> {
64        let dirname = path.parent();
65
66        // Get the entry, or if it doesn't exist, make an empty dictionary.
67        let any = match self.entries.get_mut(&dirname) {
68            Some(dir) => dir,
69            None => {
70                let dict = self.make_dict_with_not_found_logging(dirname.to_string());
71                self.entries.add(&dirname, Capability::Dictionary(dict))?
72            }
73        };
74
75        // Cast the namespace entry as a Dict. This may fail if the user added a duplicate
76        // namespace entry that is not a Dict (see `add_entry`).
77        let dict = match any {
78            Capability::Dictionary(d) => d,
79            _ => Err(NamespaceError::Duplicate(path.clone().into()))?,
80        };
81
82        // Insert the capability into the Dict.
83        dict.insert(path.basename().into(), cap)
84            .map_err(|_| NamespaceError::Duplicate(path.clone().into()).into())
85    }
86
87    /// Add a capability `cap` at `path`. As a result, the framework will create a
88    /// namespace entry at `path` directly. The capability will be exercised when the user
89    /// opens the `path`.
90    pub fn add_entry(
91        self: &mut Self,
92        cap: Capability,
93        path: &NamespacePath,
94    ) -> Result<(), BuildNamespaceError> {
95        match &cap {
96            Capability::Directory(_)
97            | Capability::Dictionary(_)
98            | Capability::DirEntry(_)
99            | Capability::DirConnector(_)
100            | Capability::DirConnectorRouter(_) => {}
101            _ => return Err(NamespaceError::EntryError(EntryError::UnsupportedType).into()),
102        }
103        self.entries.add(path, cap)?;
104        Ok(())
105    }
106
107    pub fn serve(self: Self) -> Result<Namespace, BuildNamespaceError> {
108        let mut entries = vec![];
109        for (path, cap) in self.entries.flatten() {
110            let client_end: ClientEnd<fio::DirectoryMarker> = match cap {
111                Capability::Directory(d) => d.into(),
112                Capability::DirConnector(c) => {
113                    let (client, server) =
114                        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
115                    // We don't wait to sit around for the results of this task, so we drop the
116                    // fuchsia_async::JoinHandle which causes the task to be detached.
117                    let _ = self.namespace_scope.spawn(async move {
118                        let res =
119                            fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
120                        if res.is_err() {
121                            return;
122                        }
123                        // We set the rights to `None` because no rights are given to us by the
124                        // client for this operation (opening a directory in their namespace). The
125                        // `DirConnector` should apply the "default" rights for this connection, as
126                        // determined by capability routing.
127                        let _ = c.send(server, RelativePath::dot(), None);
128                    });
129                    client
130                }
131                Capability::DirConnectorRouter(c) => {
132                    let (client, server) =
133                        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
134                    // We don't wait to sit around for the results of this task, so we drop the
135                    // fuchsia_async::JoinHandle which causes the task to be detached.
136                    let _ = self.namespace_scope.spawn(async move {
137                        let res =
138                            fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
139                        if res.is_err() {
140                            return;
141                        }
142                        match c.route(None, false).await {
143                            Ok(RouterResponse::Capability(dir_connector)) => {
144                                // See the comment in the `DirConnector` branch for why rights are
145                                // `None`.
146                                let _ = dir_connector.send(server, RelativePath::dot(), None);
147                            }
148                            Ok(RouterResponse::Unavailable) => {
149                                let _ = server.close_with_epitaph(fidl::Status::NOT_FOUND);
150                            }
151                            Ok(RouterResponse::Debug(_)) => {
152                                panic!("debug response wasn't requested");
153                            }
154                            Err(e) => {
155                                // Error logging will be performed by the ErrorReporter router set
156                                // up by sandbox construction, so we don't need to log about
157                                // routing errors here.
158                                let _ = server.close_with_epitaph(e.as_zx_status());
159                            }
160                        }
161                    });
162                    client
163                }
164                Capability::Dictionary(dict) => {
165                    let entry =
166                        dict.try_into_directory_entry(self.namespace_scope.clone()).map_err(
167                            |err| BuildNamespaceError::Conversion { path: path.clone(), err },
168                        )?;
169                    if entry.entry_info().type_() != fio::DirentType::Directory {
170                        return Err(BuildNamespaceError::Conversion {
171                            path: path.clone(),
172                            err: sandbox::ConversionError::NotSupported,
173                        });
174                    }
175                    serve_directory(
176                        entry,
177                        &self.namespace_scope,
178                        fio::Flags::PROTOCOL_DIRECTORY
179                            | fio::PERM_READABLE
180                            | fio::Flags::PERM_INHERIT_WRITE
181                            | fio::Flags::PERM_INHERIT_EXECUTE,
182                    )
183                    .map_err(|err| BuildNamespaceError::Serve { path: path.clone(), err })?
184                }
185                _ => return Err(NamespaceError::EntryError(EntryError::UnsupportedType).into()),
186            };
187            entries.push(NamespaceEntry { path, directory: client_end.into() })
188        }
189        let ns = entries.try_into()?;
190        Ok(ns)
191    }
192
193    fn make_dict_with_not_found_logging(&self, root_path: String) -> Dict {
194        let not_found = self.not_found.clone();
195        let new_dict = Dict::new_with_not_found(move |key| {
196            let requested_path = format!("{}/{}", root_path, key);
197            // Ignore the result of sending. The receiver is free to break away to ignore all the
198            // not-found errors.
199            let _ = not_found.unbounded_send(requested_path);
200        });
201        new_dict
202    }
203}
204
205/// Returns a disconnected sender which should ignore all the path-not-found errors.
206pub fn ignore_not_found() -> UnboundedSender<String> {
207    let (sender, _receiver) = unbounded();
208    sender
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use anyhow::Result;
215    use assert_matches::assert_matches;
216    use fidl::Peered;
217    use fidl::endpoints::{self, Proxy};
218    use fuchsia_fs::directory::DirEntry;
219    use futures::channel::mpsc;
220    use futures::{StreamExt, TryStreamExt};
221    use sandbox::{Connector, Directory, Receiver};
222    use std::sync::Arc;
223    use test_case::test_case;
224    use vfs::directory::entry::{DirectoryEntry, EntryInfo, GetEntryInfo, OpenRequest};
225    use vfs::remote::RemoteLike;
226    use vfs::{ObjectRequestRef, path, pseudo_directory};
227    use zx::AsHandleRef;
228    use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
229
230    fn multishot() -> (Connector, Receiver) {
231        let (receiver, sender) = Connector::new();
232        (sender, receiver)
233    }
234
235    fn connector_cap() -> Capability {
236        let (sender, _receiver) = multishot();
237        Capability::Connector(sender)
238    }
239
240    fn directory_cap() -> Capability {
241        let (client, _server) = endpoints::create_endpoints();
242        Capability::Directory(Directory::new(client))
243    }
244
245    fn ns_path(str: &str) -> NamespacePath {
246        str.parse().unwrap()
247    }
248
249    fn path(str: &str) -> Path {
250        str.parse().unwrap()
251    }
252
253    fn parents_valid(paths: Vec<&str>) -> Result<(), BuildNamespaceError> {
254        let scope = ExecutionScope::new();
255        let mut shadow = NamespaceBuilder::new(scope, ignore_not_found());
256        for p in paths {
257            shadow.add_object(connector_cap(), &path(p))?;
258        }
259        Ok(())
260    }
261
262    #[fuchsia::test]
263    async fn test_shadow() {
264        assert_matches!(parents_valid(vec!["/svc/foo/bar/Something", "/svc/Something"]), Err(_));
265        assert_matches!(parents_valid(vec!["/svc/Something", "/svc/foo/bar/Something"]), Err(_));
266        assert_matches!(parents_valid(vec!["/svc/Something", "/foo"]), Err(_));
267
268        assert_matches!(parents_valid(vec!["/foo/bar/a", "/foo/bar/b", "/foo/bar/c"]), Ok(()));
269        assert_matches!(parents_valid(vec!["/a", "/b", "/c"]), Ok(()));
270
271        let scope = ExecutionScope::new();
272        let mut shadow = NamespaceBuilder::new(scope, ignore_not_found());
273        shadow.add_object(connector_cap(), &path("/svc/foo")).unwrap();
274        assert_matches!(shadow.add_object(connector_cap(), &path("/svc/foo/bar")), Err(_));
275
276        let scope = ExecutionScope::new();
277        let mut not_shadow = NamespaceBuilder::new(scope, ignore_not_found());
278        not_shadow.add_object(connector_cap(), &path("/svc/foo")).unwrap();
279        assert_matches!(not_shadow.add_entry(directory_cap(), &ns_path("/svc2")), Ok(_));
280    }
281
282    #[fuchsia::test]
283    async fn test_duplicate_object() {
284        let scope = ExecutionScope::new();
285        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
286        namespace.add_object(connector_cap(), &path("/svc/a")).expect("");
287        // Adding again will fail.
288        assert_matches!(
289            namespace.add_object(connector_cap(), &path("/svc/a")),
290            Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
291            if path.to_string() == "/svc/a"
292        );
293    }
294
295    #[fuchsia::test]
296    async fn test_duplicate_entry() {
297        let scope = ExecutionScope::new();
298        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
299        namespace.add_entry(directory_cap(), &ns_path("/svc/a")).expect("");
300        // Adding again will fail.
301        assert_matches!(
302            namespace.add_entry(directory_cap(), &ns_path("/svc/a")),
303            Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
304            if path.to_string() == "/svc/a"
305        );
306    }
307
308    #[fuchsia::test]
309    async fn test_duplicate_object_and_entry() {
310        let scope = ExecutionScope::new();
311        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
312        namespace.add_object(connector_cap(), &path("/svc/a")).expect("");
313        assert_matches!(
314            namespace.add_entry(directory_cap(), &ns_path("/svc/a")),
315            Err(BuildNamespaceError::NamespaceError(NamespaceError::Shadow(path)))
316            if path.to_string() == "/svc/a"
317        );
318    }
319
320    /// If we added a namespaced object at "/foo/bar", thus creating a namespace entry at "/foo",
321    /// we cannot add another entry directly at "/foo" again.
322    #[fuchsia::test]
323    async fn test_duplicate_entry_at_object_parent() {
324        let scope = ExecutionScope::new();
325        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
326        namespace.add_object(connector_cap(), &path("/foo/bar")).expect("");
327        assert_matches!(
328            namespace.add_entry(directory_cap(), &ns_path("/foo")),
329            Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
330            if path.to_string() == "/foo"
331        );
332    }
333
334    /// If we directly added an entry at "/foo", it's not possible to add a namespaced object at
335    /// "/foo/bar", as that would've required overwriting "/foo" with a namespace entry served by
336    /// the framework.
337    #[fuchsia::test]
338    async fn test_duplicate_object_parent_at_entry() {
339        let scope = ExecutionScope::new();
340        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
341        namespace.add_entry(directory_cap(), &ns_path("/foo")).expect("");
342        assert_matches!(
343            namespace.add_object(connector_cap(), &path("/foo/bar")),
344            Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
345            if path.to_string() == "/foo/bar"
346        );
347    }
348
349    #[fuchsia::test]
350    async fn test_empty() {
351        let scope = ExecutionScope::new();
352        let namespace = NamespaceBuilder::new(scope, ignore_not_found());
353        let ns = namespace.serve().unwrap();
354        assert_eq!(ns.flatten().len(), 0);
355    }
356
357    #[fuchsia::test]
358    async fn test_one_connector_end_to_end() {
359        let (sender, receiver) = multishot();
360
361        let scope = ExecutionScope::new();
362        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
363        namespace.add_object(sender.into(), &path("/svc/a")).unwrap();
364        let ns = namespace.serve().unwrap();
365
366        let mut ns = ns.flatten();
367        assert_eq!(ns.len(), 1);
368        assert_eq!(ns[0].path.to_string(), "/svc");
369
370        // Check that there is exactly one protocol inside the svc directory.
371        let dir = ns.pop().unwrap().directory.into_proxy();
372        let entries = fuchsia_fs::directory::readdir(&dir).await.unwrap();
373        assert_eq!(
374            entries,
375            vec![DirEntry { name: "a".to_string(), kind: fio::DirentType::Service }]
376        );
377
378        // Connect to the protocol using namespace functionality.
379        let (client_end, server_end) = zx::Channel::create();
380        fdio::service_connect_at(&dir.into_channel().unwrap().into_zx_channel(), "a", server_end)
381            .unwrap();
382
383        // Make sure the server_end is received, and test connectivity.
384        let server_end: zx::Channel = receiver.receive().await.unwrap().channel.into();
385        client_end.signal_peer(zx::Signals::empty(), zx::Signals::USER_0).unwrap();
386        server_end.wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST).unwrap();
387    }
388
389    #[fuchsia::test]
390    async fn test_two_connectors_in_same_namespace_entry() {
391        let scope = ExecutionScope::new();
392        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
393        namespace.add_object(connector_cap(), &path("/svc/a")).unwrap();
394        namespace.add_object(connector_cap(), &path("/svc/b")).unwrap();
395        let ns = namespace.serve().unwrap();
396
397        let mut ns = ns.flatten();
398        assert_eq!(ns.len(), 1);
399        assert_eq!(ns[0].path.to_string(), "/svc");
400
401        // Check that there are exactly two protocols inside the svc directory.
402        let dir = ns.pop().unwrap().directory.into_proxy();
403        let mut entries = fuchsia_fs::directory::readdir(&dir).await.unwrap();
404        let mut expectation = vec![
405            DirEntry { name: "a".to_string(), kind: fio::DirentType::Service },
406            DirEntry { name: "b".to_string(), kind: fio::DirentType::Service },
407        ];
408        entries.sort();
409        expectation.sort();
410        assert_eq!(entries, expectation);
411
412        drop(dir);
413    }
414
415    #[fuchsia::test]
416    async fn test_two_connectors_in_different_namespace_entries() {
417        let scope = ExecutionScope::new();
418        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
419        namespace.add_object(connector_cap(), &path("/svc1/a")).unwrap();
420        namespace.add_object(connector_cap(), &path("/svc2/b")).unwrap();
421        let ns = namespace.serve().unwrap();
422
423        let ns = ns.flatten();
424        assert_eq!(ns.len(), 2);
425        let (mut svc1, ns): (Vec<_>, Vec<_>) =
426            ns.into_iter().partition(|e| e.path.to_string() == "/svc1");
427        let (mut svc2, _ns): (Vec<_>, Vec<_>) =
428            ns.into_iter().partition(|e| e.path.to_string() == "/svc2");
429
430        // Check that there are one protocol inside each directory.
431        {
432            let dir = svc1.pop().unwrap().directory.into_proxy();
433            assert_eq!(
434                fuchsia_fs::directory::readdir(&dir).await.unwrap(),
435                vec![DirEntry { name: "a".to_string(), kind: fio::DirentType::Service },]
436            );
437        }
438        {
439            let dir = svc2.pop().unwrap().directory.into_proxy();
440            assert_eq!(
441                fuchsia_fs::directory::readdir(&dir).await.unwrap(),
442                vec![DirEntry { name: "b".to_string(), kind: fio::DirentType::Service },]
443            );
444        }
445
446        drop(svc1);
447        drop(svc2);
448    }
449
450    #[fuchsia::test]
451    async fn test_not_found() {
452        let (not_found_sender, mut not_found_receiver) = unbounded();
453        let scope = ExecutionScope::new();
454        let mut namespace = NamespaceBuilder::new(scope, not_found_sender);
455        namespace.add_object(connector_cap(), &path("/svc/a")).unwrap();
456        let ns = namespace.serve().unwrap();
457
458        let mut ns = ns.flatten();
459        assert_eq!(ns.len(), 1);
460        assert_eq!(ns[0].path.to_string(), "/svc");
461
462        let dir = ns.pop().unwrap().directory.into_proxy();
463        let (client_end, server_end) = zx::Channel::create();
464        let _ = fdio::service_connect_at(
465            &dir.into_channel().unwrap().into_zx_channel(),
466            "non_existent",
467            server_end,
468        );
469
470        // Server endpoint is closed because the path does not exist.
471        fasync::Channel::from_channel(client_end).on_closed().await.unwrap();
472
473        // We should get a notification about this path.
474        assert_eq!(not_found_receiver.next().await, Some("/svc/non_existent".to_string()));
475
476        drop(ns);
477    }
478
479    #[fuchsia::test]
480    async fn test_not_directory() {
481        let (not_found_sender, _) = unbounded();
482        let scope = ExecutionScope::new();
483        let mut namespace = NamespaceBuilder::new(scope, not_found_sender);
484        let (_, sender) = sandbox::Connector::new();
485        assert_matches!(
486            namespace.add_entry(sender.into(), &ns_path("/a")),
487            Err(BuildNamespaceError::NamespaceError(NamespaceError::EntryError(
488                EntryError::UnsupportedType
489            )))
490        );
491    }
492
493    #[test_case(fio::PERM_READABLE)]
494    #[test_case(fio::PERM_READABLE | fio::PERM_EXECUTABLE)]
495    #[test_case(fio::PERM_READABLE | fio::PERM_WRITABLE)]
496    #[test_case(fio::PERM_READABLE | fio::Flags::PERM_INHERIT_WRITE | fio::Flags::PERM_INHERIT_EXECUTE)]
497    #[fuchsia::test]
498    async fn test_directory_rights(rights: fio::Flags) {
499        let (open_tx, mut open_rx) = mpsc::channel::<()>(1);
500
501        struct MockDir {
502            tx: mpsc::Sender<()>,
503            rights: fio::Flags,
504        }
505        impl DirectoryEntry for MockDir {
506            fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
507                request.open_remote(self)
508            }
509        }
510        impl GetEntryInfo for MockDir {
511            fn entry_info(&self) -> EntryInfo {
512                EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
513            }
514        }
515        impl RemoteLike for MockDir {
516            fn open(
517                self: Arc<Self>,
518                _scope: ExecutionScope,
519                relative_path: path::Path,
520                flags: fio::Flags,
521                _object_request: ObjectRequestRef<'_>,
522            ) -> Result<(), zx::Status> {
523                assert_eq!(relative_path.into_string(), "");
524                assert_eq!(flags, fio::Flags::PROTOCOL_DIRECTORY | self.rights);
525                self.tx.clone().try_send(()).unwrap();
526                Ok(())
527            }
528        }
529
530        let mock = Arc::new(MockDir { tx: open_tx, rights });
531
532        let fs = pseudo_directory! {
533            "foo" => mock,
534        };
535        let dir = Directory::from(vfs::directory::serve(fs, rights).into_client_end().unwrap());
536
537        let scope = ExecutionScope::new();
538        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
539        namespace.add_entry(dir.into(), &ns_path("/dir")).unwrap();
540        let mut ns = namespace.serve().unwrap();
541        let dir_proxy = ns.remove(&"/dir".parse().unwrap()).unwrap();
542        let dir_proxy = dir_proxy.into_proxy();
543        let (_, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
544        dir_proxy
545            .open(
546                "foo",
547                fio::Flags::PROTOCOL_DIRECTORY | rights,
548                &fio::Options::default(),
549                server_end.into_channel(),
550            )
551            .unwrap();
552
553        // The MockDir should receive the Open request.
554        open_rx.next().await.unwrap();
555    }
556
557    #[fuchsia::test]
558    async fn test_directory_non_executable() {
559        let (open_tx, mut open_rx) = mpsc::channel::<()>(1);
560
561        struct MockDir(mpsc::Sender<()>);
562        impl DirectoryEntry for MockDir {
563            fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
564                request.open_remote(self)
565            }
566        }
567        impl GetEntryInfo for MockDir {
568            fn entry_info(&self) -> EntryInfo {
569                EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
570            }
571        }
572        impl RemoteLike for MockDir {
573            fn open(
574                self: Arc<Self>,
575                _scope: ExecutionScope,
576                relative_path: path::Path,
577                flags: fio::Flags,
578                _object_request: ObjectRequestRef<'_>,
579            ) -> Result<(), zx::Status> {
580                assert_eq!(relative_path.into_string(), "");
581                assert_eq!(flags, fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE);
582                self.0.clone().try_send(()).unwrap();
583                Ok(())
584            }
585        }
586
587        let mock = Arc::new(MockDir(open_tx));
588
589        let fs = pseudo_directory! {
590            "foo" => mock,
591        };
592        let dir = Directory::from(
593            vfs::directory::serve(fs, fio::PERM_READABLE).into_client_end().unwrap(),
594        );
595
596        let scope = ExecutionScope::new();
597        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
598        namespace.add_entry(dir.into(), &ns_path("/dir")).unwrap();
599        let mut ns = namespace.serve().unwrap();
600        let dir_proxy = ns.remove(&"/dir".parse().unwrap()).unwrap();
601        let dir_proxy = dir_proxy.into_proxy();
602
603        // Try to open as executable. Should fail (ACCESS_DENIED)
604        let (node, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
605        dir_proxy
606            .open(
607                "foo",
608                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE | fio::PERM_EXECUTABLE,
609                &fio::Options::default(),
610                server_end.into_channel(),
611            )
612            .unwrap();
613        let node = node.into_proxy();
614        let mut node = node.take_event_stream();
615        assert_matches!(
616            node.try_next().await,
617            Err(fidl::Error::ClientChannelClosed { status: zx::Status::ACCESS_DENIED, .. })
618        );
619
620        // Try to open as read-only. Should succeed.
621        let (_, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
622        dir_proxy
623            .open(
624                "foo",
625                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
626                &fio::Options::default(),
627                server_end.into_channel(),
628            )
629            .unwrap();
630
631        // The MockDir should receive the Open request.
632        open_rx.next().await.unwrap();
633    }
634}