How to Fix: `utime()` does not accept a file descriptor under preview 2 (as found by Python)
utime() on a file descriptor breaks under WASI Preview 2 because the host interface only supports path-based timestamp updates, while Python’s test also exercises the fd-based variant.
That mismatch is why the CPython POSIX test fails: the runtime can update timestamps when given a pathname, but not when given an already-open file descriptor. Under Preview 2, this is not just a missing convenience overload; it is a real capability-model gap between what os.utime() expects on POSIX-like systems and what the underlying WASI surface currently exposes.
Problem Overview
The GitHub issue reports that utime() does not accept a file descriptor under WASI Preview 2, as detected by Python’s POSIX test suite. In practical terms, code shaped like this is expected to work on Unix-style platforms:
fd = os.open(path, os.O_RDONLY)
os.utime(fd, None)
But in a Preview 2 environment, the call fails because the implementation path can only operate on a filesystem path, not directly on an open descriptor. That is especially visible in CPython because os.utime() supports multiple calling conventions:
os.utime(path)os.utime(path, dir_fd=...)os.utime(path, follow_symlinks=False)os.utime(fd)on platforms exposing descriptor-based semantics
Python’s test suite is validating platform behavior, and the failure reveals that the runtime’s Preview 2 implementation is incomplete relative to POSIX expectations.
Understanding the Root Cause
The root cause is a semantic mismatch between POSIX file descriptor APIs and WASI Preview 2 filesystem capabilities.
On traditional Unix systems, updating file timestamps via a descriptor is typically backed by APIs equivalent to futimens() or futimes(). These APIs operate on an already-open file handle, which has several advantages:
- They avoid path re-resolution.
- They preserve capability boundaries once the file is open.
- They reduce races such as time-of-check/time-of-use issues.
In contrast, many Preview 2 filesystem implementations currently expose timestamp mutation through path-oriented operations or lack a direct descriptor-based timestamp update primitive. If the host interface does not provide a method analogous to futimens(fd, ...), then a language runtime like Python has only three options:
- Reject descriptor-based
utime(). - Emulate it by reconstructing a path from the descriptor.
- Add a non-standard host extension.
Option 2 is usually unsafe or impossible. In a capability-oriented runtime, a file descriptor is not guaranteed to have a reversible stable pathname. Even if a path could sometimes be reconstructed, doing so would introduce correctness and security issues:
- The file may have been renamed after opening.
- The descriptor may refer to an object with no meaningful path.
- Path re-resolution may target the wrong file.
- Symlink behavior may change unexpectedly.
So the failure is not simply “Python is wrong” or “the binding forgot an overload.” The real issue is that Preview 2 currently lacks a safe, standard, fd-native timestamp update path matching POSIX behavior.
At the implementation layer, the failing stack usually looks like this:
- Python calls
os.utime()with an integer file descriptor. - The CPython WASI port routes the operation into the platform abstraction layer.
- The runtime checks whether the host can perform an fd-based timestamp mutation.
- No suitable Preview 2 operation exists, so the call returns an error or behaves as unsupported.
This is why the issue is specifically visible in Python tests, but the underlying bug belongs to the Preview 2 filesystem contract and its runtime mapping.
Step-by-Step Solution
The correct fix depends on whether you are maintaining the runtime, the WASI shim/binding layer, or an application affected by the bug.
1. Confirm the failing behavior
Start by reproducing the descriptor-specific case separately from path-based utime().
import os
import tempfile
import time
fd, path = tempfile.mkstemp()
try:
now = time.time()
# Path-based call: usually works if path timestamp mutation exists
os.utime(path, (now, now))
# FD-based call: this is the failing Preview 2 path
os.utime(fd, (now, now))
finally:
os.close(fd)
os.unlink(path)
If the path call succeeds but the fd call fails, you have confirmed the Preview 2 capability gap rather than a general filesystem timestamp issue.
2. Inspect how the platform layer dispatches utime()
In language runtimes, this logic usually branches based on argument type:
if input_is_path:
return utime_by_path(path, atime, mtime, flags)
elif input_is_fd:
return utime_by_fd(fd, atime, mtime)
else:
raise TypeError
Under Preview 2, the second branch often has no valid host implementation. The immediate engineering task is to make this explicit rather than letting it fail ambiguously.
3. Implement a clear unsupported-path error for fd-based calls
If Preview 2 has no standard fd-native timestamp operation available in your runtime, the minimum correct behavior is to return a deterministic error such as ENOSYS, EOPNOTSUPP, or the runtime’s equivalent unsupported exception.
int wasi_utime_fd(int fd, timestamp_spec atime, timestamp_spec mtime) {
errno = ENOSYS;
return -1;
}
This does not add functionality, but it prevents misleading fallbacks and makes the platform limitation testable.
4. Gate CPython or runtime tests based on fd-utime support
If you maintain the Python port or another language runtime test suite, detect support before asserting descriptor-based behavior.
import os
import unittest
def supports_fd_utime(test_file):
fd = os.open(test_file, os.O_RDONLY)
try:
try:
os.utime(fd, None)
return True
except (AttributeError, NotImplementedError, OSError):
return False
finally:
os.close(fd)
class UtimeTests(unittest.TestCase):
def test_fd_utime(self):
path = "tmp_test_file"
with open(path, "wb") as f:
f.write(b"x")
if not supports_fd_utime(path):
self.skipTest("fd-based utime unsupported on this WASI Preview 2 runtime")
fd = os.open(path, os.O_RDONLY)
try:
os.utime(fd, None)
finally:
os.close(fd)
os.unlink(path)
This is the safest short-term fix for platform conformance while the underlying WASI support remains incomplete.
5. If you own the runtime, add fd-native support only if the host actually exposes it
If your Preview 2 runtime has an internal extension or host-level primitive for changing timestamps on an open file handle, wire that explicitly into the descriptor branch.
int wasi_utime_fd(int fd, timestamp_spec atime, timestamp_spec mtime) {
host_descriptor desc = lookup_host_descriptor(fd);
if (!desc.valid) {
errno = EBADF;
return -1;
}
host_result rc = host_set_file_times_by_descriptor(desc, atime, mtime);
if (!rc.ok) {
errno = map_host_error(rc.error);
return -1;
}
return 0;
}
The key rule is simple: do not emulate descriptor semantics with a reconstructed pathname unless your platform can prove identity and race-safety. In most WASI environments, that guarantee does not exist.
6. For application developers, use pathname-based utime() as the workaround
If you are not modifying the runtime itself, the practical workaround is to avoid fd-based calls under Preview 2.
import os
import time
path = "data.txt"
now = time.time()
os.utime(path, (now, now))
If your code receives either a path or descriptor, normalize intentionally:
import os
def safe_utime(target, times=None):
if isinstance(target, int):
raise NotImplementedError(
"fd-based os.utime() is unsupported on this WASI Preview 2 target"
)
return os.utime(target, times)
This avoids hidden portability bugs and communicates the limitation clearly.
7. Document the limitation in your compatibility matrix
This issue belongs in platform support docs. A concise compatibility note helps downstream developers understand why their test suite differs from Linux or macOS.
Feature: os.utime(path) Supported
Feature: os.utime(fd) Not supported on WASI Preview 2
Reason: No standard fd-based timestamp mutation primitive
Workaround: Use path-based os.utime()
That documentation is often as important as the code fix because it prevents repeated bug reports.
Common Edge Cases
Symlinks and follow_symlinks=False
Even if path-based utime() works, symlink semantics may still differ. Some environments can update the target but not the link itself. If your tests cover nofollow behavior, verify that your runtime maps this separately.
dir_fd support without raw fd target support
A runtime may support a directory descriptor for resolving a relative path but still not support applying utime() directly to the descriptor of the final file. Those are distinct features and should not be conflated.
Renamed or unlinked files
On POSIX, an open descriptor can still identify a file after rename or unlink. A path-based fallback cannot reproduce that reliably. This is one reason descriptor emulation is dangerous.
Timestamp precision differences
Some runtimes store timestamps with coarser granularity than CPython tests expect. After fixing the fd/path mismatch, you may still see failures due to precision truncation rather than missing support.
Read-only descriptors
On some hosts, metadata updates through a descriptor may require permissions beyond merely opening the file. If a future Preview 2 extension adds fd-based support, permission mapping will still need careful validation.
Special files
Regular files, directories, preopened paths, virtual filesystem nodes, and host-mapped resources may behave differently. A runtime should define whether utime() is supported uniformly or only for specific node types.
FAQ
Why does os.utime(path) work but os.utime(fd) fail?
Because Preview 2 implementations commonly provide a path-based timestamp update operation but not a descriptor-based one equivalent to POSIX futimens().
Can the runtime just convert the file descriptor back into a path?
No, not safely in the general case. A descriptor may outlive renames, may refer to objects without a stable pathname, and path re-resolution can point to the wrong file. That would break correctness and potentially security.
Is this a Python bug or a WASI bug?
It is primarily a WASI Preview 2 support gap exposed by Python’s broader POSIX compatibility surface. Python is correctly testing behavior that exists on many Unix-like platforms, but the underlying runtime cannot currently provide the same semantics.
The durable fix is to either add a real fd-based timestamp primitive to the host/runtime stack or explicitly mark descriptor-based utime() as unsupported and gate tests accordingly. Until then, pathname-based updates are the correct workaround for applications targeting WASI Preview 2. For reference, review the related CPython test from the Python POSIX test suite and track runtime compatibility expectations in your project documentation.