Standard C library functions in WASM: stat, opendir, access, fseek

Version: 1.5.9.0

SDK Version: 1.4.2

Frequency: Consistently

Severity: High

Bug description:

C function stat doesn’t work on directories (it works correctly on files)

Repro steps:

In a WASM standalone module, if I do “stat” on any dir of my package I get error 29:

struct stat st;
auto result = stat("./modules", &st);

if (result == -1) {	
	int error_code = errno;

	printf("  FAILURE: stat() returned -1.\n");
	printf("  Error Code (errno): %d\n", error_code);
	printf("  Error Message: %s\n", strerror(error_code));
}
else {	
	printf("  SUCCESS: stat() returned 0.\n");	
}

result is always -1 with errno 29

1 Like

Hi @MaxHype

I have just checked the code and indeed we never implemented the WASI function “path_filestat_get” for file descriptors targeting directories.
We’ll see if we can add this in the future but I have to say this rather low priority at this stage.

Best regards,

Eric / Asobo

2 Likes

Ok thank you @EPellissier,
in the meantime I’ve found a workaround using opendir function (that instead uses “__wasilibc_nocwd_opendirat” and “__wasi_path_open”)

another low priority similar issue is with fseek and negative offsets:

auto f = fopen("./file.dat", "rb");
auto result = fseek(f, -2, SEEK_END);
if (result == -1)
{
	fprintf(stderr, "Failed to seek in file ./file.dat: %s\n", strerror(errno));
	fclose(f);
	return;
}

result is always -1 with errno 29

the workaround is avoiding negative offset using SEEK_SET with absolute position from the start calculated using file size

1 Like

@EPellissier unfortunately I still haven’t found a reliable way to check if a directory exist.
I noticed that opendir never returns NULL even if the directory is not existent:

DIR* dir = opendir("./modules");
if (dir) {
	fprintf(stdout, "Directory ./modules opened successfully.\n");
	closedir(dir);
}
else {
	fprintf(stderr, "Failed to open directory ./modules: %s\n", strerror(errno));
	return;
}

dir = opendir("./notexist");
if (dir) {
	fprintf(stdout, "Directory ./notexist opened successfully.\n");
	closedir(dir);
}
else {
	fprintf(stderr, "Failed to open directory ./notexist: %s\n", strerror(errno));
	return;
}

In the example /modules is opened successfully, but also /notexist is opened successfully (wrongly)

Moreover, no luck also trying with access function:

auto acc = access("./modules", F_OK); // Check if the directory exists
if (acc == 0) {
	fprintf(stdout, "Access Directory ./modules exists.\n");
}
else {
	fprintf(stderr, "Access Directory ./modules does not exist: %s %d\n", strerror(errno), errno); //EIO
	return;
}

this last returns always -1 with errno 29

Hi, I found that also mkdir has a problem, in this case when called the second time (SDK 1.6.4):

mkdir("./work/logs", 0777);
mkdir("./work/logs", 0777);

the second call gives 0xC0000005 access violation error.
It’s worth noticing also that the first call to mkdir always works, even if the dir is already there (errno is set), while a second call always crashes.

This is not resolved checking with opendir cause it returns always true as we said before:

dir = opendir("./work/logs");
if (!dir)
{    
    mkdir("./work/logs", 0777);
}

@EPellissier maybe there could be a chance for any of these WASI filesystem functions to be fixed in future releases?
Thank you for your support

1 Like

In addition, in latest beta version 1.7.10.0 (SDK 1.6.4)
I get access violation 0xC0000005 if I try to do stat on work dir

struct stat st;
result = stat("/work", &st);

It worked fine before, it seems this behavior has been introduced in latest versions

Hello @MaxHype

I do not reproduce a access violation error, neither with the double mkdir or the stat call in the latest flighting build.
Do you have more information on the context of execution ? During which callback the lines are executed, for example. Or any information you think may be useful.
Could you share with us your project (or a minimal one) so we can investigate in the same condition

Regards,
Edalyn

Hello @esoriaAsobo,

Thanks for looking into this. I’ve done extensive investigation and I think we probably found the root cause of the double mkdir/stat crash.

The key condition to reproduce: chdir must be called before the failing calls.

If your WASM module doesn’t call chdir, you won’t see the crash. That’s why a simple test with just two mkdir calls may work fine on your end.

Minimal reproducer:

MSFS_CALLBACK void module_init(void) {
    chdir("/work");               // this activates the broken code path
    mkdir("./work/test1", 0755);  // works
    mkdir("./work/test1", 0755);  // 0xC0000005 crash
}

Remove the chdir call → no crash.
Add it back → crash on second mkdir (or stat, open, fopen, etc.).

