[package-directory] Rework tests to use VFS connections

Ensure package directory tests serve a connection to the directories
being tested, and all child nodes are opened through fuchsia.io. This
ensures that the VFS connection logic (including hierarchal rights
enforcement and path handling) is also tested. This results in a
few test-only changes:

 * Hierarchal rights are now enforced, so in cases where NOT_SUPPORTED
   was being checked before, we now need ACCESS_DENIED.
 * The VFS handles checking certain flags already reducing the verbosity
   of these tests. This includes attempts to create new nodes, which
   requires the parent connection to be served as writable. This is not
   possible with how the nodes are implemented, so these tests have been
   removed in lieu of assertions that guarantee we cannot create
   new files, and tests that ensure we cannot create mutable connections
 * Callers of the fuchsia.io/Directory.DeprecatedOpen method now reject
   requests to open files where the path ends with a trailing slash,
   since the VFS adds OpenFlags.DIRECTORY in these cases. This is NOT
   done for the new fuchsia.io/Directory.Open method, so that is still
   allowed for those tests.

As we are now testing through a DirectoryProxy instead of calling the
VFS Node trait methods directly, we can simplify a lot of test code by
using the helpers in the fuchsia-fs crate. This improves test
readability significantly in some cases, and also ensures that we test
end-to-end behavior that clients will encounter.

There should be no change to code coverage with these tests, but there
is a small functional change: in certain cases when calling
fuchsia.io/Directory.DeprecatedOpen, the APPEND flag was erroneously
allowed. We cannot easily fix this without disabling the old CTF tests,
so for now we just update them going forwards and leave the old
behavior. We need to address this as part of the io2 migration soon.

Remove redundant test cases where the VFS already enforces certain
checks, and instead add assertions which ensure no regressions in
behavior can occur.

Bug: 324111518
Test: fx test package-directory-tests
Change-Id: Idac8441d889ffdec3a3f93dc92ab0c7b0aaff036
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/1234244
Reviewed-by: Ben Keller <galbanum@google.com>
Commit-Queue: Brandon Castellano <bcastell@google.com>
diff --git a/src/sys/pkg/lib/package-directory/src/lib.rs b/src/sys/pkg/lib/package-directory/src/lib.rs
index 6dd3b52..458c592 100644
--- a/src/sys/pkg/lib/package-directory/src/lib.rs
+++ b/src/sys/pkg/lib/package-directory/src/lib.rs
@@ -24,11 +24,13 @@
 pub use root_dir::{PathError, ReadFileError, RootDir, SubpackagesError};
 pub use root_dir_cache::RootDirCache;
 pub use vfs::execution_scope::ExecutionScope;
-pub use vfs::path::Path as VfsPath;
 
 pub(crate) const DIRECTORY_ABILITIES: fio::Abilities =
     fio::Abilities::GET_ATTRIBUTES.union(fio::Abilities::ENUMERATE).union(fio::Abilities::TRAVERSE);
 
+pub(crate) const MUTABLE_FLAGS: fio::Flags =
+    fio::Flags::PERM_MODIFY.union(fio::Flags::PERM_SET_ATTRIBUTES).union(fio::Flags::PERM_WRITE);
+
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
     #[error("the meta.far was not found")]
@@ -257,7 +259,7 @@
         non_meta_storage,
         meta_far,
         flags,
-        VfsPath::dot(),
+        vfs::Path::dot(),
         server_end.into_channel().into(),
     )
 }
@@ -273,7 +275,7 @@
     non_meta_storage: impl NonMetaStorage,
     meta_far: fuchsia_hash::Hash,
     flags: fio::Flags,
-    path: VfsPath,
+    path: vfs::Path,
     server_end: ServerEnd<fio::NodeMarker>,
 ) -> Result<(), Error> {
     let root_dir = match RootDir::new(non_meta_storage, meta_far).await {
@@ -365,9 +367,6 @@
     use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
     use fuchsia_pkg_testing::PackageBuilder;
     use futures::StreamExt;
-    use std::any::Any;
-    use std::sync::Arc;
-    use vfs::directory::dirents_sink::{self, AppendResult, Sealed, Sink};
     use vfs::directory::helper::DirectlyMutable;
 
     #[fuchsia_async::run_singlethreaded(test)]
@@ -410,7 +409,7 @@
             blobfs_client,
             metafar_blob.merkle,
             fio::PERM_READABLE,
-            VfsPath::validate_and_split(".").unwrap(),
+            vfs::Path::validate_and_split(".").unwrap(),
             server_end.into_channel().into(),
         )
         .await
@@ -438,7 +437,7 @@
             blobfs_client,
             metafar_blob.merkle,
             fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
-            VfsPath::validate_and_split("meta").unwrap(),
+            vfs::Path::validate_and_split("meta").unwrap(),
             server_end.into_channel().into(),
         )
         .await
@@ -464,7 +463,7 @@
                 blobfs_client,
                 metafar_blob.merkle,
                 fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
-                VfsPath::validate_and_split("not-present").unwrap(),
+                vfs::Path::validate_and_split("not-present").unwrap(),
                 server_end.into_channel().into(),
             )
             .await,
@@ -487,7 +486,7 @@
                 blobfs_client,
                 Hash::from([0u8; 32]),
                 fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
-                VfsPath::validate_and_split(".").unwrap(),
+                vfs::Path::validate_and_split(".").unwrap(),
                 server_end.into_channel().into(),
             )
             .await,
@@ -561,77 +560,17 @@
         assert_eq!(get_dir_children(["a/b/c/d"], "a/"), vec![(dir(), "b".to_string())]);
     }
 
-    /// Implementation of vfs::directory::dirents_sink::Sink.
-    /// Sink::append begins to fail (returns Sealed) after `max_entries` entries have been appended.
-    #[derive(Clone)]
-    pub(crate) struct FakeSink {
-        max_entries: usize,
-        pub(crate) entries: Vec<(String, EntryInfo)>,
-        sealed: bool,
-    }
-
-    impl FakeSink {
-        pub(crate) fn new(max_entries: usize) -> Self {
-            FakeSink { max_entries, entries: Vec::with_capacity(max_entries), sealed: false }
-        }
-
-        pub(crate) fn from_sealed(sealed: Box<dyn dirents_sink::Sealed>) -> Box<FakeSink> {
-            sealed.into()
-        }
-    }
-
-    impl From<Box<dyn dirents_sink::Sealed>> for Box<FakeSink> {
-        fn from(sealed: Box<dyn dirents_sink::Sealed>) -> Self {
-            sealed.open().downcast::<FakeSink>().unwrap()
-        }
-    }
-
-    impl Sink for FakeSink {
-        fn append(mut self: Box<Self>, entry: &EntryInfo, name: &str) -> AppendResult {
-            assert!(!self.sealed);
-            if self.entries.len() == self.max_entries {
-                AppendResult::Sealed(self.seal())
-            } else {
-                self.entries.push((name.to_owned(), entry.clone()));
-                AppendResult::Ok(self)
-            }
-        }
-
-        fn seal(mut self: Box<Self>) -> Box<dyn Sealed> {
-            self.sealed = true;
-            self
-        }
-    }
-
-    impl Sealed for FakeSink {
-        fn open(self: Box<Self>) -> Box<dyn Any> {
-            self
-        }
-    }
-
     const BLOB_CONTENTS: &[u8] = b"blob-contents";
 
     fn blob_contents_hash() -> Hash {
         fuchsia_merkle::from_slice(BLOB_CONTENTS).root()
     }
 
