File
Blob: src/workerd/io/bundle-fs-test.c++
| 1 | // Copyright (c) 2024-2029 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #include <workerd/io/bundle-fs.h> |
| 6 | #include <workerd/tests/test-fixture.h> |
| 7 | |
| 8 | #include <capnp/message.h> |
| 9 | #include <kj/debug.h> |
| 10 | #include <kj/encoding.h> |
| 11 | #include <kj/test.h> |
| 12 | |
| 13 | namespace workerd { |
| 14 | namespace { |
| 15 | workerd::WorkerSource readConfig() { |
| 16 | |
| 17 | kj::Vector<WorkerSource::Module> modules(8); |
| 18 | |
| 19 | modules.add(WorkerSource::Module{.name = "a/esModule"_kj, |
| 20 | .content = WorkerSource::EsModule{.body = "this is an esm module"_kj}}); |
| 21 | |
| 22 | modules.add(WorkerSource::Module{.name = "a/commonJsModule"_kj, |
| 23 | .content = WorkerSource::CommonJsModule{.body = "this is a commonjs module"_kj}}); |
| 24 | |
| 25 | modules.add(WorkerSource::Module{ |
| 26 | .name = "b/text"_kj, .content = WorkerSource::TextModule{.body = "this is a text module"_kj}}); |
| 27 | |
| 28 | modules.add(WorkerSource::Module{.name = "b/data"_kj, |
| 29 | .content = WorkerSource::DataModule{.body = "this is a data module"_kj.asArray().asBytes()}}); |
| 30 | |
| 31 | modules.add(WorkerSource::Module{.name = "c/wasm"_kj, |
| 32 | .content = WorkerSource::WasmModule{.body = "this is a wasm module"_kj.asArray().asBytes()}}); |
| 33 | |
| 34 | modules.add(WorkerSource::Module{ |
| 35 | .name = "c/json"_kj, .content = WorkerSource::JsonModule{.body = "this is a json module"_kj}}); |
| 36 | |
| 37 | modules.add(WorkerSource::Module{.name = "a/pythonModule"_kj, |
| 38 | .content = WorkerSource::PythonModule{.body = "this is a python module"_kj}}); |
| 39 | |
| 40 | return workerd::WorkerSource(workerd::WorkerSource::ModulesSource{ |
| 41 | .mainModule = "worker"_kj, .modules = modules.releaseAsArray()}); |
| 42 | } |
| 43 | |
| 44 | KJ_TEST("The BundleDirectoryDelegate works") { |
| 45 | TestFixture fixture; |
| 46 | |
| 47 | fixture.runInIoContext([&](const TestFixture::Environment& env) { |
| 48 | auto config = readConfig(); |
| 49 | auto dir = getBundleDirectory(config); |
| 50 | |
| 51 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"a"}))); |
| 52 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"a", "esModule"}))); |
| 53 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"a", "commonJsModule"}))); |
| 54 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"b"}))); |
| 55 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"b", "text"}))); |
| 56 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"b", "data"}))); |
| 57 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"c"}))); |
| 58 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"c", "wasm"}))); |
| 59 | KJ_REQUIRE_NONNULL(dir->tryOpen(env.js, kj::Path({"c", "json"}))); |
| 60 | KJ_EXPECT(dir->tryOpen(env.js, kj::Path({"a", "foo"})) == kj::none); |
| 61 | KJ_EXPECT(dir->tryOpen(env.js, kj::Path({"zzz", "yyy"})) == kj::none); |
| 62 | |
| 63 | // Iterating over the directory should work. |
| 64 | size_t counter = 0; |
| 65 | for (auto& _ KJ_UNUSED: *dir.get()) { |
| 66 | counter++; |
| 67 | } |
| 68 | KJ_EXPECT(counter, 3); |
| 69 | |
| 70 | KJ_EXPECT(dir->count(env.js) == 3); |
| 71 | |
| 72 | auto maybeEsModule = dir->tryOpen(env.js, kj::Path({"a", "esModule"})); |
| 73 | auto& esmodule = KJ_ASSERT_NONNULL(maybeEsModule).get<kj::Rc<File>>(); |
| 74 | auto stat = esmodule->stat(env.js); |
| 75 | KJ_EXPECT(stat.type == FsType::FILE); |
| 76 | KJ_EXPECT(stat.size == 21); |
| 77 | |
| 78 | auto maybeFile = dir->tryOpen(env.js, kj::Path({"a", "commonJsModule"})); |
| 79 | auto& file = KJ_ASSERT_NONNULL(maybeFile).get<kj::Rc<File>>(); |
| 80 | |
| 81 | auto readText = file->readAllText(env.js).get<jsg::JsString>(); |
| 82 | KJ_EXPECT(readText == env.js.str("this is a commonjs module"_kj)); |
| 83 | |
| 84 | auto readBytes = file->readAllBytes(env.js).get<jsg::BufferSource>(); |
| 85 | KJ_EXPECT(readBytes.asArrayPtr() == "this is a commonjs module"_kjb); |
| 86 | |
| 87 | // Reading five bytes from offset 20 should return "odule". |
| 88 | kj::byte buffer[5]{}; |
| 89 | size_t r = file->read(env.js, 20, buffer); |
| 90 | KJ_EXPECT(r == 5); |
| 91 | KJ_EXPECT(kj::ArrayPtr<kj::byte>(buffer, r) == "odule"_kjb); |
| 92 | |
| 93 | // Attempt to read beyond EOF return nothing. |
| 94 | KJ_EXPECT(file->read(env.js, 100, buffer) == 0); |
| 95 | |
| 96 | // Attempts to modify anything should fail. |
| 97 | auto error = dir->remove(env.js, kj::Path({"a", "esModule"})).get<FsError>(); |
| 98 | KJ_EXPECT(error == FsError::READ_ONLY); |
| 99 | |
| 100 | // Attempting to create a file should fail. |
| 101 | auto maybeErr = dir->tryOpen(env.js, kj::Path({"a", "something", "else"}), |
| 102 | Directory::OpenOptions{ |
| 103 | .createAs = FsType::FILE, |
| 104 | }); |
| 105 | KJ_EXPECT(KJ_ASSERT_NONNULL(maybeErr).get<FsError>() == FsError::READ_ONLY); |
| 106 | }); |
| 107 | } |
| 108 | |
| 109 | KJ_TEST("Guarding against circular symlinks works") { |
| 110 | // This isn't the best location for this particular test since it is |
| 111 | // not specific to bundle-fs. However, the test needs the TestFixture |
| 112 | // and it's a bit of a pain to move it to the other test file so for |
| 113 | // now it will live here. |
| 114 | TestFixture fixture; |
| 115 | |
| 116 | fixture.runInIoContext([&](const TestFixture::Environment& env) { |
| 117 | // We don't need to set up a TmpDirStorageScope since we are running |
| 118 | // within a test fixture that sets up an IoContext... |
| 119 | auto vfs = newVirtualFileSystem(kj::heap<FsMap>(), getTmpDirectoryImpl()); |
| 120 | |
| 121 | // Set up circular symlinks |
| 122 | auto maybeTemp = KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///"_url)); |
| 123 | auto& tempDir = maybeTemp.get<kj::Rc<Directory>>(); |
| 124 | |
| 125 | KJ_EXPECT(tempDir->add(env.js, "a", vfs->newSymbolicLink(env.js, "file:///b"_url)) == kj::none); |
| 126 | KJ_EXPECT(tempDir->add(env.js, "b", vfs->newSymbolicLink(env.js, "file:///c"_url)) == kj::none); |
| 127 | KJ_EXPECT(tempDir->add(env.js, "c", vfs->newSymbolicLink(env.js, "file:///a"_url)) == kj::none); |
| 128 | KJ_EXPECT(tempDir->add(env.js, "d", vfs->newSymbolicLink(env.js, "file:///e"_url)) == kj::none); |
| 129 | |
| 130 | // This symlink goes no where. A kj::none should be returned. |
| 131 | KJ_EXPECT(vfs->resolve(env.js, "file:///d"_url) == kj::none); |
| 132 | |
| 133 | auto resolved = KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///a"_url)); |
| 134 | KJ_EXPECT(resolved.get<workerd::FsError>() == workerd::FsError::SYMLINK_DEPTH_EXCEEDED); |
| 135 | |
| 136 | auto resolvedStat = KJ_ASSERT_NONNULL(vfs->resolveStat(env.js, "file:///a"_url)); |
| 137 | KJ_EXPECT(resolvedStat.get<workerd::FsError>() == workerd::FsError::SYMLINK_DEPTH_EXCEEDED); |
| 138 | |
| 139 | resolved = KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///b"_url)); |
| 140 | KJ_EXPECT(resolved.get<workerd::FsError>() == workerd::FsError::SYMLINK_DEPTH_EXCEEDED); |
| 141 | |
| 142 | resolved = KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///c"_url)); |
| 143 | KJ_EXPECT(resolved.get<workerd::FsError>() == workerd::FsError::SYMLINK_DEPTH_EXCEEDED); |
| 144 | |
| 145 | // And while we're at it, let's check that a symlink can be removed |
| 146 | KJ_EXPECT(tempDir->remove(env.js, kj::Path({"a"})).get<bool>()); |
| 147 | // Removing the symlink means that the cycle is broken and a resolve for |
| 148 | // c or b should return kj::none. |
| 149 | KJ_EXPECT(vfs->resolve(env.js, "file:///a"_url) == kj::none); |
| 150 | KJ_EXPECT(vfs->resolve(env.js, "file:///b"_url) == kj::none); |
| 151 | KJ_EXPECT(vfs->resolve(env.js, "file:///c"_url) == kj::none); |
| 152 | }); |
| 153 | } |
| 154 | |
| 155 | KJ_TEST("Guarding against deep non-circular symlink chains works") { |
| 156 | // Regression test: a deep chain of distinct symlinks (no cycle) must not |
| 157 | // cause a stack overflow. The recursion guard should reject the chain once |
| 158 | // the depth limit is exceeded. |
| 159 | TestFixture fixture; |
| 160 | |
| 161 | fixture.runInIoContext([&](const TestFixture::Environment& env) { |
| 162 | auto vfs = newVirtualFileSystem(kj::heap<FsMap>(), getTmpDirectoryImpl()); |
| 163 | |
| 164 | auto maybeTemp = KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///"_url)); |
| 165 | auto& tempDir = maybeTemp.get<kj::Rc<Directory>>(); |
| 166 | |
| 167 | // Create a target file at the end of the chain. |
| 168 | auto targetResult = KJ_ASSERT_NONNULL(tempDir->tryOpen( |
| 169 | env.js, kj::Path({"target"}), Directory::OpenOptions{.createAs = FsType::FILE})); |
| 170 | auto& targetFile = targetResult.get<kj::Rc<File>>(); |
| 171 | KJ_EXPECT(targetFile->write(env.js, 0, "hello"_kjb).get<uint32_t>() == 5); |
| 172 | |
| 173 | // Build a chain of 300 distinct symlinks: link_0 -> link_1 -> ... -> link_299 -> target. |
| 174 | // This exceeds the depth limit (256) without forming a cycle. |
| 175 | constexpr int chainLen = 300; |
| 176 | for (int i = chainLen - 1; i >= 0; i--) { |
| 177 | kj::String target = |
| 178 | i == chainLen - 1 ? kj::str("file:///target") : kj::str("file:///link_", i + 1); |
| 179 | auto targetUrl = KJ_ASSERT_NONNULL(jsg::Url::tryParse(target.asPtr())); |
| 180 | KJ_EXPECT(tempDir->add(env.js, kj::str("link_", i), |
| 181 | vfs->newSymbolicLink(env.js, targetUrl)) == kj::none); |
| 182 | } |
| 183 | |
| 184 | // Resolving a link near the end of the chain should succeed (under the limit). |
| 185 | KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///link_299"_url)); |
| 186 | |
| 187 | // Resolving from the start of the chain must fail with SYMLINK_DEPTH_EXCEEDED, |
| 188 | // not crash with a stack overflow. |
| 189 | auto resolved = KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///link_0"_url)); |
| 190 | KJ_EXPECT(resolved.get<workerd::FsError>() == workerd::FsError::SYMLINK_DEPTH_EXCEEDED); |
| 191 | |
| 192 | // stat should also fail gracefully. |
| 193 | auto resolvedStat = KJ_ASSERT_NONNULL(vfs->resolveStat(env.js, "file:///link_0"_url)); |
| 194 | KJ_EXPECT(resolvedStat.get<workerd::FsError>() == workerd::FsError::SYMLINK_DEPTH_EXCEEDED); |
| 195 | }); |
| 196 | } |
| 197 | |
| 198 | } // namespace |
| 199 | } // namespace workerd |