| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/fdio.h> |
| #include <lib/fdio/limits.h> |
| #include <lib/vfs/cpp/vmo_file.h> |
| #include <unistd.h> |
| #include <zircon/processargs.h> |
| #include <zircon/status.h> |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include <gtest/gtest.h> |
| |
| namespace { |
| |
| void FillBuffer(char* buf, size_t size) { |
| for (size_t i = 0; i < size; i++) { |
| buf[i] = i % 256; |
| } |
| } |
| |
| zx::vmo MakeTestVmo() { |
| zx::vmo ret; |
| EXPECT_EQ(ZX_OK, zx::vmo::create(4096, 0, &ret)); |
| |
| char buf[4096]; |
| FillBuffer(buf, 4096); |
| EXPECT_EQ(ZX_OK, ret.write(buf, 0, 4096)); |
| return ret; |
| } |
| |
| fuchsia::io::FileSyncPtr OpenAsFile(vfs::internal::Node* node, async_dispatcher_t* dispatcher, |
| bool writable = false) { |
| zx::channel local, remote; |
| EXPECT_EQ(ZX_OK, zx::channel::create(0, &local, &remote)); |
| fuchsia::io::OpenFlags flags = fuchsia::io::OpenFlags::RIGHT_READABLE; |
| if (writable) { |
| flags |= fuchsia::io::OpenFlags::RIGHT_WRITABLE; |
| } |
| EXPECT_EQ(ZX_OK, node->Serve(flags, std::move(remote), dispatcher)); |
| fuchsia::io::FileSyncPtr ret; |
| ret.Bind(std::move(local)); |
| return ret; |
| } |
| |
| std::vector<uint8_t> ReadVmo(const zx::vmo& vmo, size_t offset, size_t length) { |
| std::vector<uint8_t> ret; |
| ret.resize(length); |
| EXPECT_EQ(ZX_OK, vmo.read(ret.data(), offset, length)); |
| return ret; |
| } |
| |
| TEST(VmoFile, ConstructTransferOwnership) { |
| vfs::VmoFile file(MakeTestVmo(), 24, 1000); |
| std::vector<uint8_t> output; |
| EXPECT_EQ(ZX_OK, file.ReadAt(1000, 0, &output)); |
| EXPECT_EQ(1000u, output.size()); |
| } |
| |
| TEST(VmoFile, Reading) { |
| // Create a VmoFile wrapping 1000 bytes starting at offset 24 of the vmo. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 24, 1000, vfs::VmoFile::WriteOption::READ_ONLY, |
| vfs::VmoFile::Sharing::NONE); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher()); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Reading the VMO from offset 24 should match reading the file from offset 0. |
| { |
| fuchsia::io::File2_Read_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->Read(500, &result)); |
| ASSERT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(ReadVmo(test_vmo, 24, 500), result.response().data); |
| } |
| { |
| fuchsia::io::File2_Read_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->Read(500, &result)); |
| ASSERT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(ReadVmo(test_vmo, 524, 500), result.response().data); |
| } |
| } |
| |
| TEST(VmoFile, GetAttrReadOnly) { |
| // Create a VmoFile wrapping 1000 bytes starting at offset 24 of the vmo. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 24, 1000, vfs::VmoFile::WriteOption::READ_ONLY, |
| vfs::VmoFile::Sharing::NONE); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher()); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| fuchsia::io::NodeAttributes attr; |
| zx_status_t status; |
| EXPECT_EQ(ZX_OK, file_ptr->GetAttr(&status, &attr)); |
| EXPECT_EQ(ZX_OK, status); |
| EXPECT_EQ(1000u, attr.content_size); |
| EXPECT_EQ(1000u, attr.storage_size); |
| EXPECT_EQ( |
| fuchsia::io::MODE_TYPE_FILE | static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_READABLE), |
| attr.mode); |
| } |
| |
| TEST(VmoFile, GetAttrWritable) { |
| // Create a VmoFile wrapping 1000 bytes starting at offset 24 of the vmo. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 24, 1000, vfs::VmoFile::WriteOption::WRITABLE, |
| vfs::VmoFile::Sharing::NONE); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher()); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| fuchsia::io::NodeAttributes attr; |
| zx_status_t status; |
| EXPECT_EQ(ZX_OK, file_ptr->GetAttr(&status, &attr)); |
| EXPECT_EQ(ZX_OK, status); |
| EXPECT_EQ(1000u, attr.content_size); |
| EXPECT_EQ(1000u, attr.storage_size); |
| EXPECT_EQ( |
| fuchsia::io::MODE_TYPE_FILE | static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_READABLE | |
| fuchsia::io::OpenFlags::RIGHT_WRITABLE), |
| attr.mode); |
| } |
| |
| TEST(VmoFile, ReadOnlyNoSharing) { |
| // Create a VmoFile wrapping 1000 bytes starting at offset 24 of the vmo. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 24, 1000, vfs::VmoFile::WriteOption::READ_ONLY, |
| vfs::VmoFile::Sharing::NONE); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher()); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Writes should fail, since the VMO is read-only. |
| { |
| std::vector<uint8_t> value{'a', 'b', 'c', 'd'}; |
| fuchsia::io::File2_WriteAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->WriteAt(value, 0, &result)); |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(ZX_ERR_BAD_HANDLE, result.err()); |
| } |
| |
| // Reading the VMO from offset 24 should match reading the file from offset 0. |
| { |
| std::vector<uint8_t> vmo_result = ReadVmo(test_vmo, 24, 1000); |
| fuchsia::io::File2_ReadAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->ReadAt(1000, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(vmo_result, result.response().data); |
| } |
| |
| // The file should appear as a regular file, the fact that a VMO is backing it |
| // is hidden. |
| fuchsia::io::NodeInfo info; |
| EXPECT_EQ(ZX_OK, file_ptr->Describe(&info)); |
| ASSERT_TRUE(info.is_file()); |
| } |
| |
| TEST(VmoFile, WritableNoSharing) { |
| // Create a VmoFile wrapping 1000 bytes starting at offset 24 of the vmo. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 24, 1000, vfs::VmoFile::WriteOption::WRITABLE, |
| vfs::VmoFile::Sharing::NONE); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher(), /*writable=*/true); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Writes should succeed. |
| { |
| std::vector<uint8_t> value{'a', 'b', 'c', 'd'}; |
| fuchsia::io::File2_WriteAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->WriteAt(value, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(value.size(), result.response().actual_count); |
| } |
| |
| // Reading the VMO from offset 24 should match reading the file from offset 0. |
| { |
| std::vector<uint8_t> vmo_result = ReadVmo(test_vmo, 24, 1000); |
| fuchsia::io::File2_ReadAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->ReadAt(1000, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(vmo_result, result.response().data); |
| EXPECT_EQ('a', result.response().data[0]); |
| } |
| |
| // The file should appear as a regular file, the fact that a VMO is backing it |
| // is hidden. |
| fuchsia::io::NodeInfo info; |
| EXPECT_EQ(ZX_OK, file_ptr->Describe(&info)); |
| ASSERT_TRUE(info.is_file()); |
| } |
| |
| TEST(VmoFile, ReadOnlyDuplicate) { |
| // Create a VmoFile wrapping 1000 bytes starting at offset 24 of the vmo. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 24, 1000); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher()); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Writes should fail, since the VMO is read-only. |
| { |
| std::vector<uint8_t> value{'a', 'b', 'c', 'd'}; |
| fuchsia::io::File2_WriteAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->WriteAt(value, 0, &result)); |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(ZX_ERR_BAD_HANDLE, result.err()); |
| } |
| |
| // Reading the VMO from offset 24 should match reading the file from offset 0. |
| { |
| std::vector<uint8_t> vmo_result = ReadVmo(test_vmo, 24, 1000); |
| fuchsia::io::File2_ReadAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->ReadAt(1000, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(vmo_result, result.response().data); |
| } |
| |
| // Describing the VMO duplicates the handle, and we can access the entire VMO. |
| fuchsia::io::NodeInfo info; |
| EXPECT_EQ(ZX_OK, file_ptr->Describe(&info)); |
| ASSERT_TRUE(info.is_vmofile()); |
| ASSERT_EQ(1000u, info.vmofile().length); |
| ASSERT_EQ(24u, info.vmofile().offset); |
| EXPECT_EQ(ReadVmo(test_vmo, 0, 4096), ReadVmo(info.vmofile().vmo, 0, 4096)); |
| |
| // Writing should fail on the new VMO. |
| EXPECT_NE(ZX_OK, info.vmofile().vmo.write("test", 0, 4)); |
| } |
| |
| TEST(VmoFile, WritableDuplicate) { |
| // Create a VmoFile wrapping 1000 bytes starting at offset 24 of the vmo. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 24, 1000, vfs::VmoFile::WriteOption::WRITABLE); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher(), /*writable=*/true); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Writes should succeed. |
| { |
| std::vector<uint8_t> value{'a', 'b', 'c', 'd'}; |
| fuchsia::io::File2_WriteAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->WriteAt(value, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(value.size(), result.response().actual_count); |
| } |
| |
| // Reading the VMO from offset 24 should match reading the file from offset 0. |
| { |
| std::vector<uint8_t> vmo_result = ReadVmo(test_vmo, 24, 1000); |
| fuchsia::io::File2_ReadAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->ReadAt(1000, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(vmo_result, result.response().data); |
| EXPECT_EQ('a', result.response().data[0]); |
| } |
| |
| // Describing the VMO duplicates the handle, and we can access the entire VMO. |
| fuchsia::io::NodeInfo info; |
| EXPECT_EQ(ZX_OK, file_ptr->Describe(&info)); |
| ASSERT_TRUE(info.is_vmofile()); |
| ASSERT_EQ(1000u, info.vmofile().length); |
| ASSERT_EQ(24u, info.vmofile().offset); |
| EXPECT_EQ(ReadVmo(test_vmo, 0, 4096), ReadVmo(info.vmofile().vmo, 0, 4096)); |
| |
| // TODO(fxbug.dev/45287): As part of fxbug.dev/85334 it was discovered that Describe leaks |
| // writable handles even if the connection lacks OPEN_RIGHT_WRITABLE. In the long term, if handles |
| // to the underlying VMO require specific rights, they should either be obtained via GetBuffer(), |
| // or we need to allow the VmoFile node itself query the connection rights (since these are |
| // currently not available when handling the Describe call). |
| } |
| |
| TEST(VmoFile, ReadOnlyCopyOnWrite) { |
| // Create a VmoFile wrapping the VMO. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 0, 4096, vfs::VmoFile::WriteOption::READ_ONLY, |
| vfs::VmoFile::Sharing::CLONE_COW); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher()); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Writes should fail, since the VMO is read-only. |
| { |
| std::vector<uint8_t> value{'a', 'b', 'c', 'd'}; |
| fuchsia::io::File2_WriteAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->WriteAt(value, 0, &result)); |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(ZX_ERR_BAD_HANDLE, result.err()); |
| } |
| |
| // Reading the VMO should match reading the file. |
| { |
| std::vector<uint8_t> vmo_result = ReadVmo(test_vmo, 0, 4096); |
| fuchsia::io::File2_ReadAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->ReadAt(4096, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(vmo_result, result.response().data); |
| } |
| |
| // Describing the VMO clones the handle, and we can access the entire VMO. |
| fuchsia::io::NodeInfo info; |
| EXPECT_EQ(ZX_OK, file_ptr->Describe(&info)); |
| ASSERT_TRUE(info.is_vmofile()); |
| ASSERT_EQ(4096u, info.vmofile().length); |
| ASSERT_EQ(0u, info.vmofile().offset); |
| EXPECT_EQ(ReadVmo(test_vmo, 0, 4096), ReadVmo(info.vmofile().vmo, 0, 4096)); |
| |
| // Writing should succeed on the new VMO, due to copy on write. |
| EXPECT_EQ(ZX_OK, info.vmofile().vmo.write("test", 0, 4)); |
| EXPECT_NE(ReadVmo(test_vmo, 0, 4096), ReadVmo(info.vmofile().vmo, 0, 4096)); |
| } |
| |
| TEST(VmoFile, WritableCopyOnWrite) { |
| // Create a VmoFile wrapping the VMO. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 0, 4096, vfs::VmoFile::WriteOption::WRITABLE, |
| vfs::VmoFile::Sharing::CLONE_COW); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher(), /*writable=*/true); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Writes should succeed. |
| { |
| std::vector<uint8_t> value{'a', 'b', 'c', 'd'}; |
| fuchsia::io::File2_WriteAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->WriteAt(value, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(value.size(), result.response().actual_count); |
| } |
| |
| // Reading the VMO should match reading the file. |
| { |
| std::vector<uint8_t> vmo_result = ReadVmo(test_vmo, 0, 4096); |
| fuchsia::io::File2_ReadAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->ReadAt(4096, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(vmo_result, result.response().data); |
| EXPECT_EQ('a', result.response().data[0]); |
| } |
| |
| // Describing the VMO duplicates the handle, and we can access the entire VMO. |
| fuchsia::io::NodeInfo info; |
| EXPECT_EQ(ZX_OK, file_ptr->Describe(&info)); |
| ASSERT_TRUE(info.is_vmofile()); |
| ASSERT_EQ(4096u, info.vmofile().length); |
| ASSERT_EQ(0u, info.vmofile().offset); |
| EXPECT_EQ(ReadVmo(test_vmo, 0, 4096), ReadVmo(info.vmofile().vmo, 0, 4096)); |
| |
| // Writing should succeed on the new VMO, due to copy on write. |
| EXPECT_EQ(ZX_OK, info.vmofile().vmo.write("test", 0, 4)); |
| EXPECT_NE(ReadVmo(test_vmo, 0, 4096), ReadVmo(info.vmofile().vmo, 0, 4096)); |
| } |
| |
| TEST(VmoFile, VmoWithNoRights) { |
| // Create a VmoFile wrapping 1000 bytes of the VMO starting at offset 24. |
| // The vmo we use has no rights, so reading, writing, and duplication will |
| // fail. |
| zx::vmo test_vmo = MakeTestVmo(); |
| zx::vmo bad_vmo; |
| ASSERT_EQ(ZX_OK, test_vmo.duplicate(0, &bad_vmo)); |
| vfs::VmoFile file(std::move(bad_vmo), 24, 1000, vfs::VmoFile::WriteOption::WRITABLE); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher(), /*writable=*/true); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Writes should fail. |
| { |
| std::vector<uint8_t> value{'a', 'b', 'c', 'd'}; |
| fuchsia::io::File2_WriteAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->WriteAt(value, 0, &result)); |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(ZX_ERR_ACCESS_DENIED, result.err()); |
| } |
| |
| // Reading should fail. |
| { |
| fuchsia::io::File2_ReadAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->ReadAt(1000, 0, &result)); |
| EXPECT_TRUE(result.is_err()); |
| EXPECT_EQ(ZX_ERR_ACCESS_DENIED, result.err()); |
| } |
| |
| // Describing the VMO should close the connection. |
| fuchsia::io::NodeInfo info; |
| EXPECT_EQ(ZX_ERR_PEER_CLOSED, file_ptr->Describe(&info)); |
| } |
| |
| TEST(VmoFile, UnalignedCopyOnWrite) { |
| // Create a VmoFile wrapping 1000 bytes of the VMO starting at offset 24. |
| // This offset is not page-aligned, so cloning will fail. |
| zx::vmo test_vmo = MakeTestVmo(); |
| vfs::VmoFile file(zx::unowned_vmo(test_vmo), 24, 1000, vfs::VmoFile::WriteOption::WRITABLE, |
| vfs::VmoFile::Sharing::CLONE_COW); |
| |
| async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto file_ptr = OpenAsFile(&file, loop.dispatcher(), /*writable=*/true); |
| ASSERT_TRUE(file_ptr.is_bound()); |
| |
| // Writes should succeed. |
| { |
| std::vector<uint8_t> value{'a', 'b', 'c', 'd'}; |
| fuchsia::io::File2_WriteAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->WriteAt(value, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(value.size(), result.response().actual_count); |
| } |
| |
| // Reading the VMO from offset 24 should match reading the file from offset 0. |
| { |
| std::vector<uint8_t> vmo_result = ReadVmo(test_vmo, 24, 1000); |
| fuchsia::io::File2_ReadAt_Result result; |
| EXPECT_EQ(ZX_OK, file_ptr->ReadAt(1000, 0, &result)); |
| EXPECT_TRUE(result.is_response()) << zx_status_get_string(result.err()); |
| EXPECT_EQ(vmo_result, result.response().data); |
| EXPECT_EQ('a', result.response().data[0]); |
| } |
| |
| // Describing the VMO should close the connection. |
| fuchsia::io::NodeInfo info; |
| EXPECT_EQ(ZX_ERR_PEER_CLOSED, file_ptr->Describe(&info)); |
| } |
| |
| } // namespace |