Skip to content
File

Blob: src/workerd/io/bundle-fs-test.c++

8.8 KB
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 
13namespace workerd {
14namespace {
15workerd::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 
44KJ_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 
109KJ_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 
155KJ_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