How to Fix: `fd_filestat_set_times` with preopened directory file descriptors
fd_filestat_set_times on a preopened directory returns badf: what is happening and how to fix it
If fd_filestat_set_times works on regular file descriptors but fails with badf when you pass a preopened directory fd, the surprising part is not the error itself. The real issue is that a preopen is usually a directory capability, not a generic file handle, and many runtimes intentionally reject timestamp updates on that descriptor directly.
In practice, this means that calling fd_filestat_set_times against the preopened directory fd itself often fails because the implementation expects you to either:
- open the target path relative to the preopen, then operate on the returned fd, or
- use the path-based variant if your environment exposes one.
This tutorial explains why the error appears, how WASI capability-based I/O affects this behavior, and how to implement a robust fix.
Understanding the Root Cause
The root cause is the interaction between WASI file descriptor rights, preopened directories, and how fd_filestat_set_times is defined by implementations.
A preopened directory is typically provided so the module can access a subtree of the host filesystem safely. It is not equivalent to a normal POSIX file descriptor with unrestricted metadata operations. In capability-oriented systems, an fd only supports operations for which it was granted explicit rights.
Why does badf appear?
- The fd may represent a directory capability that does not include the right required for
fd_filestat_set_times. - The runtime may treat the preopen as a special descriptor intended for path resolution, not direct metadata mutation.
- Some implementations reject timestamp changes on directory fds altogether, even if the path is valid, because the preopen is used as a namespace anchor rather than as a mutable object handle.
That distinction matters. A preopen is often meant for operations like:
path_openpath_filestat_getpath_create_directory- other path-relative filesystem calls
But when you call fd_filestat_set_times on the preopened fd itself, the runtime may validate the descriptor kind and rights, then return badf because the descriptor is not accepted for that operation.
In other words, this is usually not a random bug. It is often a consequence of the runtime’s interpretation of WASI semantics: the preopened fd is a directory root for capability-scoped access, while fd_filestat_set_times expects a descriptor opened specifically for the filesystem object being mutated.
Step-by-Step Solution
The safest fix is to avoid calling fd_filestat_set_times directly on the preopened directory fd. Instead, open the directory or file relative to that preopen and call the metadata update on the returned descriptor.
1. Confirm that the fd is a preopened directory
First, inspect the descriptor type and rights if your runtime allows it. This helps distinguish a regular open fd from a namespace preopen.
// Pseudocode / WASI-style flow
fd_fdstat_get(preopen_fd, &fdstat);
// Check fs_filetype and rights before using fd_filestat_set_times
If the descriptor is a directory and was injected as a preopen by the host, assume direct timestamp mutation may be rejected.
2. Open the target relative to the preopen
If you want to update timestamps for a file or subdirectory inside the preopened directory, open that path first.
// Pseudocode
wasi_fd_t target_fd;
__wasi_rights_t base_rights = RIGHTS_FD_FILESTAT_SET_TIMES | RIGHTS_FD_READ;
__wasi_rights_t inheriting_rights = 0;
errno = path_open(
preopen_fd,
0,
"subdir-or-file",
0,
base_rights,
inheriting_rights,
0,
&target_fd
);
if (errno != 0) {
// handle open failure
}
The exact flags and rights vary by runtime, but the key idea is constant: obtain a descriptor for the actual object you want to mutate.
3. Call fd_filestat_set_times on the returned fd
// Pseudocode
errno = fd_filestat_set_times(
target_fd,
atim,
mtim,
FSTFLAGS_ATIM | FSTFLAGS_MTIM
);
if (errno != 0) {
// handle timestamp update failure
}
This works more reliably because the runtime now sees a descriptor opened for that object, with the appropriate timestamp mutation rights.
4. If you need to update the preopened directory itself, reopen it explicitly if possible
If your goal is to modify the timestamps of the directory represented by the preopen itself, do not assume the preopen fd can be used directly. Instead, reopen the same directory through a path-based lookup, if your runtime and host setup permit it.
// Conceptual example: open "." relative to the preopen
wasi_fd_t dir_fd;
errno = path_open(
preopen_fd,
0,
".",
0,
RIGHTS_FD_FILESTAT_SET_TIMES | RIGHTS_FD_READ,
0,
0,
&dir_fd
);
if (errno == 0) {
errno = fd_filestat_set_times(
dir_fd,
atim,
mtim,
FSTFLAGS_ATIM | FSTFLAGS_MTIM
);
}
This approach depends on implementation details, but it is the most portable workaround when the original preopen is treated specially.
5. Verify rights before assuming the call should succeed
Even after reopening, the call can still fail if the descriptor lacks the required rights. Check what the host granted to your module.
// Pseudocode
fd_fdstat_get(target_fd, &fdstat);
// Verify fdstat.fs_rights_base includes the right needed for
// fd_filestat_set_times before calling it.
If your environment strips rights during path_open or host configuration, the fix is not in your application code alone. You also need to adjust runtime configuration.
6. Example strategy in a real application
// High-level helper pseudocode
int set_times_relative(wasi_fd_t preopen_fd, const char* path, timestamp atim, timestamp mtim) {
wasi_fd_t fd;
int err = path_open(
preopen_fd,
0,
path,
0,
RIGHTS_FD_FILESTAT_SET_TIMES | RIGHTS_FD_READ,
0,
0,
&fd
);
if (err != 0) return err;
err = fd_filestat_set_times(fd, atim, mtim, FSTFLAGS_ATIM | FSTFLAGS_MTIM);
fd_close(fd);
return err;
}
This pattern is usually the cleanest fix because it aligns with the path-relative capability model instead of fighting it.
Common Edge Cases
1. The runtime allows reads but not metadata writes
You may successfully inspect a file or directory but fail when updating timestamps. That usually means the fd has lookup or read rights, but not file stat mutation rights.
2. You are targeting a directory, not a regular file
Some environments behave differently for directories. Even if timestamp updates work on files, the same call may fail on directories unless they were opened with the right flags and rights.
3. Reopening . still fails
If reopening the directory relative to the preopen does not work, the runtime may prevent self-referential reopening or may not grant the necessary rights for directory metadata mutation.
4. Symlink behavior is different from expected POSIX behavior
WASI path handling does not always mirror classic Unix semantics. If your target path is a symlink, the resolution rules and allowed metadata operations may differ by implementation.
5. Host runtime differences
Not all WASI runtimes behave identically. What succeeds in one engine may return badf, perm, or another error in another engine depending on how strictly it models descriptor capabilities.
6. The issue is configuration, not code
If the host launched the module with limited filesystem permissions, no amount of in-module reopening will help. In that case, review your runtime’s filesystem preopen and rights configuration through its official WASI documentation or the specific engine’s docs.
FAQ
Why does fd_filestat_set_times return badf instead of a permissions error?
Because many implementations treat the preopened directory as a descriptor type that is not valid for this direct operation. The runtime may fail descriptor validation early and return badf rather than reaching a rights-only check.
Is this behavior expected or a bug?
Usually it is expected behavior rooted in capability-based descriptor semantics. That said, runtime-specific behavior can still be inconsistent, so it is worth checking the engine’s issue tracker and specification notes if the result seems surprising.
What is the most portable fix?
The most portable fix is to use the preopen only as a path anchor, open the actual target relative to it, and then call fd_filestat_set_times on the returned descriptor with the correct rights.
The key takeaway is simple: do not treat a preopened directory fd like an ordinary mutable file handle. In WASI, it is often a capability root for path-based access, and timestamp mutation should be done on a freshly opened descriptor for the real target object.