Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion packages/preview2-shim/lib/nodejs/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ class Descriptor {

static _createPreopen(hostPreopen) {
const descriptor = new Descriptor();
descriptor.#mode = {
read: true,
write: true,
mutateDirectory: true,
};
descriptor.#hostPreopen = hostPreopen.endsWith("/")
? hostPreopen.slice(0, -1) || "/"
: hostPreopen;
Expand Down Expand Up @@ -353,6 +358,27 @@ class Descriptor {
if (preopenEntries.length === 0) {
throw "access";
}
// https://github.com/WebAssembly/WASI/blob/b7ee9febdcc0652aef5aeaf80dc329d240eb84e8/proposals/filesystem/wit/types.wit#L532-L534
// > If `flags` contains `descriptor-flags::mutate-directory`, and the base
// > descriptor doesn't have `descriptor-flags::mutate-directory` set,
// > `open-at` fails with `error-code::read-only`.
if (descriptorFlags.mutateDirectory && !this.#mode?.mutateDirectory) {
throw "read-only";
}
// https://github.com/WebAssembly/WASI/blob/b7ee9febdcc0652aef5aeaf80dc329d240eb84e8/proposals/filesystem/wit/types.wit#L536-L539
// > If `flags` contains `write` or `mutate-directory`, or `open-flags`
// > contains `truncate` or `create`, and the base descriptor doesn't have
// > `descriptor-flags::mutate-directory` set, `open-at` fails with
// > `error-code::read-only`.
if (
!this.#mode?.mutateDirectory &&
(descriptorFlags.write ||
descriptorFlags.mutateDirectory ||
openFlags.truncate ||
openFlags.create)
) {
throw "read-only";
}
const fullPath = this.#getFullPath(path, pathFlags.symlinkFollow);
let fsOpenFlags = 0x0;
if (openFlags.create) {
Expand Down Expand Up @@ -383,7 +409,7 @@ class Descriptor {
if (!pathFlags.symlinkFollow) {
fsOpenFlags |= constants.O_NOFOLLOW;
}
if (descriptorFlags.requestedWriteSync || descriptorFlags.mutateDirectory) {
if (descriptorFlags.requestedWriteSync) {
throw "unsupported";
}
// Currently throw to match Wasmtime
Expand Down
115 changes: 115 additions & 0 deletions packages/preview2-shim/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,121 @@ suite("Sandboxing", () => {
}),
);
});
suite("FS openAt descriptor flags", () => {
async function createTmpPreopen(setup) {
const { mkdtemp, mkdir, writeFile } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const { sep, normalize } = await import("node:path");
const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation");

const tmpDir = await mkdtemp(normalize(tmpdir() + sep));
if (setup) {
await setup({ tmpDir, mkdir, writeFile });
}

const shim = new WASIShim({
sandbox: { preopens: { "/work": tmpDir } },
});
const importObj = shim.getImportObject();
const [[rootDescriptor]] = importObj["wasi:filesystem/preopens"].getDirectories();
return rootDescriptor;
}

test("preopen descriptor has read, write, and mutateDirectory flags", async () => {
const { filesystem } = await import("@bytecodealliance/preview2-shim");
const [[rootDescriptor]] = filesystem.preopens.getDirectories();
const flags = rootDescriptor.getFlags();
assert.strictEqual(flags.read, true);
assert.strictEqual(flags.write, true);
assert.strictEqual(flags.mutateDirectory, true);
});

test(
"openAt with mutateDirectory descriptor flag succeeds on preopen",
testWithGCWrap(async () => {
const root = await createTmpPreopen(({ tmpDir, mkdir }) => mkdir(`${tmpDir}/subdir`));
const fd = root.openAt(
{},
"subdir",
{ directory: true },
{ read: true, mutateDirectory: true },
);
fd[symbolDispose]();
}),
);

test(
"openAt with create and write flags succeeds on preopen",
testWithGCWrap(async () => {
const root = await createTmpPreopen();
const fd = root.openAt({}, "test-file.txt", { create: true }, { write: true });
fd[symbolDispose]();
}),
);

suite("on read-only base descriptor", () => {
const readOnlySetup = ({ tmpDir, mkdir, writeFile }) =>
mkdir(`${tmpDir}/subdir`).then(() => writeFile(`${tmpDir}/subdir/existing.txt`, "hello"));

[
{
name: "read succeeds",
path: "existing.txt",
openFlags: {},
descriptorFlags: { read: true },
},
{
name: "write throws read-only",
path: "existing.txt",
openFlags: {},
descriptorFlags: { write: true },
error: "read-only",
},
{
name: "create throws read-only",
path: "new-file.txt",
openFlags: { create: true },
descriptorFlags: { read: true },
error: "read-only",
},
{
name: "truncate throws read-only",
path: "existing.txt",
openFlags: { truncate: true },
descriptorFlags: { write: true },
error: "read-only",
},
{
name: "mutateDirectory throws read-only",
path: "x",
openFlags: { directory: true },
descriptorFlags: { read: true, mutateDirectory: true },
error: "read-only",
},
].forEach(({ name, path, openFlags, descriptorFlags, error }) => {
test(
name,
testWithGCWrap(async () => {
const root = await createTmpPreopen(readOnlySetup);
const readOnlyDir = root.openAt({}, "subdir", { directory: true }, { read: true });

if (error) {
throws(
() => readOnlyDir.openAt({}, path, openFlags, descriptorFlags),
(err) => err === error,
);
} else {
const fd = readOnlyDir.openAt({}, path, openFlags, descriptorFlags);
fd[symbolDispose]();
}

readOnlyDir[symbolDispose]();
}),
);
});
});
});

suite("Browser shim guards", () => {
test("pollList throws on empty list", async () => {
const { poll } = await import("../lib/browser/io.js");
Expand Down
Loading