ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
35.09k stars 2.56k forks source link

std.fs.test `file operations on directories` fails with Wasmtime 23.0.1 #20747

Open alexrp opened 4 months ago

alexrp commented 4 months ago

Zig Version

1fc42ed3e7ca0b74b54aaa827276d995d6c7c6cd

Steps to Reproduce and Observed Behavior

  1. Install Wasmtime 23.0.1 (maybe earlier?).
  2. Run the standard library tests with -fwasmtime (but see #20745).
error: 'fs.test.test.file operations on directories' failed: expected error.IsDir, found fs.File{ .handle = 4 }
Unable to dump stack trace: not implemented for Wasm

Fails here: https://github.com/ziglang/zig/blob/eac7fd4da5992299a1f2fb59c5aa237c0c6c6761/lib/std/fs/test.zig#L767-L769

Expected Behavior

No failure.

squeek502 commented 4 months ago

Just to clarify, what's the host system? Context: https://github.com/ziglang/zig/issues/13157

alexrp commented 4 months ago
❯ uname -srm
Linux 6.5.0-44-generic x86_64
squeek502 commented 4 months ago

This could very easily be a bug in wasmtime. From the POSIX specificaiton of open:

[EISDIR]
    The named file is a directory and oflag includes O_WRONLY or O_RDWR.
squeek502 commented 4 months ago

This is actually pretty likely to be a bug in wasmtime, and it's one that manifested in exactly the same way in the past (but was fixed, so this seems like a regression):

EDIT: I'm not so certain that this is the same bug, I was doing something dumb while testing.

squeek502 commented 4 months ago

Seems to be related to wasi-libc:

Without linking libc, the tests pass:

$ zig test lib/std/std.zig --zig-lib-dir lib -target wasm32-wasi -femit-bin=test-wasi.wasm --test-no-exec
$ wasmtime --dir=. test-wasi.wasm
2592 passed; 115 skipped; 0 failed.

When linking libc, that test fails:

$ zig test lib/std/std.zig --zig-lib-dir lib -target wasm32-wasi -femit-bin=test-wasi-libc.wasm -lc --test-no-exec
$ wasmtime --dir=. test-wasi-libc.wasm
...
expected error.IsDir, found fs.File{ .handle = 7 }
1192/2707 fs.test.test.file operations on directories...FAIL (TestUnexpectedError)
Unable to dump stack trace: not implemented for Wasm
...
2589 passed; 117 skipped; 1 failed.

EDIT: The first wasmtime version that fails is 15.0.0:

$ ~/Downloads/wasmtime-v15.0.0-x86_64-linux/wasmtime --dir=. test-wasi-libc.wasm
2589 passed; 117 skipped; 1 failed.

$ ~/Downloads/wasmtime-v14.0.4-x86_64-linux/wasmtime --dir=. test-wasi-libc.wasm
2590 passed; 117 skipped; 0 failed.
squeek502 commented 4 months ago

The plot thickens...

The following test cases are built/run with

$ zig test open-dir-rw.zig -target wasm32-wasi -lc -femit-bin=open-dir-rw-test-libc.wasm --test-no-exec
$ wasmtime --dir=. open-dir-rw-test-libc.wasm
// This fails:
test "file operations on directories in tmpDir" {
    var tmp_dir = testing.tmpDir(.{});
    defer tmp_dir.cleanup();

    const test_dir_name = "testdir";
    try tmp_dir.dir.makeDir(test_dir_name);

    try testing.expectError(error.IsDir, tmp_dir.dir.openFile(test_dir_name, .{ .mode = .read_write }));
}

// But this passes:
test "file operations on directories in cwd" {
    const test_dir_name = "testdir";
    try std.fs.cwd().deleteTree(test_dir_name);
    try std.fs.cwd().makeDir(test_dir_name);

    try testing.expectError(error.IsDir, std.fs.cwd().openFile(test_dir_name, .{ .mode = .read_write }));
}

So somehow the usage of tmpDir is involved in the failure.

squeek502 commented 4 months ago

Found a reproduction with wasi-sdk 23.0, so this is indeed an upstream wasi-libc/wasmtime bug:

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    mkdirat(AT_FDCWD, "testdir", 0777);
    {
        // Opening with O_RDWR fails as expected if the dir fd is AT_FDCWD
        int dir = openat(AT_FDCWD, "testdir", O_RDWR);
        if (dir != -1) return 1;
    }

    // Open "testdir" for real now since we want to use it as the fd for openat
    int dir = openat(AT_FDCWD, "testdir", O_RDONLY|O_DIRECTORY);
    mkdirat(dir, "subdir", 0777);

    // Try to open the subdir with O_RDWR, this should fail but it succeeds
    // because the O_RDWR is lost and the openat2 syscall is called with O_RDONLY
    // instead
    int testdir = openat(dir, "subdir", O_RDWR);

    if (testdir != -1) return 1;
    return 0;
}
$ WASI_SDK=/home/ryan/Downloads/wasi-sdk-23.0-x86_64-linux
$ $WASI_SDK/bin/clang --sysroot=$WASI_SDK/share/wasi-sysroot open-dir-tmpdir-rw.c -o open-dir-tmpdir-rw-sdk-23.wasm
$ strace -e trace=openat2 wasmtime --dir=. open-dir-tmpdir-rw-sdk-23.wasm
openat2(3, "testdir", {flags=O_RDWR|O_LARGEFILE|O_CLOEXEC, resolve=RESOLVE_NO_MAGICLINKS|RESOLVE_BENEATH}, 24) = -1 EISDIR (Is a directory)
openat2(3, "testdir", {flags=O_RDONLY|O_LARGEFILE|O_CLOEXEC, resolve=RESOLVE_NO_MAGICLINKS|RESOLVE_BENEATH}, 24) = 11
openat2(11, "subdir", {flags=O_RDONLY|O_LARGEFILE|O_CLOEXEC, resolve=RESOLVE_NO_MAGICLINKS|RESOLVE_BENEATH}, 24) = 12
+++ exited with 1 +++

The O_RDWR is lost when calling openat with an fd that's not AT_FDCWD.

EDIT: Created a wasmtime issue: https://github.com/bytecodealliance/wasmtime/issues/9054