vfs/directory/
connection.rs

1// Copyright 2019 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::send_on_open_with_error;
6use crate::directory::common::check_child_connection_flags;
7use crate::directory::entry_container::{Directory, DirectoryWatcher};
8use crate::directory::traversal_position::TraversalPosition;
9use crate::directory::{DirectoryOptions, read_dirents};
10use crate::execution_scope::{ExecutionScope, yield_to_executor};
11use crate::node::OpenNode;
12use crate::object_request::Representation;
13use crate::path::Path;
14use fidl::endpoints::DiscoverableProtocolMarker as _;
15
16use anyhow::Error;
17use fidl::endpoints::ServerEnd;
18use fidl_fuchsia_io as fio;
19use storage_trace::{self as trace, TraceFutureExt};
20use zx_status::Status;
21
22use crate::common::CreationMode;
23use crate::{ObjectRequest, ObjectRequestRef, ProtocolsExt};
24
25/// Return type for `BaseConnection::handle_request`.
26pub enum ConnectionState {
27    /// Connection is still alive.
28    Alive,
29    /// Connection have received Node::Close message and should be closed.
30    Closed,
31}
32
33/// Handles functionality shared between mutable and immutable FIDL connections to a directory.  A
34/// single directory may contain multiple connections.  Instances of the `BaseConnection`
35/// will also hold any state that is "per-connection".  Currently that would be the access flags
36/// and the seek position.
37pub(in crate::directory) struct BaseConnection<DirectoryType: Directory> {
38    /// Execution scope this connection and any async operations and connections it creates will
39    /// use.
40    pub(in crate::directory) scope: ExecutionScope,
41
42    pub(in crate::directory) directory: OpenNode<DirectoryType>,
43
44    /// Flags set on this connection when it was opened or cloned.
45    pub(in crate::directory) options: DirectoryOptions,
46
47    /// Seek position for this connection to the directory.  We just store the element that was
48    /// returned last by ReadDirents for this connection.  Next call will look for the next element
49    /// in alphabetical order and resume from there.
50    ///
51    /// An alternative is to use an intrusive tree to have a dual index in both names and IDs that
52    /// are assigned to the entries in insertion order.  Then we can store an ID instead of the
53    /// full entry name.  This is what the C++ version is doing currently.
54    ///
55    /// It should be possible to do the same intrusive dual-indexing using, for example,
56    ///
57    ///     https://docs.rs/intrusive-collections/0.7.6/intrusive_collections/
58    ///
59    /// but, as, I think, at least for the pseudo directories, this approach is fine, and it simple
60    /// enough.
61    seek: TraversalPosition,
62}
63
64impl<DirectoryType: Directory> BaseConnection<DirectoryType> {
65    /// Constructs an instance of `BaseConnection` - to be used by derived connections, when they
66    /// need to create a nested `BaseConnection` "sub-object".  But when implementing
67    /// `create_connection`, derived connections should use the [`create_connection`] call.
68    pub(in crate::directory) fn new(
69        scope: ExecutionScope,
70        directory: OpenNode<DirectoryType>,
71        options: DirectoryOptions,
72    ) -> Self {
73        BaseConnection { scope, directory, options, seek: Default::default() }
74    }
75
76    /// Handle a [`DirectoryRequest`].  This function is responsible for handing all the basic
77    /// directory operations.
78    pub(in crate::directory) async fn handle_request(
79        &mut self,
80        request: fio::DirectoryRequest,
81    ) -> Result<ConnectionState, Error> {
82        match request {
83            #[cfg(any(
84                fuchsia_api_level_at_least = "PLATFORM",
85                not(fuchsia_api_level_at_least = "NEXT")
86            ))]
87            fio::DirectoryRequest::DeprecatedClone { flags, object, control_handle: _ } => {
88                trace::duration!(c"storage", c"Directory::DeprecatedClone");
89                crate::common::send_on_open_with_error(
90                    flags.contains(fio::OpenFlags::DESCRIBE),
91                    object,
92                    Status::NOT_SUPPORTED,
93                );
94            }
95            fio::DirectoryRequest::Clone { request, control_handle: _ } => {
96                trace::duration!(c"storage", c"Directory::Clone");
97                self.handle_clone(request.into_channel());
98            }
99            fio::DirectoryRequest::Close { responder } => {
100                trace::duration!(c"storage", c"Directory::Close");
101                responder.send(Ok(()))?;
102                return Ok(ConnectionState::Closed);
103            }
104            #[cfg(fuchsia_api_level_at_least = "28")]
105            fio::DirectoryRequest::DeprecatedGetAttr { responder } => {
106                async move {
107                    let (status, attrs) = crate::common::io2_to_io1_attrs(
108                        self.directory.as_ref(),
109                        self.options.rights,
110                    )
111                    .await;
112                    responder.send(status.into_raw(), &attrs)
113                }
114                .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttr"))
115                .await?;
116            }
117            #[cfg(not(fuchsia_api_level_at_least = "28"))]
118            fio::DirectoryRequest::GetAttr { responder } => {
119                async move {
120                    let (status, attrs) = crate::common::io2_to_io1_attrs(
121                        self.directory.as_ref(),
122                        self.options.rights,
123                    )
124                    .await;
125                    responder.send(status.into_raw(), &attrs)
126                }
127                .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttr"))
128                .await?;
129            }
130            fio::DirectoryRequest::GetAttributes { query, responder } => {
131                async move {
132                    // TODO(https://fxbug.dev/346585458): Restrict or remove GET_ATTRIBUTES.
133                    let attrs = self.directory.get_attributes(query).await;
134                    responder.send(
135                        attrs
136                            .as_ref()
137                            .map(|attrs| (&attrs.mutable_attributes, &attrs.immutable_attributes))
138                            .map_err(|status| status.into_raw()),
139                    )
140                }
141                .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttributes"))
142                .await?;
143            }
144            fio::DirectoryRequest::UpdateAttributes { payload: _, responder } => {
145                trace::duration!(c"storage", c"Directory::UpdateAttributes");
146                // TODO(https://fxbug.dev/324112547): Handle unimplemented io2 method.
147                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
148            }
149            fio::DirectoryRequest::ListExtendedAttributes { iterator, .. } => {
150                trace::duration!(c"storage", c"Directory::ListExtendedAttributes");
151                iterator.close_with_epitaph(Status::NOT_SUPPORTED)?;
152            }
153            fio::DirectoryRequest::GetExtendedAttribute { responder, .. } => {
154                trace::duration!(c"storage", c"Directory::GetExtendedAttribute");
155                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
156            }
157            fio::DirectoryRequest::SetExtendedAttribute { responder, .. } => {
158                trace::duration!(c"storage", c"Directory::SetExtendedAttribute");
159                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
160            }
161            fio::DirectoryRequest::RemoveExtendedAttribute { responder, .. } => {
162                trace::duration!(c"storage", c"Directory::RemoveExtendedAttribute");
163                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
164            }
165            #[cfg(fuchsia_api_level_at_least = "27")]
166            fio::DirectoryRequest::GetFlags { responder } => {
167                trace::duration!(c"storage", c"Directory::GetFlags");
168                responder.send(Ok(fio::Flags::from(&self.options)))?;
169            }
170            #[cfg(fuchsia_api_level_at_least = "27")]
171            fio::DirectoryRequest::SetFlags { flags: _, responder } => {
172                trace::duration!(c"storage", c"Directory::SetFlags");
173                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
174            }
175            #[cfg(fuchsia_api_level_at_least = "27")]
176            fio::DirectoryRequest::DeprecatedGetFlags { responder } => {
177                trace::duration!(c"storage", c"Directory::DeprecatedGetFlags");
178                responder.send(Status::OK.into_raw(), self.options.to_io1())?;
179            }
180            #[cfg(fuchsia_api_level_at_least = "27")]
181            fio::DirectoryRequest::DeprecatedSetFlags { flags: _, responder } => {
182                trace::duration!(c"storage", c"Directory::DeprecatedSetFlags");
183                responder.send(Status::NOT_SUPPORTED.into_raw())?;
184            }
185            #[cfg(not(fuchsia_api_level_at_least = "27"))]
186            fio::DirectoryRequest::GetFlags { responder } => {
187                trace::duration!(c"storage", c"Directory::GetFlags");
188                responder.send(Status::OK.into_raw(), self.options.to_io1())?;
189            }
190            #[cfg(not(fuchsia_api_level_at_least = "27"))]
191            fio::DirectoryRequest::SetFlags { flags: _, responder } => {
192                trace::duration!(c"storage", c"Directory::SetFlags");
193                responder.send(Status::NOT_SUPPORTED.into_raw())?;
194            }
195            #[cfg(fuchsia_api_level_at_least = "27")]
196            fio::DirectoryRequest::DeprecatedOpen {
197                flags,
198                mode: _,
199                path,
200                object,
201                control_handle: _,
202            } => {
203                {
204                    trace::duration!(c"storage", c"Directory::Open");
205                    self.handle_deprecated_open(flags, path, object);
206                }
207                // Since open typically spawns a task, yield to the executor now to give that task a
208                // chance to run before we try and process the next request for this directory.
209                yield_to_executor().await;
210            }
211            #[cfg(not(fuchsia_api_level_at_least = "27"))]
212            fio::DirectoryRequest::Open { flags, mode: _, path, object, control_handle: _ } => {
213                {
214                    trace::duration!(c"storage", c"Directory::Open");
215                    self.handle_deprecated_open(flags, path, object);
216                }
217                // Since open typically spawns a task, yield to the executor now to give that task a
218                // chance to run before we try and process the next request for this directory.
219                yield_to_executor().await;
220            }
221            fio::DirectoryRequest::AdvisoryLock { request: _, responder } => {
222                trace::duration!(c"storage", c"Directory::AdvisoryLock");
223                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
224            }
225            fio::DirectoryRequest::ReadDirents { max_bytes, responder } => {
226                async move {
227                    let (status, entries) = self.handle_read_dirents(max_bytes).await;
228                    responder.send(status.into_raw(), entries.as_slice())
229                }
230                .trace(trace::trace_future_args!(c"storage", c"Directory::ReadDirents"))
231                .await?;
232            }
233            fio::DirectoryRequest::Rewind { responder } => {
234                trace::duration!(c"storage", c"Directory::Rewind");
235                self.seek = Default::default();
236                responder.send(Status::OK.into_raw())?;
237            }
238            fio::DirectoryRequest::Link { src, dst_parent_token, dst, responder } => {
239                async move {
240                    let status: Status = self.handle_link(&src, dst_parent_token, dst).await.into();
241                    responder.send(status.into_raw())
242                }
243                .trace(trace::trace_future_args!(c"storage", c"Directory::Link"))
244                .await?;
245            }
246            fio::DirectoryRequest::Watch { mask, options, watcher, responder } => {
247                trace::duration!(c"storage", c"Directory::Watch");
248                let status = if options != 0 {
249                    Status::INVALID_ARGS
250                } else {
251                    self.handle_watch(mask, watcher.into()).into()
252                };
253                responder.send(status.into_raw())?;
254            }
255            fio::DirectoryRequest::Query { responder } => {
256                let () = responder.send(fio::DirectoryMarker::PROTOCOL_NAME.as_bytes())?;
257            }
258            fio::DirectoryRequest::QueryFilesystem { responder } => {
259                trace::duration!(c"storage", c"Directory::QueryFilesystem");
260                match self.directory.query_filesystem() {
261                    Err(status) => responder.send(status.into_raw(), None)?,
262                    Ok(info) => responder.send(0, Some(&info))?,
263                }
264            }
265            fio::DirectoryRequest::Unlink { name: _, options: _, responder } => {
266                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
267            }
268            fio::DirectoryRequest::GetToken { responder } => {
269                responder.send(Status::NOT_SUPPORTED.into_raw(), None)?;
270            }
271            fio::DirectoryRequest::Rename { src: _, dst_parent_token: _, dst: _, responder } => {
272                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
273            }
274            #[cfg(fuchsia_api_level_at_least = "28")]
275            fio::DirectoryRequest::DeprecatedSetAttr { flags: _, attributes: _, responder } => {
276                responder.send(Status::NOT_SUPPORTED.into_raw())?;
277            }
278            #[cfg(not(fuchsia_api_level_at_least = "28"))]
279            fio::DirectoryRequest::SetAttr { flags: _, attributes: _, responder } => {
280                responder.send(Status::NOT_SUPPORTED.into_raw())?;
281            }
282            fio::DirectoryRequest::Sync { responder } => {
283                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
284            }
285            fio::DirectoryRequest::CreateSymlink { responder, .. } => {
286                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
287            }
288            #[cfg(fuchsia_api_level_at_least = "27")]
289            fio::DirectoryRequest::Open { path, mut flags, options, object, control_handle: _ } => {
290                {
291                    // Remove POSIX flags when the respective rights are not available.
292                    if !self.options.rights.contains(fio::INHERITED_WRITE_PERMISSIONS) {
293                        flags &= !fio::Flags::PERM_INHERIT_WRITE;
294                    }
295                    if !self.options.rights.contains(fio::Rights::EXECUTE) {
296                        flags &= !fio::Flags::PERM_INHERIT_EXECUTE;
297                    }
298
299                    ObjectRequest::new(flags, &options, object)
300                        .handle_async(async |req| self.handle_open(path, flags, req).await)
301                        .trace(trace::trace_future_args!(c"storage", c"Directory::Open3"))
302                        .await;
303                }
304                // Since open typically spawns a task, yield to the executor now to give that task a
305                // chance to run before we try and process the next request for this directory.
306                yield_to_executor().await;
307            }
308            #[cfg(not(fuchsia_api_level_at_least = "27"))]
309            fio::DirectoryRequest::Open3 {
310                path,
311                mut flags,
312                options,
313                object,
314                control_handle: _,
315            } => {
316                {
317                    // Remove POSIX flags when the respective rights are not available.
318                    if !self.options.rights.contains(fio::INHERITED_WRITE_PERMISSIONS) {
319                        flags &= !fio::Flags::PERM_INHERIT_WRITE;
320                    }
321                    if !self.options.rights.contains(fio::Rights::EXECUTE) {
322                        flags &= !fio::Flags::PERM_INHERIT_EXECUTE;
323                    }
324
325                    ObjectRequest::new(flags, &options, object)
326                        .handle_async(async |req| self.handle_open(path, flags, req).await)
327                        .trace(trace::trace_future_args!(c"storage", c"Directory::Open3"))
328                        .await;
329                }
330                // Since open typically spawns a task, yield to the executor now to give that task a
331                // chance to run before we try and process the next request for this directory.
332                yield_to_executor().await;
333            }
334            fio::DirectoryRequest::_UnknownMethod { .. } => (),
335        }
336        Ok(ConnectionState::Alive)
337    }
338
339    fn handle_clone(&mut self, object: fidl::Channel) {
340        let flags = fio::Flags::from(&self.options);
341        ObjectRequest::new(flags, &Default::default(), object)
342            .handle(|req| self.directory.clone().open(self.scope.clone(), Path::dot(), flags, req));
343    }
344
345    fn handle_deprecated_open(
346        &self,
347        mut flags: fio::OpenFlags,
348        path: String,
349        server_end: ServerEnd<fio::NodeMarker>,
350    ) {
351        let describe = flags.intersects(fio::OpenFlags::DESCRIBE);
352
353        let path = match Path::validate_and_split(path) {
354            Ok(path) => path,
355            Err(status) => {
356                send_on_open_with_error(describe, server_end, status);
357                return;
358            }
359        };
360
361        if path.is_dir() {
362            flags |= fio::OpenFlags::DIRECTORY;
363        }
364
365        let flags = match check_child_connection_flags(self.options.to_io1(), flags) {
366            Ok(updated) => updated,
367            Err(status) => {
368                send_on_open_with_error(describe, server_end, status);
369                return;
370            }
371        };
372        if path.is_dot() {
373            if flags.intersects(fio::OpenFlags::NOT_DIRECTORY) {
374                send_on_open_with_error(describe, server_end, Status::INVALID_ARGS);
375                return;
376            }
377            if flags.intersects(fio::OpenFlags::CREATE_IF_ABSENT) {
378                send_on_open_with_error(describe, server_end, Status::ALREADY_EXISTS);
379                return;
380            }
381        }
382
383        // It is up to the open method to handle OPEN_FLAG_DESCRIBE from this point on.
384        let directory = self.directory.clone();
385        directory.deprecated_open(self.scope.clone(), flags, path, server_end);
386    }
387
388    async fn handle_open(
389        &self,
390        path: String,
391        flags: fio::Flags,
392        object_request: ObjectRequestRef<'_>,
393    ) -> Result<(), Status> {
394        let path = Path::validate_and_split(path)?;
395
396        // Child connection must have stricter or same rights as the parent connection.
397        if let Some(rights) = flags.rights() {
398            if rights.intersects(!self.options.rights) {
399                return Err(Status::ACCESS_DENIED);
400            }
401        }
402
403        // If requesting attributes, check permission.
404        if !object_request.attributes().is_empty()
405            && !self.options.rights.contains(fio::Operations::GET_ATTRIBUTES)
406        {
407            return Err(Status::ACCESS_DENIED);
408        }
409
410        match flags.creation_mode() {
411            CreationMode::Never => {
412                if object_request.create_attributes().is_some() {
413                    return Err(Status::INVALID_ARGS);
414                }
415            }
416            CreationMode::UnnamedTemporary | CreationMode::UnlinkableUnnamedTemporary => {
417                // We only support creating unnamed temporary files.
418                if !flags.intersects(fio::Flags::PROTOCOL_FILE) {
419                    return Err(Status::NOT_SUPPORTED);
420                }
421                // The parent connection must be able to modify directories if creating an object.
422                if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
423                    return Err(Status::ACCESS_DENIED);
424                }
425                // The ability to create an unnamed temporary file is dependent on the filesystem.
426                // We won't know if the directory the path eventually leads to supports the creation
427                // of unnamed temporary files until we have fully traversed the path. The way that
428                // Rust VFS is set up is such that the filesystem is responsible for traversing the
429                // path, so it is the filesystem's responsibility to report if it does not support
430                // this feature.
431            }
432            CreationMode::AllowExisting | CreationMode::Always => {
433                // The parent connection must be able to modify directories if creating an object.
434                if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
435                    return Err(Status::ACCESS_DENIED);
436                }
437
438                let protocol_flags = flags & fio::MASK_KNOWN_PROTOCOLS;
439                // If creating an object, exactly one protocol must be specified (the flags must be
440                // a power of two and non-zero).
441                if protocol_flags.is_empty()
442                    || (protocol_flags.bits() & (protocol_flags.bits() - 1)) != 0
443                {
444                    return Err(Status::INVALID_ARGS);
445                }
446                // Only a directory or file object can be created.
447                if !protocol_flags
448                    .intersects(fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_FILE)
449                {
450                    return Err(Status::NOT_SUPPORTED);
451                }
452            }
453        }
454
455        if path.is_dot() && flags.creation_mode() == CreationMode::Always {
456            return Err(Status::ALREADY_EXISTS);
457        }
458
459        self.directory.clone().open_async(self.scope.clone(), path, flags, object_request).await
460    }
461
462    async fn handle_read_dirents(&mut self, max_bytes: u64) -> (Status, Vec<u8>) {
463        async {
464            let (new_pos, sealed) =
465                self.directory.read_dirents(&self.seek, read_dirents::Sink::new(max_bytes)).await?;
466            self.seek = new_pos;
467            let read_dirents::Done { buf, status } = *sealed
468                .open()
469                .downcast::<read_dirents::Done>()
470                .map_err(|_: Box<dyn std::any::Any>| {
471                    #[cfg(debug)]
472                    panic!(
473                        "`read_dirents()` returned a `dirents_sink::Sealed`
474                        instance that is not an instance of the \
475                        `read_dirents::Done`. This is a bug in the \
476                        `read_dirents()` implementation."
477                    );
478                    Status::NOT_SUPPORTED
479                })?;
480            Ok((status, buf))
481        }
482        .await
483        .unwrap_or_else(|status| (status, Vec::new()))
484    }
485
486    async fn handle_link(
487        &self,
488        source_name: &str,
489        target_parent_token: fidl::Handle,
490        target_name: String,
491    ) -> Result<(), Status> {
492        if source_name.contains('/') || target_name.contains('/') {
493            return Err(Status::INVALID_ARGS);
494        }
495
496        // To avoid rights escalation, we must make sure that the connection to the source directory
497        // has the maximal set of file rights.  We do not check for EXECUTE because mutable
498        // filesystems that support link don't currently support EXECUTE rights.  The target rights
499        // are verified by virtue of the fact that it is not possible to get a token without the
500        // MODIFY_DIRECTORY right (see `MutableConnection::handle_get_token`).
501        if !self.options.rights.contains(fio::RW_STAR_DIR) {
502            return Err(Status::BAD_HANDLE);
503        }
504
505        let target_parent = self
506            .scope
507            .token_registry()
508            .get_owner(target_parent_token)?
509            .ok_or(Err(Status::NOT_FOUND))?;
510
511        target_parent.link(target_name, self.directory.clone().into_any(), source_name).await
512    }
513
514    fn handle_watch(
515        &mut self,
516        mask: fio::WatchMask,
517        watcher: DirectoryWatcher,
518    ) -> Result<(), Status> {
519        let directory = self.directory.clone();
520        directory.register_watcher(self.scope.clone(), mask, watcher)
521    }
522}
523
524impl<DirectoryType: Directory> Representation for BaseConnection<DirectoryType> {
525    type Protocol = fio::DirectoryMarker;
526
527    async fn get_representation(
528        &self,
529        requested_attributes: fio::NodeAttributesQuery,
530    ) -> Result<fio::Representation, Status> {
531        Ok(fio::Representation::Directory(fio::DirectoryInfo {
532            attributes: if requested_attributes.is_empty() {
533                None
534            } else {
535                Some(self.directory.get_attributes(requested_attributes).await?)
536            },
537            ..Default::default()
538        }))
539    }
540
541    async fn node_info(&self) -> Result<fio::NodeInfoDeprecated, Status> {
542        Ok(fio::NodeInfoDeprecated::Directory(fio::DirectoryObject))
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549    use crate::directory::immutable::Simple;
550    use assert_matches::assert_matches;
551    use fidl_fuchsia_io as fio;
552
553    #[fuchsia::test]
554    async fn test_open_not_found() {
555        let dir = Simple::new();
556        let dir_proxy = crate::directory::serve(dir, fio::PERM_READABLE);
557
558        // Try to open a file that doesn't exist.
559        let node_proxy = fuchsia_fs::directory::open_async::<fio::NodeMarker>(
560            &dir_proxy,
561            "foo",
562            fio::PERM_READABLE,
563        )
564        .unwrap();
565
566        // The channel is closed with a NOT_FOUND epitaph.
567        assert_matches!(
568            node_proxy.query().await,
569            Err(fidl::Error::ClientChannelClosed {
570                status: Status::NOT_FOUND,
571                protocol_name: "fuchsia.io.Node",
572                ..
573            })
574        );
575    }
576
577    #[fuchsia::test]
578    async fn test_open_with_send_representation_not_found() {
579        let dir = Simple::new();
580        let dir_proxy = crate::directory::serve(dir, fio::PERM_READABLE);
581
582        // Try to open a file that doesn't exist.
583        let node_proxy = fuchsia_fs::directory::open_async::<fio::NodeMarker>(
584            &dir_proxy,
585            "foo",
586            fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
587        )
588        .unwrap();
589
590        // The channel is closed with a NOT_FOUND epitaph.
591        assert_matches!(
592            node_proxy.query().await,
593            Err(fidl::Error::ClientChannelClosed {
594                status: Status::NOT_FOUND,
595                protocol_name: "fuchsia.io.Node",
596                ..
597            })
598        );
599    }
600}