Problem
moonbitlang/async/fs exposes path-based create, rename, and opendir → Directory, but Directory only supports listing (next/read_all/close). There is no way to create or rename a file relative to a pinned directory handle.
This makes it impossible to implement a fully TOCTOU-safe atomic file replace when the target path contains intermediate directory components an adversary might swap. The standard POSIX-safe pattern is:
dirfd = open("parent", O_DIRECTORY) // pin the directory inode once
openat(dirfd, "file.tmp", O_CREAT|O_EXCL, …) // create relative to the pinned fd
renameat(dirfd, "file.tmp", dirfd, "file") // rename relative to the pinned fd
Because dirfd is resolved once, later swaps of the parent name are inert. With path-based create/rename, each call re-resolves the path, so an intermediate component swapped between the create and the rename can redirect where the rename lands.
Concrete case
A spreadsheet CLI (office.mbt) writes a rendered HTML document via temp-file + rename to an --out path. The temp+rename correctly prevents truncation and is safe against a symlink/hard-link at the final path component, but an intermediate-directory-component TOCTOU cannot be closed with the current API — there is no openat/renameat to pin the output directory.
Requested API (sketch)
Handle-relative operations on Directory (or a new DirHandle):
Directory::create_at(self, name, allow_existing?, permission?) -> File
Directory::rename_at(self, old_name, new_dir, new_name, replace?) -> Unit
- (optionally)
Directory::open_at, Directory::remove_at, Directory::stat_at
These map directly to openat(2) / renameat(2) and would let downstream code implement the pin-the-directory atomic-replace pattern.
Workaround today
Path-based temp+rename with a best-effort equality guard, documented residual limitation for the intermediate-component race.
Problem
moonbitlang/async/fsexposes path-basedcreate,rename, andopendir → Directory, butDirectoryonly supports listing (next/read_all/close). There is no way to create or rename a file relative to a pinned directory handle.This makes it impossible to implement a fully TOCTOU-safe atomic file replace when the target path contains intermediate directory components an adversary might swap. The standard POSIX-safe pattern is:
Because
dirfdis resolved once, later swaps of the parent name are inert. With path-basedcreate/rename, each call re-resolves the path, so an intermediate component swapped between the create and the rename can redirect where the rename lands.Concrete case
A spreadsheet CLI (
office.mbt) writes a rendered HTML document via temp-file +renameto an--outpath. The temp+rename correctly prevents truncation and is safe against a symlink/hard-link at the final path component, but an intermediate-directory-component TOCTOU cannot be closed with the current API — there is noopenat/renameatto pin the output directory.Requested API (sketch)
Handle-relative operations on
Directory(or a newDirHandle):Directory::create_at(self, name, allow_existing?, permission?) -> FileDirectory::rename_at(self, old_name, new_dir, new_name, replace?) -> UnitDirectory::open_at,Directory::remove_at,Directory::stat_atThese map directly to
openat(2)/renameat(2)and would let downstream code implement the pin-the-directory atomic-replace pattern.Workaround today
Path-based temp+rename with a best-effort equality guard, documented residual limitation for the intermediate-component race.