Skip to content
Haiku OSDeep Dive Published Updated 6 min readViews unavailable

Haiku File-System Modules: VNode, Node, and Cookie Lifetimes

Implement Haiku file-system add-ons with clear volume, node, vnode, and per-open cookie ownership, including teardown and read-only lifecycle checks.

A Haiku file-system module is a kernel add-on, not a Storage Kit application. It implements the file-system interface that the VFS calls to mount a volume, resolve names, create vnode-backed objects, and operate on open files and directories. Many lifecycle bugs start when a module treats a directory entry, an on-disk node, a VFS vnode, and an open cookie as interchangeable handles. They are different objects with different owners and teardown points.

This guide focuses on a minimal read-only file-system add-on and the lifetime boundaries around lookup, vnodes, and cookies. Haiku’s public fs_interface.h currently identifies the interface as B_CURRENT_FS_API_VERSION "/v1". The explanatory file-system-module guide in the source tree carries a 2007 copyright notice; its object model is cross-checked here against the current public header and the current BFS kernel implementation. Treat the guide as lifecycle documentation, not a guarantee that every future API revision preserves every optional hook unchanged.

Keep volume, node, entry, and vnode separate

A volume is one mounted instance of a file system. The module associates its own volume state with the supplied fs_volume, typically through private_volume, and keeps it valid while callbacks for that mount can run. A node is the file-system’s identity for a file, directory, or symbolic link. The node ID (ino_t) is chosen by the file system and must remain stable while that node is known to the VFS on the mounted volume.

An entry is a name in a directory that refers to a node. It is not the node itself: multiple entries may refer to one node when hard links are supported, and a node can remain live after its last name is removed while an open reference still exists. A vnode is the VFS representation of an in-use node. Its private_node pointer carries the module’s private node handle; it is not a permanent pointer that the module should save beyond the vnode’s lifetime.

Those distinctions shape path resolution. lookup() receives a directory vnode plus an entry name. The module resolves the name according to its own namespace and returns the target node ID; when the VFS needs a vnode for that ID, the volume’s get_vnode() callback supplies the private node state. The BFS implementation illustrates the pattern: it finds an inode ID in the directory, calls get_vnode() for that ID, and returns the name lookup result rather than handing a cached raw pointer to userland.

At mount time, mount() initializes volume-private state and makes the root node available to the VFS, returning the root node ID. For a simple read-only image, validate its superblock and bounds before publishing the root. The VFS can then call get_vnode() as nodes become active. When it releases a vnode, the file system’s put_vnode() callback can drop or cache the corresponding private node state. If the node has been removed, the interface also has a distinct remove_vnode() path. Do not free the volume object or filesystem-wide caches while a vnode callback can still consult them.

VNode references are not open-file cookies

The VFS may hold a vnode while multiple opens exist. An open operation returns a cookie through void **_cookie; subsequent read or directory-iteration calls receive that cookie. A cookie is therefore the right place for state scoped to one open, such as a directory cursor or per-open flags. If two callers open the same directory, each must receive an independent cursor unless the file-system contract deliberately defines shared position.

Haiku separates close() from free_cookie(). The module guide explains why: close() signals that the cookie should close, but the cookie can still be in use. If a blocking file-system hook waits on the same cookie, close must arrange to wake or unblock that work. The free_cookie() hook is called when the cookie is no longer in use and must release the module’s allocation. The current interface header declares both callbacks, and BFS’s implementation demonstrates the separation: its file close hook returns without deleting the cookie, while its free hook deletes the allocated file_cookie.

Directory iteration follows the same pattern using open_dir(), read_dir(), close_dir(), and free_dir_cookie(). A minimal read-only module still needs a coherent directory contract: allocate iteration state, return a bounded sequence of dirent records, implement rewind behavior, and release the state only at the free-cookie boundary. A cursor must not refer to a buffer owned by a callback after that callback returns.

The following pseudocode emphasizes the ownership boundary rather than a complete ABI implementation:

open_dir(volume, vnode, outCookie):
    cursor = allocate_cursor_for(vnode)
    if cursor == null: return B_NO_MEMORY
    outCookie = cursor
    return B_OK

read_dir(volume, vnode, cookie, buffer, length, outCount):
    validate(cookie, buffer, length)
    fill_next_entries(cookie, buffer, length, outCount)
    return B_OK

close_dir(volume, vnode, cookie):
    stop_or_wake_work_using(cookie)
    return B_OK

free_dir_cookie(volume, vnode, cookie):
    free_cursor(cookie)
    return B_OK

The code is illustrative: use the exact Haiku typedefs, return codes, and callback signatures in the headers for the target build. In particular, do not call the final free routine from close_dir() just because that is where userland stopped iterating.

Make mount teardown a verified transition

The module’s unmount() callback is final cleanup for a mounted volume, not a veto or rollback point. In the pinned VFS implementation, an ordinary unmount first rejects busy or referenced vnodes; a forced unmount disconnects open descriptors, then the VFS frees the mount’s vnodes and removes the mount before calling the file system’s unmount() hook. The hook’s return value is not propagated to the caller: the VFS proceeds to notify, release, and delete its mount state. Therefore, do not rely on returning an error here to keep the mount active. Keep volume cleanup deterministic, release private_volume state only after vnode callbacks are done, and avoid leaving asynchronous work that can call the operation vectors after the hook returns. Test both ordinary busy-unmount refusal and forced-unmount descriptor failures in a disposable VM.

For a read-only test image, validate the entire lifecycle in a disposable VM:

  1. Mount a known image and verify that the returned root ID resolves through get_vnode() and reports a directory from read_stat().
  2. Resolve a known child with lookup(), then open and read it. Check the reported length and offset behavior at end-of-file.
  3. Open the same directory twice and interleave read_dir() calls. Each cursor should advance independently; rewind_dir() should reset only the specified cookie.
  4. Close a handle while a deliberately blocked development callback is waiting. Verify that close wakes or cancels the operation, and that free_cookie() runs only after the cookie is no longer in use.
  5. Unmount, confirm that volume state is released once, and verify that attempts to reopen the image do not reuse stale node or cookie pointers.

Do not use a real writable disk as a first test target. A read-only add-on should reject mutation operations explicitly and should never silently report success for writes it ignored. Record the Haiku build, B_CURRENT_FS_API_VERSION, module build, image checksum, node IDs, and callback sequence when diagnosing a failure.

The safest design is an ownership map: the volume owns mount state, stable node IDs identify filesystem objects, the VFS controls vnode references, and each open owns its cookie until the matching free callback. Once those boundaries are explicit, name lookup, hard links, concurrent opens, unmount, and module removal become much easier to reason about.

Related:

Sources:

Comments