-    fn serve_directory(directory: Arc<vfs::directory::immutable::Simple>) -> fio::DirectoryProxy {
-        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-        vfs::directory::entry_container::Directory::open(
-            directory,
-            vfs::execution_scope::ExecutionScope::new(),
-            fio::OpenFlags::RIGHT_READABLE,
-            vfs::path::Path::dot(),
-            fidl::endpoints::ServerEnd::new(server_end.into_channel()),
-        );
-        proxy
-    }
-
     #[fuchsia_async::run_singlethreaded(test)]
     async fn bootfs_get_vmo_blob() {
         let directory = vfs::directory::immutable::simple();
         directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
-        let proxy = serve_directory(directory);
+        let proxy = vfs::directory::serve_read_only(directory);
 
         let vmo = proxy.get_blob_vmo(&blob_contents_hash()).await.unwrap();
         assert_eq!(vmo.read_to_vec(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
@@ -641,7 +580,7 @@
     async fn bootfs_read_blob() {
         let directory = vfs::directory::immutable::simple();
         directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
-        let proxy = serve_directory(directory);
+        let proxy = vfs::directory::serve_read_only(directory);
 
         assert_eq!(proxy.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
     }
@@ -649,7 +588,7 @@
     #[fuchsia_async::run_singlethreaded(test)]
     async fn bootfs_get_vmo_blob_missing_blob() {
         let directory = vfs::directory::immutable::simple();
-        let proxy = serve_directory(directory);
+        let proxy = vfs::directory::serve_read_only(directory);
 
         let result = proxy.get_blob_vmo(&blob_contents_hash()).await;
         assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
@@ -658,7 +597,7 @@
     #[fuchsia_async::run_singlethreaded(test)]
     async fn bootfs_read_blob_missing_blob() {
         let directory = vfs::directory::immutable::simple();
-        let proxy = serve_directory(directory);
+        let proxy = vfs::directory::serve_read_only(directory);
 
         let result = proxy.read_blob(&blob_contents_hash()).await;
         assert_matches!(result, Err(NonMetaStorageError::ReadBlob(e)) if e.is_not_found_error());
diff --git a/src/sys/pkg/lib/package-directory/src/meta_as_dir.rs b/src/sys/pkg/lib/package-directory/src/meta_as_dir.rs
index 387ed42..e156512 100644
--- a/src/sys/pkg/lib/package-directory/src/meta_as_dir.rs
+++ b/src/sys/pkg/lib/package-directory/src/meta_as_dir.rs
@@ -12,8 +12,7 @@
 use vfs::directory::immutable::connection::ImmutableConnection;
 use vfs::directory::traversal_position::TraversalPosition;
 use vfs::execution_scope::ExecutionScope;
-use vfs::path::Path as VfsPath;
-use vfs::{immutable_attributes, ObjectRequestRef, ProtocolsExt, ToObjectRequest};
+use vfs::{immutable_attributes, ObjectRequestRef, ProtocolsExt as _, ToObjectRequest as _};
 
 pub(crate) struct MetaAsDir<S: crate::NonMetaStorage> {
     root_dir: Arc<RootDir<S>>,
@@ -54,25 +53,31 @@
         self: Arc<Self>,
         scope: ExecutionScope,
         flags: fio::OpenFlags,
-        path: VfsPath,
+        path: vfs::Path,
         server_end: ServerEnd<fio::NodeMarker>,
     ) {
         let flags = flags & !(fio::OpenFlags::POSIX_WRITABLE | fio::OpenFlags::POSIX_EXECUTABLE);
         let describe = flags.contains(fio::OpenFlags::DESCRIBE);
-
-        if flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT) {
+        // Disallow creating a writable or executable connection to this node or any children. We
+        // also disallow file flags which do not apply. Note that the latter is not required for
+        // Open3, as we require writable rights for the latter flags already.
+        if flags.intersects(
+            fio::OpenFlags::RIGHT_WRITABLE
+                | fio::OpenFlags::RIGHT_EXECUTABLE
+                | fio::OpenFlags::TRUNCATE,
+        ) {
             let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_SUPPORTED);
             return;
         }
+        // The VFS should disallow file creation since we cannot serve a mutable connection.
+        assert!(!flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT));
 
         if path.is_empty() {
             flags.to_object_request(server_end).handle(|object_request| {
-                if flags.intersects(
-                    fio::OpenFlags::RIGHT_WRITABLE
-                        | fio::OpenFlags::RIGHT_EXECUTABLE
-                        | fio::OpenFlags::TRUNCATE
-                        | fio::OpenFlags::APPEND,
-                ) {
+                // NOTE: Some older CTF tests still rely on being able to use the APPEND flag in
+                // some cases, so we cannot check this flag above. Appending is still not possible.
+                // As we plan to remove this method entirely, we can just leave this for now.
+                if flags.intersects(fio::OpenFlags::APPEND) {
                     return Err(zx::Status::NOT_SUPPORTED);
                 }
 
@@ -116,7 +121,7 @@
         }
 
         if let Some(subdir) = self.root_dir.get_meta_subdir(file_path + "/") {
-            let () = subdir.open(scope, flags, VfsPath::dot(), server_end);
+            let () = subdir.open(scope, flags, vfs::Path::dot(), server_end);
             return;
         }
 
@@ -126,23 +131,19 @@
     fn open3(
         self: Arc<Self>,
         scope: ExecutionScope,
-        path: VfsPath,
+        path: vfs::Path,
         flags: fio::Flags,
         object_request: ObjectRequestRef<'_>,
     ) -> Result<(), zx::Status> {
-        if flags.creation_mode() != vfs::CreationMode::Never {
+        // Disallow creating a mutable or executable connection to this node or any children.
+        if flags.intersects(crate::MUTABLE_FLAGS.union(fio::Flags::PERM_EXECUTE)) {
             return Err(zx::Status::NOT_SUPPORTED);
         }
+        // The VFS should disallow file creation or append/truncate as these require mutable rights.
+        assert!(flags.creation_mode() == vfs::CreationMode::Never);
+        assert!(!flags.intersects(fio::Flags::FILE_APPEND | fio::Flags::FILE_TRUNCATE));
 
         if path.is_empty() {
-            if let Some(rights) = flags.rights() {
-                if rights.intersects(fio::Operations::WRITE_BYTES)
-                    | rights.intersects(fio::Operations::EXECUTE)
-                {
-                    return Err(zx::Status::NOT_SUPPORTED);
-                }
-            }
-
             // Only MetaAsDir can be obtained from Open calls to MetaAsDir. To obtain the "meta"
             // file, the Open call must be made on RootDir. This is consistent with pkgfs behavior
             // and is needed so that Clone'ing MetaAsDir results in MetaAsDir, because VFS handles
@@ -172,7 +173,7 @@
         }
 
         if let Some(subdir) = self.root_dir.get_meta_subdir(file_path + "/") {
-            return subdir.open3(scope, VfsPath::dot(), flags, object_request);
+            return subdir.open3(scope, vfs::Path::dot(), flags, object_request);
         }
 
         Err(zx::Status::NOT_FOUND)
@@ -214,18 +215,15 @@
     use fuchsia_fs::directory::{DirEntry, DirentKind};
     use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
     use fuchsia_pkg_testing::PackageBuilder;
-    use futures::prelude::*;
-    use std::convert::TryInto as _;
-    use vfs::directory::entry::EntryInfo;
-    use vfs::directory::entry_container::Directory;
-    use vfs::node::Node;
+    use futures::TryStreamExt as _;
+    use vfs::directory::entry_container::Directory as _;
 
     struct TestEnv {
         _blobfs_fake: FakeBlobfs,
     }
 
     impl TestEnv {
-        async fn new() -> (Self, Arc<MetaAsDir<blobfs::Client>>) {
+        async fn new() -> (Self, fio::DirectoryProxy) {
             let pkg = PackageBuilder::new("pkg")
                 .add_resource_at("meta/dir/file", &b"contents"[..])
                 .build()
@@ -236,53 +234,46 @@
             blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
             let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
             let meta_as_dir = MetaAsDir::new(root_dir);
-            (Self { _blobfs_fake: blobfs_fake }, meta_as_dir)
+            (Self { _blobfs_fake: blobfs_fake }, vfs::directory::serve_read_only(meta_as_dir))
         }
     }
 
+    /// Ensure connections to a [`MetaAsDir`] cannot be created as mutable (i.e. with
+    /// [`fio::PERM_WRITABLE`]) or executable ([`fio::PERM_EXECUTABLE`]). This ensures that the VFS
+    /// will disallow any attempts to create a new file/directory, modify the attributes of any
+    /// nodes, or open any files as writable/executable.
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_rejects_disallowed_flags() {
-        let (_env, meta_as_dir) = TestEnv::new().await;
-
-        for forbidden_flag in [
-            fio::OpenFlags::RIGHT_WRITABLE,
-            fio::OpenFlags::RIGHT_EXECUTABLE,
-            fio::OpenFlags::CREATE,
-            fio::OpenFlags::CREATE_IF_ABSENT,
-            fio::OpenFlags::TRUNCATE,
-            fio::OpenFlags::APPEND,
-        ] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-            meta_as_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::DESCRIBE | forbidden_flag,
-                VfsPath::dot(),
-                server_end.into_channel().into(),
-            );
-
+    async fn meta_as_dir_cannot_be_served_as_mutable() {
+        let pkg = PackageBuilder::new("pkg")
+            .add_resource_at("meta/dir/file", &b"contents"[..])
+            .build()
+            .await
+            .unwrap();
+        let (metafar_blob, _) = pkg.contents();
+        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
+        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
+        let meta_as_dir =
+            MetaAsDir::new(RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap());
+        for flags in [fio::PERM_WRITABLE, fio::PERM_EXECUTABLE] {
+            let (proxy, server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
+            let request = flags.to_object_request(server);
+            request.handle(|request: &mut vfs::ObjectRequest| {
+                meta_as_dir.clone().open3(ExecutionScope::new(), vfs::Path::dot(), flags, request)
+            });
             assert_matches!(
-                proxy.take_event_stream().next().await,
-                Some(Ok(fio::DirectoryEvent::OnOpen_{ s, info: None}))
-                    if s == zx::Status::NOT_SUPPORTED.into_raw()
+                proxy.take_event_stream().try_next().await,
+                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
             );
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_self() {
+    async fn meta_as_dir_readdir() {
         let (_env, meta_as_dir) = TestEnv::new().await;
-        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-
-        meta_as_dir.open(
-            ExecutionScope::new(),
-            fio::OpenFlags::RIGHT_READABLE,
-            VfsPath::dot(),
-            server_end.into_channel().into(),
-        );
-
         assert_eq!(
-            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
+            fuchsia_fs::directory::readdir_inclusive(&meta_as_dir).await.unwrap(),
             vec![
+                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
                 DirEntry { name: "contents".to_string(), kind: DirentKind::File },
                 DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
                 DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
@@ -292,93 +283,12 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_file() {
+    async fn meta_as_dir_get_attributes() {
         let (_env, meta_as_dir) = TestEnv::new().await;
-
-        for path in ["dir/file", "dir/file/"] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
-            meta_as_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
-            assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"contents".to_vec());
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_directory() {
-        let (_env, meta_as_dir) = TestEnv::new().await;
-
-        for path in ["dir", "dir/"] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-            meta_as_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
-            assert_eq!(
-                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_read_dirents() {
-        let (_env, meta_as_dir) = TestEnv::new().await;
-        let (pos, sealed) = Directory::read_dirents(
-            meta_as_dir.as_ref(),
-            &TraversalPosition::Start,
-            Box::new(crate::tests::FakeSink::new(5)),
-        )
-        .await
-        .expect("read_dirents failed");
+        let (mutable_attributes, immutable_attributes) =
+            meta_as_dir.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap().unwrap();
         assert_eq!(
-            crate::tests::FakeSink::from_sealed(sealed).entries,
-            vec![
-                (".".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
-                ("contents".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)),
-                ("dir".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
-                (
-                    "fuchsia.abi".to_string(),
-                    EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
-                ),
-                ("package".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)),
-            ]
-        );
-        assert_eq!(pos, TraversalPosition::End);
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_register_watcher_not_supported() {
-        let (_env, meta_as_dir) = TestEnv::new().await;
-
-        let (_client, server) = fidl::endpoints::create_endpoints();
-
-        assert_eq!(
-            Directory::register_watcher(
-                meta_as_dir,
-                ExecutionScope::new(),
-                fio::WatchMask::empty(),
-                server.try_into().unwrap(),
-            ),
-            Err(zx::Status::NOT_SUPPORTED)
-        );
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_get_attributes() {
-        let (_env, meta_as_dir) = TestEnv::new().await;
-
-        assert_eq!(
-            Node::get_attributes(meta_as_dir.as_ref(), fio::NodeAttributesQuery::all())
-                .await
-                .unwrap(),
+            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
             immutable_attributes!(
                 fio::NodeAttributesQuery::all(),
                 Immutable {
@@ -393,38 +303,34 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_self() {
+    async fn meta_as_dir_watch_not_supported() {
         let (_env, meta_as_dir) = TestEnv::new().await;
-        let proxy = vfs::directory::serve(meta_as_dir, fio::PERM_READABLE);
-        assert_eq!(
-            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-            vec![
-                DirEntry { name: "contents".to_string(), kind: DirentKind::File },
-                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
-                DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
-                DirEntry { name: "package".to_string(), kind: DirentKind::File }
-            ]
+        let (_client, server) = fidl::endpoints::create_endpoints();
+        let status = zx::Status::from_raw(
+            meta_as_dir.watch(fio::WatchMask::empty(), 0, server).await.unwrap(),
         );
+        assert_eq!(status, zx::Status::NOT_SUPPORTED);
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_file() {
+    async fn meta_as_dir_open_file() {
         let (_env, meta_as_dir) = TestEnv::new().await;
-
         for path in ["dir/file", "dir/file/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_file(meta_as_dir.clone(), path, fio::PERM_READABLE);
+            let proxy = fuchsia_fs::directory::open_file(&meta_as_dir, path, fio::PERM_READABLE)
+                .await
+                .unwrap();
             assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"contents".to_vec());
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_directory() {
+    async fn meta_as_dir_open_directory() {
         let (_env, meta_as_dir) = TestEnv::new().await;
-
         for path in ["dir", "dir/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_directory(meta_as_dir.clone(), path, fio::PERM_READABLE);
+            let proxy =
+                fuchsia_fs::directory::open_directory(&meta_as_dir, path, fio::PERM_READABLE)
+                    .await
+                    .unwrap();
             assert_eq!(
                 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
                 vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
@@ -433,43 +339,37 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_rejects_invalid_flags() {
+    async fn meta_as_dir_deprecated_open_file() {
         let (_env, meta_as_dir) = TestEnv::new().await;
-
-        for invalid_flags in [
-            fio::Flags::FLAG_MUST_CREATE,
-            fio::Flags::FLAG_MAYBE_CREATE,
-            fio::PERM_WRITABLE,
-            fio::PERM_EXECUTABLE,
-        ] {
-            let proxy =
-                vfs::directory::serve(meta_as_dir.clone(), fio::PERM_READABLE | invalid_flags);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
-            );
-        }
+        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
+        meta_as_dir
+            .deprecated_open(
+                fio::OpenFlags::RIGHT_READABLE,
+                Default::default(),
+                "dir/file",
+                server_end.into_channel().into(),
+            )
+            .unwrap();
+        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"contents".to_vec());
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_rejects_file_flags() {
+    async fn meta_as_dir_deprecated_open_directory() {
         let (_env, meta_as_dir) = TestEnv::new().await;
+        for path in ["dir", "dir/"] {
+            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
+            meta_as_dir
+                .deprecated_open(
+                    fio::OpenFlags::RIGHT_READABLE,
+                    Default::default(),
+                    path,
+                    server_end.into_channel().into(),
+                )
+                .unwrap();
 
-        // Requesting to open with `PROTOCOL_FILE` should return a `NOT_FILE` error.
-        {
-            let proxy = vfs::directory::serve(meta_as_dir.clone(), fio::Flags::PROTOCOL_FILE);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_FILE, .. })
-            );
-        }
-
-        // Opening with file flags is also invalid.
-        for file_flags in [fio::Flags::FILE_APPEND, fio::Flags::FILE_TRUNCATE] {
-            let proxy = vfs::directory::serve(meta_as_dir.clone(), file_flags);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::INVALID_ARGS, .. })
+            assert_eq!(
+                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
+                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
             );
         }
     }
diff --git a/src/sys/pkg/lib/package-directory/src/meta_subdir.rs b/src/sys/pkg/lib/package-directory/src/meta_subdir.rs
index 78c26a2..a8af3f0 100644
--- a/src/sys/pkg/lib/package-directory/src/meta_subdir.rs
+++ b/src/sys/pkg/lib/package-directory/src/meta_subdir.rs
@@ -11,8 +11,7 @@
 use vfs::directory::immutable::connection::ImmutableConnection;
 use vfs::directory::traversal_position::TraversalPosition;
 use vfs::execution_scope::ExecutionScope;
-use vfs::path::Path as VfsPath;
-use vfs::{immutable_attributes, ObjectRequestRef, ProtocolsExt as _, ToObjectRequest};
+use vfs::{immutable_attributes, ObjectRequestRef, ProtocolsExt as _, ToObjectRequest as _};
 
 pub(crate) struct MetaSubdir<S: crate::NonMetaStorage> {
     root_dir: Arc<RootDir<S>>,
@@ -57,28 +56,33 @@
         self: Arc<Self>,
         scope: ExecutionScope,
         flags: fio::OpenFlags,
-        path: VfsPath,
+        path: vfs::Path,
         server_end: ServerEnd<fio::NodeMarker>,
     ) {
         let flags = flags & !(fio::OpenFlags::POSIX_WRITABLE | fio::OpenFlags::POSIX_EXECUTABLE);
         let describe = flags.contains(fio::OpenFlags::DESCRIBE);
-
-        if flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT) {
+        // Disallow creating a writable or executable connection to this node or any children. We
+        // also disallow file flags which do not apply. Note that the latter is not required for
+        // Open3, as we require writable rights for the latter flags already.
+        if flags.intersects(
+            fio::OpenFlags::RIGHT_WRITABLE
+                | fio::OpenFlags::RIGHT_EXECUTABLE
+                | fio::OpenFlags::TRUNCATE,
+        ) {
             let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_SUPPORTED);
             return;
         }
+        // The VFS should disallow file creation since we cannot serve a mutable connection.
+        assert!(!flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT));
 
         if path.is_empty() {
             flags.to_object_request(server_end).handle(|object_request| {
-                if flags.intersects(
-                    fio::OpenFlags::RIGHT_WRITABLE
-                        | fio::OpenFlags::RIGHT_EXECUTABLE
-                        | fio::OpenFlags::TRUNCATE
-                        | fio::OpenFlags::APPEND,
-                ) {
+                // NOTE: Some older CTF tests still rely on being able to use the APPEND flag in
+                // some cases, so we cannot check this flag above. Appending is still not possible.
+                // As we plan to remove this method entirely, we can just leave this for now.
+                if flags.intersects(fio::OpenFlags::APPEND) {
                     return Err(zx::Status::NOT_SUPPORTED);
                 }
-
                 object_request
                     .take()
                     .create_connection_sync::<ImmutableConnection<_>, _>(scope, self, flags);
@@ -116,7 +120,7 @@
         }
 
         if let Some(subdir) = self.root_dir.get_meta_subdir(file_path + "/") {
-            let () = subdir.open(scope, flags, VfsPath::dot(), server_end);
+            let () = subdir.open(scope, flags, vfs::Path::dot(), server_end);
             return;
         }
 
@@ -126,22 +130,19 @@
     fn open3(
         self: Arc<Self>,
         scope: ExecutionScope,
-        path: VfsPath,
+        path: vfs::Path,
         flags: fio::Flags,
         object_request: ObjectRequestRef<'_>,
     ) -> Result<(), zx::Status> {
-        if flags.creation_mode() != vfs::CreationMode::Never {
+        // Disallow creating a mutable or executable connection to this node or any children.
+        if flags.intersects(crate::MUTABLE_FLAGS.union(fio::Flags::PERM_EXECUTE)) {
             return Err(zx::Status::NOT_SUPPORTED);
         }
+        // The VFS should disallow file creation or append/truncate as these require mutable rights.
+        assert!(flags.creation_mode() == vfs::CreationMode::Never);
+        assert!(!flags.intersects(fio::Flags::FILE_APPEND | fio::Flags::FILE_TRUNCATE));
 
         if path.is_empty() {
-            if let Some(rights) = flags.rights() {
-                if rights.intersects(fio::Operations::WRITE_BYTES)
-                    | rights.intersects(fio::Operations::EXECUTE)
-                {
-                    return Err(zx::Status::NOT_SUPPORTED);
-                }
-            }
             // `ImmutableConnection` checks that only directory flags are specified.
             object_request
                 .take()
@@ -168,7 +169,7 @@
         }
 
         if let Some(subdir) = self.root_dir.get_meta_subdir(file_path + "/") {
-            return subdir.open3(scope, VfsPath::dot(), flags, object_request);
+            return subdir.open3(scope, vfs::Path::dot(), flags, object_request);
         }
 
         Err(zx::Status::NOT_FOUND)
@@ -210,20 +211,18 @@
 mod tests {
     use super::*;
     use assert_matches::assert_matches;
+    use fuchsia_fs::directory::{DirEntry, DirentKind};
     use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
     use fuchsia_pkg_testing::PackageBuilder;
     use futures::prelude::*;
-    use std::convert::TryInto as _;
-    use vfs::directory::entry::EntryInfo;
-    use vfs::directory::entry_container::Directory;
-    use vfs::node::Node;
+    use vfs::directory::entry_container::Directory as _;
 
     struct TestEnv {
         _blobfs_fake: FakeBlobfs,
     }
 
     impl TestEnv {
-        async fn new() -> (Self, Arc<MetaSubdir<blobfs::Client>>) {
+        async fn new() -> (Self, fio::DirectoryProxy) {
             let pkg = PackageBuilder::new("pkg")
                 .add_resource_at("meta/dir/dir/file", &b"contents"[..])
                 .build()
@@ -234,143 +233,58 @@
             blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
             let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
             let sub_dir = MetaSubdir::new(root_dir, "meta/dir/".to_string());
-            (Self { _blobfs_fake: blobfs_fake }, sub_dir)
+            (Self { _blobfs_fake: blobfs_fake }, vfs::directory::serve_read_only(sub_dir))
         }
     }
 
+    /// Ensure connections to a [`MetaSubdir`] cannot be created as mutable (i.e. with
+    /// [`fio::PERM_WRITABLE`]) or executable ([`fio::PERM_EXECUTABLE`]). This ensures that the VFS
+    /// will disallow any attempts to create a new file/directory, modify the attributes of any
+    /// nodes, or open any files as writable/executable.
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_rejects_disallowed_flags() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
-        for forbidden_flag in [
-            fio::OpenFlags::RIGHT_WRITABLE,
-            fio::OpenFlags::RIGHT_EXECUTABLE,
-            fio::OpenFlags::CREATE,
-            fio::OpenFlags::CREATE_IF_ABSENT,
-            fio::OpenFlags::TRUNCATE,
-            fio::OpenFlags::APPEND,
-        ] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-            sub_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::DESCRIBE | forbidden_flag,
-                VfsPath::dot(),
-                server_end.into_channel().into(),
-            );
-
+    async fn meta_subdir_cannot_be_served_as_mutable() {
+        let pkg = PackageBuilder::new("pkg")
+            .add_resource_at("meta/dir/dir/file", &b"contents"[..])
+            .build()
+            .await
+            .unwrap();
+        let (metafar_blob, _) = pkg.contents();
+        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
+        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
+        let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
+        let sub_dir = MetaSubdir::new(root_dir, "meta/dir/".to_string());
+        for flags in [fio::PERM_WRITABLE, fio::PERM_EXECUTABLE] {
+            let (proxy, server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
+            let request = flags.to_object_request(server);
+            request.handle(|request: &mut vfs::ObjectRequest| {
+                sub_dir.clone().open3(ExecutionScope::new(), vfs::Path::dot(), flags, request)
+            });
             assert_matches!(
-                proxy.take_event_stream().next().await,
-                Some(Ok(fio::DirectoryEvent::OnOpen_{ s, info: None}))
-                    if s == zx::Status::NOT_SUPPORTED.into_raw()
+                proxy.take_event_stream().try_next().await,
+                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
             );
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_self() {
+    async fn meta_subdir_readdir() {
         let (_env, sub_dir) = TestEnv::new().await;
-        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-
-        sub_dir.open(
-            ExecutionScope::new(),
-            fio::OpenFlags::RIGHT_READABLE,
-            VfsPath::dot(),
-            server_end.into_channel().into(),
-        );
-
         assert_eq!(
-            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-            vec![fuchsia_fs::directory::DirEntry {
-                name: "dir".to_string(),
-                kind: fuchsia_fs::directory::DirentKind::Directory
-            }]
-        );
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_file() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
-        for path in ["dir/file", "dir/file/"] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
-            sub_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
-            assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"contents".to_vec());
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_directory() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
-        for path in ["dir", "dir/"] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-            sub_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
-            assert_eq!(
-                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-                vec![fuchsia_fs::directory::DirEntry {
-                    name: "file".to_string(),
-                    kind: fuchsia_fs::directory::DirentKind::File
-                }]
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_read_dirents() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
-        let (pos, sealed) = Directory::read_dirents(
-            sub_dir.as_ref(),
-            &TraversalPosition::Start,
-            Box::new(crate::tests::FakeSink::new(3)),
-        )
-        .await
-        .expect("read_dirents failed");
-        assert_eq!(
-            crate::tests::FakeSink::from_sealed(sealed).entries,
+            fuchsia_fs::directory::readdir_inclusive(&sub_dir).await.unwrap(),
             vec![
-                (".".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
-                ("dir".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
+                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
+                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory }
             ]
         );
-        assert_eq!(pos, TraversalPosition::End);
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_register_watcher_not_supported() {
+    async fn meta_subdir_get_attributes() {
         let (_env, sub_dir) = TestEnv::new().await;
-
-        let (_client, server) = fidl::endpoints::create_endpoints();
-
+        let (mutable_attributes, immutable_attributes) =
+            sub_dir.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap().unwrap();
         assert_eq!(
-            Directory::register_watcher(
-                sub_dir,
-                ExecutionScope::new(),
-                fio::WatchMask::empty(),
-                server.try_into().unwrap(),
-            ),
-            Err(zx::Status::NOT_SUPPORTED)
-        );
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_get_attributes() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
-        assert_eq!(
-            Node::get_attributes(sub_dir.as_ref(), fio::NodeAttributesQuery::all()).await.unwrap(),
+            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
             immutable_attributes!(
                 fio::NodeAttributesQuery::all(),
                 Immutable {
@@ -385,36 +299,31 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_self() {
+    async fn meta_subdir_watch_not_supported() {
         let (_env, sub_dir) = TestEnv::new().await;
-        let proxy = vfs::directory::serve(sub_dir, fio::PERM_READABLE);
-        assert_eq!(
-            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-            vec![fuchsia_fs::directory::DirEntry {
-                name: "dir".to_string(),
-                kind: fuchsia_fs::directory::DirentKind::Directory
-            }]
-        );
+        let (_client, server) = fidl::endpoints::create_endpoints();
+        let status =
+            zx::Status::from_raw(sub_dir.watch(fio::WatchMask::empty(), 0, server).await.unwrap());
+        assert_eq!(status, zx::Status::NOT_SUPPORTED);
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_file() {
+    async fn meta_subdir_open_file() {
         let (_env, sub_dir) = TestEnv::new().await;
-
         for path in ["dir/file", "dir/file/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_file(sub_dir.clone(), path, fio::PERM_READABLE);
+            let proxy =
+                fuchsia_fs::directory::open_file(&sub_dir, path, fio::PERM_READABLE).await.unwrap();
             assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"contents".to_vec());
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_directory() {
+    async fn meta_subdir_open_directory() {
         let (_env, sub_dir) = TestEnv::new().await;
-
         for path in ["dir", "dir/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_directory(sub_dir.clone(), path, fio::PERM_READABLE);
+            let proxy = fuchsia_fs::directory::open_directory(&sub_dir, path, fio::PERM_READABLE)
+                .await
+                .unwrap();
             assert_eq!(
                 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
                 vec![fuchsia_fs::directory::DirEntry {
@@ -426,45 +335,39 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_rejects_invalid_flags() {
+    async fn meta_subdir_deprecated_open_file() {
         let (_env, sub_dir) = TestEnv::new().await;
-
-        for invalid_flags in [
-            fio::Flags::FLAG_MUST_CREATE,
-            fio::Flags::FLAG_MAYBE_CREATE,
-            fio::PERM_WRITABLE,
-            fio::PERM_EXECUTABLE,
-        ] {
-            let proxy = vfs::directory::serve(sub_dir.clone(), fio::PERM_READABLE | invalid_flags);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
-            );
-        }
+        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
+        sub_dir
+            .deprecated_open(
+                fio::OpenFlags::RIGHT_READABLE,
+                Default::default(),
+                "dir/file",
+                server_end.into_channel().into(),
+            )
+            .unwrap();
+        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"contents".to_vec());
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_rejects_file_flags() {
+    async fn meta_subdir_deprecated_open_directory() {
         let (_env, sub_dir) = TestEnv::new().await;
-
-        // Requesting to open with `PROTOCOL_FILE` should return a `NOT_FILE` error.
-        {
-            let proxy = vfs::directory::serve(
-                sub_dir.clone(),
-                fio::Flags::PROTOCOL_FILE | fio::PERM_READABLE,
-            );
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_FILE, .. })
-            );
-        }
-
-        // Opening with file flags is also invalid.
-        for file_flags in [fio::Flags::FILE_APPEND, fio::Flags::FILE_TRUNCATE] {
-            let proxy = vfs::directory::serve(sub_dir.clone(), fio::PERM_READABLE | file_flags);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::INVALID_ARGS, .. })
+        for path in ["dir", "dir/"] {
+            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
+            sub_dir
+                .deprecated_open(
+                    fio::OpenFlags::RIGHT_READABLE,
+                    Default::default(),
+                    path,
+                    server_end.into_channel().into(),
+                )
+                .unwrap();
+            assert_eq!(
+                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
+                vec![fuchsia_fs::directory::DirEntry {
+                    name: "file".to_string(),
+                    kind: fuchsia_fs::directory::DirentKind::File
+                }]
             );
         }
     }
diff --git a/src/sys/pkg/lib/package-directory/src/non_meta_subdir.rs b/src/sys/pkg/lib/package-directory/src/non_meta_subdir.rs
index f1e8a51..68f0afb7 100644
--- a/src/sys/pkg/lib/package-directory/src/non_meta_subdir.rs
+++ b/src/sys/pkg/lib/package-directory/src/non_meta_subdir.rs
@@ -13,10 +13,7 @@
 use vfs::directory::immutable::connection::ImmutableConnection;
 use vfs::directory::traversal_position::TraversalPosition;
 use vfs::execution_scope::ExecutionScope;
-use vfs::path::Path as VfsPath;
-use vfs::{
-    immutable_attributes, CreationMode, ObjectRequestRef, ProtocolsExt as _, ToObjectRequest,
-};
+use vfs::{immutable_attributes, ObjectRequestRef, ProtocolsExt as _, ToObjectRequest as _};
 
 pub(crate) struct NonMetaSubdir<S: crate::NonMetaStorage> {
     root_dir: Arc<RootDir<S>>,
@@ -58,24 +55,27 @@
         self: Arc<Self>,
         scope: ExecutionScope,
         flags: fio::OpenFlags,
-        path: VfsPath,
+        path: vfs::Path,
         server_end: ServerEnd<fio::NodeMarker>,
     ) {
         let flags = flags & !fio::OpenFlags::POSIX_WRITABLE;
         let describe = flags.contains(fio::OpenFlags::DESCRIBE);
-
-        if flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT) {
+        // Disallow creating a writable connection to this node or any children. We also disallow
+        // file flags which do not apply. Note that the latter is not required for Open3, as we
+        // require writable rights for the latter flags already.
+        if flags.intersects(fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::TRUNCATE) {
             let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_SUPPORTED);
             return;
         }
+        // The VFS should disallow file creation since we cannot serve a mutable connection.
+        assert!(!flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT));
 
         if path.is_empty() {
             flags.to_object_request(server_end).handle(|object_request| {
-                if flags.intersects(
-                    fio::OpenFlags::RIGHT_WRITABLE
-                        | fio::OpenFlags::TRUNCATE
-                        | fio::OpenFlags::APPEND,
-                ) {
+                // NOTE: Some older CTF tests still rely on being able to use the APPEND flag in
+                // some cases, so we cannot check this flag above. Appending is still not possible.
+                // As we plan to remove this method entirely, we can just leave this for now.
+                if flags.intersects(fio::OpenFlags::APPEND) {
                     return Err(zx::Status::NOT_SUPPORTED);
                 }
 
@@ -109,7 +109,7 @@
         }
 
         if let Some(subdir) = self.root_dir.get_non_meta_subdir(file_path + "/") {
-            let () = subdir.open(scope, flags, VfsPath::dot(), server_end);
+            let () = subdir.open(scope, flags, vfs::Path::dot(), server_end);
             return;
         }
 
@@ -119,21 +119,19 @@
     fn open3(
         self: Arc<Self>,
         scope: ExecutionScope,
-        path: VfsPath,
+        path: vfs::Path,
         flags: fio::Flags,
         object_request: ObjectRequestRef<'_>,
     ) -> Result<(), zx::Status> {
-        if flags.creation_mode() != CreationMode::Never {
+        // Disallow creating a mutable connection to this node or any children.
+        if flags.intersects(crate::MUTABLE_FLAGS) {
             return Err(zx::Status::NOT_SUPPORTED);
         }
+        // The VFS should disallow file creation or append/truncate as these require mutable rights.
+        assert!(flags.creation_mode() == vfs::CreationMode::Never);
+        assert!(!flags.intersects(fio::Flags::FILE_APPEND | fio::Flags::FILE_TRUNCATE));
 
         if path.is_empty() {
-            if let Some(rights) = flags.rights() {
-                if rights.intersects(fio::Operations::WRITE_BYTES) {
-                    return Err(zx::Status::NOT_SUPPORTED);
-                }
-            }
-
             // `ImmutableConnection` checks that only directory flags are specified.
             object_request
                 .take()
@@ -152,7 +150,7 @@
         }
 
         if let Some(subdir) = self.root_dir.get_non_meta_subdir(file_path + "/") {
-            return subdir.open3(scope, VfsPath::dot(), flags, object_request);
+            return subdir.open3(scope, vfs::Path::dot(), flags, object_request);
         }
 
         Err(zx::Status::NOT_FOUND)
@@ -194,20 +192,18 @@
 mod tests {
     use super::*;
     use assert_matches::assert_matches;
+    use fuchsia_fs::directory::{DirEntry, DirentKind};
     use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
     use fuchsia_pkg_testing::PackageBuilder;
     use futures::prelude::*;
-    use std::convert::TryInto as _;
-    use vfs::directory::entry::EntryInfo;
-    use vfs::directory::entry_container::Directory;
-    use vfs::node::Node;
+    use vfs::directory::entry_container::Directory as _;
 
     struct TestEnv {
         _blobfs_fake: FakeBlobfs,
     }
 
     impl TestEnv {
-        async fn new() -> (Self, Arc<NonMetaSubdir<blobfs::Client>>) {
+        async fn new() -> (Self, fio::DirectoryProxy) {
             let pkg = PackageBuilder::new("pkg")
                 .add_resource_at("dir0/dir1/file", "bloblob".as_bytes())
                 .build()
@@ -221,16 +217,58 @@
             }
             let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
             let sub_dir = NonMetaSubdir::new(root_dir, "dir0/".to_string());
-            (Self { _blobfs_fake: blobfs_fake }, sub_dir)
+            (Self { _blobfs_fake: blobfs_fake }, vfs::directory::serve_read_only(sub_dir))
         }
     }
 
+    /// Ensure connections to a [`NonMetaSubdir`] cannot be created as mutable (i.e. with
+    /// [`fio::PERM_WRITABLE`]) This ensures that the VFS will disallow any attempts to create a new
+    /// file/directory, modify the attributes of any nodes, or open any files as writable.
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_get_attributes() {
-        let (_env, sub_dir) = TestEnv::new().await;
+    async fn non_meta_subdir_cannot_be_served_as_mutable() {
+        let pkg = PackageBuilder::new("pkg")
+            .add_resource_at("dir0/dir1/file", "bloblob".as_bytes())
+            .build()
+            .await
+            .unwrap();
+        let (metafar_blob, content_blobs) = pkg.contents();
+        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
+        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
+        for (hash, bytes) in content_blobs {
+            blobfs_fake.add_blob(hash, bytes);
+        }
+        let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
+        let sub_dir = NonMetaSubdir::new(root_dir, "dir0/".to_string());
+        let (proxy, server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
+        let request = fio::PERM_WRITABLE.to_object_request(server);
+        request.handle(|request: &mut vfs::ObjectRequest| {
+            sub_dir.open3(ExecutionScope::new(), vfs::Path::dot(), fio::PERM_WRITABLE, request)
+        });
+        assert_matches!(
+            proxy.take_event_stream().try_next().await,
+            Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
+        );
+    }
 
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn non_meta_subdir_readdir() {
+        let (_env, sub_dir) = TestEnv::new().await;
         assert_eq!(
-            Node::get_attributes(sub_dir.as_ref(), fio::NodeAttributesQuery::all()).await.unwrap(),
+            fuchsia_fs::directory::readdir_inclusive(&sub_dir).await.unwrap(),
+            vec![
+                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
+                DirEntry { name: "dir1".to_string(), kind: DirentKind::Directory }
+            ]
+        );
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn non_meta_subdir_get_attributes() {
+        let (_env, sub_dir) = TestEnv::new().await;
+        let (mutable_attributes, immutable_attributes) =
+            sub_dir.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap().unwrap();
+        assert_eq!(
+            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
             immutable_attributes!(
                 fio::NodeAttributesQuery::all(),
                 Immutable {
@@ -243,207 +281,70 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_register_watcher_not_supported() {
+    async fn non_meta_subdir_watch_not_supported() {
         let (_env, sub_dir) = TestEnv::new().await;
-
         let (_client, server) = fidl::endpoints::create_endpoints();
-
-        assert_eq!(
-            Directory::register_watcher(
-                sub_dir,
-                ExecutionScope::new(),
-                fio::WatchMask::empty(),
-                server.try_into().unwrap(),
-            ),
-            Err(zx::Status::NOT_SUPPORTED)
-        );
+        let status =
+            zx::Status::from_raw(sub_dir.watch(fio::WatchMask::empty(), 0, server).await.unwrap());
+        assert_eq!(status, zx::Status::NOT_SUPPORTED);
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_read_dirents() {
+    async fn non_meta_subdir_open_directory() {
         let (_env, sub_dir) = TestEnv::new().await;
-
-        let (pos, sealed) = Directory::read_dirents(
-            sub_dir.as_ref(),
-            &TraversalPosition::Start,
-            Box::new(crate::tests::FakeSink::new(3)),
-        )
-        .await
-        .expect("read_dirents failed");
-        assert_eq!(
-            crate::tests::FakeSink::from_sealed(sealed).entries,
-            vec![
-                (".".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
-                ("dir1".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
-            ]
-        );
-        assert_eq!(pos, TraversalPosition::End);
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_directory() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
         for path in ["dir1", "dir1/"] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-            sub_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
+            let proxy = fuchsia_fs::directory::open_directory(&sub_dir, path, fio::PERM_READABLE)
+                .await
+                .unwrap();
             assert_eq!(
                 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-                vec![fuchsia_fs::directory::DirEntry {
-                    name: "file".to_string(),
-                    kind: fuchsia_fs::directory::DirentKind::File
-                }]
+                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
             );
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_file() {
+    async fn non_meta_subdir_open_file() {
         let (_env, sub_dir) = TestEnv::new().await;
-
         for path in ["dir1/file", "dir1/file/"] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
-            sub_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
-            assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"bloblob".to_vec());
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_rejects_disallowed_flags() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
-        for forbidden_flag in [
-            fio::OpenFlags::RIGHT_WRITABLE,
-            fio::OpenFlags::CREATE,
-            fio::OpenFlags::CREATE_IF_ABSENT,
-            fio::OpenFlags::TRUNCATE,
-            fio::OpenFlags::APPEND,
-        ] {
-            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-            sub_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::DESCRIBE | forbidden_flag,
-                VfsPath::dot(),
-                server_end.into_channel().into(),
-            );
-
-            assert_matches!(
-                proxy.take_event_stream().next().await,
-                Some(Ok(fio::DirectoryEvent::OnOpen_{ s, info: None}))
-                    if s == zx::Status::NOT_SUPPORTED.into_raw()
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_self() {
-        let (_env, sub_dir) = TestEnv::new().await;
-        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
-
-        sub_dir.open(
-            ExecutionScope::new(),
-            fio::OpenFlags::RIGHT_READABLE,
-            VfsPath::dot(),
-            server_end.into_channel().into(),
-        );
-
-        assert_eq!(
-            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-            vec![fuchsia_fs::directory::DirEntry {
-                name: "dir1".to_string(),
-                kind: fuchsia_fs::directory::DirentKind::Directory
-            }]
-        );
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_self() {
-        let (_env, sub_dir) = TestEnv::new().await;
-        let proxy = vfs::directory::serve(sub_dir, fio::PERM_READABLE);
-        assert_eq!(
-            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-            vec![fuchsia_fs::directory::DirEntry {
-                name: "dir1".to_string(),
-                kind: fuchsia_fs::directory::DirentKind::Directory
-            }]
-        );
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_directory() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
-        for path in ["dir1", "dir1/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_directory(sub_dir.clone(), path, fio::PERM_READABLE);
-            assert_eq!(
-                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-                vec![fuchsia_fs::directory::DirEntry {
-                    name: "file".to_string(),
-                    kind: fuchsia_fs::directory::DirentKind::File
-                }]
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_file() {
-        let (_env, sub_dir) = TestEnv::new().await;
-
-        for path in ["dir1/file", "dir1/file/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_file(sub_dir.clone(), path, fio::PERM_READABLE);
+            let proxy =
+                fuchsia_fs::directory::open_file(&sub_dir, path, fio::PERM_READABLE).await.unwrap();
             assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"bloblob".to_vec())
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_rejects_invalid_flags() {
+    async fn non_meta_subdir_deprecated_open_directory() {
         let (_env, sub_dir) = TestEnv::new().await;
-
-        for invalid_flags in
-            [fio::Flags::FLAG_MUST_CREATE, fio::Flags::FLAG_MAYBE_CREATE, fio::PERM_WRITABLE]
-        {
-            let proxy = vfs::directory::serve(sub_dir.clone(), fio::PERM_READABLE | invalid_flags);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
+        for path in ["dir1", "dir1/"] {
+            let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
+            sub_dir
+                .deprecated_open(
+                    fio::OpenFlags::RIGHT_READABLE,
+                    Default::default(),
+                    path,
+                    server_end.into_channel().into(),
+                )
+                .unwrap();
+            assert_eq!(
+                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
+                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
             );
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_rejects_file_flags() {
+    async fn non_meta_subdir_deprecated_open_file() {
         let (_env, sub_dir) = TestEnv::new().await;
-
-        // Requesting to open with `PROTOCOL_FILE` should return a `NOT_FILE` error.
-        {
-            let proxy = vfs::directory::serve(sub_dir.clone(), fio::Flags::PROTOCOL_FILE);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_FILE, .. })
-            );
-        }
-
-        // Opening with file flags is also invalid.
-        for file_flags in [fio::Flags::FILE_APPEND, fio::Flags::FILE_TRUNCATE] {
-            let proxy = vfs::directory::serve(sub_dir.clone(), fio::PERM_READABLE | file_flags);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::INVALID_ARGS, .. })
-            );
-        }
+        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
+        sub_dir
+            .deprecated_open(
+                fio::OpenFlags::RIGHT_READABLE,
+                Default::default(),
+                "dir1/file",
+                server_end.into_channel().into(),
+            )
+            .unwrap();
+        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"bloblob".to_vec());
     }
 }
diff --git a/src/sys/pkg/lib/package-directory/src/root_dir.rs b/src/sys/pkg/lib/package-directory/src/root_dir.rs
index 183f4373..04af861 100644
--- a/src/sys/pkg/lib/package-directory/src/root_dir.rs
+++ b/src/sys/pkg/lib/package-directory/src/root_dir.rs
@@ -18,10 +18,7 @@
 use vfs::directory::traversal_position::TraversalPosition;
 use vfs::execution_scope::ExecutionScope;
 use vfs::file::vmo::VmoFile;
-use vfs::path::Path as VfsPath;
-use vfs::{
-    immutable_attributes, CreationMode, ObjectRequestRef, ProtocolsExt as _, ToObjectRequest,
-};
+use vfs::{immutable_attributes, ObjectRequestRef, ProtocolsExt as _, ToObjectRequest as _};
 
 /// The root directory of Fuchsia package.
 #[derive(Debug)]
@@ -272,27 +269,29 @@
         self: Arc<Self>,
         scope: ExecutionScope,
         flags: fio::OpenFlags,
-        path: VfsPath,
+        path: vfs::Path,
         server_end: ServerEnd<fio::NodeMarker>,
     ) {
         let flags = flags & !fio::OpenFlags::POSIX_WRITABLE;
         let describe = flags.contains(fio::OpenFlags::DESCRIBE);
-
-        if flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT) {
+        // Disallow creating a writable connection to this node or any children. We also disallow
+        // file flags which do not apply. Note that the latter is not required for Open3, as we
+        // require writable rights for the latter flags already.
+        if flags.intersects(fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::TRUNCATE) {
             let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_SUPPORTED);
             return;
         }
+        // The VFS should disallow file creation since we cannot serve a mutable connection.
+        assert!(!flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT));
 
         if path.is_empty() {
             flags.to_object_request(server_end).handle(|object_request| {
-                if flags.intersects(
-                    fio::OpenFlags::RIGHT_WRITABLE
-                        | fio::OpenFlags::TRUNCATE
-                        | fio::OpenFlags::APPEND,
-                ) {
+                // NOTE: Some older CTF tests still rely on being able to use the APPEND flag in
+                // some cases, so we cannot check this flag above. Appending is still not possible.
+                // As we plan to remove this method entirely, we can just leave this for now.
+                if flags.intersects(fio::OpenFlags::APPEND) {
                     return Err(zx::Status::NOT_SUPPORTED);
                 }
-
                 object_request
                     .take()
                     .create_connection_sync::<ImmutableConnection<_>, _>(scope, self, flags);
@@ -328,7 +327,7 @@
                     vfs::file::serve(file, scope, &flags, object_request)
                 });
             } else {
-                let () = MetaAsDir::new(self).open(scope, flags, VfsPath::dot(), server_end);
+                let () = MetaAsDir::new(self).open(scope, flags, vfs::Path::dot(), server_end);
             }
             return;
         }
@@ -349,7 +348,7 @@
             }
 
             if let Some(subdir) = self.get_meta_subdir(canonical_path.to_string() + "/") {
-                let () = subdir.open(scope, flags, VfsPath::dot(), server_end);
+                let () = subdir.open(scope, flags, vfs::Path::dot(), server_end);
                 return;
             }
 
@@ -366,7 +365,7 @@
         }
 
         if let Some(subdir) = self.get_non_meta_subdir(canonical_path.to_string() + "/") {
-            let () = subdir.open(scope, flags, VfsPath::dot(), server_end);
+            let () = subdir.open(scope, flags, vfs::Path::dot(), server_end);
             return;
         }
 
@@ -376,21 +375,19 @@
     fn open3(
         self: Arc<Self>,
         scope: ExecutionScope,
-        path: VfsPath,
+        path: vfs::Path,
         flags: fio::Flags,
         object_request: ObjectRequestRef<'_>,
     ) -> Result<(), zx::Status> {
-        if flags.creation_mode() != CreationMode::Never {
+        // Disallow creating a mutable connection to this node or any children.
+        if flags.intersects(crate::MUTABLE_FLAGS) {
             return Err(zx::Status::NOT_SUPPORTED);
         }
+        // The VFS should disallow file creation or append/truncate as these require mutable rights.
+        assert!(flags.creation_mode() == vfs::CreationMode::Never);
+        assert!(!flags.intersects(fio::Flags::FILE_APPEND | fio::Flags::FILE_TRUNCATE));
 
         if path.is_empty() {
-            if let Some(rights) = flags.rights() {
-                if rights.intersects(fio::Operations::WRITE_BYTES) {
-                    return Err(zx::Status::NOT_SUPPORTED);
-                }
-            }
-
             // `ImmutableConnection` checks that only directory flags are specified.
             object_request
                 .take()
@@ -427,7 +424,7 @@
                 })?;
                 vfs::file::serve(file, scope, &flags, object_request)
             } else if flags.is_node() || flags.is_dir_allowed() {
-                MetaAsDir::new(self).open3(scope, VfsPath::dot(), flags, object_request)
+                MetaAsDir::new(self).open3(scope, vfs::Path::dot(), flags, object_request)
             } else {
                 // Reject opening as a symlink.
                 Err(zx::Status::WRONG_TYPE)
@@ -440,7 +437,7 @@
             }
 
             if let Some(subdir) = self.get_meta_subdir(canonical_path.to_string() + "/") {
-                return subdir.open3(scope, VfsPath::dot(), flags, object_request);
+                return subdir.open3(scope, vfs::Path::dot(), flags, object_request);
             }
             return Err(zx::Status::NOT_FOUND);
         }
@@ -450,7 +447,7 @@
         }
 
         if let Some(subdir) = self.get_non_meta_subdir(canonical_path.to_string() + "/") {
-            return subdir.open3(scope, VfsPath::dot(), flags, object_request);
+            return subdir.open3(scope, vfs::Path::dot(), flags, object_request);
         }
 
         Err(zx::Status::NOT_FOUND)
@@ -543,21 +540,18 @@
     use fuchsia_fs::directory::{DirEntry, DirentKind};
     use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
     use fuchsia_pkg_testing::PackageBuilder;
-    use futures::{StreamExt as _, TryStreamExt as _};
+    use futures::TryStreamExt as _;
     use pretty_assertions::assert_eq;
-    use std::convert::TryInto as _;
     use std::io::Cursor;
-    use vfs::directory::entry::GetEntryInfo;
-    use vfs::directory::entry_container::Directory;
-    use vfs::node::Node;
-    use vfs::ObjectRequest;
+    use vfs::directory::entry_container::Directory as _;
 
     struct TestEnv {
         _blobfs_fake: FakeBlobfs,
+        root_dir: Arc<RootDir<blobfs::Client>>,
     }
 
     impl TestEnv {
-        async fn with_subpackages_content(
+        async fn with_subpackages(
             subpackages_content: Option<&[u8]>,
         ) -> (Self, Arc<RootDir<blobfs::Client>>) {
             let mut pkg = PackageBuilder::new("base-package-0")
@@ -577,11 +571,12 @@
             }
 
             let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
-            (Self { _blobfs_fake: blobfs_fake }, root_dir)
+            (Self { _blobfs_fake: blobfs_fake, root_dir: root_dir.clone() }, root_dir)
         }
 
-        async fn new() -> (Self, Arc<RootDir<blobfs::Client>>) {
-            Self::with_subpackages_content(None).await
+        async fn new() -> (Self, fio::DirectoryProxy) {
+            let (env, root) = Self::with_subpackages(None).await;
+            (env, vfs::directory::serve_read_only(root))
         }
     }
 
@@ -618,7 +613,7 @@
 
     #[fuchsia_async::run_singlethreaded(test)]
     async fn new_initializes_maps() {
-        let (_env, root_dir) = TestEnv::new().await;
+        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
 
         let meta_files = HashMap::from([
             (String::from("meta/contents"), MetaFileLocation { offset: 4096, length: 148 }),
@@ -700,7 +695,7 @@
 
     #[fuchsia_async::run_singlethreaded(test)]
     async fn read_file() {
-        let (_env, root_dir) = TestEnv::new().await;
+        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
 
         assert_eq!(root_dir.read_file("resource").await.unwrap().as_slice(), b"blob-contents");
         assert_eq!(root_dir.read_file("meta/file").await.unwrap().as_slice(), b"meta-contents0");
@@ -712,7 +707,7 @@
 
     #[fuchsia_async::run_singlethreaded(test)]
     async fn has_file() {
-        let (_env, root_dir) = TestEnv::new().await;
+        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
 
         assert!(root_dir.has_file("resource"));
         assert!(root_dir.has_file("meta/file"));
@@ -721,7 +716,7 @@
 
     #[fuchsia_async::run_singlethreaded(test)]
     async fn external_file_hashes() {
-        let (_env, root_dir) = TestEnv::new().await;
+        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
 
         let mut actual = root_dir.external_file_hashes().copied().collect::<Vec<_>>();
         actual.sort();
@@ -736,7 +731,7 @@
 
     #[fuchsia_async::run_singlethreaded(test)]
     async fn path() {
-        let (_env, root_dir) = TestEnv::new().await;
+        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
 
         assert_eq!(
             root_dir.path().await.unwrap(),
@@ -752,31 +747,63 @@
         )]);
         let mut subpackages_bytes = vec![];
         let () = subpackages.serialize(&mut subpackages_bytes).unwrap();
-        let (_env, root_dir) = TestEnv::with_subpackages_content(Some(&*subpackages_bytes)).await;
+        let (_env, root_dir) = TestEnv::with_subpackages(Some(&*subpackages_bytes)).await;
 
         assert_eq!(root_dir.subpackages().await.unwrap(), subpackages);
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
     async fn subpackages_absent() {
-        let (_env, root_dir) = TestEnv::with_subpackages_content(None).await;
+        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
 
         assert_eq!(root_dir.subpackages().await.unwrap(), fuchsia_pkg::MetaSubpackages::default());
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
     async fn subpackages_error() {
-        let (_env, root_dir) = TestEnv::with_subpackages_content(Some(b"invalid-json")).await;
+        let (_env, root_dir) = TestEnv::with_subpackages(Some(b"invalid-json")).await;
 
         assert_matches!(root_dir.subpackages().await, Err(SubpackagesError::Parse(_)));
     }
 
+    /// Ensure connections to a [`RootDir`] cannot be created as mutable (i.e. with
+    /// [`fio::PERM_WRITABLE`]). This ensures that the VFS will disallow any attempts to create a
+    /// new file/directory, modify the attributes of any nodes, open any files as writable.
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_get_attributes() {
-        let (_env, root_dir) = TestEnv::new().await;
+    async fn root_dir_cannot_be_served_as_mutable() {
+        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
+        let (proxy, server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
+        let request = fio::PERM_WRITABLE.to_object_request(server);
+        request.handle(|request: &mut vfs::ObjectRequest| {
+            root_dir.open3(ExecutionScope::new(), vfs::Path::dot(), fio::PERM_WRITABLE, request)
+        });
+        assert_matches!(
+            proxy.take_event_stream().try_next().await,
+            Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
+        );
+    }
 
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_readdir() {
+        let (_env, root_dir) = TestEnv::new().await;
         assert_eq!(
-            Node::get_attributes(root_dir.as_ref(), fio::NodeAttributesQuery::all()).await.unwrap(),
+            fuchsia_fs::directory::readdir_inclusive(&root_dir).await.unwrap(),
+            vec![
+                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
+                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
+                DirEntry { name: "meta".to_string(), kind: DirentKind::Directory },
+                DirEntry { name: "resource".to_string(), kind: DirentKind::File }
+            ]
+        );
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_get_attributes() {
+        let (_env, root_dir) = TestEnv::new().await;
+        let (mutable_attributes, immutable_attributes) =
+            root_dir.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap().unwrap();
+        assert_eq!(
+            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
             immutable_attributes!(
                 fio::NodeAttributesQuery::all(),
                 Immutable {
@@ -789,174 +816,53 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_entry_info() {
+    async fn root_dir_watch_not_supported() {
         let (_env, root_dir) = TestEnv::new().await;
-
-        assert_eq!(
-            GetEntryInfo::entry_info(root_dir.as_ref()),
-            EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
-        );
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_read_dirents() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        let (pos, sealed) = Directory::read_dirents(
-            root_dir.as_ref(),
-            &TraversalPosition::Start,
-            Box::new(crate::tests::FakeSink::new(4)),
-        )
-        .await
-        .expect("read_dirents failed");
-
-        assert_eq!(
-            crate::tests::FakeSink::from_sealed(sealed).entries,
-            vec![
-                (".".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
-                ("dir".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
-                ("meta".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)),
-                ("resource".to_string(), EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File))
-            ]
-        );
-        assert_eq!(pos, TraversalPosition::End);
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_register_watcher_not_supported() {
-        let (_env, root_dir) = TestEnv::new().await;
-
         let (_client, server) = fidl::endpoints::create_endpoints();
-
-        assert_eq!(
-            Directory::register_watcher(
-                root_dir,
-                ExecutionScope::new(),
-                fio::WatchMask::empty(),
-                server.try_into().unwrap(),
-            ),
-            Err(zx::Status::NOT_SUPPORTED)
-        );
+        let status =
+            zx::Status::from_raw(root_dir.watch(fio::WatchMask::empty(), 0, server).await.unwrap());
+        assert_eq!(status, zx::Status::NOT_SUPPORTED);
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_rejects_invalid_flags() {
+    async fn root_dir_open_non_meta_file() {
         let (_env, root_dir) = TestEnv::new().await;
-
-        for forbidden_flag in [
-            fio::OpenFlags::RIGHT_WRITABLE,
-            fio::OpenFlags::CREATE,
-            fio::OpenFlags::CREATE_IF_ABSENT,
-            fio::OpenFlags::TRUNCATE,
-            fio::OpenFlags::APPEND,
-        ] {
-            let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
-
-            root_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::DESCRIBE | forbidden_flag,
-                VfsPath::dot(),
-                server_end.into_channel().into(),
-            );
-
-            assert_matches!(
-                proxy.take_event_stream().next().await,
-                Some(Ok(fio::DirectoryEvent::OnOpen_{ s, info: None}))
-                    if s == zx::Status::NOT_SUPPORTED.into_raw()
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_self() {
-        let (_env, root_dir) = TestEnv::new().await;
-        let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
-
-        root_dir.open(
-            ExecutionScope::new(),
-            fio::OpenFlags::RIGHT_READABLE,
-            VfsPath::dot(),
-            server_end.into_channel().into(),
-        );
-
-        assert_eq!(
-            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-            vec![
-                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
-                DirEntry { name: "meta".to_string(), kind: DirentKind::Directory },
-                DirEntry { name: "resource".to_string(), kind: DirentKind::File }
-            ]
-        );
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_non_meta_file() {
-        let (_env, root_dir) = TestEnv::new().await;
-
         for path in ["resource", "resource/"] {
-            let (proxy, server_end) = create_proxy();
-
-            root_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end,
-            );
-
-            assert_eq!(
-                fuchsia_fs::file::read(&fio::FileProxy::from_channel(
-                    proxy.into_channel().unwrap()
-                ))
+            let proxy = fuchsia_fs::directory::open_file(&root_dir, path, fio::PERM_READABLE)
                 .await
-                .unwrap(),
-                b"blob-contents".to_vec()
-            );
+                .unwrap();
+            assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"blob-contents".to_vec());
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_meta_as_file() {
-        let (_env, root_dir) = TestEnv::new().await;
-
+    async fn root_dir_open_meta_as_file() {
+        let (env, root_dir) = TestEnv::new().await;
         for path in ["meta", "meta/"] {
-            let (proxy, server_end) = create_proxy::<fio::FileMarker>();
-
-            root_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::NOT_DIRECTORY,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
+            let proxy = fuchsia_fs::directory::open_file(&root_dir, path, fio::PERM_READABLE)
+                .await
+                .unwrap();
             assert_eq!(
                 fuchsia_fs::file::read(&proxy).await.unwrap(),
-                root_dir.hash.to_string().as_bytes()
+                env.root_dir.hash.to_string().as_bytes()
             );
-
-            // Cloning meta_as_file yields meta_as_file
+            // Ensure the connection is cloned correctly (i.e. we don't get meta-as-dir).
             let (cloned_proxy, server_end) = create_proxy::<fio::FileMarker>();
-            let () = proxy.clone(server_end.into_channel().into()).unwrap();
+            proxy.clone(server_end.into_channel().into()).unwrap();
             assert_eq!(
                 fuchsia_fs::file::read(&cloned_proxy).await.unwrap(),
-                root_dir.hash.to_string().as_bytes()
+                env.root_dir.hash.to_string().as_bytes()
             );
         }
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_meta_as_dir() {
+    async fn root_dir_open_meta_as_dir() {
         let (_env, root_dir) = TestEnv::new().await;
-
         for path in ["meta", "meta/"] {
-            let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
-
-            root_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::DIRECTORY,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
+            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
+                .await
+                .unwrap();
             assert_eq!(
                 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
                 vec![
@@ -967,10 +873,9 @@
                     DirEntry { name: "package".to_string(), kind: DirentKind::File },
                 ]
             );
-
-            // Cloning meta_as_dir yields meta_as_dir
+            // Ensure the connection is cloned correctly (i.e. we don't get meta-as-file).
             let (cloned_proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
-            let () = proxy.clone(server_end.into_channel().into()).unwrap();
+            proxy.clone(server_end.into_channel().into()).unwrap();
             assert_eq!(
                 fuchsia_fs::directory::readdir(&cloned_proxy).await.unwrap(),
                 vec![
@@ -985,19 +890,195 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_meta_as_node_reference() {
+    async fn root_dir_open_meta_as_node() {
         let (_env, root_dir) = TestEnv::new().await;
+        for path in ["meta", "meta/"] {
+            let proxy = fuchsia_fs::directory::open_node(
+                &root_dir,
+                path,
+                fio::Flags::PROTOCOL_NODE | fio::Flags::PERM_GET_ATTRIBUTES,
+            )
+            .await
+            .unwrap();
+            let (mutable_attributes, immutable_attributes) = proxy
+                .get_attributes(
+                    fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
+                )
+                .await
+                .unwrap()
+                .unwrap();
+            assert_eq!(
+                fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
+                immutable_attributes!(
+                    fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
+                    Immutable {
+                        protocols: fio::NodeProtocolKinds::DIRECTORY,
+                        abilities: crate::DIRECTORY_ABILITIES
+                    }
+                )
+            );
+        }
+    }
 
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_open_meta_as_symlink_wrong_type() {
+        let (_env, root_dir) = TestEnv::new().await;
+        // Opening as symlink should return an error
+        for path in ["meta", "meta/"] {
+            let (proxy, server_end) = create_proxy::<fio::SymlinkMarker>();
+            root_dir
+                .open(
+                    path,
+                    fio::Flags::PROTOCOL_SYMLINK | fio::Flags::FLAG_SEND_REPRESENTATION,
+                    &Default::default(),
+                    server_end.into_channel(),
+                )
+                .unwrap();
+            assert_matches!(
+                proxy.take_event_stream().try_next().await,
+                Err(fidl::Error::ClientChannelClosed { status: zx::Status::WRONG_TYPE, .. })
+            );
+        }
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_open_meta_file() {
+        let (_env, root_dir) = TestEnv::new().await;
+        for path in ["meta/file", "meta/file/"] {
+            let proxy = fuchsia_fs::directory::open_file(&root_dir, path, fio::PERM_READABLE)
+                .await
+                .unwrap();
+            assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"meta-contents0".to_vec());
+        }
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_open_meta_subdir() {
+        let (_env, root_dir) = TestEnv::new().await;
+        for path in ["meta/dir", "meta/dir/"] {
+            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
+                .await
+                .unwrap();
+            assert_eq!(
+                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
+                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
+            );
+        }
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_open_non_meta_subdir() {
+        let (_env, root_dir) = TestEnv::new().await;
+        for path in ["dir", "dir/"] {
+            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
+                .await
+                .unwrap();
+            assert_eq!(
+                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
+                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
+            );
+        }
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_deprecated_open_self() {
+        let (_env, root_dir) = TestEnv::new().await;
+        let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
+        root_dir
+            .deprecated_open(
+                fio::OpenFlags::RIGHT_READABLE,
+                Default::default(),
+                ".",
+                server_end.into_channel().into(),
+            )
+            .unwrap();
+        assert_eq!(
+            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
+            vec![
+                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
+                DirEntry { name: "meta".to_string(), kind: DirentKind::Directory },
+                DirEntry { name: "resource".to_string(), kind: DirentKind::File }
+            ]
+        );
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_deprecated_open_non_meta_file() {
+        let (_env, root_dir) = TestEnv::new().await;
+        let (proxy, server_end) = create_proxy();
+        root_dir
+            .deprecated_open(
+                fio::OpenFlags::RIGHT_READABLE,
+                Default::default(),
+                "resource",
+                server_end,
+            )
+            .unwrap();
+        assert_eq!(
+            fuchsia_fs::file::read(&fio::FileProxy::from_channel(proxy.into_channel().unwrap()))
+                .await
+                .unwrap(),
+            b"blob-contents".to_vec()
+        );
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_deprecated_open_meta_as_file() {
+        let (env, root_dir) = TestEnv::new().await;
+        let (proxy, server_end) = create_proxy::<fio::FileMarker>();
+        root_dir
+            .deprecated_open(
+                fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::NOT_DIRECTORY,
+                Default::default(),
+                "meta",
+                server_end.into_channel().into(),
+            )
+            .unwrap();
+        assert_eq!(
+            fuchsia_fs::file::read(&proxy).await.unwrap(),
+            env.root_dir.hash.to_string().as_bytes()
+        );
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_deprecated_open_meta_as_dir() {
+        let (_env, root_dir) = TestEnv::new().await;
+        for path in ["meta", "meta/"] {
+            let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
+            root_dir
+                .deprecated_open(
+                    fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::DIRECTORY,
+                    Default::default(),
+                    path,
+                    server_end.into_channel().into(),
+                )
+                .unwrap();
+            assert_eq!(
+                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
+                vec![
+                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
+                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
+                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
+                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
+                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
+                ]
+            );
+        }
+    }
+
+    #[fuchsia_async::run_singlethreaded(test)]
+    async fn root_dir_deprecated_open_meta_as_node_reference() {
+        let (_env, root_dir) = TestEnv::new().await;
         for path in ["meta", "meta/"] {
             let (proxy, server_end) = create_proxy::<fio::NodeMarker>();
-
-            root_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::NODE_REFERENCE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
+            root_dir
+                .deprecated_open(
+                    fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::NODE_REFERENCE,
+                    Default::default(),
+                    path,
+                    server_end.into_channel().into(),
+                )
+                .unwrap();
             // Check that open as a node reference passed by calling `get_attr()` on the proxy.
             // The returned attributes should indicate the meta is a directory.
             let (status, attr) = proxy.get_attr().await.expect("get_attr failed");
@@ -1007,234 +1088,33 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_meta_file() {
+    async fn root_dir_deprecated_open_meta_file() {
         let (_env, root_dir) = TestEnv::new().await;
-
-        for path in ["meta/file", "meta/file/"] {
-            let (proxy, server_end) = create_proxy::<fio::FileMarker>();
-
-            root_dir.clone().open(
-                ExecutionScope::new(),
+        let (proxy, server_end) = create_proxy::<fio::FileMarker>();
+        root_dir
+            .deprecated_open(
                 fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
+                Default::default(),
+                "meta/file",
                 server_end.into_channel().into(),
-            );
-
-            assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"meta-contents0".to_vec());
-        }
+            )
+            .unwrap();
+        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"meta-contents0".to_vec());
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_meta_subdir() {
+    async fn root_dir_deprecated_open_meta_subdir() {
         let (_env, root_dir) = TestEnv::new().await;
-
         for path in ["meta/dir", "meta/dir/"] {
             let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
-
-            root_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
-            assert_eq!(
-                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open_non_meta_subdir() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        for path in ["dir", "dir/"] {
-            let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
-
-            root_dir.clone().open(
-                ExecutionScope::new(),
-                fio::OpenFlags::RIGHT_READABLE,
-                VfsPath::validate_and_split(path).unwrap(),
-                server_end.into_channel().into(),
-            );
-
-            assert_eq!(
-                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_self() {
-        let (_env, root_dir) = TestEnv::new().await;
-        let proxy = vfs::directory::serve(root_dir, fio::PERM_READABLE);
-        assert_eq!(
-            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-            vec![
-                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
-                DirEntry { name: "meta".to_string(), kind: DirentKind::Directory },
-                DirEntry { name: "resource".to_string(), kind: DirentKind::File }
-            ]
-        );
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_non_meta_file() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        for path in ["resource", "resource/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_file(root_dir.clone(), path, fio::PERM_READABLE);
-            assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"blob-contents".to_vec());
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_meta_as_file() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        for path in ["meta", "meta/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_file(
-                root_dir.clone(),
-                path,
-                fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
-            );
-            assert_eq!(
-                fuchsia_fs::file::read(&proxy).await.unwrap(),
-                root_dir.hash.to_string().as_bytes()
-            );
-
-            // Cloning meta_as_file yields meta_as_file
-            let (cloned_proxy, server_end) = create_proxy::<fio::FileMarker>();
-            let () = proxy.clone(server_end.into_channel().into()).unwrap();
-            assert_eq!(
-                fuchsia_fs::file::read(&cloned_proxy).await.unwrap(),
-                root_dir.hash.to_string().as_bytes()
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_meta_as_dir() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        for path in ["meta", "meta/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_directory(
-                root_dir.clone(),
-                path,
-                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
-            );
-            assert_eq!(
-                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
-                vec![
-                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
-                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
-                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
-                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
-                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
-                ]
-            );
-
-            // Cloning meta_as_dir yields meta_as_dir
-            let (cloned_proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
-            let () = proxy.clone(server_end.into_channel().into()).unwrap();
-            assert_eq!(
-                fuchsia_fs::directory::readdir(&cloned_proxy).await.unwrap(),
-                vec![
-                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
-                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
-                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
-                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
-                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
-                ]
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_meta_as_node_reference() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        for path in ["meta", "meta/"] {
-            let (proxy, server_end) = create_proxy::<fio::NodeMarker>();
-            let scope = ExecutionScope::new();
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let flags = fio::Flags::PROTOCOL_NODE
-                | fio::Flags::PERM_GET_ATTRIBUTES
-                | fio::Flags::FLAG_SEND_REPRESENTATION;
-            let options = fio::Options {
-                attributes: Some(fio::NodeAttributesQuery::PROTOCOLS),
-                ..Default::default()
-            };
-            ObjectRequest::new(flags, &options, server_end.into())
-                .handle(|req| root_dir.clone().open3(scope, path, flags, req));
-
-            let event = proxy
-                .take_event_stream()
-                .try_next()
-                .await
-                .expect("take_event_stream failed")
-                .expect("expected an OnRepresentation event");
-            let representation = match event {
-                fio::NodeEvent::OnRepresentation { payload } => payload,
-                fio::NodeEvent::OnOpen_ { .. } => panic!("unexpected OnOpen representation"),
-                fio::NodeEvent::_UnknownEvent { ordinal, .. } => panic!("unknown event {ordinal}"),
-            };
-            assert_matches!(representation,
-                fio::Representation::Node(fio::NodeInfo {
-                    attributes: Some(node_attributes),
-                    ..
-                })
-                if node_attributes == immutable_attributes!(
-                    fio::NodeAttributesQuery::PROTOCOLS,
-                    Immutable { protocols: fio::NodeProtocolKinds::DIRECTORY, abilities: crate::DIRECTORY_ABILITIES }
+            root_dir
+                .deprecated_open(
+                    fio::OpenFlags::RIGHT_READABLE,
+                    Default::default(),
+                    path,
+                    server_end.into_channel().into(),
                 )
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_meta_as_symlink_wrong_type() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        // Opening as symlink should return an error
-        for path in ["meta", "meta/"] {
-            let (proxy, server_end) = create_proxy::<fio::SymlinkMarker>();
-            let scope = ExecutionScope::new();
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let flags = fio::Flags::PROTOCOL_SYMLINK;
-            ObjectRequest::new(flags, &fio::Options::default(), server_end.into())
-                .handle(|req| root_dir.clone().open3(scope, path, flags, req));
-
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::WRONG_TYPE, .. })
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_meta_file() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        for path in ["meta/file", "meta/file/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_file(root_dir.clone(), path, fio::PERM_READABLE);
-            assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"meta-contents0".to_vec());
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_meta_subdir() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        for path in ["meta/dir", "meta/dir/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_directory(root_dir.clone(), path, fio::PERM_READABLE);
+                .unwrap();
             assert_eq!(
                 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
                 vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
@@ -1243,57 +1123,22 @@
     }
 
     #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_non_meta_subdir() {
+    async fn root_dir_deprecated_open_non_meta_subdir() {
         let (_env, root_dir) = TestEnv::new().await;
-
         for path in ["dir", "dir/"] {
-            let path = VfsPath::validate_and_split(path).unwrap();
-            let proxy = vfs::serve_directory(root_dir.clone(), path, fio::PERM_READABLE);
+            let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
+            root_dir
+                .deprecated_open(
+                    fio::OpenFlags::RIGHT_READABLE,
+                    Default::default(),
+                    path,
+                    server_end.into_channel().into(),
+                )
+                .unwrap();
             assert_eq!(
                 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
                 vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
             );
         }
     }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_rejects_invalid_flags() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        for invalid_flags in
-            [fio::Flags::FLAG_MUST_CREATE, fio::Flags::FLAG_MAYBE_CREATE, fio::PERM_WRITABLE]
-        {
-            let proxy = vfs::directory::serve(
-                root_dir.clone(),
-                fio::Flags::FLAG_SEND_REPRESENTATION | invalid_flags,
-            );
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
-            );
-        }
-    }
-
-    #[fuchsia_async::run_singlethreaded(test)]
-    async fn directory_entry_open3_rejects_file_flags() {
-        let (_env, root_dir) = TestEnv::new().await;
-
-        // Requesting to open with `PROTOCOL_FILE` should return a `NOT_FILE` error.
-        {
-            let proxy = vfs::directory::serve(root_dir.clone(), fio::Flags::PROTOCOL_FILE);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_FILE, .. })
-            );
-        }
-
-        // Opening with file flags is also invalid.
-        for file_flags in [fio::Flags::FILE_APPEND, fio::Flags::FILE_TRUNCATE] {
-            let proxy = vfs::directory::serve(root_dir.clone(), file_flags);
-            assert_matches!(
-                proxy.take_event_stream().try_next().await,
-                Err(fidl::Error::ClientChannelClosed { status: zx::Status::INVALID_ARGS, .. })
-            );
-        }
-    }
 }