If it can be helpful, here are my detailed findings about this subtle issue, with a hypothesis of the underlying cause:

Bug details

Root cause: compiler bug in the MSFS WASM toolchain’s clang

The MSFS SDK ships a custom LLVM/Clang fork (from AsoboStudio/innative-llvm.git, clang 15.0.1). This compiler seems to have a bug in how it handles static __thread variables in static (file-scope) functions when targeting WASM.

In wasi-libc’s posix.c, the internal function find_relpath has two static variables:

static int find_relpath(const char *path, char **relative) {
    static __thread char *relative_buf = NULL;
    static __thread size_t relative_buf_len = 0;
    // ...passes &relative_buf and &relative_buf_len to __wasilibc_find_relpath_alloc
}

The compiler inlines find_relpath into every POSIX function that calls it (mkdir, stat, open, etc.). After inlining, it miscompiles the storage:

  • relative_buf (the char*) → demoted to a stack local (zeroed every call)
  • relative_buf_len (the size_t) → correctly placed in .data (persists between calls)

I verified this with llvm-nm on the posix.o inside the SDK’s libc.a:

Asobo’s libc.a (from MSFS SDK wasi-sysroot):

00000000 d find_relpath.relative_buf_len

find_relpath.relative_buf is completely absent — demoted to stack.

Official wasi-sdk-17 libc.a (compiled by upstream clang 15):

00000000 d find_relpath.relative_buf
00000004 d find_relpath.relative_buf_len

Both correctly in .data.

The crash sequence:

These two variables are passed by address to __wasilibc_find_relpath_alloc (defined in wasi-libc’s chdir.c), which manages a reusable path buffer:

  1. First call: *relative_buf_len is 0, so _alloc does realloc(NULL, needed) → allocates buffer, stores pointer in *relative_buf (stack slot), sets *relative_buf_len (.data). strcpy(*relative_buf, rel) succeeds. Function returns, stack frame is destroyed, buffer pointer is lost.

  2. Second call: Fresh stack frame → *relative_buf is NULL. But *relative_buf_len still has the old value from .data. _alloc sees len >= needed, skips realloc, does strcpy(*relative_buf, rel) → strcpy(NULL, ...) → access violation 0xC0000005.

Why chdir is the trigger:

__wasilibc_find_relpath_alloc is defined in chdir.o as a weak symbol. Without chdir linked into the binary, chdir.o isn’t pulled in, _alloc stays NULL, and wasi-libc’s find_relpath2 takes a fallback path through __wasilibc_find_relpath → __wasilibc_find_abspath. That fallback path simply sets a pointer to an existing string in the preopens table — no strcpy, no buffer management — so the broken statics are harmless.

Once chdir is linked (by calling it), _alloc becomes non-NULL, find_relpath2 takes the _alloc path, and the mismatched statics become fatal.

For now, I’m using a workaround for this, using linker’s –wrap option to intercept __wasilibc_find_relpath_alloc and wrapping it with correct buffers

Meanwhile I can confirm that in the latest flighting build 1.7.11.0, opendir returning NULL for nonexistent paths, lseek with negative SEEK_END offsets, and stat on directories are all fixed — thank you for those.

However seems there’s a new regression on fstat on stdio file descriptors

fstat on fd 0 (stdin), 1 (stdout), and 2 (stderr) reports st_mode = 0x4000 (S_IFDIR, directory) instead of the expected 0x2000 (S_IFCHR, character device).

Reproducer:

struct stat st;
for (int fd = 0; fd <= 2; fd++) {
    int ret = fstat(fd, &st);
    const char* name = fd == 0 ? "stdin" : fd == 1 ? "stdout" : "stderr";
    if (ret == 0) {
        fprintf(stdout, "fstat(%s/fd=%d): mode=0x%x isdir=%d ischr=%d isreg=%d\n",
            name, fd, st.st_mode,
            S_ISDIR(st.st_mode), S_ISCHR(st.st_mode), S_ISREG(st.st_mode));
    }
    else {
        fprintf(stderr, "fstat(%s/fd=%d): FAILED errno=%d (%s)\n",
            name, fd, errno, strerror(errno));
    }
}

Output on latest flighting build:

fstat(stdin/fd=0): mode=0x4000 isdir=1 ischr=0 isreg=0
fstat(stdout/fd=1): mode=0x4000 isdir=1 ischr=0 isreg=0
fstat(stderr/fd=2): mode=0x4000 isdir=1 ischr=0 isreg=0

Expected: mode=0x2000 isdir=0 ischr=1 — stdio fds should be character devices, not directories.